Implements Row-Level Security (RLS) context propagation via NestJS interceptor and AsyncLocalStorage. Core Implementation: - RlsContextInterceptor sets PostgreSQL session variables (app.current_user_id, app.current_workspace_id) within transaction boundaries - Uses SET LOCAL for transaction-scoped variables, preventing connection pool leakage - AsyncLocalStorage propagates transaction-scoped Prisma client to services - Graceful handling of unauthenticated routes - 30-second transaction timeout with 10-second max wait Security Features: - Error sanitization prevents information disclosure to clients - TransactionClient type provides compile-time safety, prevents invalid method calls - Defense-in-depth security layer for RLS policy enforcement Quality Rails Compliance: - Fixed 154 lint errors in llm-usage module (package-level enforcement) - Added proper TypeScript typing for Prisma operations - Resolved all type safety violations Test Coverage: - 19 tests (7 provider + 9 interceptor + 3 integration) - 95.75% overall coverage (100% statements on implementation files) - All tests passing, zero lint errors Documentation: - Comprehensive RLS-CONTEXT-USAGE.md with examples and migration guide Files Created: - apps/api/src/common/interceptors/rls-context.interceptor.ts - apps/api/src/common/interceptors/rls-context.interceptor.spec.ts - apps/api/src/common/interceptors/rls-context.integration.spec.ts - apps/api/src/prisma/rls-context.provider.ts - apps/api/src/prisma/rls-context.provider.spec.ts - apps/api/src/prisma/RLS-CONTEXT-USAGE.md Fixes #351 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Common Guards and Decorators
This directory contains shared guards and decorators for workspace-based permission management in the Mosaic Stack API.
Overview
The permission system provides:
- Workspace isolation via Row-Level Security (RLS)
- Role-based access control (RBAC) using workspace member roles
- Declarative permission requirements using decorators
Guards
AuthGuard
Located in ../auth/guards/auth.guard.ts
Verifies user authentication and attaches user data to the request.
Sets on request:
request.user- Authenticated user objectrequest.session- User session data
WorkspaceGuard
Validates workspace access and sets up RLS context.
Responsibilities:
- Extracts workspace ID from request (header, param, or body)
- Verifies user is a member of the workspace
- Sets the current user context for RLS policies
- Attaches workspace context to the request
Sets on request:
request.workspace.id- Validated workspace IDrequest.user.workspaceId- Workspace ID (for backward compatibility)
Workspace ID Sources (in priority order):
X-Workspace-Idheader:workspaceIdURL parameterworkspaceIdin request body
Example:
@Controller("tasks")
@UseGuards(AuthGuard, WorkspaceGuard)
export class TasksController {
@Get()
async getTasks(@Workspace() workspaceId: string) {
// workspaceId is validated and RLS context is set
}
}
PermissionGuard
Enforces role-based access control using workspace member roles.
Responsibilities:
- Reads required permission from
@RequirePermission()decorator - Fetches user's role in the workspace
- Checks if role satisfies the required permission
- Attaches role to request for convenience
Sets on request:
request.user.workspaceRole- User's role in the workspace
Must be used after AuthGuard and WorkspaceGuard.
Example:
@Controller("admin")
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
export class AdminController {
@RequirePermission(Permission.WORKSPACE_ADMIN)
@Delete("data")
async deleteData() {
// Only ADMIN or OWNER can execute
}
}
Decorators
@RequirePermission(permission: Permission)
Specifies the minimum permission level required for a route.
Permission Levels:
| Permission | Allowed Roles | Use Case |
|---|---|---|
WORKSPACE_OWNER |
OWNER | Critical operations (delete workspace, transfer ownership) |
WORKSPACE_ADMIN |
OWNER, ADMIN | Administrative functions (manage members, settings) |
WORKSPACE_MEMBER |
OWNER, ADMIN, MEMBER | Standard operations (create/edit content) |
WORKSPACE_ANY |
All roles including GUEST | Read-only or basic access |
Example:
@RequirePermission(Permission.WORKSPACE_ADMIN)
@Post('invite')
async inviteMember(@Body() inviteDto: InviteDto) {
// Only admins can invite members
}
@Workspace()
Parameter decorator to extract the validated workspace ID.
Example:
@Get()
async getTasks(@Workspace() workspaceId: string) {
// workspaceId is guaranteed to be valid
}
@WorkspaceContext()
Parameter decorator to extract the full workspace context.
Example:
@Get()
async getTasks(@WorkspaceContext() workspace: { id: string }) {
console.log(workspace.id);
}
@CurrentUser()
Located in ../auth/decorators/current-user.decorator.ts
Extracts the authenticated user from the request.
Example:
@Post()
async create(@CurrentUser() user: any, @Body() dto: CreateDto) {
// user contains authenticated user data
}
Usage Patterns
Basic Controller Setup
import { Controller, Get, Post, UseGuards } from "@nestjs/common";
import { AuthGuard } from "../auth/guards/auth.guard";
import { WorkspaceGuard, PermissionGuard } from "../common/guards";
import { Workspace, Permission, RequirePermission } from "../common/decorators";
import { CurrentUser } from "../auth/decorators/current-user.decorator";
@Controller("resources")
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
export class ResourcesController {
@Get()
@RequirePermission(Permission.WORKSPACE_ANY)
async list(@Workspace() workspaceId: string) {
// All members can list
}
@Post()
@RequirePermission(Permission.WORKSPACE_MEMBER)
async create(@Workspace() workspaceId: string, @CurrentUser() user: any, @Body() dto: CreateDto) {
// Members and above can create
}
@Delete(":id")
@RequirePermission(Permission.WORKSPACE_ADMIN)
async delete(@Param("id") id: string) {
// Only admins can delete
}
}
Mixed Permissions
Different endpoints can have different permission requirements:
@Controller("projects")
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
export class ProjectsController {
@Get()
@RequirePermission(Permission.WORKSPACE_ANY)
async list() {
/* Anyone can view */
}
@Post()
@RequirePermission(Permission.WORKSPACE_MEMBER)
async create() {
/* Members can create */
}
@Patch("settings")
@RequirePermission(Permission.WORKSPACE_ADMIN)
async updateSettings() {
/* Only admins */
}
@Delete()
@RequirePermission(Permission.WORKSPACE_OWNER)
async deleteProject() {
/* Only owner */
}
}
Workspace ID in Request
The workspace ID can be provided in multiple ways:
Via Header (Recommended for SPAs):
// Frontend
fetch("/api/tasks", {
headers: {
Authorization: "Bearer <token>",
"X-Workspace-Id": "workspace-uuid",
},
});
Via URL Parameter:
@Get(':workspaceId/tasks')
async getTasks(@Param('workspaceId') workspaceId: string) {
// workspaceId extracted from URL
}
Via Request Body:
@Post()
async create(@Body() dto: { workspaceId: string; name: string }) {
// workspaceId extracted from body
}
Row-Level Security (RLS)
When WorkspaceGuard is applied, it automatically:
- Calls
setCurrentUser(userId)to set the RLS context - All subsequent database queries are automatically filtered by RLS policies
- Users can only access data in workspaces they're members of
See: docs/design/multi-tenant-rls.md for full RLS documentation.
Testing
Tests are provided for both guards:
workspace.guard.spec.ts- WorkspaceGuard testspermission.guard.spec.ts- PermissionGuard tests
Run tests:
npm test -- workspace.guard.spec
npm test -- permission.guard.spec
Error Handling
WorkspaceGuard Errors
ForbiddenException("User not authenticated")- No authenticated userBadRequestException("Workspace ID is required...")- No workspace ID providedForbiddenException("You do not have access to this workspace")- User is not a workspace member
PermissionGuard Errors
ForbiddenException("Authentication and workspace context required")- Missing user or workspace contextForbiddenException("You are not a member of this workspace")- User not found in workspaceForbiddenException("Insufficient permissions. Required: ...")- User role doesn't meet requirements
Migration Guide
Before (Manual Checks):
@Get()
async getTasks(@Request() req: any) {
const workspaceId = req.user?.workspaceId;
if (!workspaceId) {
throw new UnauthorizedException("Authentication required");
}
return this.tasksService.findAll(workspaceId);
}
After (Guard-Based):
@Get()
@UseGuards(AuthGuard, WorkspaceGuard, PermissionGuard)
@RequirePermission(Permission.WORKSPACE_ANY)
async getTasks(@Workspace() workspaceId: string) {
return this.tasksService.findAll(workspaceId);
}
Benefits
✅ Declarative - Permission requirements are clear from decorators
✅ DRY - No repetitive auth/workspace checks in every handler
✅ Type-safe - Workspace ID is guaranteed to exist when using @Workspace()
✅ Secure - RLS context automatically set, defense in depth
✅ Testable - Guards are independently testable
✅ Maintainable - Permission changes in one place
Related Files
apps/api/src/lib/db-context.ts- RLS utility functionsdocs/design/multi-tenant-rls.md- RLS architecture documentationapps/api/prisma/schema.prisma- Database schema with role definitions