Skip to content

RBAC 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).

  1. Role gate (RBAC) — coarse: can this acting hat ever attempt this action type (X-CAP-PERSONA; JWT is identity only).
  2. 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 checks

assertScope lives in the service layer (needs the loaded resource). Layer-1 failures map to UnauthorizedError; layer-2 to ScopeError (see @yourorg/common).

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.

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.

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
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)
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

From the architecture handover:

  1. Whether Awarding Body Staff and Platform Staff need role tiering the way Centre Staff does — currently modeled as single roles each.
  2. 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.