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>
This commit is contained in:
@@ -12,6 +12,7 @@ import { FederationAuditService } from "./audit.service";
|
|||||||
import { ConnectionService } from "./connection.service";
|
import { ConnectionService } from "./connection.service";
|
||||||
import { AuthGuard } from "../auth/guards/auth.guard";
|
import { AuthGuard } from "../auth/guards/auth.guard";
|
||||||
import { AdminGuard } from "../auth/guards/admin.guard";
|
import { AdminGuard } from "../auth/guards/admin.guard";
|
||||||
|
import { WorkspaceGuard } from "../common/guards/workspace.guard";
|
||||||
import { CsrfGuard } from "../common/guards/csrf.guard";
|
import { CsrfGuard } from "../common/guards/csrf.guard";
|
||||||
import { SkipCsrf } from "../common/decorators/skip-csrf.decorator";
|
import { SkipCsrf } from "../common/decorators/skip-csrf.decorator";
|
||||||
import type { PublicInstanceIdentity } from "./types/instance.types";
|
import type { PublicInstanceIdentity } from "./types/instance.types";
|
||||||
@@ -76,11 +77,11 @@ export class FederationController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Initiate a connection to a remote instance
|
* Initiate a connection to a remote instance
|
||||||
* Requires authentication
|
* Requires authentication and workspace access
|
||||||
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
||||||
*/
|
*/
|
||||||
@Post("connections/initiate")
|
@Post("connections/initiate")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard, WorkspaceGuard)
|
||||||
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
||||||
async initiateConnection(
|
async initiateConnection(
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
@@ -99,11 +100,11 @@ export class FederationController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Accept a pending connection
|
* Accept a pending connection
|
||||||
* Requires authentication
|
* Requires authentication and workspace access
|
||||||
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
||||||
*/
|
*/
|
||||||
@Post("connections/:id/accept")
|
@Post("connections/:id/accept")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard, WorkspaceGuard)
|
||||||
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
||||||
async acceptConnection(
|
async acceptConnection(
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
@@ -127,11 +128,11 @@ export class FederationController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reject a pending connection
|
* Reject a pending connection
|
||||||
* Requires authentication
|
* Requires authentication and workspace access
|
||||||
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
||||||
*/
|
*/
|
||||||
@Post("connections/:id/reject")
|
@Post("connections/:id/reject")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard, WorkspaceGuard)
|
||||||
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
||||||
async rejectConnection(
|
async rejectConnection(
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
@@ -149,11 +150,11 @@ export class FederationController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Disconnect an active connection
|
* Disconnect an active connection
|
||||||
* Requires authentication
|
* Requires authentication and workspace access
|
||||||
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
* Rate limit: "medium" tier (20 req/min) - authenticated endpoint
|
||||||
*/
|
*/
|
||||||
@Post("connections/:id/disconnect")
|
@Post("connections/:id/disconnect")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard, WorkspaceGuard)
|
||||||
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
@Throttle({ medium: { limit: 20, ttl: 60000 } })
|
||||||
async disconnectConnection(
|
async disconnectConnection(
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
@@ -171,11 +172,11 @@ export class FederationController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all connections for the workspace
|
* Get all connections for the workspace
|
||||||
* Requires authentication
|
* Requires authentication and workspace access
|
||||||
* Rate limit: "long" tier (200 req/hour) - read-only endpoint
|
* Rate limit: "long" tier (200 req/hour) - read-only endpoint
|
||||||
*/
|
*/
|
||||||
@Get("connections")
|
@Get("connections")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard, WorkspaceGuard)
|
||||||
@Throttle({ long: { limit: 200, ttl: 3600000 } })
|
@Throttle({ long: { limit: 200, ttl: 3600000 } })
|
||||||
async getConnections(
|
async getConnections(
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
@@ -190,11 +191,11 @@ export class FederationController {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a single connection
|
* Get a single connection
|
||||||
* Requires authentication
|
* Requires authentication and workspace access
|
||||||
* Rate limit: "long" tier (200 req/hour) - read-only endpoint
|
* Rate limit: "long" tier (200 req/hour) - read-only endpoint
|
||||||
*/
|
*/
|
||||||
@Get("connections/:id")
|
@Get("connections/:id")
|
||||||
@UseGuards(AuthGuard)
|
@UseGuards(AuthGuard, WorkspaceGuard)
|
||||||
@Throttle({ long: { limit: 200, ttl: 3600000 } })
|
@Throttle({ long: { limit: 200, ttl: 3600000 } })
|
||||||
async getConnection(
|
async getConnection(
|
||||||
@Req() req: AuthenticatedRequest,
|
@Req() req: AuthenticatedRequest,
|
||||||
|
|||||||
200
apps/api/src/federation/workspace-access.integration.spec.ts
Normal file
200
apps/api/src/federation/workspace-access.integration.spec.ts
Normal file
@@ -0,0 +1,200 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.controller.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 21:49:34
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.controller.ts_20260203-2149_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.controller.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 2
|
||||||
|
**Generated:** 2026-02-03 21:49:39
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.controller.ts_20260203-2149_2_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/federation.controller.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 21:50:03
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-federation.controller.ts_20260203-2150_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
|
||||||
|
**Tool Used:** Write
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 21:48:25
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2148_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 2
|
||||||
|
**Generated:** 2026-02-03 21:48:42
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2148_2_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 3
|
||||||
|
**Generated:** 2026-02-03 21:48:56
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2148_3_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 1
|
||||||
|
**Generated:** 2026-02-03 21:49:01
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2149_1_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 2
|
||||||
|
**Generated:** 2026-02-03 21:49:06
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2149_2_remediation_needed.md"
|
||||||
|
```
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# QA Remediation Report
|
||||||
|
|
||||||
|
**File:** /home/jwoltje/src/mosaic-stack/apps/api/src/federation/workspace-access.integration.spec.ts
|
||||||
|
**Tool Used:** Edit
|
||||||
|
**Epic:** general
|
||||||
|
**Iteration:** 3
|
||||||
|
**Generated:** 2026-02-03 21:49:21
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
Pending QA validation
|
||||||
|
|
||||||
|
## Next Steps
|
||||||
|
|
||||||
|
This report was created by the QA automation hook.
|
||||||
|
To process this report, run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
claude -p "Use Task tool to launch universal-qa-agent for report: /home/jwoltje/src/mosaic-stack/docs/reports/qa-automation/pending/home-jwoltje-src-mosaic-stack-apps-api-src-federation-workspace-access.integration.spec.ts_20260203-2149_3_remediation_needed.md"
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user