01 — Product Overview (RBAC Module)
- 1. Purpose
- 2. The guard pipeline (derived, exact)
- 3. Domain model (derived)
- 4. Default roles (seeded, immutable)
- 5. Permission catalog (complete, quoted from source)
- 6. Business goals
- 7. Edge cases (derived from source)
- 8. Success metrics (proposed)
- 9. PRD conflict (flagged)
- 10. Module assumptions
- 11. Open questions (module; global ledger in
00-shared/12) - 12. Glossary (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/**, andsrc/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:
| Responsibility | Source |
|---|---|
| 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 bootstrap | auth.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, setsreq.user = { id, tenantId, roles }, and populates theTenantContextService(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 cachesl:{tenantId}:perm:{userId}with 300 s TTL, then falls back to the DB (rbac.service.ts:44-73); - failure →
ForbiddenException→ 403PERMISSION_DENIED(http-exception.filter.ts:30).
- no metadata → allow (
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 createsactive(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).
| Name | Slug | Priority | Permissions | Locked |
|---|---|---|---|---|
| Platform Admin | platform_admin | 1000 | [] (cross-tenant; bypasses tenant scope via base.repository.ts:21) | system |
| Organization Admin | org_admin | 100 | all 95 | system |
| Teacher | teacher | 50 | student.read, attendance.mark, attendance.edit | system |
| Staff | staff | 40 | student.read | system |
| Accountant | accountant | 30 | fees.collect, student.read | system |
| Parent | parent | 20 | student.read | system |
| Student | student | 10 | [] | 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-97 — the 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
| Goal | Measure |
|---|---|
| Least-privilege out of the box | 7 seeded roles cover school staff; custom roles are tenant-scoped |
| Enforcement everywhere | Global RbacGuard on all routes; @Roles/@Permissions decorators (roles.decorator.ts:3, permissions.decorator.ts:3) |
| Fast enforcement | Permission resolution cached 300 s in Redis (rbac.service.ts:68); JWT roles are claim-carried (no DB hit on role checks) |
| Tenant isolation | tenantId injected in every query; unique {tenantId, slug} / {tenantId, userId} (role.schema.ts:89, organization-member.schema.ts:48) |
| Auditability | RBAC writes surface in audit-logs (Phase-5 permission audit (planned), docs/IMPLEMENTATION_PLAN.md:241; audit.read perm exists) |
| No accidental lockout of admins | system roles immutable; org_admin can never be deleted/edited |
7. Edge cases (derived from source)
- Custom role created without
isSystem:falsebecomes system-locked: schema default isisSystem: true(role.schema.ts:78-79) andupdateRole/deleteRolerefuse system roles (rbac.service.ts:94-95,104-105). The client must always sendisSystem:falsefor custom roles (OQ-R2). - Duplicate role slug → 409
ConflictException(rbac.service.ts:86-87; unique indexrole.schema.ts:89). PATCH /rbac/members/:idtakesAddMemberDto(requiresuserIdandroles) but the service ignoresuserId(rbac.controller.ts:71-72;rbac.service.ts:129-136).addMemberfor an already-member user hits the unique index → Mongo duplicate-key error → 500 (no 409 mapping inhttp-exception.filter.ts:47-55); client treats as conflict (OQ-R5).removeMemberon unknown id is silent success (no 404;rbac.service.ts:138-140).- Permission set is not validated against
ALL_PERMISSIONSserver-side (create-role.dto.ts:34-37@IsArrayonly) — 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_adminhas no permissions and is notorg_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/@Permissionsafter Phase-5 audit ((planned),docs/IMPLEMENTATION_PLAN.md:241) — today only webhooks/files/scheduler/ search controllers use@Permissions(see12_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 ledger00-shared/12A1, 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
usersdoc (add-member.dto.ts:6@IsMongoId); the members list does not join user profiles (rbac.service.ts:109-111) — display names require a paralleluser.readfetch or themetadatafield (OQ-R7).
11. Open questions (module; global ledger in 00-shared/12)
| # | Item | Impact |
|---|---|---|
| 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-R2 | isSystem 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-R3 | MemberStatus.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-R4 | JWT 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-R5 | Duplicate member → 500 (Mongo E11000) instead of 409 DUPLICATE_RESOURCE. Map in filter? | Add-member error UX |
| OQ-R6 | organizations.service.ts:58 seeds defaults with org.slug as tenantId (vs auth.service.ts:93 using user.tenantId) — tenantId semantics inconsistency | Tenant-scope tests |
| OQ-R7 | Members 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)
| Term | Meaning |
|---|---|
| Role | Named permission set, tenant-scoped; slug unique per tenant; system roles immutable |
| Permission | domain.action string (95 total); domain ≈ owning module |
| Membership | organization_members doc tying userId to roles within a tenant |
| Priority | Int; higher = listed first and "more powerful" by convention |
| Permission cache | Redis sl:{tenantId}:perm:{userId} TTL 300 s (rbac.service.ts:48,68) |
403 PERMISSION_DENIED | Envelope code for role/permission refusal (http-exception.filter.ts:30) |
| System role | isSystem: true; cannot be edited/deleted (rbac.service.ts:94-95,104-105) |