02 — Permissions Matrix
A complete map of which role can do what, broken down by module. The grid below is the source of truth for "if I assign X to a user, what can they touch?"
How to read this doc
- ✅ — the role grants this permission
- ⬛ — the role does NOT grant this permission
- The Super Admin column is always ✅; it's omitted from per-module tables to keep them readable. Super Admin holds every permission in the system.
- Permission keys follow
resource.action(e.g.,asset.create). - The rightmost column shows whether a permission is flagged dangerous — the role editor renders dangerous permissions with a red badge so admins notice when they're handing one out.
Common reads — granted to every workflow role
Every workflow role inherits this set so that any logged-in field user can navigate, see master data, render hierarchy pickers, manage their own profile, and toggle theme/language.
| Permission | What it gates |
|---|---|
asset.read |
View assets list and detail. Required by every workflow that references an asset. |
organization.read / location.read / classification.read |
See the master-data hierarchy in pickers and labels. |
hierarchy-config.read |
Read the configured level labels (Building / Floor / Room) so dropdowns can label themselves. |
document.read / document.upload |
View and upload documents on entities the user has access to. |
me.profile.read |
See own profile page. |
me.notification.read |
See own notification inbox + bell. |
me.notification-preference.update |
Manage own per-template channel preferences. |
me.theme.update |
Change own theme (light / dark) and PrimeNG preset. |
me.language.update |
Change own UI language. |
global-search.use |
Use the top-bar search widget. |
If you provision a strict-role user without these (e.g., for an API service account), the corresponding top-nav widgets and account-menu items are hidden, and the matching routes return 403.
Identity (Users / Roles / Permissions / Sessions / Login audit)
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
user.read |
⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | |
user.create |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
user.update |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
user.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
user.reset-password |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
user.impersonate |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
user.export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
role.read |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
role.create / .update / .assign / .export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
role.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
permission.read / .export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
permission.assign-direct |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
login-audit.read |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ |
Identity management is admin-only by design. Audit Planner and Checkout Issuer get
user.readso they can pick from the user list (assigning an audit, choosing a custodian); the rest of identity stays with Super Admin.
Master Data (Organizations / Locations / Classifications / Vendors / Manufacturers)
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
organization.read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
organization.view-tree |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
organization.create / .update / .export / .import |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
organization.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
location.* (same shape as organization) |
reads ✅ for all roles, mutations admin-only | ||||||||||
classification.* (same shape) |
reads ✅ for all roles, mutations admin-only | ||||||||||
vendor.read / .create / .update / .export / .import |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
vendor.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
manufacturer.read / .create / .update / .export / .import |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
manufacturer.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
Master data is admin-curated. Workflow roles read but don't write. Vendor and Manufacturer read isn't included in
commonReads— workflow users can still see them on the asset detail page (which loads them as part of the Asset record); the dedicated/vendorsand/manufacturerspages are admin-only.
Assets
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
asset.read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
asset.create / .update / .export / .import |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
asset.print-label |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ |
Asset CRUD is admin-only by default. Workflow flows mutate the asset indirectly — a transfer's Complete writes the new location, an audit's Approve writes back observed values. Direct edit lives at
/assets/:idand needsasset.update.
Audits
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
audit-plan.read |
⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
audit-plan.create |
⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
audit-plan.update |
⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
audit-plan.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
audit-plan.assign |
⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
audit-plan.export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
audit-assignment.read |
⬛ | ⬛ | ⬛ | ✅ | ✅ | ✅ | ✅ | ⬛ | ⬛ | ⬛ | |
audit-assignment.submit |
⬛ | ⬛ | ⬛ | ⬛ | ✅ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | |
audit-result.read |
⬛ | ⬛ | ⬛ | ⬛ | ✅ | ✅ | ✅ | ⬛ | ⬛ | ⬛ | |
audit-result.review |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ |
The four audit roles split the lifecycle cleanly: Audit Planner designs and assigns; Auditor / Mobile Auditor execute; Audit Reviewer reviews. None can do all three by default. A senior auditor commonly gets all four for self-service.
Transfers
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
asset-transfer.read |
✅ | ✅ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.create |
✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.submit |
✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.approve |
⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.reject |
⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.receive |
⬛ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.complete |
⬛ | ⬛ | ✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.cancel |
✅ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
asset-transfer.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
The three transfer roles together cover the full Draft → Submit → Approve → InTransit → Receive → Complete chain. No single seeded role can take a transfer end-to-end by design — separation of duties.
Custody (Check-outs)
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
check-out.read |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ✅ | ✅ | ✅ | |
check-out.create |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | |
check-out.return |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ✅ | ⬛ | |
check-out.cancel |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ✅ | ⬛ | ⬛ | |
check-out.export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ |
Custody splits cleanly: Issuer creates and cancels; Returner accepts; Custodian reads (their own list, scoped by
mineOnlyfilter on the read endpoint).
Maintenance
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
maintenance-plan.read / .create / .update / .export / .generate |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
maintenance-plan.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
maintenance-request.read / .create / .review / .export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
work-order.read / .create / .update / .assign / .close / .export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ |
Maintenance has no seeded workflow role. All maintenance permissions are held only by Super Admin out of the box. Real deployments typically create custom roles like:
- Maintenance Planner —
maintenance-plan.read/.create/.update/.generate(designs the recurring schedules)- Maintenance Reviewer —
maintenance-request.read/.review+work-order.create/.assign(triages incoming requests, dispatches WOs)- Technician —
work-order.read/.update/.close(executes assigned WOs and reports completion)- Reporter (often == every employee) —
maintenance-request.create(file a new "broken X" request)Create them at
/roles/newand pick the matching permissions.
Permission split between Update and Close on Work Orders
| Permission | Gates these actions |
|---|---|
work-order.update |
Start (Assigned → InProgress) and Cancel (any non-terminal status) |
work-order.close |
Complete (InProgress → Completed) — captures cost, downtime, resolution notes |
Splitting them lets organizations route the "kick off and abandon" path to the technician while reserving "officially complete with cost data" for a senior. Common bundling: same person gets both — no harm.
Notifications
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
notification-template.read / .update |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
notification-template.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
The notification templates are admin-only. The "do I receive this notification?" toggle (per template, per channel) is a personal-account permission (
me.notification-preference.update) and granted to every workflow role viacommonReads.
Documents
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
document.read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
document.upload |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
document.delete |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
Workflow users can attach files (a damaged-asset photo, a signed transfer form, a vendor invoice) to entities they have access to. Deleting a document is admin-only.
Reporting (per-report keys)
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
report.asset-inventory.read / .export-excel / .export-pdf |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
report.audit-results.read / .export-excel / .export-pdf |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
report.audit-history.read / .export-excel / .export-pdf |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
report.maintenance-history.read / .export-excel / .export-pdf |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
report.transfer-history.read / .export-excel / .export-pdf |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | |
report.checkout-activity.read / .export-excel / .export-pdf |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ |
Reports are admin-only by default — none of the seeded workflow roles include them. Grant per-report so that, e.g., a Branch Manager sees only
report.asset-inventory.readfor their cost center, not the audit-history feed.The split between
.read,.export-excel, and.export-pdflets you allow a manager to view a report in the UI but only export to one format — useful if PDF is the official archive and Excel exports are gated to a smaller group.
System
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
audit-log.read |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ |
The audit log (every change to every business entity, with before/after JSON) is sensitive — Super Admin only by default. Compliance officers usually get this and nothing else.
Settings
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
hierarchy-config.read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
hierarchy-config.update |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
app-settings.read / .update |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | .update is 🔴 |
app-language.manage |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
translation.read / .update / .export |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | .update is 🔴 |
email-settings.manage |
⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | ⬛ | 🔴 |
Workflow roles get only
hierarchy-config.read(so dropdown labels render correctly). All other settings are admin-only.
Account (self-service)
| T.Req | T.App | T.Rec | A.Plan | Audit | A.Rev | Mobile | C.Iss | C.Ret | A.Cust | Dangerous | |
|---|---|---|---|---|---|---|---|---|---|---|---|
me.profile.read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
me.notification.read |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
me.notification-preference.update |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
me.theme.update |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
me.language.update |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | |
global-search.use |
✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
These are deliberately separated so an admin can provision a strict integration / service account without personal-UI affordances. For human users they're always granted.
How permissions resolve at runtime
Every API call goes through this:
flowchart LR
REQ[Incoming request] --> A{Has access<br/>token?}
A -- no --> REJ401[401 Unauthorized]
A -- yes --> EFF[Compute effective<br/>permissions]
EFF -- "(role permissions)<br/>∪ (direct grants)<br/>− (direct denies)" --> CHECK{Has the<br/>required permission?}
CHECK -- yes --> OK[Action executes]
CHECK -- no --> REJ403[403 Forbidden<br/>+ which permission was missing]
The result is cached for 30 minutes. When you change a user's roles, or change a role's permissions, the cache is invalidated immediately so the next request re-evaluates.
Building custom roles
Use the role designer at /roles/new (Super Admin only). The editor groups the permission catalog by module (Identity, Master Data, Assets, …) into collapsible cards. Search filters across name and key. A "Selected: X / Y" badge tracks total selection. A dirty indicator on the save bar tells you that you've changed something but haven't saved yet.
Tips when designing a new role:
- Always include the common reads. A role without them is a service-account role; humans need them to see the UI.
- Avoid mixing approve + create. A role that can both create and approve a transfer breaks separation of duties.
- Be explicit about export. A role with
*.readdoesn't automatically get*.export— that's intentional. Add it if you want analysts to download CSV. - Dangerous permissions (the 🔴 ones) get a red badge in the role editor. Sleep on it before granting
deleteorupdaterights to users.
Per-user overrides
The role × permission grid is the primary mechanism, but the system also supports per-user overrides (UserPermission rows):
- Grant — add a single permission to a single user, bypassing roles. Useful when one person on a team needs a special capability without creating a new role.
- Deny — explicitly remove a permission from a user even if a role grants it. Deny always wins.
Manage these at /users/:id → "Direct permissions" tab. Both have time-bound ValidFrom / ValidUntil columns — you can grant a permission "until end of next month" automatically.
Where to go next
| To learn… | See |
|---|---|
| What role bundles work well in real organizations | 01 §Combining roles |
| How each workflow flows end-to-end | 03 — Business Processes |
| Which screens each permission unlocks | 04 — Features by Module |