Authorization reference — roles, permissions, gates
For adminsAuthorization reference — roles, permissions, gates
Section titled “Authorization reference — roles, permissions, gates”This is a code-grounded reference for the authorization model. For the conceptual intro to users, roles and departments — start with Users & roles first; come back here when you need to know exactly which permission gates which route.
Every fact below is derived from the running source under app/Modules/Identity/Permissions/ and the per-module routes.php files. If something doesn’t match what the app actually does, the doc is wrong — file paths are quoted throughout so you can verify.
Overview
Section titled “Overview”- Library — Spatie
spatie/laravel-permissionv6, configured inconfig/permission.php. - Workspace scoping —
teamsmode is on, with a customKeystoneTeamResolver(app/Modules/Identity/Permissions/KeystoneTeamResolver.php) that mirrors the active workspace. Roles and permissions are global templates (rows haveworkspace_id = NULL); the assignments (model_has_roles,model_has_permissions) are per-workspace. - Naming —
<scope>::<area>[::subarea]. Scope is either a product slug (helpdesk,assets,estates,people,scr,governance,hs) orplatformfor cross-product admin concerns. The bareadminpermission is the super-admin escape hatch. - Single source of truth —
app/Modules/Identity/Permissions/PermissionCatalogue.php. The seeder, the admin UI and this document all read from it. - Seeding —
database/seeders/RolePermissionSeeder.php. Idempotent; safe on every deploy. It syncs roles via$role->syncPermissions($perms)so removing a permission from the catalogue strips it from existing seeded roles.
Ten system roles are seeded from PermissionCatalogue::roles():
| Role | Product | Intended persona | Notes |
|---|---|---|---|
admin | Cross-cutting | Trust / platform super-admin | Holds the bare admin permission; passes every gate via Gate::before. |
helpdesk::agent | Helpdesk | IT / ops agent working tickets, changes, projects, problems | |
helpdesk::requester | Helpdesk | Default role for any logged-in staff member | Also carries governance::policy::acknowledge so anyone can ack a published policy. |
assets::agent | Assets | IT asset coordinator | Discovery configuration is admin-only — see carve-outs. |
estates::agent | Estates | Site / facilities manager | Contractors are admin-only — see carve-outs. |
people::agent | People | HR staff managing employee records | Confidential records / NI numbers need an explicit grant. |
scr::dsl | SCR | Designated Safeguarding Lead | Named for the persona rather than scr::agent. Includes people::agent so a DSL can look up subjects. |
governance::officer | Governance | Governance officer / clerk | Risk, compliance, policy, actions, evidence. |
hs::lead | Health & Safety | Deputy head or dedicated H&S officer | Can publish risk assessments and manage checklist templates. |
hs::operative | Health & Safety | Cleaner / caretaker | Only ever runs checklists — never sees the RA library. |
In addition to the seeded set, Platform admin → Roles lets you create custom trust-specific roles. Custom roles can hold any combination of catalogue permissions. The 10 above are flagged is_system: true — you can edit their permission set but you can’t rename or delete them.
Permissions
Section titled “Permissions”63 permissions live in PermissionCatalogue::all(). Each one is shown alongside the UI grouping label that the admin Roles editor uses.
Cross-cutting
Section titled “Cross-cutting”| Permission | Grouping | What it does |
|---|---|---|
admin | Cross-cutting | Super-admin escape hatch. Wired in IdentityServiceProvider::boot() via Gate::before — anyone holding it implicitly passes every subsequent gate. |
Platform — cross-product admin
Section titled “Platform — cross-product admin”| Permission | Grouping | What it gates |
|---|---|---|
platform::users::manage | Platform / Users & roles | /admin/users, /admin/roles, /admin/departments. |
platform::workspaces::manage | Platform / Workspaces | /admin/workspaces CRUD. |
platform::settings::manage | Platform / Settings | Modules toggle, branding, system settings, Wonde integration, status page admin, mail config (with demo.readonly:mail), licence admin. |
platform::audit::view | Platform / Audit log | /admin/audit, DSAR builder. |
platform::schools::manage | Platform / Schools | Multi-school / trust school configuration. |
platform::auth::manage | Platform / Authentication | SSO / authentication provider configuration. |
platform::ai::manage | Platform / AI settings | LLM provider configuration. |
platform::ai::generate | Platform / AI insights | Run AI insight generation endpoints. |
platform::imports::run | Platform / Imports | /admin/imports — execute import jobs (controllers abort_unless on a per-importer permission too). |
Helpdesk
Section titled “Helpdesk”| Permission | Grouping | What it gates |
|---|---|---|
helpdesk::admin | Helpdesk / Admin | Inline-gated admin pages within the Agent route group (custom reports config, change templates, project templates, etc.). The Helpdesk admin shell was merged into /agent/manage in 2026-05. |
helpdesk::agent | Helpdesk / Agent | Entry to /agent via EnsureAgent middleware. |
helpdesk::requester | Helpdesk / Requester | Marker for the requester persona. |
helpdesk::tickets::manage | Helpdesk / Tickets | Custom report builder (can:helpdesk::tickets::manage). |
helpdesk::departments::manage | Helpdesk / Departments | Department CRUD. |
helpdesk::catalog::manage | Helpdesk / Catalog | Service catalog management. |
helpdesk::solutions::manage | Helpdesk / Solutions | Knowledge base management. |
helpdesk::sla::manage | Helpdesk / SLA | SLA policy configuration. |
helpdesk::automation::manage | Helpdesk / Automation | Workflow automation rules. |
helpdesk::change::manage | Helpdesk / Change | Change templates + change management. |
helpdesk::project::manage | Helpdesk / Project | Project templates + project management. |
helpdesk::problem::manage | Helpdesk / Problem | Problem management. |
helpdesk::filters::share | Helpdesk / Share saved filters | Share saved ticket filters with the team. |
helpdesk::webhooks::manage | Helpdesk / Outbound webhooks | Outbound webhook configuration. |
Assets
Section titled “Assets”| Permission | Grouping | What it gates |
|---|---|---|
assets::admin | Assets / Admin | Asset categories, discovery agents, advanced configuration. |
assets::agent | Assets / Agent | Entry to /agent/assets. |
assets::registry::manage | Assets / Registry | Asset registry CRUD. |
assets::licensing::manage | Assets / Licensing | Licence tracking. |
assets::discovery::manage | Assets / Discovery | Inventory discovery daemon configuration (admin-only by convention; see carve-out). |
assets::stocktake::manage | Assets / Stocktake | Stocktake audits. |
Estates
Section titled “Estates”| Permission | Grouping | What it gates |
|---|---|---|
estates::admin | Estates / Admin | Contractor management, advanced compliance config. |
estates::agent | Estates / Agent | Entry to /agent/estates. |
estates::places::manage | Estates / Places | Facility places and room management. |
estates::compliance::manage | Estates / Compliance | Compliance and maintenance records. |
estates::contractors::manage | Estates / Contractors | PII-sensitive contractor records (admin-only — see carve-out). |
estates::bookings::manage | Estates / Bookings | Room / facility bookings. |
People
Section titled “People”| Permission | Grouping | What it gates |
|---|---|---|
people::admin | People / Admin | People catalogues (/agent/people/... admin pages, gated by EnsurePermission::class.':people::admin'). |
people::agent | People / Agent | Entry to /agent/people. |
people::records::manage | People / Records | Employee records and engagements. |
people::engagements::manage | People / Engagements | Employment engagement lifecycle. |
people::confidential::view | People / Confidential records | Access flagged-confidential HR notes — explicit grant required, even for HR agents. |
people::ni::view | People / NI number access | View the NI-number field (subset of confidential). |
people::erase | People / GDPR erasure | GDPR erasure requests. |
people::export | People / GDPR export | GDPR export requests. |
| Permission | Grouping | What it gates |
|---|---|---|
scr::admin | SCR / Admin | SCR admin surfaces. |
scr::agent | SCR / Agent | Entry to /agent/scr. |
scr::register::manage | SCR / Single Central Record | SCR + check-type management (can:scr::register::manage). |
Governance
Section titled “Governance”| Permission | Grouping | What it gates |
|---|---|---|
governance::admin | Governance / Admin | DfE Handbook install, governance admin tools (many can:governance::admin route groups inside the Agent rail). |
governance::agent | Governance / Agent | Entry to /agent/governance. |
governance::risk::manage | Governance / Risk register | Risk register and mitigations. |
governance::compliance::manage | Governance / Compliance | Compliance framework control assessment. |
governance::policy::manage | Governance / Policy | Policy authoring; review-reminder console command also ->filter(fn ($u) => $u->can('governance::policy::manage')). |
governance::policy::approve | Governance / Approve published policies | Approve + publish policies. |
governance::policy::acknowledge | Governance / Acknowledge policies | Wider permission — granted to helpdesk::requester so any logged-in staff can acknowledge. Gates /me/policies via EnsurePermission::class.':governance::policy::acknowledge'. |
governance::actions::manage | Governance / Actions | Action register. |
governance::evidence::view | Governance / View internal evidence | Standard internal evidence access. |
governance::evidence::view::restricted | Governance / View restricted evidence | Restricted-classification evidence (e.g. board-only documents). |
governance::evidence::upload | Governance / Upload evidence | Evidence uploads. |
Health & Safety
Section titled “Health & Safety”| Permission | Grouping | What it gates |
|---|---|---|
hs::admin | Health & Safety / Admin | Accident book mutations + most H&S admin sub-features (many can:hs::admin route groups in HealthSafety/Agent/routes.php). |
hs::agent | Health & Safety / Agent | Entry to /agent/hs. |
hs::risk_assessments::manage | Health & Safety / Risk assessments | RA publish / archive (can:hs::risk_assessments::manage). |
hs::checklists::manage | Health & Safety / Checklists | Checklist template management. |
hs::checklists::execute | Health & Safety / Run checklists | Submit checklist runs. |
Role → permission mapping
Section titled “Role → permission mapping”Verbatim from PermissionCatalogue::roles(). Anything not in this list is a deliberate omission documented in the source.
admin—admin. That’s all; everything else falls out ofGate::before.helpdesk::agent—helpdesk::agent,helpdesk::tickets::manage,helpdesk::change::manage,helpdesk::project::manage,helpdesk::problem::manage.helpdesk::requester—helpdesk::requester,governance::policy::acknowledge.assets::agent—assets::agent,assets::registry::manage,assets::licensing::manage,assets::stocktake::manage.estates::agent—estates::agent,estates::places::manage,estates::compliance::manage,estates::bookings::manage.people::agent—people::agent,people::records::manage,people::engagements::manage.scr::dsl—scr::agent,scr::register::manage,people::agent,people::records::manage.governance::officer—governance::agent,governance::risk::manage,governance::compliance::manage,governance::policy::manage,governance::actions::manage,governance::evidence::view,governance::evidence::upload.hs::lead—hs::agent,hs::risk_assessments::manage,hs::checklists::manage,hs::checklists::execute.hs::operative—hs::agent,hs::checklists::execute.
Deliberate carve-outs
Section titled “Deliberate carve-outs”assets::discovery::manageis not onassets::agent. Discovery daemon configuration is admin-only — agents see results but don’t wire up new agents.estates::contractors::manageis not onestates::agent. Contractor records contain insurance + PII docs, so they sit behindestates::admin.people::confidential::viewis not onpeople::agent. Even HR agents need an explicit grant to read flagged-confidential notes.people::ni::viewis a subset for the NI-number field specifically.governance::evidence::view::restrictedis not ongovernance::officer. Restricted-classification evidence is granted per-user.governance::policy::approveis not ongovernance::officer. Onlyadmin(viaGate::before) currently passes it.helpdesk::requestercarriesgovernance::policy::acknowledge— broader than the other role badges, by design.scr::dslincludespeople::agent+people::records::manageso DSLs can look up subjects, but notpeople::confidential::view.
Catalogue permissions that no seeded role holds
Section titled “Catalogue permissions that no seeded role holds”These exist in PermissionCatalogue::all() but aren’t on any role in ::roles(). They’re reached today only via the admin super-power or explicit per-user grant:
- All
platform::*(9 permissions). Platform admins are individual users granted the specific permissions they need. helpdesk::admin,helpdesk::departments::manage,helpdesk::catalog::manage,helpdesk::solutions::manage,helpdesk::sla::manage,helpdesk::automation::manage,helpdesk::filters::share,helpdesk::webhooks::manage.assets::admin,assets::discovery::manage.estates::admin,estates::contractors::manage.people::admin,people::confidential::view,people::ni::view,people::erase,people::export.scr::admin.governance::admin,governance::policy::approve,governance::evidence::view::restricted.hs::admin.
That’s expected — the catalogue is intentionally wider than the default role set so Platform admin → Roles can compose custom roles.
Five mechanisms enforce permissions across the codebase.
1. Gate::before super-admin short-circuit
Section titled “1. Gate::before super-admin short-circuit”app/Modules/Identity/IdentityServiceProvider.php lines 95–108:
Gate::before(function (?User $user) { if (! $user) { return null; } try { if ($user->hasPermissionTo('admin')) { return true; } } catch (PermissionDoesNotExist) { // Permissions not seeded — fall through to other gate handlers. } return null;});Anyone with the bare admin permission passes every subsequent gate — no per-feature exceptions, no overrides. This is how the admin role reaches helpdesk::admin, platform::*, governance::policy::approve and every other carve-out without holding them explicitly.
2. EnsurePermission middleware
Section titled “2. EnsurePermission middleware”app/Modules/Identity/Http/Middleware/EnsurePermission.php. Used as EnsurePermission::class.':permission.name'. Calls Gate::allows($permission); on denial, fires the AuthorizationDenied event (with permission name, path, method, IP, user-agent) and abort(403).
Applied to product entry rails:
| Route prefix | Middleware | Source |
|---|---|---|
/agent (Helpdesk) | EnsureAgent::class → checks helpdesk::agent | app/Modules/Helpdesk/Agent/routes.php:23 |
/agent/assets | EnsurePermission::class.':assets::agent' | app/Modules/Assets/Agent/routes.php:15 |
/agent/estates | EnsurePermission::class.':estates::agent' | app/Modules/Estates/Agent/routes.php:21 |
/agent/people (admin pages) | EnsurePermission::class.':people::admin' | app/Modules/People/Agent/routes.php:13 |
/agent/people | EnsurePermission::class.':people::agent' | app/Modules/People/Agent/routes.php:37 |
/agent/scr | EnsurePermission::class.':scr::agent' | app/Modules/Scr/Agent/routes.php:9 |
/agent/governance | EnsurePermission::class.':governance::agent' | app/Modules/Governance/Agent/routes.php:19 |
/agent/hs | EnsurePermission::class.':hs::agent' | app/Modules/HealthSafety/Agent/routes.php:16 |
/me (governance portal) | EnsurePermission::class.':governance::policy::acknowledge' | app/Modules/Governance/Portal/routes.php:9 |
3. EnsurePlatformAccess middleware
Section titled “3. EnsurePlatformAccess middleware”app/Modules/Platform/Admin/Http/Middleware/EnsurePlatformAccess.php. Gates the /admin shell. It’s a loose OR check over six platform permissions:
private const PLATFORM_PERMISSIONS = [ 'platform::users::manage', 'platform::workspaces::manage', 'platform::settings::manage', 'platform::audit::view', 'platform::schools::manage', 'platform::auth::manage',];If you hold any one of them you can enter /admin; per-page gates take over from there. The admin super-power also passes via Gate::before.
The full /admin route group also requires mfa.required-for-admins and the ProvideAdminNavigation middleware (app/Modules/Platform/Admin/routes.php:25).
4. Per-route can:permission.name middleware
Section titled “4. Per-route can:permission.name middleware”Standard Laravel gate middleware. The notable applications:
| Permission | Routes (file:line) |
|---|---|
helpdesk::tickets::manage | Helpdesk/Agent/routes.php:119 (custom reports) |
helpdesk::admin | Helpdesk/Agent/routes.php:134 (admin pages inline within /agent) |
helpdesk::change::manage | Helpdesk/Agent/routes.php:148 |
helpdesk::project::manage | Helpdesk/Agent/routes.php:170 |
helpdesk::catalog::manage | Helpdesk/Agent/routes.php:247 |
helpdesk::solutions::manage | Helpdesk/Agent/routes.php:284 |
assets::registry::manage | Assets/Agent/routes.php:20 |
assets::admin | Assets/Agent/routes.php:34, 47, 60, 76, 87 |
scr::register::manage | Scr/Agent/routes.php:33 |
governance::admin | Governance/Agent/routes.php:37, 57, 72, 90, 110, 157, 172, 187, 207 |
hs::risk_assessments::manage | HealthSafety/Agent/routes.php:21 |
hs::admin | HealthSafety/Agent/routes.php:36, 52, 68, 103, 126, 138, 153 |
platform::users::manage | Platform/Admin/routes.php:58, 62, 66, 70 |
platform::workspaces::manage | Platform/Admin/routes.php:81–96 |
platform::settings::manage | Platform/Admin/routes.php:39, 101–107, 186–235; Integrations/Wonde/routes.php:8; Core/Status/routes.php:35 |
platform::audit::view | Platform/Admin/routes.php:50, 147, 150 |
platform::schools::manage | Platform/Admin/routes.php:112–142 |
platform::auth::manage | Platform/Admin/routes.php:155–181 |
platform::ai::generate | Core/Ai/routes.php:24, 29, 34, 39 |
platform::ai::manage | Core/Ai/routes.php:48, 52, 56 |
platform::imports::run | Core/Imports/routes.php:15 |
5. Controller-level checks
Section titled “5. Controller-level checks”A handful of controllers check inline rather than (or in addition to) route middleware:
app/Modules/Core/Imports/Http/Controllers/ImportsController.php:103—abort_unless($request->user()?->can($importer->permission()), 403)— each importer carries its own permission slug.app/Modules/Helpdesk/Tickets/Channels/TicketPresenceChannel.php—$user->can('helpdesk::agent')to decide presence-channel access.app/Modules/Helpdesk/Agent/Http/Controllers/TicketsController.php:131—abort_unless(request()->user()->canAccessDepartment($ticket->department_id), 403)— department scoping is enforced inline (see Department scoping below).app/Modules/Helpdesk/Admin/Http/Controllers/CustomReportsAdminController.php:79— additional gating on top ofcan:helpdesk::tickets::manage.app/Modules/Estates/Agent/Http/Controllers/LoneWorkController.php—$request->user()->can('estates::admin').app/Modules/Governance/Console/SendPolicyReviewReminders.php:77— recipient filter->filter(fn (User $u) => $u->can('governance::policy::manage')).app/Modules/HealthSafety/Console/Commands/SendRiskAssessmentReviewReminders.php:69— recipient filter onhs::admin.
Policies
Section titled “Policies”There is no app/Policies directory in use. Authorization is entirely permission/gate-based today. If you add an authorize() call in a controller, it’ll resolve through Gate::allows — which routes back to Spatie’s permission check (with the admin short-circuit).
Access map — what each role reaches
Section titled “Access map — what each role reaches”Each role’s authenticated routes. “Reaches” here means the gate lets you in; you may still be scoped by workspace, school pinning, or department membership.
/admin(Platform shell) — every section, viaGate::before.- Every product agent rail (
/agent,/agent/assets,/agent/estates,/agent/people,/agent/scr,/agent/governance,/agent/hs). - Every per-feature
can:*gate, by virtue of holdingadmin. - Console commands and AI endpoints under
platform::ai::*.
helpdesk::agent
Section titled “helpdesk::agent”/agent— full Helpdesk agent rail.- Tickets, custom report builder (
helpdesk::tickets::manage). - Changes (
helpdesk::change::manage), Projects (helpdesk::project::manage), Problems (helpdesk::problem::manage). - Does not reach:
helpdesk::adminpages (KB / catalog / SLA / automation / departments / webhooks / filter sharing / mail / reports config) — those are inlinecan:helpdesk::admingroups within/agentand require the admin permission or theadminsuper-role. - Department scoping (see internals) further narrows what tickets they actually see.
helpdesk::requester
Section titled “helpdesk::requester”/tickets,/help,/catalog,/me(the portal — Helpdesk self-service)./me/policies— policy acknowledgement (viagovernance::policy::acknowledge).- Does not reach: any
/agentor/adminrail.
assets::agent
Section titled “assets::agent”/agent/assets— IT asset rail.- Registry CRUD + categories list (
assets::registry::manage). - Licence tracking (
assets::licensing::manage). - Stocktake (
assets::stocktake::manage). - Does not reach: discovery agent config, asset categories admin, or anything else inside the five
can:assets::adminroute groups inAssets/Agent/routes.php.
estates::agent
Section titled “estates::agent”/agent/estates— Estates rail.- Places (
estates::places::manage), Compliance (estates::compliance::manage), Bookings (estates::bookings::manage). - Does not reach: contractor records (admin-only, PII), lone-work admin (
LoneWorkControllerchecksestates::admindirectly).
people::agent
Section titled “people::agent”/agent/people— People rail.- Employee records (
people::records::manage), engagements (people::engagements::manage). - Does not reach:
/agent/people/...catalogues (requirepeople::admin), confidential records, NI-number fields, GDPR erasure/export, orpeople::adminpages.
scr::dsl
Section titled “scr::dsl”/agent/scr— SCR rail.- SCR + check types (
scr::register::manage). /agent/peoplerail and employee records — granted explicitly so a DSL can look up subjects (people::agent+people::records::manage).- Does not reach: SCR admin surfaces (
scr::admin),people::confidential::view/people::ni::view, GDPR controls.
governance::officer
Section titled “governance::officer”/agent/governance— Governance rail entry.- Risk register (
governance::risk::manage), Compliance (governance::compliance::manage), Policy authoring (governance::policy::manage), Actions (governance::actions::manage). - Evidence view (
governance::evidence::view) + upload (governance::evidence::upload). - Does not reach: the many
can:governance::adminsub-route-groups insideGovernance/Agent/routes.php(lines 37–207), policy approval (governance::policy::approve), restricted evidence (governance::evidence::view::restricted). These need explicit grants oradmin.
hs::lead
Section titled “hs::lead”/agent/hs— Health & Safety rail.- RA publish / archive (
hs::risk_assessments::manage). - Checklist templates (
hs::checklists::manage) and execution (hs::checklists::execute). - Does not reach: the seven
can:hs::adminroute groups (accident book mutations, etc.) — needshs::adminoradmin.
hs::operative
Section titled “hs::operative”/agent/hs— entry only, restricted view.- Execute checklists (
hs::checklists::execute). - Does not reach: RA library, RA management, checklist template management, or any
hs::adminpage.
Auth internals
Section titled “Auth internals”Workspace scoping
Section titled “Workspace scoping”Custom resolver: app/Modules/Identity/Permissions/KeystoneTeamResolver.php. Mirrors WorkspaceContext rather than using Spatie’s DefaultTeamResolver.
- Permission catalogues live globally — one set for the whole platform.
- Role rows live globally too (
workspace_id = NULLon every seeded row). model_has_rolesandmodel_has_permissionscarry aworkspace_idFK — that’s where the per-workspace assignment is stored.
A user who is helpdesk::agent in workspace A and helpdesk::requester in workspace B has two rows in model_has_roles, one per workspace.
MFA enforcement
Section titled “MFA enforcement”app/Modules/Identity/Models/User.php::mustEnrollMfa() returns true when the user:
- Holds the bare
adminpermission, or - Has any direct
platform::*permission, or - Holds a role that grants any
platform::*permission.
The mfa.required-for-admins middleware on /admin (Platform/Admin/routes.php:25) enforces enrolment before the admin shell loads.
Trust admin vs school pinning
Section titled “Trust admin vs school pinning”User::isTrustAdmin() returns true when the user has the admin role and is not school-pinned. User::isSchoolPinned() returns true when restricted_to_school_id is set. The SchoolSwitchController (Core/Schools/Http/SchoolSwitchController.php) aborts 403 if a pinned user tries to switch schools.
DSLs who should only see their own school’s safeguarding data are the common case for pinning.
Department scoping (Helpdesk)
Section titled “Department scoping (Helpdesk)”Orthogonal to roles. Users can be assigned to one or more helpdesk departments (User::canAccessDepartment(...)). The check is enforced inline in TicketsController (app/Modules/Helpdesk/Agent/Http/Controllers/TicketsController.php:131). Empty membership means “no scoping” — the user sees all tickets in the product.
Confidential / restricted carve-outs
Section titled “Confidential / restricted carve-outs”people::confidential::view— flagged-confidential HR notes. Not onpeople::agentby default.people::ni::view— narrower; just the NI-number field.governance::evidence::view::restricted— restricted-classification governance evidence.governance::policy::approve— held by no seeded role; effectivelyadmin-only today.
Audit events
Section titled “Audit events”Permission mutations are audited via Spatie events, wired in IdentityServiceProvider:
RoleAttachedEvent/RoleDetachedEvent/PermissionAttachedEvent/PermissionDetachedEvent→AuditPermissionMutationhandler.AuthorizationDenied(custom,app/Modules/Identity/Events/AuthorizationDenied.php) — fired byEnsurePermissionon every 403. Records permission name, path, method, IP, user-agent. Useful for spotting privilege-escalation probes.
Seeding
Section titled “Seeding”database/seeders/RolePermissionSeeder.php. Idempotent; reads PermissionCatalogue::all() + ::roles() and:
- Upserts every permission.
- Upserts every system role with
is_system => true. - Calls
$role->syncPermissions($perms)— so removing a permission from the catalogue strips it from the role on the next deploy.
Safe to run on every deploy. Custom roles created in the admin UI are untouched.
Files map
Section titled “Files map”| File | What’s there |
|---|---|
app/Modules/Identity/Permissions/PermissionCatalogue.php | Permissions + roles catalogue (single source of truth). |
app/Modules/Identity/Permissions/KeystoneTeamResolver.php | Workspace resolver for Spatie teams mode. |
app/Modules/Identity/IdentityServiceProvider.php | Gate::before registration; Spatie audit-event listeners. |
app/Modules/Identity/Http/Middleware/EnsurePermission.php | Generic permission-gate middleware + AuthorizationDenied fire. |
app/Modules/Identity/Events/AuthorizationDenied.php | Forensic event for 403 from EnsurePermission. |
app/Modules/Identity/Models/User.php | HasRoles trait + mustEnrollMfa(), isTrustAdmin(), isSchoolPinned(), canAccessDepartment(). |
app/Modules/Platform/Admin/Http/Middleware/EnsurePlatformAccess.php | Loose OR gate for /admin shell. |
app/Modules/Platform/Admin/Http/Controllers/RolesController.php | Admin UI for system + custom roles. |
config/permission.php | Spatie config (teams on, custom resolver, workspace_id column). |
database/migrations/identity/2026_04_26_000200_create_permission_tables.php | Permission tables with UUIDv7 + workspace columns. |
database/seeders/RolePermissionSeeder.php | Idempotent role/permission sync. |
app/Modules/*/Agent/routes.php | Per-product agent rails (entry + per-feature can:* groups). |
app/Modules/Platform/Admin/routes.php | /admin route group with EnsurePlatformAccess + per-section can:*. |
Known divergence from users-roles.md
Section titled “Known divergence from users-roles.md”The companion Users & roles article describes “standard roles” that include Platform admin, Workspace admin, Finance lead, DPO, HR lead and DSL (in the users-roles.md sense). Of those, only DSL maps cleanly onto the seeded role catalogue (as scr::dsl). The others reflect the design intent — they aren’t seeded today.
When in doubt, this document is authoritative for what the code does; users-roles.md is authoritative for the admin-onboarding mental model. Reconciling the two is a separate piece of work.