The 10 permission strings, and the three levels they are enforced at.
Defined in platform/contracts/src/permissions.ts. The required deployment policy grants
app permissions through app-local roles and email membership; platform administration is an explicit
operator grant. Auth0 authenticates identities and affiliations, but provider roles are not authority.
See application access.
| Permission | Grants |
|---|---|
templates:read | View templates. Also gates the Templates nav item. |
processes:read | Read own processes. |
processes:read_all | Read all processes in the app. Also confers write. |
processes:write | Act on processes — update and complete steps. |
processes:delete | Hard-delete a process. |
processes:audit | Cross-process read with no write. Gates records and the Records nav item. |
processes:reopen | Reopen a completed process. |
database:read | View the document database. Gates the Database nav item. |
database:write | Create and edit collections and documents. |
user:impersonate | Act as any user in one of their orgs, tenant-wide. |
templates:write is retired: templates are code-owned and published on deploy, and there
is no template write endpoint. A deployment policy that still lists it for an operator loads
normally and the grant is ignored (RETIRED_OPERATOR_PERMISSIONS in
platform/backend/src/deployment-access-configuration.ts); remove it from the policy at your
convenience. Unknown permissions are still rejected.
processes:audit exists specifically because processes:read_all confers write. An
auditor needs to read every run without being able to change one — grant audit, not
read_all.
Permissions are checked at three widths, narrowing as you go in.
The canonical service enforces process start permission. Legacy source template.permissions[] is
retained as presentation metadata, not newly introduced as an enforcement gate.
step.permissions[] on an input step gates who may edit and complete it.
The canonical application service performs the check.
Separately, completeExpression gates whether the step can be finished at all — a
different question from who may edit it. It has hasPermission("name") in scope.
input.permissions[] on an individual field. Users without a match see the field
read-only. Writes are filtered to permitted keys before the merge, so a rejected
field write is dropped silently rather than rejected loudly.
This is what lets two actors own different fields on the same step.
Matching is OR at every level: holding any one of the listed permissions is enough. An empty or omitted array means no restriction beyond the level above.
{
key: "allocation-review",
permissions: ["processes:write"], // 1. who may start a run
steps: [{
key: "review",
type: "input",
permissions: ["risk:review"], // 2. who may act on this step
inputs: [
{ key: "summary", type: "string", title: "Summary" },
{ key: "approved", type: "bool", title: "Approved",
permissions: ["risk:signoff"] }, // 3. who may edit this one field
],
completeExpression: 'hasPermission("risk:signoff")',
}],
}
A holder of risk:review opens the step, edits summary, sees approved read-only,
and cannot finish. A holder of risk:signoff can do all three.
When adding a route, see add an API route for which helper to reach for.
| Helper | Use |
|---|---|
lib/require-permission.ts | Assert a permission on an API route. |
lib/require-process-read.ts | Read access to one process, honoring read_all and sharing. |
lib/require-process-write.ts | Write access to one process. |
lib/input-permissions.ts | Filter a field-write payload to permitted keys. |
template.roles[] maps a friendly actor name to one permission string:
{ id: "govops", name: "GovOps", permission: "processes:write", color: "#3b82f6" }
Editor metadata only. Roles compile down to the same permissions[] arrays the
engine already reads — the engine has no concept of a role. People are never stored
in presentation metadata. App access declarations own membership and role assignments.
An actor holding user:impersonate may send X-Impersonate-User-Id plus optional
X-Impersonate-Organization-Id (defaults to the actor's own org).
getPrincipalFromRequest then resolves the effective principal as that user in the
target affiliation, with that user's verified identity resolved through Auth0 and authority derived
from the deployment policy. Each subsequent process operation rebinds to the target app.
Two things stay put:
X-Process-Platform-Organization header remain on the actor's own org.principal.impersonatedBy, surfaced as actor on /api/me.Only the effective principal switches. The UI searches /api/impersonation/targets and
stores the chosen user and org in localStorage for every subsequent authFetch.
Frontend profiles are not an access boundary. Hiding a route from a skin's sidebar does not prevent anyone from typing the URL. The deployment policy, app membership and canonical process/activity checks enforce access regardless of which frontend sent a request.
See whitelabel.
Use /api/me?processId=… or ?templateKey=… for target-app permission checks, never a union of
applicationPermissions. Unscoped permissions contains platform grants only. The backend rechecks
commands independently of these UI hints. Document exports require an explicitly app-owned asset.
File deletion requires recorded activity ownership and current field editability; upload permission
alone is not permission to delete any attachment.