Authorization models

NativeSqlEngine evaluates three classical authorization models in one pass. This page defines each
formally and shows how they combine. The runtime is in the PDP decision pipeline.

The decision, abstractly

A decision is a function of a subject ss, a permission pp, an organization oo, an optional resource rr
and a request context cc:

decide(s,p,o,r,c){allow,deny} \text{decide}(s, p, o, r, c) \in \{\textsf{allow}, \textsf{deny}\}

Three sources can each grant; the combiner is deny-overrides:

allow    (RBACABACReBAC)    ¬ExplicitDeny \text{allow} \iff \big(\text{RBAC} \lor \text{ABAC} \lor \text{ReBAC}\big) \;\land\; \lnot\,\text{ExplicitDeny}

RBAC — role-based

The subject’s roles — direct and inherited — grant permissions by slug. Let R(s)R(s) be the role set assigned
to ss, and let \preceq be the role-inheritance order. The role closure is:

R(s)={rrR(s),  rr} R^{*}(s) = \{\, r' \mid \exists r \in R(s),\; r' \preceq r \,\}

and perms(R(s))\text{perms}(R^{*}(s)) is the union of the permissions those roles grant. RBAC grants pp iff
pperms(R(s))p \in \text{perms}(R^{*}(s)).

In this server

Roles and their inheritance come from applied manifests. warehouse:supervisor that inherits
warehouse:operator contributes both role’s permissions to the closure.

ABAC — attribute-based

A permission may carry a declared condition — a predicate over request attributes. ConditionEvaluator
evaluates it against the context cc you pass:

condp(c){true,false},e.g. (amount1000) \textsf{cond}_p(c) \in \{\textsf{true}, \textsf{false}\}, \qquad \text{e.g. } (\texttt{amount} \le 1000)

ABAC grants pp iff the subject holds pp and condp(c)=true\textsf{cond}_p(c) = \textsf{true}. A held permission
with a failing condition does not grant — and the failed condition is reported in
Decision::$failedConditions.

In this server

Conditions are declared in the manifest as { "attr": ..., "op": ..., "value": ... }. Supported operators
are deterministic comparisons (ordering, equality, membership, time windows) — no arbitrary code.

ReBAC — relationship-based

Access can follow from a relationship to a specific resource. Relationships are tuples
(s,rel,r)(s, \text{rel}, r) in a graph GG. ReBAC grants iff the requested relation is reachable:

ReBAC    (s,rel,r)G \text{ReBAC} \iff (s, \text{rel}, r) \in G^{*}

where GG^{*} is the bounded closure under group nesting, resource hierarchy (parent) and
relation implication (ownereditorviewer\textsf{owner} \supseteq \textsf{editor} \supseteq \textsf{viewer}). The closure
is depth-bounded and cycle-guarded; exceeding the bound yields deny. Full treatment in
ReBAC relationships.

Combining them

flowchart TD Q["DecisionQuery (s, p, o, r, c)"] --> RBAC["RBAC: p ∈ perms(R*(s))?"] Q --> ABAC["ABAC: cond_p(c)?"] Q --> REBAC["ReBAC: (s,rel,r) ∈ G*?"] RBAC --> COMB["combine"] ABAC --> COMB REBAC --> COMB DENY["explicit deny?"] --> COMB COMB -->|deny-overrides| OUT["allow / deny + explanation"]

The combiner is monotone in deny: adding any policy can only keep a result the same or turn it to deny.
That is what makes the engine safe to extend — a new policy can never silently widen access past an existing
deny.

ADR — one engine, not three

Problem. RBAC, ABAC and ReBAC are often separate systems, producing three answers a caller must
reconcile — and three audit trails.

Decision. Evaluate all three in a single NativeSqlEngine::decide pass that emits one Decision with
one explanation and one decisionId.

Consequences. A single, citable answer; deny-overrides applied uniformly across models; no reconciliation
bugs. The trade-off is a more complex engine, contained behind the AuthorizationEngine interface.

Holding a permission is necessary, not sufficient

RBAC establishes that a subject could hold pp; ABAC conditions and explicit denies can still withhold it.
Never infer “allowed” from “has the role” — always ask the PDP with the real context.

Next