fix(#338): Validate DEFAULT_WORKSPACE_ID as UUID
- Add federation.config.ts with UUID v4 validation for DEFAULT_WORKSPACE_ID - Validate at module initialization (fail fast if misconfigured) - Replace hardcoded "default" fallback with proper validation - Add 18 tests covering valid UUIDs, invalid formats, and missing values - Clear error messages with expected UUID format Refs #338 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
164
apps/api/src/federation/federation.config.spec.ts
Normal file
164
apps/api/src/federation/federation.config.spec.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* Federation Configuration Tests
|
||||
*
|
||||
* Issue #338: Tests for DEFAULT_WORKSPACE_ID validation
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import {
|
||||
isValidUuidV4,
|
||||
getDefaultWorkspaceId,
|
||||
validateFederationConfig,
|
||||
} from "./federation.config";
|
||||
|
||||
describe("federation.config", () => {
|
||||
const originalEnv = process.env.DEFAULT_WORKSPACE_ID;
|
||||
|
||||
afterEach(() => {
|
||||
// Restore original environment
|
||||
if (originalEnv === undefined) {
|
||||
delete process.env.DEFAULT_WORKSPACE_ID;
|
||||
} else {
|
||||
process.env.DEFAULT_WORKSPACE_ID = originalEnv;
|
||||
}
|
||||
});
|
||||
|
||||
describe("isValidUuidV4", () => {
|
||||
it("should return true for valid UUID v4", () => {
|
||||
const validUuids = [
|
||||
"123e4567-e89b-42d3-a456-426614174000",
|
||||
"550e8400-e29b-41d4-a716-446655440000",
|
||||
"6ba7b810-9dad-41d1-80b4-00c04fd430c8",
|
||||
"f47ac10b-58cc-4372-a567-0e02b2c3d479",
|
||||
];
|
||||
|
||||
for (const uuid of validUuids) {
|
||||
expect(isValidUuidV4(uuid)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("should return true for uppercase UUID v4", () => {
|
||||
expect(isValidUuidV4("123E4567-E89B-42D3-A456-426614174000")).toBe(true);
|
||||
});
|
||||
|
||||
it("should return false for non-v4 UUID (wrong version digit)", () => {
|
||||
// UUID v1 (version digit is 1)
|
||||
expect(isValidUuidV4("123e4567-e89b-12d3-a456-426614174000")).toBe(false);
|
||||
// UUID v3 (version digit is 3)
|
||||
expect(isValidUuidV4("123e4567-e89b-32d3-a456-426614174000")).toBe(false);
|
||||
// UUID v5 (version digit is 5)
|
||||
expect(isValidUuidV4("123e4567-e89b-52d3-a456-426614174000")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for invalid variant digit", () => {
|
||||
// Variant digit should be 8, 9, a, or b
|
||||
expect(isValidUuidV4("123e4567-e89b-42d3-0456-426614174000")).toBe(false);
|
||||
expect(isValidUuidV4("123e4567-e89b-42d3-7456-426614174000")).toBe(false);
|
||||
expect(isValidUuidV4("123e4567-e89b-42d3-c456-426614174000")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for non-UUID strings", () => {
|
||||
expect(isValidUuidV4("")).toBe(false);
|
||||
expect(isValidUuidV4("default")).toBe(false);
|
||||
expect(isValidUuidV4("not-a-uuid")).toBe(false);
|
||||
expect(isValidUuidV4("123e4567-e89b-12d3-a456")).toBe(false);
|
||||
expect(isValidUuidV4("123e4567e89b12d3a456426614174000")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return false for UUID with wrong length", () => {
|
||||
expect(isValidUuidV4("123e4567-e89b-42d3-a456-4266141740001")).toBe(false);
|
||||
expect(isValidUuidV4("123e4567-e89b-42d3-a456-42661417400")).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getDefaultWorkspaceId", () => {
|
||||
it("should return valid UUID when DEFAULT_WORKSPACE_ID is set correctly", () => {
|
||||
const validUuid = "123e4567-e89b-42d3-a456-426614174000";
|
||||
process.env.DEFAULT_WORKSPACE_ID = validUuid;
|
||||
|
||||
expect(getDefaultWorkspaceId()).toBe(validUuid);
|
||||
});
|
||||
|
||||
it("should trim whitespace from UUID", () => {
|
||||
const validUuid = "123e4567-e89b-42d3-a456-426614174000";
|
||||
process.env.DEFAULT_WORKSPACE_ID = ` ${validUuid} `;
|
||||
|
||||
expect(getDefaultWorkspaceId()).toBe(validUuid);
|
||||
});
|
||||
|
||||
it("should throw error when DEFAULT_WORKSPACE_ID is not set", () => {
|
||||
delete process.env.DEFAULT_WORKSPACE_ID;
|
||||
|
||||
expect(() => getDefaultWorkspaceId()).toThrow(
|
||||
"DEFAULT_WORKSPACE_ID environment variable is required for federation but is not set"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error when DEFAULT_WORKSPACE_ID is empty string", () => {
|
||||
process.env.DEFAULT_WORKSPACE_ID = "";
|
||||
|
||||
expect(() => getDefaultWorkspaceId()).toThrow(
|
||||
"DEFAULT_WORKSPACE_ID environment variable is required for federation but is not set"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error when DEFAULT_WORKSPACE_ID is only whitespace", () => {
|
||||
process.env.DEFAULT_WORKSPACE_ID = " ";
|
||||
|
||||
expect(() => getDefaultWorkspaceId()).toThrow(
|
||||
"DEFAULT_WORKSPACE_ID environment variable is required for federation but is not set"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw error when DEFAULT_WORKSPACE_ID is 'default' (not a valid UUID)", () => {
|
||||
process.env.DEFAULT_WORKSPACE_ID = "default";
|
||||
|
||||
expect(() => getDefaultWorkspaceId()).toThrow("DEFAULT_WORKSPACE_ID must be a valid UUID v4");
|
||||
expect(() => getDefaultWorkspaceId()).toThrow('Current value "default" is not a valid UUID');
|
||||
});
|
||||
|
||||
it("should throw error when DEFAULT_WORKSPACE_ID is invalid UUID format", () => {
|
||||
process.env.DEFAULT_WORKSPACE_ID = "not-a-valid-uuid";
|
||||
|
||||
expect(() => getDefaultWorkspaceId()).toThrow("DEFAULT_WORKSPACE_ID must be a valid UUID v4");
|
||||
});
|
||||
|
||||
it("should throw error for UUID v1 (wrong version)", () => {
|
||||
process.env.DEFAULT_WORKSPACE_ID = "123e4567-e89b-12d3-a456-426614174000";
|
||||
|
||||
expect(() => getDefaultWorkspaceId()).toThrow("DEFAULT_WORKSPACE_ID must be a valid UUID v4");
|
||||
});
|
||||
|
||||
it("should include helpful error message with expected format", () => {
|
||||
process.env.DEFAULT_WORKSPACE_ID = "invalid";
|
||||
|
||||
expect(() => getDefaultWorkspaceId()).toThrow(
|
||||
"Expected format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx"
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("validateFederationConfig", () => {
|
||||
it("should not throw when DEFAULT_WORKSPACE_ID is valid", () => {
|
||||
process.env.DEFAULT_WORKSPACE_ID = "123e4567-e89b-42d3-a456-426614174000";
|
||||
|
||||
expect(() => validateFederationConfig()).not.toThrow();
|
||||
});
|
||||
|
||||
it("should throw when DEFAULT_WORKSPACE_ID is missing", () => {
|
||||
delete process.env.DEFAULT_WORKSPACE_ID;
|
||||
|
||||
expect(() => validateFederationConfig()).toThrow(
|
||||
"DEFAULT_WORKSPACE_ID environment variable is required for federation"
|
||||
);
|
||||
});
|
||||
|
||||
it("should throw when DEFAULT_WORKSPACE_ID is invalid", () => {
|
||||
process.env.DEFAULT_WORKSPACE_ID = "invalid-uuid";
|
||||
|
||||
expect(() => validateFederationConfig()).toThrow(
|
||||
"DEFAULT_WORKSPACE_ID must be a valid UUID v4"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
58
apps/api/src/federation/federation.config.ts
Normal file
58
apps/api/src/federation/federation.config.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
/**
|
||||
* Federation Configuration
|
||||
*
|
||||
* Validates federation-related environment variables at startup.
|
||||
* Issue #338: Validate DEFAULT_WORKSPACE_ID is a valid UUID
|
||||
*/
|
||||
|
||||
/**
|
||||
* UUID v4 regex pattern
|
||||
* Matches standard UUID format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
|
||||
* where y is 8, 9, a, or b
|
||||
*/
|
||||
const UUID_V4_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
||||
|
||||
/**
|
||||
* Check if a string is a valid UUID v4
|
||||
*/
|
||||
export function isValidUuidV4(value: string): boolean {
|
||||
return UUID_V4_REGEX.test(value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the configured default workspace ID for federation
|
||||
* @throws Error if DEFAULT_WORKSPACE_ID is not set or is not a valid UUID
|
||||
*/
|
||||
export function getDefaultWorkspaceId(): string {
|
||||
const workspaceId = process.env.DEFAULT_WORKSPACE_ID;
|
||||
|
||||
if (!workspaceId || workspaceId.trim() === "") {
|
||||
throw new Error(
|
||||
"DEFAULT_WORKSPACE_ID environment variable is required for federation but is not set. " +
|
||||
"Please configure a valid UUID v4 workspace ID for handling incoming federation connections."
|
||||
);
|
||||
}
|
||||
|
||||
const trimmedId = workspaceId.trim();
|
||||
|
||||
if (!isValidUuidV4(trimmedId)) {
|
||||
throw new Error(
|
||||
`DEFAULT_WORKSPACE_ID must be a valid UUID v4. ` +
|
||||
`Current value "${trimmedId}" is not a valid UUID format. ` +
|
||||
`Expected format: xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx (where y is 8, 9, a, or b)`
|
||||
);
|
||||
}
|
||||
|
||||
return trimmedId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates federation configuration at startup.
|
||||
* Call this during module initialization to fail fast if misconfigured.
|
||||
*
|
||||
* @throws Error if DEFAULT_WORKSPACE_ID is not set or is not a valid UUID
|
||||
*/
|
||||
export function validateFederationConfig(): void {
|
||||
// Validate DEFAULT_WORKSPACE_ID - this will throw if invalid
|
||||
getDefaultWorkspaceId();
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import { Throttle } from "@nestjs/throttler";
|
||||
import { FederationService } from "./federation.service";
|
||||
import { FederationAuditService } from "./audit.service";
|
||||
import { ConnectionService } from "./connection.service";
|
||||
import { getDefaultWorkspaceId } from "./federation.config";
|
||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||
import { AdminGuard } from "../auth/guards/admin.guard";
|
||||
import { WorkspaceGuard } from "../common/guards/workspace.guard";
|
||||
@@ -225,8 +226,8 @@ export class FederationController {
|
||||
// LIMITATION: Incoming connections are created in a default workspace
|
||||
// TODO: Future enhancement - Allow configuration of which workspace handles incoming connections
|
||||
// This could be based on routing rules, instance configuration, or a dedicated federation workspace
|
||||
// For now, uses DEFAULT_WORKSPACE_ID environment variable or falls back to "default"
|
||||
const workspaceId = process.env.DEFAULT_WORKSPACE_ID ?? "default";
|
||||
// Issue #338: Validate DEFAULT_WORKSPACE_ID is a valid UUID (throws if invalid/missing)
|
||||
const workspaceId = getDefaultWorkspaceId();
|
||||
|
||||
const connection = await this.connectionService.handleIncomingConnectionRequest(
|
||||
workspaceId,
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
*
|
||||
* Provides instance identity and federation management with DoS protection via rate limiting.
|
||||
* Issue #272: Rate limiting added to prevent DoS attacks on federation endpoints
|
||||
* Issue #338: Validate DEFAULT_WORKSPACE_ID at startup
|
||||
*/
|
||||
|
||||
import { Module } from "@nestjs/common";
|
||||
import { Module, Logger, OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigModule } from "@nestjs/config";
|
||||
import { HttpModule } from "@nestjs/axios";
|
||||
import { ThrottlerModule } from "@nestjs/throttler";
|
||||
@@ -20,6 +21,7 @@ import { OIDCService } from "./oidc.service";
|
||||
import { CommandService } from "./command.service";
|
||||
import { QueryService } from "./query.service";
|
||||
import { FederationAgentService } from "./federation-agent.service";
|
||||
import { validateFederationConfig } from "./federation.config";
|
||||
import { PrismaModule } from "../prisma/prisma.module";
|
||||
import { TasksModule } from "../tasks/tasks.module";
|
||||
import { EventsModule } from "../events/events.module";
|
||||
@@ -83,4 +85,22 @@ import { RedisProvider } from "../common/providers/redis.provider";
|
||||
FederationAgentService,
|
||||
],
|
||||
})
|
||||
export class FederationModule {}
|
||||
export class FederationModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(FederationModule.name);
|
||||
|
||||
/**
|
||||
* Validate federation configuration at module initialization.
|
||||
* Issue #338: Fail fast if DEFAULT_WORKSPACE_ID is not a valid UUID.
|
||||
*/
|
||||
onModuleInit(): void {
|
||||
try {
|
||||
validateFederationConfig();
|
||||
this.logger.log("Federation configuration validated successfully");
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Federation configuration validation failed: ${error instanceof Error ? error.message : String(error)}`
|
||||
);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user