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

The acls-rs crate provides Python bindings via PyO3, allowing you to use the algebraically-correct permissions system from Python with native Rust performance.

Installation

Prerequisites

  • Python 3.8 or higher
  • Rust 1.70 or higher (for building from source)
  • maturin for building

Building from Source

# Install maturin
pip install maturin

# Navigate to the acls-python crate (Python bindings are in a separate crate)
cd crates/acls-python

# Build and install in development mode
maturin develop

# Or build a release wheel
maturin build --release

Quick Start

from acls_rs import AtomicPermission, RuleBuilder, AbacPolicy

# Create a policy
policy = AbacPolicy()

# Define permissions
read = AtomicPermission("file", "read")
write = AtomicPermission("file", "write")

# Create a rule: engineers can read files (fluent API)
rule = (RuleBuilder()
    .grant(read, {"department": "engineering"})
    .build())
policy.add_rule(rule)

# Evaluate against a context
context = {"department": "engineering"}
permissions = policy.evaluate(context)

for perm in permissions:
    print(f"Allowed: {perm}")

API Reference

AtomicPermission

Represents an atomic permission with a namespace (resource type) and action.

AtomicPermission(namespace: str, action: str)

Parameters:

  • namespace: The resource type (e.g., “file”, “user”, “document”)
  • action: The operation (e.g., “read”, “write”, “delete”)

Properties:

  • namespace (str): The namespace
  • action (str): The action

Example:

perm = AtomicPermission("file", "read")
print(perm)               # file:read
print(perm.namespace)   # file
print(perm.action)      # read

RuleBuilder

Builder for creating ABAC rules with grants and denials.

RuleBuilder()

Methods:

grant(permission, attributes) -> RuleBuilder

Add a grant condition to the rule. Returns self for method chaining.

  • permission (AtomicPermission): The permission to grant
  • attributes (dict[str, str]): Required attribute matches

Returns: RuleBuilder - Returns self to enable fluent chaining

deny(permission, attributes) -> RuleBuilder

Add a denial condition to the rule. Returns self for method chaining.

  • permission (AtomicPermission): The permission to deny
  • attributes (dict[str, str]): Required attribute matches

Returns: RuleBuilder - Returns self to enable fluent chaining

build()

Build the rule and consume the builder.

Returns: AttributeRule

Example:

# Fluent API for chaining method calls
rule = (RuleBuilder()
    .grant(
        AtomicPermission("file", "read"),
        {"department": "engineering"}
    )
    .deny(
        AtomicPermission("file", "delete"),
        {"level": "junior"}
    )
    .build())
policy.add_rule(rule)

Subject

Represents a user with optional role memberships and explicit permission grants/denials.

Subject(name: str)

Parameters:

  • name: The user’s identifier

Methods:

  • add_role(role: str) -> None: Add a role membership.
  • grant(perm: AtomicPermission) -> None: Explicitly grant a permission to this subject.
  • revoke(perm: AtomicPermission) -> None: Remove an explicit grant.
  • deny(perm: AtomicPermission) -> None: Explicitly deny a permission (takes precedence over grants).
  • effective_permissions_at(timestamp: int) -> PermissionSet: The net effective permissions at a given millisecond timestamp (accounts for temporal grants/denials).

Properties:

  • effective_permissions (PermissionSet): The net effective permissions after applying all grants and denials.

Builder pattern:

user = (Subject.builder("alice")
    .role("developers")
    .grant(AtomicPermission("file", "read"))
    .build())

Example:

from acls_rs import Subject, AtomicPermission

user = Subject("alice")
user.add_role("developers")
user.grant(AtomicPermission("file", "read"))
user.deny(AtomicPermission("secrets", "read"))

perms = user.effective_permissions
print(AtomicPermission("file", "read") in perms)    # True
print(AtomicPermission("secrets", "read") in perms) # False

PermissionSet

An unordered set of AtomicPermission values with set-algebraic operations.

PermissionSet()

Methods:

  • insert(perm: AtomicPermission) -> None: Insert a permission.
  • remove(perm: AtomicPermission) -> bool: Remove a permission; returns True if it was present.
  • union(other: PermissionSet) -> PermissionSet: Return the union of two sets.
  • intersection(other: PermissionSet) -> PermissionSet: Return the intersection of two sets.
  • difference(other: PermissionSet) -> PermissionSet: Return permissions in self that are not in other.

PermissionSet supports in for membership tests and for perm in ps for iteration.

Builder pattern:

ps = (PermissionSet.builder()
    .insert(AtomicPermission("file", "read"))
    .insert(AtomicPermission("file", "write"))
    .build())

Example:

from acls_rs import PermissionSet, AtomicPermission

ps = PermissionSet()
ps.insert(AtomicPermission("file", "read"))
ps.insert(AtomicPermission("file", "write"))

other = PermissionSet()
other.insert(AtomicPermission("file", "write"))
other.insert(AtomicPermission("file", "delete"))

print(AtomicPermission("file", "read") in ps)   # True
print(list(ps.intersection(other)))              # [file:write]
removed = ps.remove(AtomicPermission("file", "read"))
print(removed)  # True

GrantDenialPair

Holds a paired set of granted and denied permissions. Denials always take precedence over grants.

from acls_rs import GrantDenialPair, PermissionSet

pair = GrantDenialPair(grants=PermissionSet(), denials=PermissionSet())

Methods:

  • grant(perm: AtomicPermission) -> None: Add a permission to the grants set.
  • revoke(perm: AtomicPermission) -> None: Remove a permission from the grants set.
  • deny(perm: AtomicPermission) -> None: Add a permission to the denials set.
  • undeny(perm: AtomicPermission) -> None: Remove a permission from the denials set.

Properties:

  • effective_permissions (PermissionSet): Grants minus denials.

Example:

from acls_rs import GrantDenialPair, PermissionSet, AtomicPermission

pair = GrantDenialPair(grants=PermissionSet(), denials=PermissionSet())
pair.grant(AtomicPermission("file", "read"))
pair.grant(AtomicPermission("file", "write"))
pair.deny(AtomicPermission("file", "write"))   # takes precedence

effective = pair.effective_permissions
print(AtomicPermission("file", "read") in effective)   # True
print(AtomicPermission("file", "write") in effective)  # False

TemporalPermission

An AtomicPermission with an optional validity window defined by millisecond timestamps.

TemporalPermission(perm: AtomicPermission, valid_from: int | None = None, valid_until: int | None = None)

Raises: ValueError if valid_from > valid_until.

Static factory methods:

  • TemporalPermission.valid_for_duration(perm, duration_ms: int) — active from now for duration_ms milliseconds.
  • TemporalPermission.valid_from(perm, from_ts: int) — active from from_ts onwards.
  • TemporalPermission.valid_until(perm, deadline: int) — active until deadline.

Properties:

  • is_currently_valid (bool): Active at the current wall-clock time.
  • permission (AtomicPermission): The wrapped permission.
  • start_time / not_before (int | None): The valid_from timestamp (both names are aliases for the same value).
  • end_time / not_after (int | None): The valid_until timestamp (both names are aliases for the same value).

Methods:

  • is_valid_at(timestamp: int) -> bool: Active at the given millisecond timestamp.

TemporalPermissionSet

A collection of TemporalPermission values. Evaluates which permissions are currently active or active at a given point in time.

from acls_rs import TemporalPermissionSet

tps = TemporalPermissionSet()

Methods:

  • add(tp: TemporalPermission) -> None: Add a temporal permission.
  • remove(perm: AtomicPermission) -> bool: Remove all temporal entries for perm; returns True if any were present.
  • effective_now() -> PermissionSet: Permissions that are currently valid.
  • effective_at(timestamp: int) -> PermissionSet: Permissions valid at the given millisecond timestamp.

Example:

import time
from acls_rs import TemporalPermission, TemporalPermissionSet, AtomicPermission

now = time.time_ns() // 1_000_000
perm = AtomicPermission("file", "read")

tp = TemporalPermission(perm, valid_from=now, valid_until=now + 3600000)

tps = TemporalPermissionSet()
tps.add(tp)

print(AtomicPermission("file", "read") in tps.effective_now())  # True
print(AtomicPermission("file", "read") in tps.effective_at(now + 7200000))  # False

PermissionDelta

Represents a set of changes (additions and removals) to a PermissionSet. Useful for computing and applying incremental permission updates.

from acls_rs import PermissionDeltaBuilder

delta = (PermissionDeltaBuilder()
    .grant(AtomicPermission("file", "write"))
    .remove(AtomicPermission("file", "read"))
    .build())

Methods:

  • apply_to(ps: PermissionSet) -> PermissionSet: Apply the delta to ps and return the resulting set.
  • invert() -> PermissionDelta: Return a delta that undoes this one.

Properties:

  • is_empty (bool): True if the delta contains no changes.
  • total_changes (int): Total number of individual add/remove operations.

PermissionEffect

Previews or applies the effect of a sequence of grant and revoke operations on a base PermissionSet.

from acls_rs import PermissionEffectBuilder, PermissionSet, AtomicPermission

builder = (PermissionEffectBuilder(PermissionSet())
    .grant(AtomicPermission("file", "write"))
    .revoke(AtomicPermission("file", "read")))

preview = builder.preview_effect()   # PermissionEffect (non-consuming preview)
has_changes = builder.has_changes  # bool
grants, effect = builder.build()     # tuple[GrantDenialPair, PermissionEffect]

Methods on the builder:

  • grant(perm: AtomicPermission) -> PermissionEffectBuilder: Stage a grant.
  • revoke(perm: AtomicPermission) -> PermissionEffectBuilder: Stage a revocation.
  • deny(perm: AtomicPermission) -> PermissionEffectBuilder: Stage a denial.
  • undeny(perm: AtomicPermission) -> PermissionEffectBuilder: Stage removal of a denial.
  • apply_delta(delta: PermissionDelta) -> PermissionEffectBuilder: Stage the grants/removals from a PermissionDelta.
  • preview_effect() -> PermissionEffect: Compute the effect without consuming the builder.
  • build() -> tuple[GrantDenialPair, PermissionEffect]: Finalise and return the pair plus the resulting effect.

Properties on the builder:

  • has_changes (bool): True if any grants or revocations have been staged.

The PermissionEffect result itself exposes:

  • grants_added, grants_removed, denials_added, denials_removed (PermissionSet): The individual components of the transition.
  • net_effective_change (PermissionSet): The overall change in effective permissions.
  • is_empty (bool): True if the effect represents no change.
  • total_changes_count (int): Total number of individual add/remove operations across grants and denials.
  • to_delta() -> PermissionDelta: Combined delta covering both grants and denials.
  • grants_delta() -> PermissionDelta: Delta covering only the grants side.
  • denials_delta() -> PermissionDelta: Delta covering only the denials side.
  • PermissionEffect.from_transition(before: GrantDenialPair, after: GrantDenialPair) -> PermissionEffect (static method): Compute the effect directly from two GrantDenialPair snapshots, without a builder.

PermissionMapping

Maps bitmask values to named AtomicPermission values. Different object types need different mappings because the same bit positions carry different semantics.

from acls_rs import PermissionMapping

Constructor:

PermissionMapping(namespace: str)

Methods:

  • add(bit: int, action: str) -> PermissionMapping: Map a bit value to an action. Returns self for chaining.
  • to_permission_set(bits: int) -> PermissionSet: Convert a bitmask to a PermissionSet.
  • from_permission_set(perms: PermissionSet) -> int: Convert a PermissionSet back to a bitmask.

Properties:

  • namespace (str): The namespace for mapped permissions.

Static factory methods:

  • PermissionMapping.posix() -> PermissionMapping: Pre-built POSIX mapping (namespace "posix", bits: 4=read, 2=write, 1=execute).

Example:

from acls_rs import PermissionMapping, AtomicPermission

# Custom mapping
custom = (PermissionMapping("myapp")
    .add(0x01, "view")
    .add(0x02, "edit")
    .add(0x04, "admin"))

perms = custom.to_permission_set(0x05)  # view + admin
assert custom.from_permission_set(perms) == 0x05

# POSIX mapping
posix = PermissionMapping.posix()
perms = posix.to_permission_set(0b101)  # read + execute
assert AtomicPermission("posix", "read") in perms
assert AtomicPermission("posix", "execute") in perms

PermissionPreview

Preview what permissions a subject would have after a set of changes. Supports the context manager protocol for scoped preview.

from acls_rs import PermissionPreview, Subject, AtomicPermission

user = Subject("alice")
user.grant(AtomicPermission("file", "read"))

# Use as a context manager
with PermissionPreview(user) as preview:
    preview.grant(AtomicPermission("file", "write"))
    print(preview.would_have(AtomicPermission("file", "write")))  # True
    print(preview.added)   # permissions that would be added
    print(preview.removed) # permissions that would be removed

RbacPolicy

Role-Based Access Control policy. Used directly for role management and also consumed by ComposedPolicy in the hbac_rs and abac_rs modules.

from acls_rs import RbacPolicy, Role, AtomicPermission

rbac = RbacPolicy()

Methods:

  • add_role(role: Role) -> None: Add a role definition.
  • get_role(name: str) -> Role | None: Retrieve a role by name.
  • remove_role(name: str) -> Role | None: Remove and return a role.
  • roles() -> list[Role]: Return all roles.

Role

A named collection of granted and denied permissions, with optional parent role inheritance.

role = Role("developers",
    grants=[AtomicPermission("code", "read"), AtomicPermission("code", "write")],
    denials=[])

Parameters:

  • name (str): Role identifier
  • grants (list[AtomicPermission]): Permissions this role grants
  • denials (list[AtomicPermission]): Permissions this role explicitly denies

Methods:

  • with_parent(parent_name: str) -> Role: Declare a parent role for inheritance (returns self).

Example:

from acls_rs import RbacPolicy, Role, AtomicPermission

# Build role hierarchy
employee = Role("employees",
    grants=[AtomicPermission("office", "enter")],
    denials=[])

developer = (Role("developers",
    grants=[AtomicPermission("code", "read"), AtomicPermission("code", "write")],
    denials=[])
    .with_parent("employees"))

rbac = RbacPolicy()
rbac.add_role(employee)
rbac.add_role(developer)

print(rbac.get_role("developers") is not None)  # True

Getting Timestamps

All temporal APIs accept millisecond Unix timestamps. Use Python’s time module:

import time

now = time.time_ns() // 1_000_000
future = now + 3600 * 1000  # one hour from now

AbacPolicy

Attribute-Based Access Control policy engine.

AbacPolicy()

Methods:

add_rule(rule) -> AbacPolicy

Add a rule to the policy. Returns self for method chaining (fluent API).

  • rule (AttributeRule): The rule to add

Returns: AbacPolicy - Returns self to enable fluent chaining

evaluate(context)

Evaluate the policy against an attribute context.

  • context (dict[str, str]): The attribute context

Returns: list[AtomicPermission] - The effective permissions after conflict resolution

rule_count() -> int

The number of rules in the policy. Note this is a method call, not a property.

Returns: int

Example:

# Non-fluent style
policy = AbacPolicy()
policy.add_rule(rule1.build())
policy.add_rule(rule2.build())

# Fluent style - chain rule additions
policy = (AbacPolicy()
    .add_rule(rule1.build())
    .add_rule(rule2.build()))

context = {"department": "sales", "level": "manager"}
permissions = policy.evaluate(context)
print(f"Policy has {policy.rule_count()} rules")

Examples

Basic ABAC Evaluation

from acls_rs import AtomicPermission, RuleBuilder, AbacPolicy

policy = AbacPolicy()

# Define permissions
read = AtomicPermission("document", "read")
write = AtomicPermission("document", "write")

# Rule: Managers can read and write (fluent API)
manager_rule = (RuleBuilder()
    .grant(read, {"role": "manager"})
    .grant(write, {"role": "manager"})
    .build())
policy.add_rule(manager_rule)

# Rule: Employees can only read (fluent API)
employee_rule = (RuleBuilder()
    .grant(read, {"role": "employee"})
    .build())
policy.add_rule(employee_rule)

# Evaluate for a manager
manager_perms = policy.evaluate({"role": "manager"})
print([str(p) for p in manager_perms])
# ['document:read', 'document:write']

# Evaluate for an employee
employee_perms = policy.evaluate({"role": "employee"})
print([str(p) for p in employee_perms])
# ['document:read']

Multi-Attribute Matching

ABAC rules support multiple attributes. All attributes in the rule must match for the permission to apply.

from acls_rs import AtomicPermission, RuleBuilder, AbacPolicy

policy = AbacPolicy()

# Rule: Senior security engineers can access secrets (fluent API)
secret_rule = (RuleBuilder()
    .grant(
        AtomicPermission("secrets", "read"),
        {
            "department": "engineering",
            "team": "security",
            "level": "senior"
        }
    )
    .build())
policy.add_rule(secret_rule)

# All attributes match - access granted
context1 = {
    "department": "engineering",
    "team": "security",
    "level": "senior"
}
result1 = policy.evaluate(context1)
print(len(result1))  # 1

# Missing "team" attribute - access denied
context2 = {
    "department": "engineering",
    "level": "senior"
}
result2 = policy.evaluate(context2)
print(len(result2))  # 0

Denial Rules

Denials take precedence over grants in the default conflict resolution strategy.

from acls_rs import AtomicPermission, RuleBuilder, AbacPolicy

policy = AbacPolicy()
delete = AtomicPermission("file", "delete")

# Grant delete to admins (fluent API)
admin_rule = (RuleBuilder()
    .grant(delete, {"role": "admin"})
    .build())
policy.add_rule(admin_rule)

# But deny delete to read-only admins (fluent API)
readonly_rule = (RuleBuilder()
    .deny(delete, {"readonly": "true"})
    .build())
policy.add_rule(readonly_rule)

# Regular admin can delete
result1 = policy.evaluate({"role": "admin", "readonly": "false"})
print(len(result1))  # 1

# Read-only admin cannot delete (denial wins)
result2 = policy.evaluate({"role": "admin", "readonly": "true"})
print(len(result2))  # 0

Dynamic Role Assignment

from acls_rs import AtomicPermission, RuleBuilder, AbacPolicy

policy = AbacPolicy()

# Define permissions
read = AtomicPermission("api", "read")
write = AtomicPermission("api", "write")
admin = AtomicPermission("api", "admin")

# Viewers can read (fluent API)
viewer_rule = (RuleBuilder()
    .grant(read, {"role": "viewer"})
    .build())
policy.add_rule(viewer_rule)

# Editors can read and write (fluent API)
editor_rule = (RuleBuilder()
    .grant(read, {"role": "editor"})
    .grant(write, {"role": "editor"})
    .build())
policy.add_rule(editor_rule)

# Admins can do everything (fluent API)
admin_rule = (RuleBuilder()
    .grant(read, {"role": "admin"})
    .grant(write, {"role": "admin"})
    .grant(admin, {"role": "admin"})
    .build())
policy.add_rule(admin_rule)

# Evaluate for different roles
def check_permissions(role):
    perms = policy.evaluate({"role": role})
    print(f"{role}: {[str(p) for p in perms]}")

check_permissions("viewer")
# viewer: ['api:read']

check_permissions("editor")
# editor: ['api:read', 'api:write']

check_permissions("admin")
# admin: ['api:admin', 'api:read', 'api:write']

Performance

The Python bindings use PyO3’s zero-cost abstractions, providing:

  • Native speed: Policy evaluation runs at Rust performance
  • Minimal overhead: Type conversions happen only at the Python/Rust boundary
  • Memory efficiency: Rules and policies are stored in Rust memory

For high-throughput scenarios, consider:

  • Building your policy once and reusing it for multiple evaluations
  • Batching context evaluations if possible
  • Using the Rust API directly for maximum performance

See Also