Person permissions

provides_permissions and provides_roles — declaring what a person may do rather than what your app may reach, gating a capability on a permission, and require_principal.

permissions says what your app may reach. provides_permissions and provides_roles are the other direction entirely: what a person may do.

BlockDirectionAnswers
permissionsApp → resourceMay this app read that table, call that capability, reach that host?
provides_permissionsPerson → operationMay this human perform this operation?
provides_rolesPerson → permission setWhich permissions does this human hold?

Both provides_* blocks are contributions to the platform, and each name in them is globally unique — a second app claiming the same permission key or role subject is rejected at publish or apply time.

provides_permissions

The permission keys this app defines, and optionally the capabilities the gateway gates on them.

manifest.json
"provides_permissions": [
  {
    "permission": "serials.register",
    "title": { "en": "Register a device serial" },
    "description": { "en": "Add a new serial number to the registry." },
    "capabilities": ["serials.create"],
    "require_principal": false
  }
]
FieldRequiredDescription
permissionYesGlobally unique dotted <area>.<verb> key. One definer platform-wide.
titleYesShort label, shown in the role editor.
descriptionNoWhat holding the permission lets a person do.
capabilitiesNoCapability keys the gateway gates on this permission. Each must be defined or implemented by this same app; a capability may require at most one permission. Omit to keep enforcement inside the app.
require_principalNofalse (default) — a request carrying no principal assertion still passes. true — such a request is rejected with 403.

Naming a capability in capabilities moves the check to the gateway: a caller without the permission gets a 403 and your function is never invoked. That is the version to prefer — it is one place, it is declarative, and it is visible to whoever administers roles.

Omitting capabilities keeps the decision in your handler, which is what you need when the answer depends on the request rather than on the route: this order but not that one, this market but not another.

title and description are read by a human building a role, so write them as sentences about the job, not about your architecture.

require_principal

Not every call to your app is made by a person. A scheduled tick, an event delivery, a machine-to-machine integration with an API key: none of those carry a human principal.

  • false (default) — a request with no principal assertion passes. The permission still gates requests that do carry one.
  • true — a request with no principal is rejected with 403.

So true is the right value for an operation that only makes sense as a deliberate human act — approving something, releasing a hold, overriding a price. It is the wrong value for anything a cron job, an integration or another app has to be able to do, and switching it on for a capability your own schedule calls will lock your app out of itself.

provides_roles

Declares this app as the platform's role provider for one kind of principal — the app that decides which permissions a given person holds. Exactly one provider per subject platform-wide.

manifest.json
"provides_roles": {
  "subject": "customer_contact",
  "resolve_capability": "customers.contacts.resolve"
}

The gateway calls resolve_capability once per request to turn a principal reference into a permission set. resolve_capability must be a capability this app defines.

Most apps never declare this. You declare it if you are the app that owns a population of people — a customers app owning contacts, for instance — and are therefore the only thing on the platform that can answer "what may this person do?".

If you do declare it, that capability is on the hot path of every request for that subject, so it needs to be fast, defensive and probably cached with a principal scope.

Reading the principal at runtime

The gateway resolves the principal before invoking you and injects the result into your invocation alongside the tenant context: the acting contact, the organisation it belongs to, and the permission set it holds. You read them like any other header:

src/main.js
// Confirm the header names against a real request before you rely on them —
// they come from whichever app provides roles for this subject.
app.post('/serials/{id}/release', async (c) => {
  c.log(JSON.stringify(c.ctx.req.headers));           // once, to see what you are sent

  const held = (c.header(PERMISSIONS_HEADER) ?? '').split(',').filter(Boolean);
  if (!held.includes('serials.release')) throw forbidden('you may not release a serial');
  // …
});

Three rules for handling it:

Read the headers; never parse the context token yourself. The resolution has already happened. Decoding a token to reach a claim couples you to a format that is not your contract.

Never take a principal from the request body. A contact id in a body is a claim by the caller. The injected value is a fact from the gateway. This is the same distinction that applies to the tenant.

Log the injected headers once, on a real request, to see exactly what your tenant's role provider supplies. The set depends on which app provides roles for that subject, so the authoritative answer for your tenant is the request you are actually being sent — not a list on a documentation page.

Prefer declaring capabilities on the permission over checking in code wherever the whole route is gated the same way. Reserve the runtime check for the per-record decisions the gateway cannot make.

Cockpit view gates are a third thing

A cockpit.json view's own permissions array — ["device_serials.read"] — decides whether an operator sees the view. It is a UI gate, not a grant and not an API check.

Use both. The view gate stops an operator being shown a screen they cannot use; the capability gate stops anybody calling the route regardless of how they found it. A UI gate alone is a hidden button, not a permission.

Next steps

Was this page helpful?