sandloom.sdk.authz¶
Authorization: policies and atoms, access facts, the billing-account subject
(Account).
Public API 1.0 (spec §2.2, Appendix C).
from sandloom.sdk.authz import <name>. Signatures in a terminal: sandloom docs <name>. Beginner names are the ones generated code and the examples use; any other name is advanced (mark its import line # sandloom: advanced <reason> in generated code and examples).
Start with (beginner)¶
| Name | Kind | Summary |
|---|---|---|
authorize_resource |
function | Load a resource by path id, authorize, and return it to the endpoint. |
Credits |
class | Subject's live credit balance is at least minimum. |
OrgRole |
class | Subject holds at least minimum role in the request's organization. |
Owner |
class | Subject owns the request resource. |
require |
function | Return a dependency enforcing policy (no request resource). |
Subscribed |
class | Subject's subscription grants access (coarse, claim-based). |
SubscriptionActive |
class | Subject's subscription is active right now (live, authoritative). |
Advanced¶
| Name | Kind | Summary |
|---|---|---|
AccessDecision |
class | The outcome of evaluating a policy against an AuthzContext. |
AccessDenied |
exception | Raised by enforce (and the HTTP guards) when a policy is not satisfied. |
AccessFact |
class | Declarative binding of a fact name to its three resolution paths. |
Account |
class | The billing account an atom checks (of=); build it with user, org or bound_tenant. |
ACTIVE_SUBSCRIPTION_STATUSES |
constant | |
And |
class | Conjunction: all operands must hold. Short-circuits on first non-allow. |
authorize |
async function | Inline check over an already-loaded resource; raises on failure. |
AuthzContext |
class | Inputs available to a Requirement during evaluation. |
Category |
class | Mandatory denial categories (TZ §10.2). |
ClaimRule |
class | Declarative claim -> bool interpretation: wire-safe, no callables. |
current_context |
async function | Authenticated subject from the Bearer access token (cached per request). |
enforce |
async function | Evaluate policy for subject; raise AccessDenied on deny. |
Entitled |
class | Subject satisfies the registered access fact fact_name. |
ENTITLEMENTS |
constant | |
EntitlementsAdapter |
class | Answers subscription / credit questions (SubscriptionActive/Credits). |
FactDenied |
exception | A resolver's definite denial for a reason other than "the fact is False". |
FactOwnerUnreachable |
exception | The fact's owner is bound but could not describe its facts (503). |
FactUnavailable |
exception | A fact could not be resolved in any acceptable mode (a misconfiguration). |
Feature |
class | The account's plan enables the feature flag key (of=, §7.2). |
lookup_access_fact |
async function | The fact name: the local registration, else the owner's public one. |
make_rebuild_gate |
function | A startup task that rebuilds an authz projection from its sources. |
MEMBERSHIP |
constant | |
MembershipAdapter |
class | Answers organization-role questions for OrgRole. |
MODE_AUTO |
constant | |
MODE_CLAIM |
constant | |
MODE_LIVE |
constant | |
MODE_PROJECTION |
constant | |
Not |
class | Negation of a single requirement. |
Or |
class | Disjunction: any operand may hold. Short-circuits on first allow. |
Perm |
class | Subject holds the fine-grained permission perm. |
Permissions |
class | Predefined fine-grained permission codes. |
PolicyIndeterminate |
exception | Raised by an atom's evaluate when it cannot answer at all. |
register_authz_projection |
function | Register an authorization projection, its consumers and rebuilder. |
register_live_adapter |
function | Bind the adapter the live atoms use for kind — CONSUMER-side config. |
Requirement |
class | An authorization predicate, composable with & \| ~. |
resolve_fact |
async function | Resolve fact to a boolean, caching the result on ctx. |
Role |
class | Subject holds the global role role. |
Roles |
class | Predefined global role codes. |
service_subject |
function | A subject representing another service calling across a module boundary. |
SOURCE_HTTP |
constant | |
SOURCE_INTERNAL |
constant | |
SOURCE_SYSTEM |
constant | |
SOURCE_TASK |
constant | |
system_subject |
function | A subject representing the platform itself (trusted background work). |
TIER_CLAIM |
constant | |
TIER_LIVE |
constant | |
TIER_RESOURCE |
constant | |
WithinLimit |
class | used is strictly below the account's plan limit for key. |
Beginner names¶
authorize_resource¶
def authorize_resource(
*,
loader: Callable[[Any, AsyncSession], Awaitable[Any]],
by: str,
policy: Requirement,
cast: Callable[[str], Any] | None = None,
not_found_detail: str = 'Not found',
status_code: int | None = None,
) -> Callable[..., Awaitable[Any]]
Load a resource by path id, authorize, and return it to the endpoint.
Args:
loader:async (id_value, db) -> entity | None— the module supplies its own loader (keepssharedfree of ORM imports).by: name of the path parameter holding the resource id.policy: requirement to enforce; resource atoms (Owner) read the loaded entity.cast: optional converter applied to the raw path value beforeloader(e.g.UUID). Defaults to passing the raw string.not_found_detail: 404 message when the loader returnsNone.status_code: explicit status override for a definite denial (default: the decision's own status).
The resource is fetched once: claim atoms evaluate first, so a failing cheap check raises without ever loading; otherwise the entity loaded for the ownership check is the same one returned to the endpoint.
Credits¶
class Credits(_TenantScopedLiveAtom)(
minimum: int,
*,
of: Account = <object object>,
adapter: Any = None,
) -> None
Subject's live credit balance is at least minimum.
Reads the real-time balance via the entitlements contract (never a stale
token claim — this guards money). of= names the billing account.
Credits.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Credits.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
OrgRole¶
class OrgRole(Requirement)(
minimum: str,
*,
org_param: str = 'org_id',
adapter: Any = None,
) -> None
Subject holds at least minimum role in the request's organization.
Relationship (ReBAC) authorization: org membership/role is per-organization
and not carried in the global JWT, so it is resolved live through the
membership contract (local call in a monolith, an RPC when tenancy is a
separate service; 501 when neither). The organization id is read from the
request path parameter org_param, else X-Tenant-Id, else the bound
tenant; a missing organization is indeterminate (denies even under Not).
For a hot path, a module may instead maintain an authorization projection of
memberships and gate on a projection-backed Entitled fact.
OrgRole.org_params¶
def org_params(self) -> frozenset[str]
Path parameters this requirement may read an organisation from.
Non-empty for an atom that resolves an organisation (path parameter,
X-Tenant-Id, else the bound tenant): create_app then refuses a
route that solves the gate before its TenantScope() (SANDLOOM-B007).
OrgRole.risks¶
def risks(self) -> frozenset[str]
The framework risks a gate enforcing this requirement carries
(ADR-0009 item 6): {"admin"} for an admin role, else none. An
Or carries its branches' risks; a Not grants no authority.
OrgRole.contracts¶
def contracts(self) -> frozenset[str]
Sync contracts evaluating this requirement may call (wiring time).
A module's derived consumes includes them (optional), so the
startup preflight reports an unbound one and sandloom add deploy
binds it in a split deployment. Default: none (a claims-only atom).
OrgRole.adapter¶
def adapter(self) -> Any
OrgRole.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
OrgRole.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
Owner¶
class Owner(Requirement)(get_owner: Callable[[Any], UUID | None]) -> None
Subject owns the request resource.
get_owner maps the loaded resource to its owner id; it is supplied by
the module declaring the policy (e.g. lambda post: post.author_id), so
the atom never needs to know the resource's type.
Owner.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Owner.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
require¶
def require(
policy: Requirement,
*,
status_code: int | None = None,
) -> Callable[..., Awaitable[UserContext]]
Return a dependency enforcing policy (no request resource).
Use in dependencies=[Depends(require(...))] or as a parameter when the
endpoint wants the resulting UserContext. A denial answers with the
decision's own status (e.g. Subscribed(of=Account.user()) → 402, a
missing contract → 501);
status_code is an explicit override for definite denials.
A claims-only policy (every atom tier 1) never opens a database session /
Unit of Work; only policies that may reach a live atom depend on
get_db.
Raises ValueError at wiring time if policy contains a resource-scoped
atom (e.g. Owner): require has no request resource, so such a policy
belongs in authorize_resource / authorize.
Subscribed¶
class Subscribed(Requirement)(
statuses: frozenset[str] | None = None,
*,
of: Account = <object object>,
claim_key: str = 'subscription_status',
) -> None
Subject's subscription grants access (coarse, claim-based).
of=Account.user(): reads the token's
subscription_status claim with ZERO I/O, interpreted by the
subscription.active access fact — billing's declarative rule, the same
predicate as the live check (active/trialing pass; past_due
passes during the grace period unless BILLING_DENY_ON_PAST_DUE). The
fact is the local registration or, in a process without billing, the one
billing publishes over billing.facts.v1 (described once, cached).
Provenance is honoured: when the token's authoritative provenance shows
billing did NOT contribute (it failed at mint, or the issuer could not
reach it) the claim proves nothing, so the atom escalates to billing's LIVE
answer instead of denying 402 for the token's whole lifetime (503
indeterminate when no live path exists). With no subscription owner in
the deployment at all, only active/trialing pass. May be up to the
token TTL stale; for real-time / money-sensitive checks use
sandloom.shared.authz.atoms_live.SubscriptionActive / Credits.
of=Account.org(...): the token carries NO per-organization
subscription claim (claims are personal-only), so this atom resolves LIVE
— it delegates to SubscriptionActive(of=...) and is a tier-3 atom.
of=Account.bound_tenant() resolves the bound tenant's kind first
(personal → the claim, organization → live).
Passing explicit statuses pins the accepted set verbatim (no grace
interpretation).
Subscribed.tenant_type¶
tenant_type (property)
Subscribed.org_params¶
def org_params(self) -> frozenset[str]
Path parameters this requirement may read an organisation from.
Non-empty for an atom that resolves an organisation (path parameter,
X-Tenant-Id, else the bound tenant): create_app then refuses a
route that solves the gate before its TenantScope() (SANDLOOM-B007).
Subscribed.contracts¶
def contracts(self) -> frozenset[str]
Sync contracts evaluating this requirement may call (wiring time).
A module's derived consumes includes them (optional), so the
startup preflight reports an unbound one and sandloom add deploy
binds it in a split deployment. Default: none (a claims-only atom).
Subscribed.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Subscribed.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
SubscriptionActive¶
class SubscriptionActive(_TenantScopedLiveAtom)(
*,
of: Account = <object object>,
adapter: Any = None,
) -> None
Subject's subscription is active right now (live, authoritative).
Live counterpart to the claim-based sandloom.shared.authz.atoms_claims.Subscribed. Use this when staleness is unacceptable (e.g. gating immediately
after a purchase); use Subscribed for the cheap, zero-I/O common path.
of= names the billing account (spec §7).
SubscriptionActive.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
SubscriptionActive.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
Advanced names¶
AccessDecision¶
class AccessDecision(
allowed: bool,
code: str = 'ok',
category: str | None = None,
reason: str = '',
http_status: int = 200,
failed_requirement: str | None = None,
policy_code: str | None = None,
metadata: dict[str, Any] = <factory>,
indeterminate: bool = False,
) -> None
The outcome of evaluating a policy against an AuthzContext.
allowed is the only field that matters for the boolean question; the
rest exist so a denial is reportable, auditable and observable.
AccessDecision.allow¶
@classmethod
def allow(cls, *, policy_code: str | None = None) -> AccessDecision
AccessDecision.deny¶
@classmethod
def deny(
cls,
*,
code: str,
category: str = 'forbidden',
reason: str = '',
http_status: int | None = None,
failed_requirement: str | None = None,
policy_code: str | None = None,
metadata: dict[str, Any] | None = None,
) -> AccessDecision
AccessDecision.error¶
@classmethod
def error(
cls,
*,
code: str,
category: str = 'policy_misconfigured',
reason: str = '',
http_status: int | None = None,
failed_requirement: str | None = None,
metadata: dict[str, Any] | None = None,
) -> AccessDecision
An indeterminate denial: the requirement could not be evaluated.
Distinct from deny (the subject definitively lacks the
requirement): combinators propagate it unchanged, so ~Entitled(x)
with x unregistered / its service down DENIES with this category and
status instead of inverting the failure into an allow.
AccessDecision.with_policy_code¶
def with_policy_code(self, policy_code: str | None) -> AccessDecision
Copy of this decision tagged with policy_code.
AccessDecision.audit_fields¶
def audit_fields(self) -> dict[str, Any]
The subset safe and useful to record on a deny (no PII).
AccessDenied¶
class AccessDenied(HTTPException)(
decision: AccessDecision,
*,
status_code: int | None = None,
) -> None
Raised by enforce (and the HTTP guards) when a policy is not satisfied.
Carries the full AccessDecision so the caller (HTTP adapter, task
runner) can map it to the right transport response. Subclasses FastAPI's
HTTPException so a module calling enforce() inside an endpoint gets
the decision's status (402/403/501/…) and {"detail", "code"} body from
the framework HTTP handler instead of an unhandled 500. Non-HTTP callers
(workers, services) still catch the typed AccessDenied and read
.decision. It also exposes the framework error-protocol attributes
http_status / code / detail.
status_code explicitly overrides the decision's status (transport
override, e.g. require(..., status_code=402)).
AccessFact¶
class AccessFact(
name: str,
owner_module: str,
claim_key: str | None = None,
projection_reader: ProjectionReader | None = None,
live_service: str | None = None,
live_resolver: LiveResolver | None = None,
min_freshness: str = 'claim',
default_mode: str = 'auto',
deny_category: str | None = None,
claim_rule: ClaimRule | Callable[[], ClaimRule] | None = None,
) -> None
Declarative binding of a fact name to its three resolution paths.
A module registers one of these in register(core) via
core.register_access_fact(...), wiring together mechanisms that already
exist: its claims contributor (claim_key), an optional local projection
(projection_reader) and its service contract (live_service +
live_resolver).
AccessFact.current_claim_rule¶
def current_claim_rule(self) -> ClaimRule | None
The declarative claim rule in effect now (None: not declarative).
AccessFact.interpret_claim¶
def interpret_claim(self, value: Any) -> bool
Claim value -> bool, exactly as the owner interprets it.
AccessFact.to_wire¶
def to_wire(self) -> dict[str, Any]
Wire-safe descriptor published by the owner's describe().
AccessFact.build_facts_contract¶
@classmethod
def build_facts_contract(cls, core: Any, owner: str) -> Any
The <owner>.facts.v1 provider (called by the kernel on register).
A duck-typed hook, so the kernel exposes the contract without importing the authorization subsystem.
Account¶
class Account(
kind: AccountKind,
param: str | None = None,
reason: str | None = None,
) -> None
The billing account an atom checks (of=); build it with
user, org or bound_tenant.
Account.user¶
@staticmethod
def user(*, reason: str | None = None) -> Account
The caller's personal account. reason= waives SANDLOOM-C027 on a
route that carries an organization parameter.
Account.org¶
@staticmethod
def org(*, param: str | None = 'org_id') -> Account
An organization account: param (path, then query), else the
X-Tenant-Id header, else the bound tenant (param=None: header
or bound tenant only).
Account.bound_tenant¶
@staticmethod
def bound_tenant() -> Account
The account of the bound tenant, by its kind (personal or organization; canonical / unknown are indeterminate).
Account.describe¶
def describe(self) -> str
ACTIVE_SUBSCRIPTION_STATUSES¶
ACTIVE_SUBSCRIPTION_STATUSES: frozenset = frozenset({'active', 'trialing'})
(no docstring)
And¶
class And(Requirement)(*parts: Requirement) -> None
Conjunction: all operands must hold. Short-circuits on first non-allow.
A definite deny or an indeterminate operand both stop evaluation and are returned as-is (both deny; the indeterminate keeps its 5xx category).
And.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
And.decide¶
async def decide(self, ctx: AuthzContext) -> AccessDecision
Structured result. Default: allow iff evaluate is True.
And.mentions_resource¶
def mentions_resource(self) -> bool
True if evaluating this requirement needs the request resource.
Used by require() to reject resource-scoped policies at wiring time
(they belong in authorize_resource/authorize), turning a runtime
500 into a clear startup error.
And.org_params¶
def org_params(self) -> frozenset[str]
Path parameters this requirement may read an organisation from.
Non-empty for an atom that resolves an organisation (path parameter,
X-Tenant-Id, else the bound tenant): create_app then refuses a
route that solves the gate before its TenantScope() (SANDLOOM-B007).
And.contracts¶
def contracts(self) -> frozenset[str]
Sync contracts evaluating this requirement may call (wiring time).
A module's derived consumes includes them (optional), so the
startup preflight reports an unbound one and sandloom add deploy
binds it in a split deployment. Default: none (a claims-only atom).
And.risks¶
def risks(self) -> frozenset[str]
The framework risks a gate enforcing this requirement carries
(ADR-0009 item 6): {"admin"} for an admin role, else none. An
Or carries its branches' risks; a Not grants no authority.
And.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
authorize¶
async def authorize(
resource: Any,
policy: Requirement,
*,
ctx: UserContext,
request: Request | None = None,
db: AsyncSession | None = None,
status_code: int | None = None,
) -> None
Inline check over an already-loaded resource; raises on failure.
The escape hatch for cases that don't fit authorize_resource
(multiple resources, ownership known only after a non-trivial query, authz
interleaved with business logic). The endpoint owns the single fetch.
Raises AccessDenied with the decision's status (or the explicit
status_code override for a definite denial).
AuthzContext¶
class AuthzContext(
subject: UserContext,
*,
request: Request | None = None,
db: AsyncSession | None = None,
resource: Any = <object object>,
loader: Callable[[AuthzContext], Awaitable[Any]] | None = None,
source: str = 'http',
core: Any = None,
) -> None
Inputs available to a Requirement during evaluation.
AuthzContext.get_resource¶
async def get_resource(self) -> Any
Return the request resource, loading it once on first access.
Loading is lazy so that a policy like Perm("x") & Owner(...) never
fetches the resource when the cheap claim atom already fails.
AuthzContext.has_resource¶
has_resource (property)
AuthzContext.core¶
def core(self) -> Any
Resolve the bound CoreFacade, or None.
Order: an explicitly-passed core (non-HTTP paths pass core= to
enforce) → the app the request was served by. There is deliberately
no process-global fallback — the subsystem never reaches a global
singleton (that would break multi-app / test isolation). When no core is
bound, atoms that need one (live / fact / OrgRole) degrade
explicitly (indeterminate 501 / FactUnavailable).
Category¶
class Category
Mandatory denial categories (TZ §10.2).
ClaimRule¶
class ClaimRule(kind: str = 'truthy', values: frozenset[Any] = frozenset()) -> None
Declarative claim -> bool interpretation: wire-safe, no callables.
ClaimRule.one_of({"active", "trialing"}) is True iff the claim value is
one of the listed scalars; ClaimRule.truthy() is bool(value). The
owner describes its facts with these, so a consumer in another process
interprets the token claim EXACTLY like the owner does locally (one source
of truth). A rule that depends on the owner's settings (billing's
past_due grace) is a zero-argument factory on the fact
(AccessFact(claim_rule=factory)), serialized with the owner's current
settings at describe() time.
ClaimRule.one_of¶
@classmethod
def one_of(cls, values: Iterable[Any]) -> ClaimRule
ClaimRule.truthy¶
@classmethod
def truthy(cls) -> ClaimRule
ClaimRule.interpret¶
def interpret(self, value: Any) -> bool
ClaimRule.to_wire¶
def to_wire(self) -> dict[str, Any]
ClaimRule.from_wire¶
@classmethod
def from_wire(cls, raw: Any) -> ClaimRule | None
Parse a wire rule; None for anything not understood.
Fail closed: a consumer that cannot parse the rule never reads the claim and escalates to the owner's live answer instead.
current_context¶
async def current_context(
request: Request,
credentials: HTTPAuthorizationCredentials | None = Depends(dependency=<fastapi.security.http.HTTPBearer object>, use_cache=True, scope=None),
) -> UserContext
Authenticated subject from the Bearer access token (cached per request).
enforce¶
async def enforce(
policy: Requirement,
*,
subject: UserContext,
db: AsyncSession | None = None,
request: Request | None = None,
resource: Any = <object object>,
loader: Callable[[AuthzContext], Awaitable[Any]] | None = None,
source: str = 'internal',
core: Any = None,
policy_code: str | None = None,
) -> AccessDecision
Evaluate policy for subject; raise AccessDenied on deny.
The one chokepoint. HTTP/use-case/task adapters all call this. Returns the
allow AccessDecision on success so callers may inspect it.
Entitled¶
class Entitled(Requirement)(
fact_name: str,
*,
category: str | None = None,
code: str | None = None,
mode: str | None = None,
) -> None
Subject satisfies the registered access fact fact_name.
Args:
fact_name: name of anAccessFactregistered viacore.register_access_fact(...).category: denial category when the fact is False. Defaults to the fact's owndeny_category(billing's subscription facts declareCategory.PAYMENT_REQUIRED→ 402), elseforbidden(403). A resolver raisingFactDeniedpicks its own category.mode: force a resolution mode, overriding theACCESSsetting (for tests / special endpoints). Normally leftNone.
Entitled.contracts¶
def contracts(self) -> frozenset[str]
Sync contracts evaluating this requirement may call (wiring time).
A module's derived consumes includes them (optional), so the
startup preflight reports an unbound one and sandloom add deploy
binds it in a split deployment. Default: none (a claims-only atom).
Entitled.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Entitled.decide¶
async def decide(self, ctx: AuthzContext) -> AccessDecision
Structured result. Default: allow iff evaluate is True.
Entitled.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
ENTITLEMENTS¶
ENTITLEMENTS: str = 'entitlements'
(no docstring)
EntitlementsAdapter¶
class EntitlementsAdapter(Protocol)
Answers subscription / credit questions (SubscriptionActive/Credits).
EntitlementsAdapter.subscription_active¶
async def subscription_active(
self,
service: Any,
*,
tenant_type: str,
tenant_id: UUID,
ctx: AuthzContext,
) -> bool
EntitlementsAdapter.credit_balance¶
async def credit_balance(
self,
service: Any,
*,
tenant_type: str,
tenant_id: UUID,
ctx: AuthzContext,
) -> int
FactDenied¶
class FactDenied(Exception)(
reason: str,
*,
category: str = 'forbidden',
code: str | None = None,
) -> None
A resolver's definite denial for a reason other than "the fact is False".
Raised by a projection reader / live resolver when the fact is
inapplicable to the subject — e.g. organization.subscription.active
with no organization in context, or for an org the subject is not a
member of. sandloom.shared.authz.atoms_fact.Entitled maps it to a
definite deny with category (default forbidden, 403) instead of
the fact's deny_category — "you may not ask" is not "pay to unlock".
FactOwnerUnreachable¶
class FactOwnerUnreachable(FactUnavailable)
The fact's owner is bound but could not describe its facts (503).
Distinct from "not configured" (501): the deployment is correct, the owner is down. Never a silent allow or a definite deny.
FactUnavailable¶
class FactUnavailable(Exception)
A fact could not be resolved in any acceptable mode (a misconfiguration).
Raised when an explicitly-requested mode cannot be honored (live service not
bound, no projection reader) or auto exhausts every option. The atom
maps this to a policy_misconfigured decision — never a silent allow.
Feature¶
class Feature(_AccountClaimAtom)(
key: str,
*,
of: Account = <object object>,
claim_key: str = 'features',
) -> None
The account's plan enables the feature flag key (of=, §7.2).
Feature.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
lookup_access_fact¶
async def lookup_access_fact(ctx: AuthzContext, name: str) -> AccessFact | None
The fact name: the local registration, else the owner's public one.
Order: core.get_access_fact(name) (owner co-deployed) → every bound
<owner>.facts.v1 with no local provider (the owner named by the fact's
first dotted segment first), whose describe() is cached on the core for
REMOTE_FACT_TTL_SECONDS. None when no owner knows the fact (the
caller reports 501); FactOwnerUnreachable (503) when a bound owner
could not be asked and none of the others knows it.
make_rebuild_gate¶
def make_rebuild_gate(rebuilder: Any, *, name: str = '') -> StartupTask
A startup task that rebuilds an authz projection from its sources.
Captures the rebuilder supplied at registration in its closure (no
process-global / registry lookup at run time). Install as critical=True
so the process refuses traffic until the projection is populated — otherwise
an empty table makes every fact resolve False and the consumer
fail-closed-denies all callers. The rebuilder must be idempotent (the
protocol guarantees it), so re-running on boot is safe; for large
projections prefer sandloom projection rebuild <name> in the one-shot migrate
step with rebuild_on_startup=False.
MEMBERSHIP¶
MEMBERSHIP: str = 'membership'
(no docstring)
MembershipAdapter¶
class MembershipAdapter(Protocol)
Answers organization-role questions for OrgRole.
MembershipAdapter.has_role_at_least¶
async def has_role_at_least(
self,
service: Any,
*,
org_id: UUID,
user_id: UUID,
minimum: str,
ctx: AuthzContext,
) -> bool
MODE_AUTO¶
MODE_AUTO: str = 'auto'
(no docstring)
MODE_CLAIM¶
MODE_CLAIM: str = 'claim'
(no docstring)
MODE_LIVE¶
MODE_LIVE: str = 'live'
(no docstring)
MODE_PROJECTION¶
MODE_PROJECTION: str = 'projection'
(no docstring)
Not¶
class Not(Requirement)(inner: Requirement) -> None
Negation of a single requirement.
Negates only a definite answer: an indeterminate inner decision is
returned unchanged (deny), so ~Entitled("account.suspended") never
allows because the fact is unregistered or its service is down.
Not.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Not.decide¶
async def decide(self, ctx: AuthzContext) -> AccessDecision
Structured result. Default: allow iff evaluate is True.
Not.mentions_resource¶
def mentions_resource(self) -> bool
True if evaluating this requirement needs the request resource.
Used by require() to reject resource-scoped policies at wiring time
(they belong in authorize_resource/authorize), turning a runtime
500 into a clear startup error.
Not.org_params¶
def org_params(self) -> frozenset[str]
Path parameters this requirement may read an organisation from.
Non-empty for an atom that resolves an organisation (path parameter,
X-Tenant-Id, else the bound tenant): create_app then refuses a
route that solves the gate before its TenantScope() (SANDLOOM-B007).
Not.contracts¶
def contracts(self) -> frozenset[str]
Sync contracts evaluating this requirement may call (wiring time).
A module's derived consumes includes them (optional), so the
startup preflight reports an unbound one and sandloom add deploy
binds it in a split deployment. Default: none (a claims-only atom).
Not.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
Or¶
class Or(Requirement)(*parts: Requirement) -> None
Disjunction: any operand may hold. Short-circuits on first allow.
Only a genuinely satisfied operand allows — and one that does allows even
if a sibling is indeterminate (Role("admin") | Entitled("x") admits an
admin while the x fact is unregistered): the grant comes from the
satisfied operand, never from inverting or guessing the broken one. When
none allows and at least one operand was indeterminate, the disjunction is
indeterminate (the first such decision is returned, preserving its
category/status) — a broken branch is reported, not silently folded into a
plain 403.
Or.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Or.decide¶
async def decide(self, ctx: AuthzContext) -> AccessDecision
Structured result. Default: allow iff evaluate is True.
Or.mentions_resource¶
def mentions_resource(self) -> bool
True if evaluating this requirement needs the request resource.
Used by require() to reject resource-scoped policies at wiring time
(they belong in authorize_resource/authorize), turning a runtime
500 into a clear startup error.
Or.org_params¶
def org_params(self) -> frozenset[str]
Path parameters this requirement may read an organisation from.
Non-empty for an atom that resolves an organisation (path parameter,
X-Tenant-Id, else the bound tenant): create_app then refuses a
route that solves the gate before its TenantScope() (SANDLOOM-B007).
Or.contracts¶
def contracts(self) -> frozenset[str]
Sync contracts evaluating this requirement may call (wiring time).
A module's derived consumes includes them (optional), so the
startup preflight reports an unbound one and sandloom add deploy
binds it in a split deployment. Default: none (a claims-only atom).
Or.risks¶
def risks(self) -> frozenset[str]
The framework risks a gate enforcing this requirement carries
(ADR-0009 item 6): {"admin"} for an admin role, else none. An
Or carries its branches' risks; a Not grants no authority.
Or.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
Perm¶
class Perm(Requirement)(perm: str) -> None
Subject holds the fine-grained permission perm.
The permission is stored as perm — code stays the machine-readable
denial code of the atom ("Perm"), not the permission name.
Perm.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Perm.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
Permissions¶
class Permissions
Predefined fine-grained permission codes.
PolicyIndeterminate¶
class PolicyIndeterminate(Exception)(decision: AccessDecision) -> None
Raised by an atom's evaluate when it cannot answer at all.
evaluate is boolean, so an atom that cannot evaluate (service absent,
fact unregistered, no tenant context) must not answer False — a caller
composing not await atom.evaluate(ctx) would turn the failure into an
allow. It raises this instead; Requirement.decide (and enforce)
convert it into the carried indeterminate AccessDecision. Never an
HTTPException — atoms run in workers too.
register_authz_projection¶
def register_authz_projection(
core: Any,
*,
descriptor: Any,
consumers: list[Any],
rebuilder: Any,
rebuild_on_startup: bool = False,
critical: bool = True,
) -> None
Register an authorization projection, its consumers and rebuilder.
descriptor is a QueryProjectionDescriptor; consumers a list of
ProjectionConsumerDescriptor (one per source channel); rebuilder a
ProjectionRebuilder. When rebuild_on_startup is True a rebuild gate
is installed (critical controls whether its failure aborts startup).
register_live_adapter¶
def register_live_adapter(kind: str, adapter: Any) -> None
Bind the adapter the live atoms use for kind — CONSUMER-side config.
kind is ENTITLEMENTS or MEMBERSHIP; adapter must
satisfy EntitlementsAdapter / MembershipAdapter.
The table is per PROCESS and is read where the atom is EVALUATED — the
consumer. Register an adapter from the consumer's composition root or the
consuming module's register(core), in every process that evaluates
Credits / SubscriptionActive / OrgRole. A provider module that
registers an adapter for its OWN contract only affects processes that
load it: in a split deployment the consumer process silently keeps the
default — so that is logged as a warning.
Requirement¶
class Requirement(ABC)
An authorization predicate, composable with & | ~.
evaluate is the primitive boolean check (it raises
PolicyIndeterminate when it cannot answer). decide wraps it
into a structured AccessDecision (the engine/enforcement path uses
decide so a denial carries a machine-readable code/category and the
failing requirement); the two never disagree.
Requirement.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Requirement.decide¶
async def decide(self, ctx: AuthzContext) -> AccessDecision
Structured result. Default: allow iff evaluate is True.
Requirement.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
Requirement.mentions_resource¶
def mentions_resource(self) -> bool
True if evaluating this requirement needs the request resource.
Used by require() to reject resource-scoped policies at wiring time
(they belong in authorize_resource/authorize), turning a runtime
500 into a clear startup error.
Requirement.org_params¶
def org_params(self) -> frozenset[str]
Path parameters this requirement may read an organisation from.
Non-empty for an atom that resolves an organisation (path parameter,
X-Tenant-Id, else the bound tenant): create_app then refuses a
route that solves the gate before its TenantScope() (SANDLOOM-B007).
Requirement.contracts¶
def contracts(self) -> frozenset[str]
Sync contracts evaluating this requirement may call (wiring time).
A module's derived consumes includes them (optional), so the
startup preflight reports an unbound one and sandloom add deploy
binds it in a split deployment. Default: none (a claims-only atom).
Requirement.risks¶
def risks(self) -> frozenset[str]
The framework risks a gate enforcing this requirement carries
(ADR-0009 item 6): {"admin"} for an admin role, else none. An
Or carries its branches' risks; a Not grants no authority.
resolve_fact¶
async def resolve_fact(
fact: AccessFact,
ctx: AuthzContext,
*,
mode: str | None = None,
) -> bool
Resolve fact to a boolean, caching the result on ctx.
mode overrides the configured mode (used by tests and to force a path);
otherwise the mode comes from settings, clamped to the fact's floor.
Role¶
class Role(Requirement)(role: str) -> None
Subject holds the global role role.
The role is stored as role — code stays the machine-readable denial
code of the atom ("Role"), not the role name.
Role.evaluate¶
async def evaluate(self, ctx: AuthzContext) -> bool
Return True iff the subject satisfies this requirement.
Raise PolicyIndeterminate (never return False) when the
requirement cannot be evaluated at all.
Role.risks¶
def risks(self) -> frozenset[str]
The framework risks a gate enforcing this requirement carries
(ADR-0009 item 6): {"admin"} for an admin role, else none. An
Or carries its branches' risks; a Not grants no authority.
Role.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.
Roles¶
class Roles
Predefined global role codes.
service_subject¶
def service_subject(*, roles: frozenset[str] = frozenset({'service'})) -> UserContext
A subject representing another service calling across a module boundary.
SOURCE_HTTP¶
SOURCE_HTTP: str = 'http'
(no docstring)
SOURCE_INTERNAL¶
SOURCE_INTERNAL: str = 'internal'
(no docstring)
SOURCE_SYSTEM¶
SOURCE_SYSTEM: str = 'system'
(no docstring)
SOURCE_TASK¶
SOURCE_TASK: str = 'task'
(no docstring)
system_subject¶
def system_subject(*, roles: frozenset[str] = frozenset({'system'})) -> UserContext
A subject representing the platform itself (trusted background work).
TIER_CLAIM¶
TIER_CLAIM: int = 1
(no docstring)
TIER_LIVE¶
TIER_LIVE: int = 3
(no docstring)
TIER_RESOURCE¶
TIER_RESOURCE: int = 2
(no docstring)
WithinLimit¶
class WithinLimit(_AccountClaimAtom)(
key: str,
used: int,
*,
of: Account = <object object>,
claim_key: str = 'limits',
) -> None
used is strictly below the account's plan limit for key.
For Account.user() the limit comes from the token claim; used is
the current count the caller already knows (e.g. number of seats). Absent
/ non-integer limit ⇒ treated as 0 (i.e. denied), so a missing entitlement
fails closed.
WithinLimit.describe¶
def describe(self) -> str
Human-readable label used in 403 detail messages.