POSIX ACL Python Bindings
The posix-acls-python crate provides Python bindings for POSIX ACL
(POSIX.1e) modeling via PyO3, allowing you to build,
parse, validate, and check POSIX ACLs 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 posix-acls-python crate
cd crates/posix-acls-python
# Build and install in development mode
maturin develop
# Or build a release wheel
maturin build --release
Quick Start
from posix_acls import PosixPerm, AclBuilder
# Build an ACL for a shared project directory
acl = (AclBuilder("alice", "devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.NONE.bits)
.add_user("bob", PosixPerm.RW.bits)
.build())
# Serialise to getfacl text format
print(acl.to_getfacl())
# Check access -- bob can read but not execute
result = acl.check_access("bob", ["staff"], PosixPerm.R.bits)
print(result) # Granted(effective=rw-)
print(bool(result)) # True
result = acl.check_access("bob", ["staff"], PosixPerm.X.bits)
print(bool(result)) # False
Differences from Rust API
The Python bindings closely mirror the Rust API with a few adjustments:
- Permission parameters are integers: builder methods and mutation
methods accept
int(the raw permission bits 0–7), notPosixPermobjects. UsePosixPerm.RWX.bitsto get the integer value. This avoids wrapping overhead on hot paths. - Special bits use booleans:
special_bits(suid, sgid, sticky)takes threeboolarguments instead of aSpecialBitsobject. - Builder pattern: all builder methods return
selffor fluent chaining, same as Rust. - Errors are ValueError: all error conditions (parse failures, invalid
ACL structure, policy errors, out-of-range permission bits) raise
ValueErrorwith a descriptive message. Permission bits are validated at the boundary: values outside 0–7 are rejected immediately. - No lifetime parameters: Python’s garbage collector handles memory.
API Reference
PosixPerm
The rwx permission triple. Immutable, hashable, and equality-comparable.
PosixPerm(read: bool = False, write: bool = False, execute: bool = False)
Parameters:
read: set the read bitwrite: set the write bitexecute: set the execute bit
Class attributes (constants):
PosixPerm.NONE,PosixPerm.R,PosixPerm.W,PosixPerm.X,PosixPerm.RW,PosixPerm.RX,PosixPerm.WX,PosixPerm.RWX
Properties (read-only):
read(bool): whether the read bit is setwrite(bool): whether the write bit is setexecute(bool): whether the execute bit is setbits(int): the raw 3-bit value (0–7)
Methods:
contains(other: PosixPerm) -> bool
Check whether this permission set is a superset of other.
mask_with(mask: PosixPerm) -> PosixPerm
Apply an effective-rights mask (bitwise AND).
Operators:
p1 & p2: bitwise AND (intersection)p1 | p2: bitwise OR (union)bool(p):Trueif notNONEp1 == p2: equality comparisonhash(p): hashable (can be used as dict key)
Example:
from posix_acls import PosixPerm
# Constants
print(PosixPerm.RWX) # rwx
print(PosixPerm.R) # r--
print(PosixPerm.NONE) # ---
# Constructor
rw = PosixPerm(read=True, write=True)
print(rw) # rw-
# Operators
print(PosixPerm.R | PosixPerm.W) # rw-
print(PosixPerm.RWX & PosixPerm.RX) # r-x
# Containment
print(PosixPerm.RWX.contains(PosixPerm.R)) # True
print(PosixPerm.R.contains(PosixPerm.W)) # False
# Bits for passing to builders
print(PosixPerm.RWX.bits) # 7
print(PosixPerm.R.bits) # 4
# Truthiness
print(bool(PosixPerm.NONE)) # False
print(bool(PosixPerm.R)) # True
SpecialBits
SUID, SGID, and sticky bits. Immutable and hashable.
SpecialBits(suid: bool = False, sgid: bool = False, sticky: bool = False)
Parameters:
suid: set the set-user-ID bitsgid: set the set-group-ID bitsticky: set the sticky bit
Class attributes: SpecialBits.NONE
Properties (read-only):
suid(bool)sgid(bool)sticky(bool)
Operators:
bool(s):Trueif any bit is set
Example:
from posix_acls import SpecialBits
sgid = SpecialBits(sgid=True)
print(sgid) # -s-
print(sgid.sgid) # True
print(sgid.suid) # False
print(sgid.sticky) # False
print(bool(SpecialBits.NONE)) # False
print(bool(sgid)) # True
AclEntry
A single POSIX ACL entry (tag + permissions). Read-only – instances are
created internally by PosixAcl and returned from entries and
default_entries.
Properties (read-only):
tag(str): the ACL tag string (e.g.,"user::rwx","user:bob:rw-","mask::rwx")perms(PosixPerm): the permission triple
Operators:
e1 == e2: equality comparison
Example:
from posix_acls import AclBuilder, PosixPerm
acl = (AclBuilder("alice", "devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.R.bits)
.build())
for entry in acl.entries:
print(entry) # user::rwx, group::r-x, other::r--
AccessResult
Result of a POSIX ACL access check. Truthy when access is granted, so you
can use it directly in if statements.
Properties (read-only):
granted(bool): whether the requested permissions are fully heldmatched_tag(str): which ACL entry determined the resulteffective_perms(PosixPerm): actual permissions after masking
Operators:
bool(result): returnsgranted(soif result:works naturally)
Example:
from posix_acls import AclBuilder, PosixPerm
acl = (AclBuilder("alice", "devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.NONE.bits)
.add_user("bob", PosixPerm.RW.bits)
.build())
result = acl.check_access("alice", [], PosixPerm.RWX.bits)
print(result) # Granted(effective=rwx)
print(result.matched_tag) # user::
print(result.effective_perms) # rwx
result = acl.check_access("eve", [], PosixPerm.R.bits)
print(result) # Denied(effective=---)
if not result:
print("Access denied")
PosixAcl
A complete POSIX ACL for a file or directory.
PosixAcl(owner: str, owning_group: str)
Parameters:
owner: the file owner nameowning_group: the owning group name
Creates a minimal ACL with UserObj, GroupObj, and Other all set to
NONE.
Properties (read-only):
owner(str)owning_group(str)special_bits(SpecialBits)entries(list[AclEntry]): access ACL entriesdefault_entries(list[AclEntry]): default (inherited) ACL entriesmask(PosixPerm): the effective-rights maskis_extended(bool):Trueif named user/group entries exist
Mutation methods:
set_owner_perms(bits: int) -> None
Set the owner (user::) permissions. Raises ValueError if bits is
not in 0–7.
set_group_perms(bits: int) -> None
Set the owning group (group::) permissions. Raises ValueError if
bits is not in 0–7.
set_other_perms(bits: int) -> None
Set the other (other::) permissions. Raises ValueError if bits is
not in 0–7.
set_special_bits(suid: bool, sgid: bool, sticky: bool) -> None
Set the SUID/SGID/sticky bits.
add_user(name: str, bits: int) -> None
Add or update a named user entry. Raises ValueError if bits is not
in 0–7.
add_group(name: str, bits: int) -> None
Add or update a named group entry. Raises ValueError if bits is not
in 0–7.
remove_user(name: str) -> None
Remove a named user entry.
remove_group(name: str) -> None
Remove a named group entry.
set_mask(bits: int) -> None
Set the mask entry directly. Raises ValueError if bits is not in
0–7.
compute_mask() -> PosixPerm
Recompute the mask from current entries, set it, and return the new value.
Query methods:
validate() -> None
Check structural validity.
Raises: ValueError if the ACL is invalid (missing entries, missing mask,
etc.)
check_access(user: str, groups: list[str], required_bits: int) -> AccessResult
Run the POSIX.1e access-check algorithm.
user: the requesting user’s namegroups: the user’s group membershipsrequired_bits: permission bits to check (usePosixPerm.R.bitsetc.)
Returns: AccessResult
Raises: ValueError if required_bits is not in 0–7
to_mode() -> int
Convert to a Unix mode word (12-bit integer).
to_getfacl() -> str
Serialise to getfacl(1) text format.
Raises: ValueError if the ACL is invalid
Class methods:
PosixAcl.from_mode(owner: str, owning_group: str, mode: int) -> PosixAcl
Build a minimal ACL from a Unix mode word.
PosixAcl.from_getfacl(text: str) -> PosixAcl
Parse a getfacl(1) text block.
Raises: ValueError on parse failure
Example:
from posix_acls import PosixAcl, PosixPerm
# Build from mode word
acl = PosixAcl.from_mode("root", "root", 0o644)
print(f"mode = 0o{acl.to_mode():03o}") # mode = 0o644
# Parse getfacl text
text = """\
# owner: alice
# group: devs
user::rwx
user:carol:r--
group::r-x
mask::r-x
other::---
"""
parsed = PosixAcl.from_getfacl(text)
print(parsed.owner) # alice
print(parsed.is_extended) # True
print(parsed.mask) # r-x
# Direct construction
acl = PosixAcl("alice", "devs")
acl.set_owner_perms(PosixPerm.RWX.bits)
acl.set_group_perms(PosixPerm.RX.bits)
acl.set_other_perms(PosixPerm.NONE.bits)
acl.add_user("bob", PosixPerm.RW.bits)
acl.compute_mask()
acl.validate() # raises if invalid
AclBuilder
Fluent builder for PosixAcl. Auto-computes the mask entry when named
entries are present.
AclBuilder(owner: str, owning_group: str)
Parameters:
owner: the file owner nameowning_group: the owning group name
All builder methods accept int for permission bits (use PosixPerm.R.bits
etc.) and return self for fluent chaining. Permission bits are validated:
values outside 0–7 raise ValueError.
Methods:
All return AclBuilder for fluent chaining (raise ValueError if
bits is not in 0–7):
owner_perms(bits: int) -> AclBuilderowning_group_perms(bits: int) -> AclBuilderother_perms(bits: int) -> AclBuilderspecial_bits(suid: bool, sgid: bool, sticky: bool) -> AclBuilderadd_user(name: str, bits: int) -> AclBuilder: upserts (last call wins)add_group(name: str, bits: int) -> AclBuilder: upserts (last call wins)default_owner_perms(bits: int) -> AclBuilderdefault_owning_group_perms(bits: int) -> AclBuilderdefault_other_perms(bits: int) -> AclBuilderdefault_user(name: str, bits: int) -> AclBuilder: upserts (last call wins)default_group(name: str, bits: int) -> AclBuilder: upserts (last call wins)
build() -> PosixAcl
Build the PosixAcl. The mask is auto-computed when named entries are
present.
Returns: PosixAcl
Example:
from posix_acls import AclBuilder, PosixPerm
# Fluent builder pattern
acl = (AclBuilder("alice", "devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.NONE.bits)
.add_user("bob", PosixPerm.RW.bits)
.add_group("ops", PosixPerm.R.bits)
.build())
print(acl.owner) # alice
print(acl.is_extended) # True
print(acl.mask) # rwx (auto-computed)
acl.validate() # passes
# With default ACL (for directories)
acl = (AclBuilder("alice", "devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.NONE.bits)
.default_owner_perms(PosixPerm.RWX.bits)
.default_owning_group_perms(PosixPerm.RX.bits)
.default_other_perms(PosixPerm.NONE.bits)
.default_user("bob", PosixPerm.RW.bits)
.build())
print(len(acl.default_entries)) # 5 (user::, user:bob:, group::, mask::, other::)
UserEntry
A user in the user/group model.
UserEntry(name: str, groups: list[str] = [])
Parameters:
name: the user’s login namegroups: initial group memberships (default: empty)
Properties (read-only):
name(str)groups(list[str])
Methods:
add_group(group: str) -> None
Add a group membership.
Example:
from posix_acls import UserEntry
alice = UserEntry("alice", ["devs", "ops"])
print(alice.name) # alice
print(alice.groups) # ['devs', 'ops']
bob = UserEntry("bob")
bob.add_group("devs")
print(bob.groups) # ['devs']
GroupEntry
A group in the user/group model. Immutable.
GroupEntry(name: str)
Parameters:
name: the group name
Properties (read-only):
name(str)
Example:
from posix_acls import GroupEntry
devs = GroupEntry("devs")
print(devs.name) # devs
UserGroupModel
Directory of users and groups for policy validation.
UserGroupModel()
Creates an empty model.
Methods:
add_user(user: UserEntry) -> None
Add a user. Replaces any existing entry with the same name.
add_group(group: GroupEntry) -> None
Add a group. Replaces any existing entry with the same name.
get_user(name: str) -> UserEntry | None
Look up a user by name.
get_group(name: str) -> GroupEntry | None
Look up a group by name.
groups_of(user: str) -> list[str]
Return the group memberships of a user, or an empty list if the user is not in the model.
Operators:
len(model): total count of users and groupsbool(model):Trueif not empty
Example:
from posix_acls import UserEntry, GroupEntry, UserGroupModel
model = UserGroupModel()
model.add_user(UserEntry("alice", ["devs"]))
model.add_user(UserEntry("bob", ["devs"]))
model.add_group(GroupEntry("devs"))
model.add_group(GroupEntry("ops"))
print(model.get_user("alice").name) # alice
print(model.groups_of("alice")) # ['devs']
print(model.get_user("unknown")) # None
print(len(model)) # 4
print(bool(model)) # True
AclPolicy
High-level ACL policy: assigns permissions to users and groups and derives
a concrete PosixAcl validated against a UserGroupModel. Immutable –
built via AclPolicyBuilder.
Class methods:
AclPolicy.builder() -> AclPolicyBuilder
Create a new builder.
Methods:
generate(model: UserGroupModel) -> PosixAcl
Generate a PosixAcl from this policy, validating that every named user
and group in the grant list exists in the model.
model: the user/group directory to validate against
Returns: PosixAcl
Raises: ValueError if a named user or group is absent from the model
Example:
from posix_acls import (
PosixPerm, UserEntry, GroupEntry, UserGroupModel, AclPolicy
)
model = UserGroupModel()
model.add_user(UserEntry("alice", ["devs"]))
model.add_user(UserEntry("bob", ["devs"]))
model.add_group(GroupEntry("devs"))
policy = (AclPolicy.builder()
.owner("alice")
.owning_group("devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.NONE.bits)
.add_user("bob", PosixPerm.RW.bits)
.build())
acl = policy.generate(model)
acl.validate()
print(acl.owner) # alice
print(acl.is_extended) # True
AclPolicyBuilder
Fluent builder for AclPolicy. Idiomatically reached via
AclPolicy.builder().
AclPolicyBuilder()
All builder methods accept int for permission bits and return self for
fluent chaining. Permission bits are validated: values outside 0–7 raise
ValueError.
Methods:
All return AclPolicyBuilder for fluent chaining (raise ValueError if
bits is not in 0–7):
owner(owner: str) -> AclPolicyBuilderowning_group(group: str) -> AclPolicyBuilderowner_perms(bits: int) -> AclPolicyBuilderowning_group_perms(bits: int) -> AclPolicyBuilderother_perms(bits: int) -> AclPolicyBuilderadd_user(name: str, bits: int) -> AclPolicyBuilder: upserts (last call wins)add_group(name: str, bits: int) -> AclPolicyBuilder: upserts (last call wins)default_owner_perms(bits: int) -> AclPolicyBuilderdefault_owning_group_perms(bits: int) -> AclPolicyBuilderdefault_other_perms(bits: int) -> AclPolicyBuilderdefault_user(name: str, bits: int) -> AclPolicyBuilder: upserts (last call wins)default_group(name: str, bits: int) -> AclPolicyBuilder: upserts (last call wins)
build() -> AclPolicy
Validate and build the policy. Requires owner and owning_group to be
set.
Returns: AclPolicy
Raises: ValueError if owner or owning_group is missing
Examples
Building and Checking Access
from posix_acls import PosixPerm, AclBuilder
acl = (AclBuilder("alice", "devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.NONE.bits)
.add_user("bob", PosixPerm.RW.bits)
.add_group("ops", PosixPerm.R.bits)
.build())
# Owner bypasses the mask
result = acl.check_access("alice", [], PosixPerm.RWX.bits)
print(result.granted) # True
print(result.matched_tag) # user::
# Named user is masked
result = acl.check_access("bob", ["staff"], PosixPerm.R.bits)
print(result.granted) # True
print(result.effective_perms) # rw-
# Group match
result = acl.check_access("charlie", ["ops"], PosixPerm.R.bits)
print(result.granted) # True
# Other fallback -- denied
result = acl.check_access("eve", [], PosixPerm.R.bits)
print(result.granted) # False
Parsing and Round-Tripping
from posix_acls import PosixAcl
text = """\
# owner: alice
# group: devs
# flags: -s-
user::rwx
user:bob:rw-
group::r-x
group:ops:r--
mask::rwx
other::r--
"""
# Parse
acl = PosixAcl.from_getfacl(text)
print(acl.owner) # alice
print(acl.special_bits) # -s-
# Round-trip
text2 = acl.to_getfacl()
acl2 = PosixAcl.from_getfacl(text2)
print(acl2.owner) # alice
print(len(acl2.entries)) # 6
Mode Word Conversion
from posix_acls import PosixAcl, AclBuilder, PosixPerm
# Build from a mode word
acl = PosixAcl.from_mode("alice", "devs", 0o755)
print(f"0o{acl.to_mode():03o}") # 0o755
# Extended ACLs: stat() group bits reflect the mask
acl = (AclBuilder("alice", "devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.R.bits)
.add_user("bob", PosixPerm.RW.bits)
.build())
print(f"0o{acl.to_mode():03o}") # 0o774 (group bits = mask = rwx)
Policy-Driven ACLs
from posix_acls import (
PosixPerm, UserEntry, GroupEntry, UserGroupModel, AclPolicy
)
# Define the organisation
model = UserGroupModel()
model.add_user(UserEntry("alice", ["devs"]))
model.add_user(UserEntry("bob", ["devs"]))
model.add_user(UserEntry("carol", ["ops"]))
model.add_group(GroupEntry("devs"))
model.add_group(GroupEntry("ops"))
# Build a policy
policy = (AclPolicy.builder()
.owner("alice")
.owning_group("devs")
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.NONE.bits)
.add_user("bob", PosixPerm.RW.bits)
.add_group("ops", PosixPerm.R.bits)
.build())
# Generate a concrete ACL
acl = policy.generate(model)
acl.validate()
print(acl.to_getfacl())
# Validation catches unknown principals
bad_policy = (AclPolicy.builder()
.owner("alice")
.owning_group("devs")
.add_user("unknown", PosixPerm.R.bits)
.build())
try:
bad_policy.generate(model)
except ValueError as e:
print(f"Policy error: {e}") # unknown user: "unknown"
Default ACLs for Directories
from posix_acls import AclBuilder, PosixPerm
# A shared project directory with inheritable permissions
acl = (AclBuilder("alice", "devs")
# Access ACL (the directory itself)
.owner_perms(PosixPerm.RWX.bits)
.owning_group_perms(PosixPerm.RX.bits)
.other_perms(PosixPerm.NONE.bits)
.add_user("bob", PosixPerm.RW.bits)
# Default ACL (inherited by new files)
.default_owner_perms(PosixPerm.RWX.bits)
.default_owning_group_perms(PosixPerm.RX.bits)
.default_other_perms(PosixPerm.NONE.bits)
.default_user("bob", PosixPerm.RW.bits)
.special_bits(False, True, False) # SGID for group inheritance
.build())
print(f"Access entries: {len(acl.entries)}")
print(f"Default entries: {len(acl.default_entries)}")
print(f"SGID: {acl.special_bits.sgid}") # True
print(acl.to_getfacl())
Error Handling
All errors from the Rust layer are raised as ValueError with a
descriptive message:
from posix_acls import PosixAcl, AclBuilder, AclPolicy, PosixPerm
# Out-of-range permission bits
try:
AclBuilder("alice", "devs").owner_perms(8) # bits must be 0-7
except ValueError as e:
print(f"Validation error: {e}") # permission bits must be 0-7, got 8
# Parse error
try:
PosixAcl.from_getfacl("not valid acl text")
except ValueError as e:
print(f"Parse error: {e}")
# Validation error
acl = PosixAcl("alice", "devs")
acl.set_owner_perms(PosixPerm.RWX.bits)
acl.set_group_perms(PosixPerm.RX.bits)
acl.set_other_perms(PosixPerm.NONE.bits)
acl.add_user("bob", PosixPerm.RW.bits)
# Forgot to compute/set mask
try:
acl.validate()
except ValueError as e:
print(f"Validation error: {e}")
# Policy builder error
try:
AclPolicy.builder().build() # no owner or owning_group
except ValueError as e:
print(f"Build error: {e}")
Performance
The Python bindings use PyO3’s zero-cost abstractions:
- Native speed: ACL construction, access checks, and parsing all run at Rust performance.
- Minimal overhead: type conversions happen only at the Python/Rust boundary. Permission parameters are raw integers to avoid wrapping overhead.
- Zero-copy where possible: string properties like
ownerandowning_groupare returned as borrowed references when the GIL allows.
For high-throughput scenarios, build your ACL or policy once and reuse it
for multiple access checks. The AclBuilder and AclPolicyBuilder
produce validated ACLs in a single call, so prefer them over incremental
PosixAcl mutation.
See Also
- posix-acls: POSIX ACL Modeling – the Rust crate documentation with detailed coverage of permissions, access checks, and the policy layer.
- Permissions – lattice algebra and the
rwxpermission model. - Access Checks – the POSIX.1e access-check algorithm in detail.
- Python Bindings: ACLS – Python bindings for the core acls-rs crate.