use crate::operation::Operation;
use crate::state::acl::{AclPerms, PosixAcl};
use crate::state::path::StatResult;
use crate::state::subject::ResolvedSubject;
use crate::state::{Probe, SystemState};
use super::LayerResult;
#[must_use]
pub fn check_acl(state: &SystemState) -> LayerResult {
if state.operation.is_metadata() {
return LayerResult::Pass {
detail: "metadata ops bypass ACL mode-bit checks".into(),
warnings: vec![],
};
}
if state.operation == Operation::Stat {
return LayerResult::Pass {
detail: "stat requires only traversal, no ACL check".into(),
warnings: vec![],
};
}
if state.operation.checks_parent() {
return check_parent_acl(state);
}
check_target_acl(state)
}
fn check_target_acl(state: &SystemState) -> LayerResult {
let Some(idx) = state.operation.target_component(state.walk.len()) else {
return degraded("path walk too short to determine target");
};
let comp = &state.walk[idx];
evaluate_component_acl(&comp.acl, &comp.stat, state)
}
fn check_parent_acl(state: &SystemState) -> LayerResult {
let Some(idx) = state.operation.target_component(state.walk.len()) else {
return degraded("path walk too short to determine parent");
};
let comp = &state.walk[idx];
evaluate_component_acl(&comp.acl, &comp.stat, state)
}
fn evaluate_component_acl(
acl_probe: &Probe<PosixAcl>,
stat_probe: &Probe<StatResult>,
state: &SystemState,
) -> LayerResult {
let acl = match acl_probe {
Probe::Known(a) => a,
Probe::Inaccessible => return degraded("ACL inaccessible"),
Probe::Unknown => return degraded("ACL unknown"),
};
if !acl.has_extended_entries() {
return LayerResult::Pass {
detail: "no extended ACL entries".into(),
warnings: vec![],
};
}
let Probe::Known(stat) = stat_probe else {
return degraded("stat unavailable for ACL evaluation");
};
let needed = required_perms(state.operation);
let (has_perm, matched_entry) = evaluate_acl(acl, stat, &state.subject, &needed);
format_acl_result(has_perm, &matched_entry, &needed)
}
fn evaluate_acl(
acl: &PosixAcl,
stat: &StatResult,
subject: &ResolvedSubject,
needed: &NeededPerms,
) -> (bool, String) {
if stat.uid == subject.uid {
let perms = acl.user_obj().map_or(AclPerms::NONE, |e| e.perms);
let granted = check_needed(perms, needed);
return (granted, "UserObj (file owner)".into());
}
if let Some(entry) = acl.named_user(subject.uid) {
let eff = acl.effective_perms(entry);
let granted = check_needed(eff, needed);
return (granted, format!("named user uid={}", subject.uid));
}
if let Some((granted, desc)) = best_group_match(acl, stat, subject, needed) {
return (granted, desc);
}
let perms = acl.other().map_or(AclPerms::NONE, |e| e.perms);
let granted = check_needed(perms, needed);
(granted, "Other".into())
}
fn best_group_match(
acl: &PosixAcl,
stat: &StatResult,
subject: &ResolvedSubject,
needed: &NeededPerms,
) -> Option<(bool, String)> {
let mut best_granted = false;
let mut matched = false;
let mut desc = String::new();
if subject.in_group(stat.gid) {
if let Some(entry) = acl.group_obj() {
let eff = acl.effective_perms(entry);
let g = check_needed(eff, needed);
if g || !matched {
best_granted = g;
desc = "GroupObj (owning group)".into();
}
matched = true;
}
}
for entry in acl.named_groups() {
if let Some(gid) = entry.qualifier {
if subject.in_group(gid) {
let eff = acl.effective_perms(entry);
let g = check_needed(eff, needed);
if g && !best_granted {
best_granted = true;
desc = format!("named group gid={gid}");
}
matched = true;
}
}
}
matched.then_some((best_granted, desc))
}
struct NeededPerms {
read: bool,
write: bool,
execute: bool,
}
fn required_perms(op: Operation) -> NeededPerms {
match op {
Operation::Read => NeededPerms {
read: true,
write: false,
execute: false,
},
Operation::Write => NeededPerms {
read: false,
write: true,
execute: false,
},
Operation::Execute => NeededPerms {
read: false,
write: false,
execute: true,
},
Operation::Delete | Operation::Create => NeededPerms {
read: false,
write: true,
execute: true,
},
Operation::Chmod
| Operation::ChownUid
| Operation::ChownGid
| Operation::SetXattr { .. } => {
unreachable!("is_metadata() guard must precede required_perms()")
}
Operation::Stat => NeededPerms {
read: false,
write: false,
execute: false,
},
}
}
fn check_needed(perms: AclPerms, needed: &NeededPerms) -> bool {
(!needed.read || perms.read)
&& (!needed.write || perms.write)
&& (!needed.execute || perms.execute)
}
fn format_acl_result(granted: bool, matched_entry: &str, needed: &NeededPerms) -> LayerResult {
let needed_str = needed_description(needed);
if granted {
LayerResult::Pass {
detail: format!("ACL {matched_entry} grants {needed_str}"),
warnings: vec![],
}
} else {
LayerResult::Fail {
detail: format!("ACL {matched_entry} denies {needed_str}"),
component_index: None,
}
}
}
fn needed_description(needed: &NeededPerms) -> String {
let mut parts = Vec::new();
if needed.read {
parts.push("read");
}
if needed.write {
parts.push("write");
}
if needed.execute {
parts.push("execute");
}
if parts.is_empty() {
"no specific permission".into()
} else {
parts.join("+")
}
}
fn degraded(reason: &str) -> LayerResult {
LayerResult::Degraded {
reason: reason.to_owned(),
}
}
#[cfg(test)]
#[path = "acl_tests.rs"]
mod tests;
#[cfg(test)]
#[path = "acl_mask_tests.rs"]
mod mask_tests;