Files
stack/apps/api/src/federation/workspace-access.integration.spec.ts
Jason Woltje 38695b3bb8 feat(#286): Add workspace access validation to federation endpoints
Security improvements:
- Apply WorkspaceGuard to all workspace-scoped federation endpoints
- Enforce workspace membership verification via Prisma
- Prevent cross-workspace access attacks
- Add comprehensive test coverage for workspace isolation

Changes:
- Add WorkspaceGuard to federation connection endpoints:
  - POST /connections/initiate
  - POST /connections/:id/accept
  - POST /connections/:id/reject
  - POST /connections/:id/disconnect
  - GET /connections
  - GET /connections/:id
- Add workspace-access.integration.spec.ts with tests for:
  - Workspace membership verification
  - Cross-workspace access prevention
  - Multiple workspace ID sources (header, param, body)

Part of M7.1 Remediation Sprint P1 security fixes.

Fixes #286

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-02-03 21:50:13 -06:00

201 lines
5.5 KiB
TypeScript

/**
* Workspace Access Integration Tests
*
* Tests that workspace-scoped federation endpoints enforce workspace access.
*/
import { describe, it, expect, beforeEach, vi } from "vitest";
import { WorkspaceGuard } from "../common/guards/workspace.guard";
import { PrismaService } from "../prisma/prisma.service";
import { ForbiddenException } from "@nestjs/common";
import type { AuthenticatedRequest } from "../common/types/user.types";
describe("Workspace Access Control - Federation", () => {
let prismaService: PrismaService;
let workspaceGuard: WorkspaceGuard;
beforeEach(() => {
const mockPrismaService = {
workspaceMember: {
findUnique: vi.fn(),
},
};
prismaService = mockPrismaService as unknown as PrismaService;
workspaceGuard = new WorkspaceGuard(prismaService);
});
describe("Workspace membership verification", () => {
it("should allow access when user is workspace member", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
workspaceId: "workspace-456",
},
headers: {
"x-workspace-id": "workspace-456",
},
params: {},
body: {},
} as AuthenticatedRequest;
// Mock workspace membership exists
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue({
workspaceId: "workspace-456",
userId: "user-123",
role: "ADMIN",
createdAt: new Date(),
updatedAt: new Date(),
} as any);
const canActivate = await workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any);
expect(canActivate).toBe(true);
});
it("should deny access when user is not workspace member", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {
"x-workspace-id": "workspace-999",
},
params: {},
body: {},
} as AuthenticatedRequest;
// Mock workspace membership does not exist
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue(null);
await expect(
workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any)
).rejects.toThrow(ForbiddenException);
});
it("should deny access when workspace ID is missing", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {},
params: {},
body: {},
} as AuthenticatedRequest;
await expect(
workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any)
).rejects.toThrow("Workspace ID is required");
});
it("should check workspace ID from URL parameter", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {},
params: {
workspaceId: "workspace-789",
},
} as any;
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue({
workspaceId: "workspace-789",
userId: "user-123",
role: "MEMBER",
createdAt: new Date(),
updatedAt: new Date(),
} as any);
const canActivate = await workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any);
expect(canActivate).toBe(true);
expect(prismaService.workspaceMember.findUnique).toHaveBeenCalledWith({
where: {
workspaceId_userId: {
workspaceId: "workspace-789",
userId: "user-123",
},
},
});
});
it("should check workspace ID from request body", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {},
params: {},
body: {
workspaceId: "workspace-111",
},
} as any;
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue({
workspaceId: "workspace-111",
userId: "user-123",
role: "ADMIN",
createdAt: new Date(),
updatedAt: new Date(),
} as any);
const canActivate = await workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any);
expect(canActivate).toBe(true);
});
});
describe("Workspace isolation", () => {
it("should prevent cross-workspace access", async () => {
const mockRequest: Partial<AuthenticatedRequest> = {
user: {
id: "user-123",
email: "test@example.com",
},
headers: {
"x-workspace-id": "workspace-attacker",
},
params: {},
body: {},
} as AuthenticatedRequest;
// User is NOT a member of the requested workspace
vi.spyOn(prismaService.workspaceMember, "findUnique").mockResolvedValue(null);
await expect(
workspaceGuard.canActivate({
switchToHttp: () => ({
getRequest: () => mockRequest,
}),
} as any)
).rejects.toThrow(ForbiddenException);
});
});
});