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

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), not PosixPerm objects. Use PosixPerm.RWX.bits to get the integer value. This avoids wrapping overhead on hot paths.
  • Special bits use booleans: special_bits(suid, sgid, sticky) takes three bool arguments instead of a SpecialBits object.
  • Builder pattern: all builder methods return self for fluent chaining, same as Rust.
  • Errors are ValueError: all error conditions (parse failures, invalid ACL structure, policy errors, out-of-range permission bits) raise ValueError with 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 bit
  • write: set the write bit
  • execute: 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 set
  • write (bool): whether the write bit is set
  • execute (bool): whether the execute bit is set
  • bits (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): True if not NONE
  • p1 == p2: equality comparison
  • hash(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 bit
  • sgid: set the set-group-ID bit
  • sticky: set the sticky bit

Class attributes: SpecialBits.NONE

Properties (read-only):

  • suid (bool)
  • sgid (bool)
  • sticky (bool)

Operators:

  • bool(s): True if 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 held
  • matched_tag (str): which ACL entry determined the result
  • effective_perms (PosixPerm): actual permissions after masking

Operators:

  • bool(result): returns granted (so if 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 name
  • owning_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 entries
  • default_entries (list[AclEntry]): default (inherited) ACL entries
  • mask (PosixPerm): the effective-rights mask
  • is_extended (bool): True if 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 name
  • groups: the user’s group memberships
  • required_bits: permission bits to check (use PosixPerm.R.bits etc.)

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 name
  • owning_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) -> AclBuilder
  • owning_group_perms(bits: int) -> AclBuilder
  • other_perms(bits: int) -> AclBuilder
  • special_bits(suid: bool, sgid: bool, sticky: bool) -> AclBuilder
  • add_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) -> AclBuilder
  • default_owning_group_perms(bits: int) -> AclBuilder
  • default_other_perms(bits: int) -> AclBuilder
  • default_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 name
  • groups: 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 groups
  • bool(model): True if 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) -> AclPolicyBuilder
  • owning_group(group: str) -> AclPolicyBuilder
  • owner_perms(bits: int) -> AclPolicyBuilder
  • owning_group_perms(bits: int) -> AclPolicyBuilder
  • other_perms(bits: int) -> AclPolicyBuilder
  • add_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) -> AclPolicyBuilder
  • default_owning_group_perms(bits: int) -> AclPolicyBuilder
  • default_other_perms(bits: int) -> AclPolicyBuilder
  • default_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 owner and owning_group are 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