# 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 object - `request.session` - User session data ### WorkspaceGuard Validates workspace access and sets up RLS context. **Responsibilities:** 1. Extracts workspace ID from request (header, param, or body) 2. Verifies user is a member of the workspace 3. Sets the current user context for RLS policies 4. Attaches workspace context to the request **Sets on request:** - `request.workspace.id` - Validated workspace ID - `request.user.workspaceId` - Workspace ID (for backward compatibility) **Workspace ID Sources (in priority order):** 1. `X-Workspace-Id` header 2. `:workspaceId` URL parameter 3. `workspaceId` in request body **Example:** ```typescript @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:** 1. Reads required permission from `@RequirePermission()` decorator 2. Fetches user's role in the workspace 3. Checks if role satisfies the required permission 4. 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:** ```typescript @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:** ```typescript @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:** ```typescript @Get() async getTasks(@Workspace() workspaceId: string) { // workspaceId is guaranteed to be valid } ``` ### @WorkspaceContext() Parameter decorator to extract the full workspace context. **Example:** ```typescript @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:** ```typescript @Post() async create(@CurrentUser() user: any, @Body() dto: CreateDto) { // user contains authenticated user data } ``` ## Usage Patterns ### Basic Controller Setup ```typescript 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: ```typescript @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):** ```typescript // Frontend fetch("/api/tasks", { headers: { Authorization: "Bearer ", "X-Workspace-Id": "workspace-uuid", }, }); ``` **Via URL Parameter:** ```typescript @Get(':workspaceId/tasks') async getTasks(@Param('workspaceId') workspaceId: string) { // workspaceId extracted from URL } ``` **Via Request Body:** ```typescript @Post() async create(@Body() dto: { workspaceId: string; name: string }) { // workspaceId extracted from body } ``` ## Row-Level Security (RLS) When `WorkspaceGuard` is applied, it automatically: 1. Calls `setCurrentUser(userId)` to set the RLS context 2. All subsequent database queries are automatically filtered by RLS policies 3. 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 tests - `permission.guard.spec.ts` - PermissionGuard tests **Run tests:** ```bash npm test -- workspace.guard.spec npm test -- permission.guard.spec ``` ## Error Handling ### WorkspaceGuard Errors - `ForbiddenException("User not authenticated")` - No authenticated user - `BadRequestException("Workspace ID is required...")` - No workspace ID provided - `ForbiddenException("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 context - `ForbiddenException("You are not a member of this workspace")` - User not found in workspace - `ForbiddenException("Insufficient permissions. Required: ...")` - User role doesn't meet requirements ## Migration Guide ### Before (Manual Checks): ```typescript @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): ```typescript @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 functions - `docs/design/multi-tenant-rls.md` - RLS architecture documentation - `apps/api/prisma/schema.prisma` - Database schema with role definitions