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

01 — Product Overview (RBAC Module)

StudyLyon — multi-tenant ERP / School Management API. This package designs the RBAC module client (Flutter, forward-looking spec) against the implemented NestJS backend. Every endpoint, DTO field, schema, permission name, and guard behaviour is derived from src/modules/rbac/**, src/modules/auth/**, src/modules/organizations/**, src/common/**, and src/app/app.module.ts. No feature is invented; gaps are flagged in the Assumptions & Open Questions section (and mirrored in 00-shared/12).


1. Purpose

RBAC (role-based access control) is the authorization spine of StudyLyon. Auth answers who you are; RBAC answers what you may do, inside which tenant. The module owns:

ResponsibilitySource
Catalog of all 95 permission names (single source of truth)permissions.constants.ts:1-97 ALL_PERMISSIONS
Role definitions (7 seeded system roles + unlimited tenant custom roles)role.schema.ts:8-65 DEFAULT_ROLES; rbac.service.ts:83 createRole
Role → permission resolution at runtime (per-tenant)rbac.service.ts:59-65 findBySlugsUnscoped + flatMap
Organization membership (user ↔ org ↔ roles)organization-member.schema.ts:13-44; rbac.service.ts:113-140
Permission enforcement on every endpoint (global guard)app.module.ts:129-133; rbac.guard.ts:20-52
Fast permission lookup via Redis (5-minute TTL)rbac.service.ts:48,68
Default-role seeding at tenant bootstrapauth.service.ts:93; organizations.service.ts:58

2. The guard pipeline (derived, exact)

Every HTTP request passes the three global APP_GUARDs registered in app.module.ts:129-133, in this order:

Request
  ↓
RateLimitGuard   (app.module.ts:129)  — Redis sliding window; default tier 'api' 100/min
  ↓
JwtAuthGuard     (app.module.ts:130)  — Bearer token → JwtPayload {sub, tenantId, roles, type}
  ↓
RbacGuard        (app.module.ts:131)  — roles (OR) + permissions (AND) from metadata
  ↓
Controller → Service → Repository (tenant-scoped) → MongoDB
  • JwtAuthGuard (jwt-auth.guard.ts:40-55) verifies the token, sets req.user = { id, tenantId, roles }, and populates the TenantContextService (isPlatformAdmin = roles.includes('platform_admin'), jwt-auth.guard.ts:54).
  • RbacGuard (rbac.guard.ts:20-52) reads @Roles() / @Permissions() metadata:
    • no metadata → allow (rbac.guard.ts:29);
    • roles → pass if any required role is in the JWT claim (rbac.guard.ts:39-41);
    • permissions → pass only if every required permission is in the resolved set (rbac.guard.ts:43-49, AND semantics); resolution hits the Redis cache sl:{tenantId}:perm:{userId} with 300 s TTL, then falls back to the DB (rbac.service.ts:44-73);
    • failure → ForbiddenException → 403 PERMISSION_DENIED (http-exception.filter.ts:30).

The RBAC controller itself re-declares @UseGuards(JwtAuthGuard, RbacGuard) and is class-gated with @Roles('org_admin') (rbac.controller.ts:21-22) — meaning today only users holding the org_admin role can manage roles/members, regardless of the rbac.* permissions that exist in the catalog. See OQ-R1.

3. Domain model (derived)

User (users collection)
  └── organization_members: 1:1 per (tenantId, userId)   [organization-member.schema.ts:48]
        └── roles: string[] of role slugs                 [organization-member.schema.ts:22]
              └── roles collection: slug → permissions[]  [role.schema.ts:84-85]
                    (unique per tenant: {tenantId, slug}) [role.schema.ts:89]
  • Every business doc carries tenantId (base.schema.ts:11); repositories inject it structurally (base.repository.ts:20-30) — cross-tenant reads are impossible at the repo layer.
  • Member statuses exist (invited | active | suspended, organization-member.schema.ts:7-11) but the service only ever creates active (rbac.service.ts:122); no endpoint changes status (OQ-R3).
  • Roles are soft-deleted (rbac.service.ts:106); members are soft-deleted (rbac.service.ts:139).

4. Default roles (seeded, immutable)

role.schema.ts:8-65 — seeded by seedDefaults at registration (auth.service.ts:93) and at organization creation (organizations.service.ts:58, note: seeded under org.slug as tenantId, OQ-R6). All seven are isSystem: true — the API refuses update or delete (rbac.service.ts:94-95,104-105).

NameSlugPriorityPermissionsLocked
Platform Adminplatform_admin1000[] (cross-tenant; bypasses tenant scope via base.repository.ts:21)system
Organization Adminorg_admin100all 95system
Teacherteacher50student.read, attendance.mark, attendance.editsystem
Staffstaff40student.readsystem
Accountantaccountant30fees.collect, student.readsystem
Parentparent20student.readsystem
Studentstudent10[]system

Roles list is server-sorted by priority: -1 (rbac.service.ts:76) — priority also acts as a rough "power" ordering the UI uses for display.

5. Permission catalog (complete, quoted from source)

ALL_PERMISSIONS, permissions.constants.ts:1-97the only source of permission names. 95 permissions. The client mirror must be generated from this list (see 13_State_Management.md); the full UI matrix is in 06_Screen_Specifications.md §3.

organization.read            organization.update           organization.delete
organization.settings.update user.read                     user.create
user.update                  user.delete                   user.import
rbac.role.read               rbac.role.create              rbac.role.update
rbac.role.delete             rbac.member.read              rbac.member.create
rbac.member.update           rbac.member.delete            staff.read
staff.create                 staff.update                  staff.delete
department.manage            designation.manage            student.read
student.create               student.update                student.delete
attendance.mark              attendance.edit               fees.collect
notification.read            notification.update           crm.read
crm.lead.manage              crm.campaign.manage           dashboard.read
dashboard.widget.manage      report.generate               report.read
biometric.log.create         biometric.log.read            biometric.device.manage
timetable.read               timetable.create              houses.read
houses.create                houses.update                 houses.delete
rooms.read                   rooms.create                  rooms.update
rooms.delete                 audit.read                    books.read
books.create                 books.update                  books.delete
books.issue                  books.return                  fines.pay
transport.vehicle.read       transport.vehicle.create      transport.vehicle.update
transport.vehicle.delete     transport.route.read          transport.route.create
transport.route.update       transport.route.delete        transport.driver.read
transport.driver.create      transport.driver.update       transport.driver.delete
transport.assign             settings.read                 settings.update
settings.delete              feature-flags.read            feature-flags.update
feature-flags.delete         payments.read                 payments.process
payments.refund              payments.reconcile            receipts.read
file.read                    file.upload                   file.delete
webhook.create               webhook.read                  webhook.update
webhook.delete               search                        scheduler.read
scheduler.create             scheduler.delete

Forward-looking (planned, not in source): docs/IMPLEMENTATION_PLAN.md:751-761 adds coaching permissions batch.manage, session.manage, test_series.manage, dpp.manage, study_material.manage, analytics.view, question_bank.manage, and :713-749 adds roles batch_coordinator, test_coordinator, content_manager, coaching_student. None exist in permissions.constants.ts today — the client must not render them until the backend ships them; the matrix renders only the 95 above.

6. Business goals

GoalMeasure
Least-privilege out of the box7 seeded roles cover school staff; custom roles are tenant-scoped
Enforcement everywhereGlobal RbacGuard on all routes; @Roles/@Permissions decorators (roles.decorator.ts:3, permissions.decorator.ts:3)
Fast enforcementPermission resolution cached 300 s in Redis (rbac.service.ts:68); JWT roles are claim-carried (no DB hit on role checks)
Tenant isolationtenantId injected in every query; unique {tenantId, slug} / {tenantId, userId} (role.schema.ts:89, organization-member.schema.ts:48)
AuditabilityRBAC writes surface in audit-logs (Phase-5 permission audit (planned), docs/IMPLEMENTATION_PLAN.md:241; audit.read perm exists)
No accidental lockout of adminssystem roles immutable; org_admin can never be deleted/edited

7. Edge cases (derived from source)

  • Custom role created without isSystem:false becomes system-locked: schema default is isSystem: true (role.schema.ts:78-79) and updateRole/deleteRole refuse system roles (rbac.service.ts:94-95,104-105). The client must always send isSystem:false for custom roles (OQ-R2).
  • Duplicate role slug → 409 ConflictException (rbac.service.ts:86-87; unique index role.schema.ts:89).
  • PATCH /rbac/members/:id takes AddMemberDto (requires userId and roles) but the service ignores userId (rbac.controller.ts:71-72; rbac.service.ts:129-136).
  • addMember for an already-member user hits the unique index → Mongo duplicate-key error → 500 (no 409 mapping in http-exception.filter.ts:47-55); client treats as conflict (OQ-R5).
  • removeMember on unknown id is silent success (no 404; rbac.service.ts:138-140).
  • Permission set is not validated against ALL_PERMISSIONS server-side (create-role.dto.ts:34-37 @IsArray only) — the client matrix is the guard rail.
  • Roles in the JWT are minted at login (auth.service.ts:145-154) and carried unchanged by refresh (auth.service.ts:192-196) — role changes don't take effect until re-login; permission changes take effect ≤ 300 s via cache expiry (OQ-R4).
  • platform_admin has no permissions and is not org_admin — cannot access /rbac/* today; but it bypasses tenant scoping in repositories (base.repository.ts:21).

8. Success metrics (proposed)

  • Role creation → first member assigned in < 2 min (admin goal).
  • Permission matrix save round-trip < 1.5 s p95 (single PATCH, no per-cell writes).
  • Zero reported cross-tenant role/member reads (QA 14).
  • 100% of endpoints covered by @Roles/@Permissions after Phase-5 audit ((planned), docs/IMPLEMENTATION_PLAN.md:241) — today only webhooks/files/scheduler/ search controllers use @Permissions (see 12_API_Mapping.md §4).

9. PRD conflict (flagged)

  • PRD FR-AUTH-07 (PRODUCT_REQUIREMENTS_DOCUMENT.md:75): "RBAC enforces role + permission checks on every endpoint." Today the global guard runs everywhere, but only four controllers carry @Permissions (webhooks, files, scheduler, search; webhooks.controller.ts:22-86, files.controller.ts:30-67, scheduler.controller.ts:24-38, search.controller.ts:16); every other controller (students, fees, audit, …) is either ungated or role-gated only. The Phase-5 "Permissions audit (all endpoints)" (docs/IMPLEMENTATION_PLAN.md:241) is (planned).
  • PRD §8 (PRODUCT_REQUIREMENTS_DOCUMENT.md:144): native mobile apps are out of Phase-1 scope ("web-first"). Per global ledger 00-shared/12 A1, this package specs the full Flutter client anyway (owner decision, documented, not a contradiction).

10. Module assumptions

  • Client is forward-looking: backend is complete and authoritative; this package is the UI-side spec.
  • The permission matrix is rendered from the server (GET /rbac/permissions) — the client never hardcodes the 95 names as a second source of truth; the quoted list above is the current snapshot.
  • Membership ≠ account: adding a member requires an existing users doc (add-member.dto.ts:6 @IsMongoId); the members list does not join user profiles (rbac.service.ts:109-111) — display names require a parallel user.read fetch or the metadata field (OQ-R7).

11. Open questions (module; global ledger in 00-shared/12)

#ItemImpact
OQ-R1/rbac/* is class-gated @Roles('org_admin') (rbac.controller.ts:21) even though rbac.role.* / rbac.member.* permissions exist. Move to @Permissions('rbac.role.read') style so custom roles can manage RBAC?Role-manager persona (02), matrix gating
OQ-R2isSystem defaults true on create (role.schema.ts:78-79) — silent lock-in of custom roles. Server default → false for non-seeded creates?Create-role form, QA privilege-escalation tests
OQ-R3MemberStatus.INVITED/SUSPENDED defined but never produced (organization-member.schema.ts:7-11; service hardcodes ACTIVE rbac.service.ts:122). Invite/suspend flows (planned)?Member lifecycle UX
OQ-R4JWT roles claim frozen at login; refresh copies old claim (auth.service.ts:192-196). Role change forces re-login. Server-side claim refresh?13 route rebuild, session UX
OQ-R5Duplicate member → 500 (Mongo E11000) instead of 409 DUPLICATE_RESOURCE. Map in filter?Add-member error UX
OQ-R6organizations.service.ts:58 seeds defaults with org.slug as tenantId (vs auth.service.ts:93 using user.tenantId) — tenantId semantics inconsistencyTenant-scope tests
OQ-R7Members list has no user profile join; avatar/name/email require user.read calls. Join endpoint or client-side merge (planned)?Members list spec

12. Glossary (module)

TermMeaning
RoleNamed permission set, tenant-scoped; slug unique per tenant; system roles immutable
Permissiondomain.action string (95 total); domain ≈ owning module
Membershiporganization_members doc tying userId to roles within a tenant
PriorityInt; higher = listed first and "more powerful" by convention
Permission cacheRedis sl:{tenantId}:perm:{userId} TTL 300 s (rbac.service.ts:48,68)
403 PERMISSION_DENIEDEnvelope code for role/permission refusal (http-exception.filter.ts:30)
System roleisSystem: true; cannot be edited/deleted (rbac.service.ts:94-95,104-105)