RBAC matrix
RBAC / permission matrix
Section titled “RBAC / permission matrix”Audience: product, backend
Status: specced
Owns: product + backend
Depends on: CAP personas, LMS personas, System architecture, Architecture handover
Most permissions are relationship-based, not purely role-based: a centre staff member may only act on applications belonging to the acting centre (X-CAP-CENTRE-ID); an assessor isn’t inherently “an IV” — they become one for a specific application via an active assignment. Authorization is two layers; both must pass. Dual-hat accounts (candidate + centre staff) send X-CAP-PERSONA; layer-1 permissions are narrowed to that hat (not unioned).
Two-layer model
Section titled “Two-layer model”- Role gate (RBAC) — coarse: can this acting hat ever attempt this action type (
X-CAP-PERSONA; JWT is identity only). - Scope check (relationship-based) — fine-grained, requires the loaded resource: is this specific instance in this actor’s scope. Queries assignment tables (
facilitator_assignments,panel_members,iv_assignments,ev_assignments,assessor_assignments) — no separate permissions table to keep in sync.
requirePermission("application.review") // layer 1 — middleware, cheap, JWT claims only → handler loads the application (needed anyway) → assertScope(actor, application) // layer 2 — service layer, not scattered inline checksassertScope lives in the service layer (needs the loaded resource). Layer-1 failures map to UnauthorizedError; layer-2 to ScopeError (see @yourorg/common).
Identity roles vs contextual roles
Section titled “Identity roles vs contextual roles”| Kind | Roles | Where stored |
|---|---|---|
| Identity (memberships on the account) | Candidate; Assessor; Centre Staff (super_admin / regular_admin / staff per centre); Awarding Body Staff; Platform Staff |
Account / CAP persona tables. Candidate+centre staff is allowed. One OL user may have many CentreStaff rows. Assessors are not CentreStaff. |
| Contextual (assignment-derived) | Facilitator; Panelist; Lead Panelist; IV/IQA; EV/EQA; UNIT_ASSESSOR (NSQ) | Active rows in assignment tables; revoked when the assignment ends |
An Assessor holds qualifications (QAA, IQM, IV, EV) from onboarding; a contextual role is granted when a centre/awarding body assigns them to a specific application. Marketplace eligibility for job postings uses declared sector experience (AssessorSectorExperience), not qualification-type filters on the posting (§27).
Detailed journeys: CAP personas.
Own-resource rule (universal)
Section titled “Own-resource rule (universal)”Any authenticated user may save/submit/view only their own onboarding record, notifications (inbox + channel preferences), personal profile (GET/PATCH /me/profile), and payment history — no role overrides this for another user’s own-resource data. Password change and account deletion are Orchestrator own-resource routes. Platform staff view aggregate data through separate admin endpoints.
Applications
Section titled “Applications”| Action | Candidate | Assessor (contextual) | Centre Staff | Awarding Body Staff | Platform Staff |
|---|---|---|---|---|---|
| Create / submit application | own only | — | — | — | — |
| View application | own only | scoped to active assignment | scoped to own centre | scoped to forwarded applications | all |
| Review at reviewer stage | — | — | scoped to own centre | — | — |
| Assign facilitator / panel / IV | — | — | super_admin, regular_admin, own centre |
— | — |
| Act as facilitator / panelist / IV | — | only if actively assigned | — | — | — |
| Schedule/reschedule interview | — | — | scoped to own centre | — | — |
| Submit appeal | own application only | — | — | — | — |
| Resolve appeal | — | only if lead panelist for this interview | — | — | — |
| Forward to awarding body | — | — | super_admin, regular_admin, own centre |
— | — |
| Assign EV | — | — | — | scoped to received applications | — |
| Act as EV | — | only if actively assigned as EV | — | — | — |
| Issue certificate | — | — | — | scoped to received applications | — |
Centre management
Section titled “Centre management”| Action | Centre super_admin |
Centre regular_admin |
Centre staff |
Platform Staff |
|---|---|---|---|---|
| Add/remove centre staff | yes (acting centre) | — | — | — |
| View/set centre financials, payments history & pricing | yes (centre.wallet.read / centre.pricing.*) |
— (explicitly excluded) | — | — |
| View centre org profile (status, logo) | yes | yes | yes | — |
| Update centre logo / org profile | yes (centre.profile.write) |
— | — | — |
| View centre dashboard (non-revenue KPIs / charts / activity) | yes | yes | yes | — |
| View centre revenue on dashboard | yes (centre.wallet.read) |
— | — | — |
| Search centre directory (v1 own centre) | yes | yes | yes | deferred (platform-wide) |
| Conversations (create / list / reply) | own threads (OL) | own threads (OL) | own threads (OL) | own threads (OL); any authenticated user |
| Withdraw wallet funds | yes (ledger debit, not a bank payout) | — | — | — |
| Personal profile / notification channels / password / delete | own (CAP /me/profile; OL prefs + delete-account). Last centre super_admin is warned via GET /me/deletion-eligibility |
same (own) | same (own) | same (own) |
| Create job postings | yes | yes | — | — |
| Close / delete / bulk-close job postings; shortlist or reject applicants | yes | yes | — | — |
| Approve/reject incoming retained-assessor requests (incl. bulk) | yes | yes | — | — |
| Revoke an approved retained relationship | yes | yes | — | — |
List/summary/detail retained roster (GET /centre/assessors*) |
yes | yes | yes | — |
| Approve/reject/suspend centre | — | — | — | yes |
Assessor management, platform admin, payments/audit
Section titled “Assessor management, platform admin, payments/audit”| Action | Assessor | Awarding Body Staff | Platform Staff |
|---|---|---|---|
| Approve/reject/suspend assessor account | — | — | yes |
| Submit a retained-assessor request to a centre | own only | — | — |
| View/update own sector experience | own only | — | — |
| Manage sectors/trades/units/NOS documents | — | — | yes |
| Manage policies, pricing minimums, terms | — | — | yes |
| View platform-wide revenue | — | — | yes |
| View/query security audit logs | — | — | yes (this access is itself logged) |
Scope-check reference (layer 2)
Section titled “Scope-check reference (layer 2)”| Resource | Scope rule |
|---|---|
| Application | Acting candidate: own applications. Acting centre: application.centreId === X-CAP-CENTRE-ID. Assessor: active assignment row. Awarding body: forwarded to that AB |
| Centre | CentreStaff row for (actor, X-CAP-CENTRE-ID); tier on that row determines read vs write |
| Job posting | Centre staff scoped to own centre’s postings (centre.job_posting.read/write). Close only from open/filled. Assessor apply/list gated by approved status + sector match; marketplace hides non-open |
| Retained assessor request | Assessor own rows; centre staff scoped to own centre’s incoming requests |
| Notification | actor.userId === notification.userId, always (prefs included) |
| Me profile | own CapUser only; /me/* skips acting-centre headers |
| Conversation / Message | caller is a participant (or creator) on that conversation; always own-resource |
| Directory (v1) | centre staff: actor.centreId matches; kinds limited to that centre’s staff, candidates with apps at the centre, approved retained assessors |
| Payment | actor.userId === payment.userId (candidate pay) or centre staff of application.centreId (quote / receipt view). Initiate pay stays candidate-owned |
Same two-layer model. Staff capabilities are a superset of instructor capabilities (broader scope). Multi-role accounts: gate on the persona’s completed onboarding + capability, not a single CAP-style persona.
| Action | Learner | Instructor (owner) |
Co-instructor | Staff support |
Staff content_manager |
Staff admin |
|---|---|---|---|---|---|---|
| Complete own onboarding | own persona | own persona | own persona | own persona | own persona | own persona |
| Browse published catalogue | yes | yes | yes | yes | yes | yes |
| Consume player / assessments | own + active entitlement | — | — | read scoped | read scoped | all |
| Create/edit course content | — | assigned courses | assigned courses | — | all courses | all courses |
| Publish / unpublish | — | own courses | — | — | all courses | all courses |
| Manage co-instructors | — | own courses | — | — | all | all |
| Invite instructors | — | — | — | — | — | yes |
| Invite / manage staff | — | — | — | — | — | yes |
Grant entitlement (admin_grant / sponsor / cap_recommendation) |
— | — | — | yes | — | yes |
| Checkout self_pay | own enrollment | — | — | — | — | — |
LMS layer-2 scope:
| Resource | Scope rule |
|---|---|
| Onboarding | own lmsUserId + persona only |
| Course (authoring) | course_instructors row or staff content/admin |
| Enrollment / progress | learner own row or instructor assigned to that course or staff |
| Entitlement grant | staff support / admin |
| Certificate | learner own or instructor/staff with course scope |
Open product decisions (not blockers)
Section titled “Open product decisions (not blockers)”From the architecture handover:
- Whether Awarding Body Staff and Platform Staff need role tiering the way Centre Staff does — currently modeled as single roles each.
- Job Posting Marketplace visibility scope — default is eligibility-gated (approved assessor + sector experience match); broader public discovery was flagged as revisitable.
Postgres RLS is a possible second line later for sensitive assessment data — decide when implementing authz, not ad hoc.