← All Modules

assay.authz

In-process authorization engine. Policy statements, grants-at-scope with typed bounds, ABAC conditions, deny-wins, asymmetric fail-closed. Pure computation — no I/O, no storage, no policy expression language. The host resolves its grants and hands them over.

Semantics

Engine options

OptionShapeMeaning
grantslist of grantsThe grant universe to consider.
synthesized_grantslist of grantsGrants an app-owned synthesizer contributes (membership roles, an open-mode baseline). Unioned with grants.
condition_keys{ [key] = { type, lowercase? } }Declared keys; type is string, number, date or ip. Merged with the built-in request:* keys.
scope_kindslist of stringsWhen given, a chain entry of any other kind denies.
default_scope_chainlist of scopesUsed when a check names none.
actionslist of { action, derives_from? }The closed action registry. A cycle, a duplicate or a wildcard is an error at build time.
action_derivation{ [child] = parent }Derivation alone, for a host with no full catalogue.

A grant is { subject = {kind, id}, scope = {kind, id}, statements = {...}, bounds? = {...} }. A statement is { effect = "allow"|"deny", actions = {...}, resources = {...}, conditions? = {...} }.

Conditions

Operators are a closed set, each valid only on its key type:

Key typeOperators
stringStringEquals, StringNotEquals, StringLike, StringIn, StringNotIn, StringLikeIn
numberNumericLessThan, NumericGreaterThan
dateDateLessThan, DateGreaterThan
ipIpAddress, NotIpAddress

Scalar operators take value; the set operators (StringIn, StringNotIn, StringLikeIn) take values. Carrying both, or the wrong one, is unreadable and therefore unmatchable. The engine populates three keys itself on every check: request:Time (RFC 3339), request:HourUTC (0-23) and request:SourceIp — the last only when the check carried one, so a condition on it fails closed.

Context values

A context value may be a string, a number, a boolean, or a table of strings. Tables are read positionally, so an empty table is "holds nothing" rather than an empty map — which is what makes a StringNotIn condition over an empty role list match, exactly as the reference does. A value of any other shape still yields a decision rather than an error: equality is strict, so a non-string never equals an authored string, while pattern and set operators compare the value as JavaScript would render it.

Check options

scope_chain, context (values for the declared keys), source_ip, now (RFC 3339), and bypass — the host declaring that this caller skips policy entirely, so a careless broad deny cannot lock an operator out.

The decision is { allowed, decision, reason, allowed_by_stored_grants }. reason is one of allowed, explicit_deny, no_matching_grant, malformed_scope_chain, undeclared_scope_kind or admin_bypass. allowed_by_stored_grants tells an explicit grant apart from one a synthesizer supplied.

local authz = require("assay.authz")

local eng = authz.engine({
  scope_kinds = { "root", "space" },
  default_scope_chain = { { kind = "root", id = "*" } },
  condition_keys = { ["app:Region"] = { type = "string" } },
  actions = {
    { action = "docs.read" },
    { action = "docs.write", derives_from = "docs.read" },
  },
  grants = {
    {
      subject = { kind = "user", id = "alice" },
      scope = { kind = "root", id = "*" },
      statements = {
        { effect = "allow", actions = { "docs.read" }, resources = { "doc:*" } },
        { effect = "deny", actions = { "docs.read" }, resources = { "doc:secret" } },
      },
      bounds = { { operator = "StringEquals", key = "app:Region", value = "eu-west" } },
    },
  },
})

local alice = { { kind = "user", id = "alice" } }
local ctx = { context = { ["app:Region"] = "eu-west" } }

assert.eq(eng:check(alice, "docs.read", "doc:42", ctx).allowed, true)
assert.eq(eng:check(alice, "docs.write", "doc:42", ctx).allowed, true) -- derives from docs.read
assert.eq(eng:check(alice, "docs.read", "doc:secret", ctx).allowed, false) -- deny wins
assert.eq(eng:check(alice, "docs.read", "doc:42").allowed, false) -- bound unsatisfied

Conformance

The engine decides every case in the agentauthz 0.6.0 conformance fixture set identically to that reference library: all 149 language-neutral golden fixtures are vendored under crates/assay-authz/conformance/cases and run on every build, through both the pure evaluator and the composed engine. Behaviour the fixtures do not reach — context values of unusual shape, bounds the write path would refuse, rows missing a required field — is pinned against the reference directly in that crate's own tests.