Permissions

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.


The 10 strings

PermissionGrants
templates:readView templates. Also gates the Templates nav item.
processes:readRead own processes.
processes:read_allRead all processes in the app. Also confers write.
processes:writeAct on processes — update and complete steps.
processes:deleteHard-delete a process.
processes:auditCross-process read with no write. Gates records and the Records nav item.
processes:reopenReopen a completed process.
database:readView the document database. Gates the Database nav item.
database:writeCreate and edit collections and documents.
user:impersonateAct 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.


Three levels

Permissions are checked at three widths, narrowing as you go in.

1. Template — can you start this?

The canonical service enforces process start permission. Legacy source template.permissions[] is retained as presentation metadata, not newly introduced as an enforcement gate.

2. Step — can you act here?

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.

3. Field — can you edit this one box?

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.

All three in one template

{
  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.


Gates in code

When adding a route, see add an API route for which helper to reach for.

HelperUse
lib/require-permission.tsAssert a permission on an API route.
lib/require-process-read.tsRead access to one process, honoring read_all and sharing.
lib/require-process-write.tsWrite access to one process.
lib/input-permissions.tsFilter a field-write payload to permitted keys.

Roles

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.


Impersonation

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:

  • The actor's JWT and X-Process-Platform-Organization header remain on the actor's own org.
  • The real actor is preserved on 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.


What permissions do not gate

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.

Scoped identity and files

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.