Python Bindings for authz-details-rs
Not every service in your stack is Rust. Your policy console, reporting
pipeline, or test harness may run on Python. The authz-details-python
crate wraps the Rust evaluation engine with PyO3, exposing parsing,
validation, rules, subset comparison, delegation and composition with
native performance.
Installing
pip install authz-details
Or build from source:
cd crates/authz-details-python
maturin develop
The package ships type stubs (py.typed), so editors and type checkers see
the full API.
The API
The Python API mirrors the Rust API and takes plain Python objects:
- Details are
AuthorizationDetailobjects, built from keyword members, a dict of members, or an RFC 9396 wire dict. Every function that takes a list of details also accepts plain{"type": ..., ...}dicts. - Schemas are dicts; comparators, rules and policies are Python objects with builders.
- Enumerations are real Python enums:
ComparisonOp.LESS_THAN_OR_EQUAL,MatchMode.ANY,Weekday.MONDAY,Verdict.NO_RULE,CompositionMode.AND. Where an operator is accepted, its symbol ("<=","not in") or name works too. - Timestamps are epoch milliseconds or timezone-aware
datetimeobjects. - Errors raise subclasses of
authz_details.AuthzDetailsError, itself aValueError.
from authz_details import AuthorizationDetail, parse_authorization_details
detail = AuthorizationDetail(
"payment_initiation",
actions=["initiate"],
instructedAmount={"currency": "EUR", "amount": "123.50"},
)
assert detail.actions == ["initiate"]
assert detail.to_dict()["type"] == "payment_initiation"
# The authorization_details request parameter, as sent on the wire
details = parse_authorization_details(
'[{"type": "payment_initiation", "actions": ["initiate"],'
' "instructedAmount": {"currency": "EUR", "amount": "123.50"}}]'
)
assert details == [detail]
Registering types and validating
A TypeRegistry compiles each JSON Schema once and checks comparator rules
when the type is registered. As a security measure, schemas must be
self-contained: a $ref to a URL or a file raises ValidationError instead
of being fetched (see
Schema Validation).
from authz_details import ComparatorConfig, TypeRegistry, TypeSchema
registry = TypeRegistry([
TypeSchema(
"payment_initiation",
{
"type": "object",
"required": ["actions", "instructedAmount"],
"properties": {
"actions": {"type": "array"},
"instructedAmount": {
"type": "object",
"properties": {
"amount": {"type": "string"},
"currency": {"type": "string"},
},
},
},
},
"Payment initiation authorization",
# Amounts may be reduced and actions narrowed in re-requests
ComparatorConfig()
.add_numeric_field("$.instructedAmount.amount", "CanReduce")
.add_array_field("$.actions", "CanNarrow"),
),
])
registry.validate(details) # raises authz_details.ValidationError on failure
Comparators can also be given in their dict form,
{"numeric_fields": [{"path": ..., "mode": ...}], "array_fields": [...]}.
Subset comparison
At the token endpoint, a re-request must be covered by the grant.
explain_subset says which field exceeded it:
from authz_details import AuthorizationDetail
granted = [AuthorizationDetail(
"payment_initiation",
actions=["initiate", "status"],
instructedAmount={"currency": "EUR", "amount": "1000.00"},
)]
smaller = AuthorizationDetail(
"payment_initiation",
actions=["initiate"],
instructedAmount={"currency": "EUR", "amount": "250.00"},
)
assert registry.is_subset([smaller], granted)
in_dollars = AuthorizationDetail(
"payment_initiation",
actions=["initiate"],
instructedAmount={"currency": "USD", "amount": "250.00"},
)
report = registry.explain_subset([in_dollars], granted)
assert not report
assert report.rejections[0].path == "$.instructedAmount.currency"
print(report.rejections[0]) # ...: $.instructedAmount.currency exceeds the grant
The free functions is_subset, explain_subset and compute_intersection
take an optional type_registry (a TypeRegistry or a list of
TypeSchema); without one, comparison is structural.
Delegation
subject_allowed = [AuthorizationDetail(
"payment_initiation",
actions=["initiate", "cancel"],
instructedAmount={"currency": "EUR", "amount": "10000.00"},
)]
actor_allowed = [AuthorizationDetail(
"payment_initiation",
actions=["initiate", "status"],
instructedAmount={"currency": "EUR", "amount": "100.00"},
)]
[effective] = registry.intersect(subject_allowed, actor_allowed)
assert effective.detail_object == {
"actions": ["initiate"],
"instructedAmount": {"currency": "EUR", "amount": "100.00"},
}
# Disjoint grants delegate nothing
assert registry.intersect(subject_allowed, [{"type": "web_search"}]) == []
Rules and policies
Rules are named and built fluently; policies manage them by name and report which rules matched:
from datetime import datetime, timezone
from authz_details import (
AuthzDetailsPolicy,
AuthzDetailsRule,
TemporalAuthzDetailsRule,
Verdict,
Weekday,
)
cap = (
AuthzDetailsRule.builder("eur-up-to-1000")
.type_id("payment_initiation")
.field("$.instructedAmount.amount").le(1000)
.field("$.instructedAmount.currency").eq("EUR")
.during("09:00", "17:00", days=Weekday.workdays())
.build()
)
policy = AuthzDetailsPolicy([cap], max_rules=100)
monday_10am = datetime(2024, 1, 15, 10, 0, tzinfo=timezone.utc)
result = policy.evaluate_at([smaller], monday_10am)
assert result.is_allowed() and result.verdict == Verdict.ALLOW
assert result.matched_rules == ["eur-up-to-1000"]
# Rules can expire, like hbac_rs.TemporalHbacRule
mandate = (
AuthzDetailsRule.builder("trip-mandate")
.type_id("payment_initiation")
.field("$.instructedAmount.amount").le(600)
.build()
)
policy.add_temporal_rule(
TemporalAuthzDetailsRule(mandate, valid_until=datetime(2024, 2, 1, tzinfo=timezone.utc))
)
assert policy.disable_rule("eur-up-to-1000")
assert len(policy) == 2
Rules round-trip through to_dict() / AuthzDetailsRule.from_dict(), so
policies can be stored as configuration. FieldMatcher objects
(FieldMatcher("$.amount", "<=", 1000), FieldMatcher.is_in(...)) and
TimeWindow objects can also be evaluated on their own.
Composition with acls-rs
ComposedPolicy combines a rule policy with an acls_rs.RbacPolicy.
Subjects may be acls_rs.Subject or hbac_rs.Subject objects, and the
modules can be imported in any order:
from acls_rs import AtomicPermission, RbacPolicy, Role, Subject
from authz_details import ComposedPolicy, CompositionMode
rbac = RbacPolicy()
rbac.add_role(Role("payer", [AtomicPermission("payment_initiation", "initiate")], []))
composed = ComposedPolicy(AuthzDetailsPolicy([cap]), rbac, CompositionMode.AND)
alice = Subject("alice")
alice.add_role("payer")
assert composed.check_access_at(alice, [smaller], monday_10am)
CompositionMode also offers the and_mode() / or_mode() /
details_first() / rbac_first() constructors used by hbac_rs and
abac_rs. For the semantics of the four modes, see the
acls-rs Bridge.
Composition with HBAC
HostScopedPolicy combines an hbac_rs.HbacPolicy (host/service access)
with a rule policy under a CombineMode (AND, OR, PREFER_FIRST for
HBAC first, PREFER_SECOND for the rules first):
import hbac_rs
from authz_details import CombineMode, HostScopedPolicy
hbac = hbac_rs.HbacPolicy()
hbac.add_rule(
hbac_rs.HbacRuleBuilder("payers-on-bank-api")
.user_group("payer")
.host("api.bank.example")
.service("https")
.enabled(True)
.build()
)
host_scoped = HostScopedPolicy(hbac, AuthzDetailsPolicy([cap]), CombineMode.AND)
assert host_scoped.check_access_at(alice, "api.bank.example", "https", [smaller], monday_10am)
assert not host_scoped.check_access_at(alice, "other.example", "https", [smaller], monday_10am)
See HBAC Integration for the semantics.
Error handling
| Exception | Raised for |
|---|---|
ValidationError | A malformed detail, a schema violation, an unknown type, a bad schema or comparator |
RuleError | A malformed rule (missing type, bad JSONPath, bad time or operator), or a type mismatch while evaluating a matcher |
PolicyError | A duplicate rule name, an invalid rule, or the rule limit |
TemporalError | An invalid validity window |
SubsetError / DelegationError | A broken comparator encountered during comparison or delegation |
All derive from AuthzDetailsError, which derives from ValueError.
Policy outcomes are values, not exceptions: a denied request is a falsy
result, an uncovered re-request is a falsy SubsetReport, and a disjoint
delegation is an empty list.
Where to go next
- The Rust reference: authz-details-rs, with topic pages on validation, matching, rules, temporal rules, subset comparison, delegation, and the acls-rs bridge.
- Runnable scripts in
crates/authz-details-python/examples/:agent_linux_workstation.py(a coding agent on a Linux host),agent_web_delegation.py(a travel agent delegating payments to a sub-agent),payment_authorization.py,composed_policy.py, andtest_api.py; see Agentic workflows.