User/Group Model and Policy
Your project has grown from a few developers to a full team with contractors, ops staff, and interns. Each shared directory needs an ACL that references specific users and groups – and if you grant permissions to a user who does not exist, nothing will work as expected. The model and policy layer (Layer 2) catches these problems at build time by validating every grant against a known directory of users and groups.
UserEntry and GroupEntry
A UserEntry represents a user with their group memberships:
#![allow(unused)]
fn main() {
use posix_acls::model::UserEntry;
let alice = UserEntry::new("alice").with_groups(["devs", "ops"]);
assert_eq!(alice.name, "alice");
assert_eq!(alice.groups, vec!["devs", "ops"]);
}
A GroupEntry is just a name:
#![allow(unused)]
fn main() {
use posix_acls::model::GroupEntry;
let devs = GroupEntry::new("devs");
assert_eq!(devs.name, "devs");
}
Both have public fields, so you can inspect and modify them directly.
UserGroupModel
UserGroupModel is a HashMap-backed directory of users and groups. It
provides O(1) lookups by name:
#![allow(unused)]
fn main() {
use posix_acls::model::{GroupEntry, UserEntry, UserGroupModel};
let mut model = UserGroupModel::new();
model.add_user(UserEntry::new("alice").with_groups(["devs", "ops"]));
model.add_user(UserEntry::new("bob").with_groups(["devs"]));
model.add_user(UserEntry::new("carol").with_groups(["ops"]));
model.add_group(GroupEntry::new("devs"));
model.add_group(GroupEntry::new("ops"));
assert!(model.get_user("alice").is_some());
assert!(model.get_group("devs").is_some());
assert_eq!(model.groups_of("alice"), &["devs", "ops"]);
// Unknown users return None / empty slice
assert!(model.get_user("unknown").is_none());
assert_eq!(model.groups_of("unknown"), &[] as &[String]);
}
Adding a user or group with a name that already exists replaces the previous entry (upsert semantics):
#![allow(unused)]
fn main() {
use posix_acls::model::{UserEntry, UserGroupModel};
let mut model = UserGroupModel::new();
model.add_user(UserEntry::new("alice").with_groups(["devs"]));
model.add_user(UserEntry::new("alice").with_groups(["ops"]));
assert_eq!(model.get_user("alice").unwrap().groups, &["ops"]);
}
You can iterate over all users and groups:
#![allow(unused)]
fn main() {
use posix_acls::model::{GroupEntry, UserEntry, UserGroupModel};
let mut model = UserGroupModel::new();
model.add_user(UserEntry::new("alice"));
model.add_user(UserEntry::new("bob"));
model.add_group(GroupEntry::new("devs"));
assert_eq!(model.users().count(), 2);
assert_eq!(model.groups().count(), 1);
assert!(!model.is_empty());
}
AclPolicy
AclPolicy declares what permissions users and groups should have. It
looks similar to AclBuilder, but it does not produce a PosixAcl directly
– instead, you call generate(model) to validate the grants against a
UserGroupModel and then derive the concrete ACL:
#![allow(unused)]
fn main() {
use posix_acls::prelude::*;
let mut model = UserGroupModel::new();
model.add_user(UserEntry::new("alice").with_groups(["devs"]));
model.add_user(UserEntry::new("bob").with_groups(["devs"]));
model.add_group(GroupEntry::new("devs"));
let policy = AclPolicy::builder()
.owner("alice")
.owning_group("devs")
.owner_perms(PosixPerm::RWX)
.owning_group_perms(PosixPerm::RX)
.other_perms(PosixPerm::NONE)
.add_user("bob", PosixPerm::RW)
.build()
.unwrap();
let acl = policy.generate(&model).unwrap();
assert!(acl.is_valid().is_ok());
assert_eq!(acl.owner(), "alice");
assert!(acl.is_extended());
}
The builder requires owner and owning_group to be set. All other
fields default to PosixPerm::NONE.
Policy validation
generate checks that every named user and group in the grant list exists
in the model. If it finds an unknown principal, it returns a PolicyError:
#![allow(unused)]
fn main() {
use posix_acls::prelude::*;
use posix_acls::policy::PolicyError;
let mut model = UserGroupModel::new();
model.add_user(UserEntry::new("alice").with_groups(["devs"]));
model.add_group(GroupEntry::new("devs"));
// Grant permissions to a user not in the model
let policy = AclPolicy::builder()
.owner("alice")
.owning_group("devs")
.add_user("unknown", PosixPerm::R)
.build()
.unwrap();
let err = policy.generate(&model).unwrap_err();
assert_eq!(err, PolicyError::UnknownUser("unknown".into()));
}
The same validation applies to default ACL grants:
#![allow(unused)]
fn main() {
use posix_acls::prelude::*;
use posix_acls::policy::PolicyError;
let mut model = UserGroupModel::new();
model.add_user(UserEntry::new("alice").with_groups(["devs"]));
model.add_group(GroupEntry::new("devs"));
let policy = AclPolicy::builder()
.owner("alice")
.owning_group("devs")
.default_group("nobody", PosixPerm::R)
.build()
.unwrap();
let err = policy.generate(&model).unwrap_err();
assert_eq!(err, PolicyError::UnknownGroup("nobody".into()));
}
The owner and owning group are not validated against the model – they
may be system principals (like root) that are not tracked in your
application’s directory:
#![allow(unused)]
fn main() {
use posix_acls::prelude::*;
let mut model = UserGroupModel::new();
model.add_group(GroupEntry::new("devs"));
let policy = AclPolicy::builder()
.owner("root")
.owning_group("devs")
.owner_perms(PosixPerm::RWX)
.owning_group_perms(PosixPerm::RX)
.other_perms(PosixPerm::NONE)
.build()
.unwrap();
let acl = policy.generate(&model).unwrap();
assert_eq!(acl.owner(), "root");
}
Policy errors
PolicyError covers four cases:
| Variant | Cause |
|---|---|
MissingOwner | builder().build() called without .owner() |
MissingOwningGroup | builder().build() called without .owning_group() |
UnknownUser(name) | A named-user grant references a user absent from the model |
UnknownGroup(name) | A named-group grant references a group absent from the model |
#![allow(unused)]
fn main() {
use posix_acls::policy::{AclPolicy, PolicyError};
// Missing owner
let err = AclPolicy::builder()
.owning_group("devs")
.build()
.unwrap_err();
assert_eq!(err, PolicyError::MissingOwner);
// Missing owning group
let err = AclPolicy::builder()
.owner("alice")
.build()
.unwrap_err();
assert_eq!(err, PolicyError::MissingOwningGroup);
}
Default ACLs in policies
Policies support the same default ACL methods as AclBuilder. Default
grants are validated against the model just like access grants:
#![allow(unused)]
fn main() {
use posix_acls::prelude::*;
let mut model = UserGroupModel::new();
model.add_user(UserEntry::new("alice").with_groups(["devs"]));
model.add_user(UserEntry::new("bob").with_groups(["devs"]));
model.add_group(GroupEntry::new("devs"));
let policy = AclPolicy::builder()
.owner("alice")
.owning_group("devs")
.owner_perms(PosixPerm::RWX)
.owning_group_perms(PosixPerm::RX)
.other_perms(PosixPerm::NONE)
.default_owner_perms(PosixPerm::RWX)
.default_owning_group_perms(PosixPerm::RX)
.default_other_perms(PosixPerm::NONE)
.default_user("bob", PosixPerm::RW)
.build()
.unwrap();
let acl = policy.generate(&model).unwrap();
assert!(!acl.default_entries().is_empty());
}
Serde support
With the serde feature enabled, UserGroupModel, UserEntry, and
GroupEntry all derive Serialize and Deserialize. This lets you load
your organisation’s user/group directory from a JSON or TOML configuration
file and use it to validate policies at build time or in CI.
Next steps
You can now declare policies and validate them against a user directory.
- acls-rs Bridge – connect POSIX permissions to the algebraic access control system in acls-rs for cross-system policies.
- Temporal Access – a full end-to-end walkthrough:
time-bounded contractor team grants, group-based POSIX ACLs on shared
mounts, per-user effective access derived via
check_access, and thesetfaclcommands to apply them.