Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

12 — API Mapping (RBAC Module)

Exact wire contract for every RBAC screen → endpoint. Base /api/v1; envelope per 00-shared/07 ({success,message,data,meta?,timestamp,requestId} / {success:false,message,error:{code,details?},timestamp,requestId}). All endpoints from src/modules/rbac/rbac.controller.ts; rules from rbac.service.ts. Tenant identity comes from the JWT claim — never from the body.


0. Module-wide request envelope & client policy

AspectContract
Basehttps://api.<domain>/api/v1
HeadersAuthorization: Bearer <accessToken>; x-request-id client UUID; Content-Type: application/json
TenancytenantId from JWT (jwt-payload.interface.ts:2); body never carries it (RBAC DTOs have no tenant field)
Rate tierdefault api 100/min (rate-limit.guard.ts:36-37); production-only enforcement (rate-limit.guard.ts:30)
Server cachingpermissions-of-user: Redis sl:{tenantId}:perm:{userId} EX 300 (rbac.service.ts:48,68) — client must tolerate ≤ 5 min propagation
Client cachingroles/members 5 min stale-while-revalidate; permissions catalog 24 h (09 §2)
Offlinereads from cache + banner; writes blocked
Retrybackoff on 5xx/network; no auto-retry on 429

1. The guard pipeline (how every endpoint is protected)

Global order, app.module.ts:129-133:

1. RateLimitGuard   (APP_GUARD #1)  — sliding window Redis (`rate-limit.guard.ts:20-59`)
2. JwtAuthGuard     (APP_GUARD #2)  — Bearer verify → req.user {id, tenantId, roles}
                                       + TenantContext fill, platform_admin flag
                                       (`jwt-auth.guard.ts:40-55`)
3. RbacGuard        (APP_GUARD #3)  — @Roles (OR, from JWT claims) + @Permissions
                                       (AND, resolved per-user) (`rbac.guard.ts:20-52`)

RbacGuard detail (rbac.guard.ts:20-52):

  • no @Roles/@Permissions metadata → true (line 29) — route is authed-only.
  • role match: requiredRoles.some(r => req.user.roles.includes(r)) (lines 38-42) → else 403 ForbiddenException "Insufficient role."
  • perm match: getPermissionsForUser(userId, tenantId) (line 44) then requiredPerms.every(p => perms.includes(p)) (line 48) → else 403 "Insufficient permission."
  • Decorators: Roles/ROLES_KEY (roles.decorator.ts:3-4), Permissions/PERMISSIONS_KEY (permissions.decorator.ts:3-4), re-exported by guards/decorators.ts:1-5.

Today's coverage (derived, (planned) for completion): only webhooks (webhooks.controller.ts:22-86), files (files.controller.ts:30-67), scheduler (scheduler.controller.ts:24-38), search (search.controller.ts:16) controllers carry @Permissions. Phase-5 "Permissions audit (all endpoints)" (docs/IMPLEMENTATION_PLAN.md:241) is (planned); the RBAC controller itself is role-gated (rbac.controller.ts:21). Client must be built to the intended model (perm-gated everywhere) while living with the current role-gated reality (OQ-R1).

2. RBAC endpoints (exact)

All under class gate @Roles('org_admin') + @UseGuards(JwtAuthGuard, RbacGuard) (rbac.controller.ts:21-22).

Roles list (S1, S2)

EndpointGET /rbac/roles (rbac.controller.ts:27-31)
Success200 data: Role[] — array, no meta (not paginated, rbac.service.ts:75-77)
Sortpriority: -1 desc (rbac.service.ts:76)
Role doc shape{_id, tenantId, name, slug, description?, isSystem, priority, permissions[], createdAt, updatedAt, version} (role.schema.ts:67-86, base.schema.ts)
Errors401 UNAUTHENTICATED; 403 PERMISSION_DENIED (non-org_admin); 429 RATE_LIMITED; 5xx

Permission catalog (S3 matrix)

EndpointGET /rbac/permissions (rbac.controller.ts:33-37)
Success200 data: string[] — the 95 names (rbac.service.ts:79-81; permissions.constants.ts:1-97)
Errorssame as above

Create role (S3)

EndpointPOST /rbac/roles (rbac.controller.ts:39-43)
RequestCreateRoleDto: {name, slug, description?, isSystem?, priority?, permissions?} (create-role.dto.ts:10-38)
Success201/200 data: Role (created with tenant from context, role.repository.ts:17-22)
Errors400 VALIDATION_ERROR (field details); 409 DUPLICATE_RESOURCE — slug exists (rbac.service.ts:86-87, index role.schema.ts:89); 403; 429
Client notealways send isSystem:false (OQ-R2); whitelist permissions client-side

Update role (S3 edit)

EndpointPATCH /rbac/roles/:id (rbac.controller.ts:45-49)
Requestsame CreateRoleDto (full-doc $set, rbac.service.ts:96)
Errors400 "Cannot modify system roles." (rbac.service.ts:94-95); 404 RESOURCE_NOT_FOUND (rbac.service.ts:93,97); 409 on slug clash; 403
Success200 data: Role (updated doc)

Delete role (S1 menu)

EndpointDELETE /rbac/roles/:id (rbac.controller.ts:51-55)
Behavioursoft delete (rbac.service.ts:106); 400 system role (rbac.service.ts:104-105); 404 unknown (rbac.service.ts:103)
Success200 data: <void> (empty)

Members list (S4)

EndpointGET /rbac/members (rbac.controller.ts:57-61)
Success200 data: Member[] — array, no meta (rbac.service.ts:109-111)
SortjoinedAt: -1 desc (rbac.service.ts:110)
Member doc shape{_id, tenantId, userId, organizationId?, roles[], permissions[], status, joinedAt, invitedBy?, acceptedAt?, lastActiveAt?, metadata?, createdAt, updatedAt, version} (organization-member.schema.ts:14-44)
Noteno user-profile join (OQ-R7); permissions[] field exists but is never written by the service (derived perms only, rbac.service.ts:63-65)

Add member (S5)

EndpointPOST /rbac/members (rbac.controller.ts:63-67)
RequestAddMemberDto: {userId (MongoId), roles: string[]} (add-member.dto.ts:4-12)
Behaviourcreates status:"active", joinedAt: now, tenant-scoped (rbac.service.ts:113-127)
Errors400 validation; 403; 500 on duplicate (E11000 on {tenantId,userId} index organization-member.schema.ts:48 — unmapped, OQ-R5) → client pre-checks
Success200/201 data: Member

Update member roles (S4 menu edit, S5 edit mode)

EndpointPATCH /rbac/members/:id (rbac.controller.ts:69-73)
RequestAddMemberDtouserId ignored by service (rbac.controller.ts:72; rbac.service.ts:129-136 $set:{roles})
Errors404 RESOURCE_NOT_FOUND (rbac.service.ts:134); 400; 403
Success200 data: Member

Remove member (S4 menu)

EndpointDELETE /rbac/members/:id (rbac.controller.ts:75-79)
Behavioursoft delete; silent 200 even if not found (rbac.service.ts:138-140)
Success200 data: <void>

Permission audit (S6) — cross-module read

EndpointGET /audit-logs?page=&limit=&action=&entityType=&actorId= (audit.controller.ts:17-34)
GuardJwtAuthGuard only today (audit.controller.ts:9) — audit.read not enforced (Phase-5 (planned); client gates anyway)
Success200 paginated: data: AuditLog[] + meta: {page, limit, totalItems, totalPages, hasNext, hasPrevious} (07 §2; default limit 50, audit.controller.ts:20)
Filteraction / entityType / actorId combined (audit.controller.ts:26-29)

3. Error-code map (RBAC screens)

ScreencodeUI
any list401silent refresh → fail → sessionExpired
any403 PERMISSION_DENIED403 screen w/ cause copy; hide offending action
role save409 DUPLICATE_RESOURCEinline under slug + focus
role save/delete400 (system role)unreachable via UI; banner fallback
role update/delete404treat as removed → pop to list + snackbar
member add500 (dup, E11000)pre-checked; fallback conflict banner + list refresh
member update404row removed snackbar
any write429countdown banner, no auto-retry
any5xxgeneric + requestId (http-exception.filter.ts:60-65)

4. Endpoint coverage audit (forward-looking)

Controller groupStatus todaySource
webhooks (11)@Permissions('webhook.*') on allwebhooks.controller.ts:22-86
files (5)@Permissions('file.*') on allfiles.controller.ts:30-67
scheduler (3)@Permissions('scheduler.*') on allscheduler.controller.ts:24-38
search (1)@Permissions('search')search.controller.ts:16
rbac (8)@Roles('org_admin') class-levelrbac.controller.ts:21
all other modulesglobal guards only (authed)app.module.ts:129-133
full permission audit(planned) Phase 5docs/IMPLEMENTATION_PLAN.md:241