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

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:

VariantCause
MissingOwnerbuilder().build() called without .owner()
MissingOwningGroupbuilder().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 the setfacl commands to apply them.