Building ACLs
Permissions alone are not useful until they are attached to principals. A
POSIX ACL is a list of entries, each pairing a tag (which principal?) with a
permission triple (what can they do?). This page covers the tag types, entry
structure, the central PosixAcl type, and the fluent AclBuilder.
ACL tags
AclTag identifies which principal an ACL entry applies to. POSIX.1e
defines six tag types:
#![allow(unused)]
fn main() {
use posix_acls::AclTag;
// Three mandatory tags -- every valid ACL has exactly one of each
let owner = AclTag::UserObj; // user::
let group = AclTag::GroupObj; // group::
let other = AclTag::Other; // other::
// Two named tags -- these make the ACL "extended"
let alice = AclTag::User("alice".to_string()); // user:alice:
let devs = AclTag::Group("devs".to_string()); // group:devs:
// The mask tag -- required when named entries are present
let mask = AclTag::Mask; // mask::
assert!(alice.is_named());
assert!(!owner.is_named());
assert!(owner.is_mandatory());
assert!(!mask.is_mandatory());
}
| Tag | getfacl syntax | Required? |
|---|---|---|
UserObj | user::perms | Always |
User(name) | user:name:perms | Makes ACL extended |
GroupObj | group::perms | Always |
Group(name) | group:name:perms | Makes ACL extended |
Mask | mask::perms | Required when extended |
Other | other::perms | Always |
ACL entries
An AclEntry pairs a tag with a PosixPerm:
#![allow(unused)]
fn main() {
use posix_acls::{AclEntry, AclTag, PosixPerm};
let entry = AclEntry::new(
AclTag::User("bob".to_string()),
PosixPerm::RW,
);
assert_eq!(entry.tag, AclTag::User("bob".to_string()));
assert_eq!(entry.perms, PosixPerm::RW);
assert_eq!(entry.to_string(), "user:bob:rw-");
}
Both tag and perms are public fields – you can inspect and modify them
directly.
PosixAcl
PosixAcl is the central type: a complete ACL for a file or directory.
It carries an owner name, an owning group name, optional special bits,
access entries, and optional default entries (for directories).
The constructor creates a minimal ACL with UserObj, GroupObj, and Other
all set to NONE:
#![allow(unused)]
fn main() {
use posix_acls::{PosixAcl, PosixPerm};
let mut acl = PosixAcl::new("alice", "devs");
acl.set_owner_perms(PosixPerm::RWX);
acl.set_group_perms(PosixPerm::RX);
acl.set_other_perms(PosixPerm::R);
assert_eq!(acl.owner(), "alice");
assert_eq!(acl.owning_group(), "devs");
assert!(!acl.is_extended());
assert!(acl.is_valid().is_ok());
}
Adding named entries makes the ACL extended and requires a mask:
#![allow(unused)]
fn main() {
use posix_acls::{PosixAcl, PosixPerm};
let mut acl = PosixAcl::new("alice", "devs");
acl.set_owner_perms(PosixPerm::RWX);
acl.set_group_perms(PosixPerm::RX);
acl.set_other_perms(PosixPerm::NONE);
acl.add_user("bob", PosixPerm::RW);
// The mask must be set explicitly when using PosixAcl directly
let mask = acl.compute_mask();
acl.set_mask(mask);
assert!(acl.is_extended());
assert!(acl.is_valid().is_ok());
}
AclBuilder
For anything beyond a minimal ACL, AclBuilder is the idiomatic way to
construct PosixAcl values. It auto-computes the mask when named entries
are present:
#![allow(unused)]
fn main() {
use posix_acls::{AclBuilder, PosixPerm};
let acl = AclBuilder::new("alice", "devs")
.owner_perms(PosixPerm::RWX)
.owning_group_perms(PosixPerm::RX)
.other_perms(PosixPerm::NONE)
.add_user("bob", PosixPerm::RW)
.add_group("ops", PosixPerm::R)
.build();
assert!(acl.is_valid().is_ok());
assert!(acl.is_extended());
// mask = union of named + group entries = RW | R | RX = RWX
assert_eq!(acl.mask(), PosixPerm::RWX);
}
All builder methods consume and return self, so you chain them fluently.
Named-entry methods use upsert semantics – if the same name appears twice,
the last call wins:
#![allow(unused)]
fn main() {
use posix_acls::{AclBuilder, PosixPerm};
let acl = AclBuilder::new("alice", "devs")
.owner_perms(PosixPerm::RWX)
.owning_group_perms(PosixPerm::RX)
.other_perms(PosixPerm::NONE)
.add_user("bob", PosixPerm::RW)
.add_user("bob", PosixPerm::R) // overwrites the previous grant
.build();
let bob = acl.entries().iter()
.find(|e| matches!(&e.tag, posix_acls::AclTag::User(n) if n == "bob"))
.unwrap();
assert_eq!(bob.perms, PosixPerm::R);
}
Default ACLs
Directories can carry a default ACL that is inherited by files and subdirectories created within them. The builder supports default entries through a parallel set of methods:
#![allow(unused)]
fn main() {
use posix_acls::{AclBuilder, PosixPerm};
let acl = AclBuilder::new("alice", "devs")
// Access ACL (applies to the directory itself)
.owner_perms(PosixPerm::RWX)
.owning_group_perms(PosixPerm::RX)
.other_perms(PosixPerm::NONE)
// Default ACL (inherited by new files in this directory)
.default_owner_perms(PosixPerm::RWX)
.default_owning_group_perms(PosixPerm::RX)
.default_other_perms(PosixPerm::NONE)
.default_user("bob", PosixPerm::RW)
.default_group("ops", PosixPerm::R)
.build();
assert!(!acl.default_entries().is_empty());
assert!(acl.is_valid().is_ok());
}
Default ACLs follow the same rules as access ACLs – they need their own
mask entry when named entries are present, and AclBuilder auto-computes
it.
Special bits
SUID, SGID, and sticky bits are not ACL entries, but AclBuilder carries
them so that a PosixAcl can represent the full 12-bit mode word:
#![allow(unused)]
fn main() {
use posix_acls::{AclBuilder, PosixPerm};
use posix_acls::perm::SpecialBits;
let acl = AclBuilder::new("alice", "devs")
.owner_perms(PosixPerm::RWX)
.owning_group_perms(PosixPerm::RX)
.other_perms(PosixPerm::RX)
.special_bits(SpecialBits::SGID)
.build();
assert!(acl.special_bits().sgid());
assert_eq!(acl.to_mode(), 0o2755);
}
Validation
PosixAcl::is_valid() checks structural correctness:
#![allow(unused)]
fn main() {
use posix_acls::{PosixAcl, PosixPerm};
// Missing mask on extended ACL
let mut acl = PosixAcl::new("alice", "devs");
acl.set_owner_perms(PosixPerm::RWX);
acl.set_group_perms(PosixPerm::RX);
acl.set_other_perms(PosixPerm::NONE);
acl.add_user("bob", PosixPerm::RW);
// Forgot to set mask!
let err = acl.is_valid().unwrap_err();
assert!(err.to_string().contains("mask"));
}
Validation errors (PosixAclValidationError):
| Variant | Meaning |
|---|---|
MissingEntry | A required tag (user::, group::, other::) is absent |
DuplicateEntry | The same named entry appears more than once |
MissingMask | Named entries exist but no mask:: entry is present |
SpuriousMask | A mask:: entry exists but there are no named entries |
AclBuilder::build() always produces a valid ACL because it auto-computes
the mask and emits entries in the correct order. You only need is_valid()
when constructing ACLs manually or after parsing untrusted input.
Next steps
You can now build ACLs programmatically. The next page shows how to
serialise them to the getfacl(1) text format and parse them back, with
round-trip guarantees.