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”)

Methods:

  • namespace() → str: Get the namespace
  • action() → str: Get 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() -> PermissionSet: Return 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.
  • effective_permissions() -> PermissionSet: Return 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.

Methods:

  • is_currently_valid() -> bool: Active at the current wall-clock time.
  • 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:

from acls_rs import TemporalPermission, TemporalPermissionSet, AtomicPermission, current_timestamp_millis

now = current_timestamp_millis()
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.
  • 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.
  • preview_effect() -> PermissionEffect: Compute the effect without consuming the builder.
  • has_changes() -> bool: True if any grants or revocations have been staged.
  • build() -> tuple[GrantDenialPair, PermissionEffect]: Finalise and return the pair plus the resulting effect.

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

current_timestamp_millis

Utility function that returns the current wall-clock time as a millisecond Unix timestamp.

from acls_rs import current_timestamp_millis

now = current_timestamp_millis()
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()

Get the number of rules in the policy.

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