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 namespaceaction() → 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 grantattributes(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 denyattributes(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; returnsTrueif 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 inselfthat are not inother.
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 forduration_msmilliseconds.TemporalPermission.valid_from(perm, from_ts: int)— active fromfrom_tsonwards.TemporalPermission.valid_until(perm, deadline: int)— active untildeadline.
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 forperm; returnsTrueif 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 topsand 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 identifiergrants(list[AtomicPermission]): Permissions this role grantsdenials(list[AtomicPermission]): Permissions this role explicitly denies
Methods:
with_parent(parent_name: str) -> Role: Declare a parent role for inheritance (returnsself).
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
- ABAC Policies in acls-rs - Core ABAC concepts
- ABAC Rules - Detailed rule semantics
- API Documentation - Full Rust API reference