8.2 KiB
M2-011 Completion Report: Permission Guards
Issue: #11 - API-level permission guards for workspace-based access control
Status: ✅ Complete
Date: January 29, 2026
Summary
Implemented comprehensive API-level permission guards that work in conjunction with the existing Row-Level Security (RLS) system. The guards provide declarative, role-based access control for all workspace-scoped API endpoints.
Implementation Details
1. Guards Created
WorkspaceGuard (apps/api/src/common/guards/workspace.guard.ts)
- Purpose: Validates workspace access and sets RLS context
- Features:
- Extracts workspace ID from multiple sources (header, URL param, body)
- Verifies user is a workspace member
- Automatically sets
app.current_user_idfor RLS policies - Attaches workspace context to request object
- Priority order:
X-Workspace-Idheader →:workspaceIdparam →body.workspaceId
PermissionGuard (apps/api/src/common/guards/permission.guard.ts)
- Purpose: Enforces role-based access control
- Features:
- Reads required permission from
@RequirePermission()decorator - Fetches user's role in the workspace
- Validates role against permission requirement
- Attaches role to request for convenience
- Reads required permission from
- Permission Levels:
WORKSPACE_OWNER- Only workspace ownersWORKSPACE_ADMIN- Owners and adminsWORKSPACE_MEMBER- Owners, admins, and membersWORKSPACE_ANY- All roles including guests
2. Decorators Created
@RequirePermission(permission: Permission)
Located in apps/api/src/common/decorators/permissions.decorator.ts
- Declarative permission specification for routes
- Type-safe permission enum
- Works with PermissionGuard via metadata reflection
@Workspace()
Located in apps/api/src/common/decorators/workspace.decorator.ts
- Parameter decorator to extract validated workspace ID
- Cleaner than accessing
req.workspace.iddirectly - Type-safe and convenient
@WorkspaceContext()
- Extracts full workspace context object
- Useful for future extensions (workspace name, settings, etc.)
3. Updated Controllers
TasksController
Before:
@Get()
async findAll(@Query() query: QueryTasksDto, @Request() req: any) {
const workspaceId = req.user?.workspaceId;
if (!workspaceId) {
throw new UnauthorizedException("Authentication required");
}
return this.tasksService.findAll({ ...query, workspaceId });
}
After:
@Get()
@RequirePermission(Permission.WORKSPACE_ANY)
async findAll(
@Query() query: QueryTasksDto,
@Workspace() workspaceId: string
) {
return this.tasksService.findAll({ ...query, workspaceId });
}
KnowledgeController
- Updated all endpoints to use new guard system
- Read endpoints:
WORKSPACE_ANY - Create/update endpoints:
WORKSPACE_MEMBER - Delete endpoints:
WORKSPACE_ADMIN
4. Database Context Updates
Updated apps/api/src/lib/db-context.ts:
- Fixed import to use local PrismaService instead of non-existent
@mosaic/database - Created
getPrismaInstance()helper for standalone usage - Updated all functions to use optional PrismaClient parameter
- Fixed TypeScript strict mode issues
- Maintained backward compatibility
5. Test Coverage
WorkspaceGuard Tests (workspace.guard.spec.ts)
- ✅ Allow access when user is workspace member (via header)
- ✅ Allow access when user is workspace member (via URL param)
- ✅ Allow access when user is workspace member (via body)
- ✅ Prioritize header over param and body
- ✅ Throw ForbiddenException when user not authenticated
- ✅ Throw BadRequestException when workspace ID missing
- ✅ Throw ForbiddenException when user not a workspace member
- ✅ Handle database errors gracefully
Result: 8/8 tests passing
PermissionGuard Tests (permission.guard.spec.ts)
- ✅ Allow access when no permission required
- ✅ Allow OWNER to access WORKSPACE_OWNER permission
- ✅ Deny ADMIN access to WORKSPACE_OWNER permission
- ✅ Allow OWNER and ADMIN to access WORKSPACE_ADMIN permission
- ✅ Deny MEMBER access to WORKSPACE_ADMIN permission
- ✅ Allow OWNER, ADMIN, and MEMBER to access WORKSPACE_MEMBER permission
- ✅ Deny GUEST access to WORKSPACE_MEMBER permission
- ✅ Allow any role (including GUEST) to access WORKSPACE_ANY permission
- ✅ Throw ForbiddenException when user context missing
- ✅ Throw ForbiddenException when workspace context missing
- ✅ Throw ForbiddenException when user not a workspace member
- ✅ Handle database errors gracefully
Result: 12/12 tests passing
Total Test Coverage: 20/20 tests passing ✅
6. Documentation
Created comprehensive apps/api/src/common/README.md covering:
- Overview of the permission system
- Detailed guard documentation
- Decorator usage examples
- Usage patterns and best practices
- Error handling guide
- Migration guide from manual checks
- RLS integration notes
- Testing instructions
Benefits
✅ Declarative - Permission requirements visible in decorators
✅ DRY - No repetitive auth/workspace checks in handlers
✅ Type-safe - Workspace ID guaranteed via @Workspace()
✅ Secure - RLS context automatically set, defense in depth
✅ Testable - Guards independently unit tested
✅ Maintainable - Permission changes centralized
✅ Documented - Comprehensive README and inline docs
Usage Example
@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
}
}
Integration with RLS
The guards work seamlessly with the existing RLS system:
- AuthGuard authenticates the user
- WorkspaceGuard validates workspace access and calls
setCurrentUser() - PermissionGuard enforces role-based permissions
- RLS policies automatically filter database queries
This provides defense in depth:
- Application-level: Guards check permissions
- Database-level: RLS prevents data leakage
Files Created/Modified
Created:
apps/api/src/common/guards/workspace.guard.ts(150 lines)apps/api/src/common/guards/workspace.guard.spec.ts(219 lines)apps/api/src/common/guards/permission.guard.ts(165 lines)apps/api/src/common/guards/permission.guard.spec.ts(278 lines)apps/api/src/common/guards/index.tsapps/api/src/common/decorators/permissions.decorator.ts(48 lines)apps/api/src/common/decorators/workspace.decorator.ts(40 lines)apps/api/src/common/decorators/index.tsapps/api/src/common/index.tsapps/api/src/common/README.md(314 lines)
Modified:
apps/api/src/lib/db-context.ts- Fixed imports and TypeScript issuesapps/api/src/tasks/tasks.controller.ts- Migrated to new guard systemapps/api/src/knowledge/knowledge.controller.ts- Migrated to new guard system
Total: 10 new files, 3 modified files, ~1,600 lines of code and documentation
Next Steps
- Migrate remaining controllers - Apply guards to all workspace-scoped controllers
- Add team-level permissions - Extend to support team-specific access control
- Audit logging - Consider logging permission checks for security audits
- Performance monitoring - Track guard execution time in production
- Frontend integration - Update frontend to send
X-Workspace-Idheader
Related Work
- M2 Database Layer - RLS policies foundation
- Issue #12 - Workspace management UI (uses these guards)
docs/design/multi-tenant-rls.md- RLS architecture documentation
Commit
The implementation was committed in:
- Commit:
5291fece- "feat(web): add workspace management UI (M2 #12)" (Note: This commit bundled multiple features; guards were part of the backend infrastructure)
Status: ✅ Complete and tested
Blockers: None
Review: Ready for code review and integration testing