Skip to navigation

Policies

Every check evaluates the requested verb against your tenant’s policy: a verb → mode map where each mode is allow, deny, require_grant, or step_up. The policy is layered. The most specific layer that names the verb decides:

  1. Per-principal override — one principal’s sparse verb→mode map. Highest precedence.
  2. Per-environment override — dev, staging, or prod. Applies when the check names that environment.
  3. Tenant default — the complete map; every verb falls back here.

Unnamed verbs inherit downward: a verb the principal’s override doesn’t set falls through to the environment override, then to the tenant default. An unknown environment name denies fail-closed without evaluating any policy.

The tenant default

PUT /v1/policy sets modes on the complete map; GET /v1/policy reads it. Unlisted verbs default to require_grant (fail closed). Every edit bumps the tenant default’s own version.

curl -X PUT https://api.withwrit.com/v1/policy \
-H "Authorization: Bearer writ_..." -H "Content-Type: application/json" \
-d '{"refund": "deny"}'

Environment overrides

PUT /v1/policies/environments/{dev|staging|prod} sets a sparse override: only the verbs you send are stored; the rest keep inheriting the tenant default. Each environment has its own independent version starting at 1 on first write (version: 0 means “no override yet”).

curl -X PUT https://api.withwrit.com/v1/policies/environments/dev \
-H "Authorization: Bearer writ_..." -H "Content-Type: application/json" \
-d '{"refund": "allow"}'

GET /v1/policies/environments/{env} returns the sparse policy plus effectivePolicy (tenant default with the override applied) and policyProvenance, which names the winning layer per verb.

Per-principal overrides

PUT /v1/policies/principals/{principalId} sets the top layer: a sparse override for one principal. Only live principals can carry policy — setting an override on a tombstoned or disabled principal is 400 (principal_inactive), because an identity that can’t authorize checks can’t carry policy either. Each principal’s override has its own independent version, untouched by environment or tenant edits.

curl -X PUT https://api.withwrit.com/v1/policies/principals/prn_9d2f4a1b2c3d \
-H "Authorization: Bearer writ_..." -H "Content-Type: application/json" \
-d '{"refund": "step_up"}'

Send {"verb": "inherit"} to remove a verb from the override so it inherits downward again.

GET /v1/policies/principals/{principalId} returns the sparse override, the fully-resolved effectivePolicy, and policyProvenance naming the winning layer per verb. Add ?environment=prod to fold the prod override into the middle layer, exactly as a check naming prod would evaluate it. Tombstoned principals still return 200 — they stay resolvable forever — with "inert": true.

GET /v1/policies lists every policy object: tenant default first, then configured environment overrides, then configured per-principal overrides.

Inert overrides

Disabling or deleting a principal makes its override inert: it never supplies a mode while the principal can’t authorize checks — checks deny with principal disabled / principal deleted before policy is even evaluated. The override data is kept, not wiped: re-enabling the principal re-arms its override, and the audit record of what was configured survives.

Receipts name the winner

Every check receipt records policyId and policyVersion — the single policy object that supplied the verb’s mode — alongside the principal snapshot (principalId / principalName). policyId is null only where no policy was evaluated (e.g. unknown-environment denials).