RequestSeal.Ash (RequestSeal v0.2.0)

Copy Markdown View Source

Explicit caller-owned identity mapping for Ash policies and actions.

scope/2 accepts a successful generic or Web Bot Auth RequestSeal.Verification and a map containing exactly three required choices, with no defaults or extra keys:

  • :actor — a one-argument function receiving only the opaque attributed principal. It must return {:ok, actor} with a non-nil actor, or :error. It never receives signature key identifiers, parameters, or labels.
  • :unattributed — :anonymous maps an unattributed verification to a nil actor without calling the actor function; :reject rejects it.
  • :tenant — :none, {:value, tenant}, or a one-argument principal function returning {:ok, tenant} or :error. The function runs only for attributed principals; anonymous scopes otherwise have a nil tenant. An explicit value is caller-trusted: do not derive it from untrusted forwarded headers.

Principal attribution belongs to the selected discovery and custody contract. This adapter neither establishes identity from key possession nor defines the principal's shape. Attributed values remain opaque caller-owned terms; nil and false are not attributed values and reject with :invalid_verification. Only the atom :unattributed selects anonymous mapping; other opaque values, including similarly named strings or atoms, are attributed. The generic verifier returns :unattributed. Pass only a result from trusted verification code; a caller-constructed or modified struct cannot establish cryptographic validity or trusted identity.

The returned RequestSeal.Ash.Scope always requests authorize?: true. Actor mapping grants no authority: Ash policies decide whether an action runs. Per Ash's scope contract, explicit actor, tenant, and authorization options take precedence over scope values; passing authorize?: false explicitly is the caller's own authorization bypass. Explicit context is deep merged by Ash, so callers also own any overrides.

Tenant scoping constrains queries, not records already loaded under another tenant on primary-key data layers such as ETS. A loaded foreign-tenant record can still be destroyed by primary key. Ash.destroy can seed an explicit tenant from loaded-record metadata before resolving the scope; pass the intended tenant: explicitly (or use Scope.to_opts/1). For destroy and update actions, add a caller-owned policy check for the tenant_id == ^tenant() condition alongside the action's permission checks. Check the original record's tenant against the action tenant before mutation, for example with an Ash.Policy.SimpleCheck; a query filter alone does not enforce that check on a loaded record. See Ash policies and tenant expressions.

The caller owns the actor value. RequestSeal's default scope inspection, projected context, and binding errors exclude it; actor options intentionally carry it into Ash. Ash's own Forbidden inspect and enabled policy-breakdown messages and logs can show the actor. Map principals to a minimal non-secret actor or give the actor a custom Inspect implementation. Configure Ash policy breakdowns with that disclosure in mind.

Context contains only request_seal facts: selected label, profile, the :attributed or :unattributed principal classification, canonical covered identifier strings, content: :not_required | :checked, freshness: :not_evaluated | :evaluated, and replay: :not_required | :claimed. A RequestSeal.Replay.Receipt projects only to :claimed; context never includes receipt contents, the principal value, keyid, parameters, message bytes, or keys. Label and covered identifiers retain the core's 256-byte label, 256-component, and 65,536-byte aggregate limits. These facts are policy input, not an authorization decision or telemetry labels.

Errors use the :binding layer: :invalid_verification, :invalid_binding, :unattributed, :actor_unbound, or :tenant_unbound. Callback errors, malformed results, raises, throws, and exits retain no callback text. See RequestSeal.Error.

The core mapping and RequestSeal.Ash.Scope.to_opts/1 require no Ash dependency. The optional protocol implementation follows the public Ash scope protocol.

Summary

Functions

Maps verified facts and explicit caller bindings to an authorization-enabled scope.

Types

binding()

@type binding() :: %{
  actor: (term() -> {:ok, term()} | :error),
  unattributed: :anonymous | :reject,
  tenant: :none | {:value, term()} | (term() -> {:ok, term()} | :error)
}

Functions

scope(verification, binding)

Maps verified facts and explicit caller bindings to an authorization-enabled scope.