5.4 Role specs on mounts (read/write/delete/invoke)

Access control is configured per mount with an access object. The runtime enforces it in the host (not in the service), uniformly for every service — prebuilt or custom. (For the bigger picture — the operator/installer tiers and the authority invariant — read 5.0 first.)

The shape

"access": {
  "read":   "all",
  "write":  "A E",
  "delete": "A",
  "invoke": "A E U"
}

Each key gates a class of HTTP methods by the action it represents:

Key Methods it gates Default
read GET, HEAD, OPTIONS all
write PUT, PATCH (idempotent upsert) A
delete DELETE = write
invoke POST and other non-idempotent verbs = write

So within an access object, an unset key defaults to world-readable and admin-writable; you tighten or loosen from there. The split rests on HTTP's own line: PUT/PATCH are an idempotent upsert (write), while POST is the non-idempotent action verb (invoke) — a store's keyless create, a pipeline run, an auth login. delete is its own key because removal is the asymmetric, irreversible operation you most often want to gate separately; it falls back to write when unset.

Shorthands. access may also be the string "open" (anyone) or "authenticated" (any logged-in principal), instead of the object form.

Fail closed. A mount with no access at all is not reachable — every request is denied (401 for an anonymous caller, 403 once authenticated). Omitting access is never an implicit "public"; a public mount must opt in explicitly with "access": "open". This way a forgotten policy fails safe instead of silently exposing the mount.

Role tokens

A role-spec value is one or more space-separated tokens:

  • all — anyone, including anonymous callers.
  • authenticated — any logged-in principal, regardless of role.
  • a role name (A, editor, …) — the principal must hold that role.
  • a role + path pattern — the role applies only under a path, with substitution: "U /user/{email}" lets a U principal act only under /user/<their-own-email> (the {email} is replaced with the caller's id). Other placeholders resolve from string claims named by auth.jwtUserProps, so "authenticated /{accountId}" scopes each caller to their account. A missing claim never matches.

How a decision is made

  1. The method maps to a key (GETread, PUTwrite, DELETEdelete, POSTinvoke).
  2. The runtime checks the caller against that key's tokens.
  3. No principal + unsatisfied spec → 401. A principal lacking the required role → 403. (The distinction tells a client whether to authenticate or give up.)

Path-scoped grants in practice

The path-pattern form expresses "users can edit their own record but not others'":

{ "path": "/data/users", "service": "data",
  "config": { "access": {
      "read":  "A",
      "write": "A, U /data/users/{email}"
  } } }

An admin (A) can write anywhere under the mount; a plain user (U) can write only to the record keyed by their own email. To go finer than whole-record — restricting individual fields (e.g. let users edit their own record but not their roles) — use the data service's fieldLevelAuthz with x-rs-read / x-rs-write schema annotations (4.5).

Inline per-pipeline access

A pipeline mount authorizes its execution surface per spec. The mount's access is the floor; a stored spec's optional inline access object ({ read?, write?, delete?, invoke? }) overrides it per key — looser or tighter — so one mount can host a public login spec alongside an admin-only one. POST→invoke here means "run this pipeline".

{ "pipeline": [ … ],
  "access": { "invoke": "all" },        // this spec is public…
  "description": "login" }

Because a spec's access defines authority, only an operator may set or change it (5.0). A non-operator may freely author and edit a spec's logic, but their write must leave the access field exactly as stored. See Part 7 for authoring; manage-style keys do not exist — a spec is gated by the same four actions as any mount.

Internal calls and trust

Pipeline steps and capability grants make internal calls. These are not trusted for being internal — the caller's principal propagates and the same role specs apply on the inner call. So a pipeline acting on behalf of a logged-in user can't be used to escalate past that user's permissions, and an anonymous trigger reaches only what an anonymous caller could. To deliberately cross a boundary, use the operator-configured elevation gateway (5.0, Part 7), which adds a configured role rather than dropping identity. Runtime-originated system calls (scheduler ticks) are trusted, since they exist only by operator config and can't be forged from the wire.

Next: letting browsers from other origins talk to your tenant — CORS.


Previous: 5.3 Tokens, cookies, bearer · Manual home · Next: 5.5 CORS and the CSRF guard →