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.read so 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 /vendors and /manufacturers pages 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/:id and needs asset.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 mineOnly filter 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 Plannermaintenance-plan.read/.create/.update/.generate (designs the recurring schedules)
  • Maintenance Reviewermaintenance-request.read/.review + work-order.create/.assign (triages incoming requests, dispatches WOs)
  • Technicianwork-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/new and 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 via commonReads.

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.read for their cost center, not the audit-history feed.

The split between .read, .export-excel, and .export-pdf lets 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:

  1. Always include the common reads. A role without them is a service-account role; humans need them to see the UI.
  2. Avoid mixing approve + create. A role that can both create and approve a transfer breaks separation of duties.
  3. Be explicit about export. A role with *.read doesn't automatically get *.export — that's intentional. Add it if you want analysts to download CSV.
  4. Dangerous permissions (the 🔴 ones) get a red badge in the role editor. Sleep on it before granting delete or update rights 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