Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 AuthorizationDetail objects, 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 datetime objects.
  • Errors raise subclasses of authz_details.AuthzDetailsError, itself a ValueError.
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

ExceptionRaised for
ValidationErrorA malformed detail, a schema violation, an unknown type, a bad schema or comparator
RuleErrorA malformed rule (missing type, bad JSONPath, bad time or operator), or a type mismatch while evaluating a matcher
PolicyErrorA duplicate rule name, an invalid rule, or the rule limit
TemporalErrorAn invalid validity window
SubsetError / DelegationErrorA 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