use super::*;
use crate::operation::Operation;
use crate::state::acl::{AclEntry, AclPerms, AclTag};
use crate::test_helpers::StateBuilder;
fn acl_entry(tag: AclTag, qualifier: Option<u32>, r: bool, w: bool, x: bool) -> AclEntry {
AclEntry {
tag,
qualifier,
perms: AclPerms {
read: r,
write: w,
execute: x,
},
}
}
#[test]
fn all_ancestors_world_traversable() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/var", 0, 0, 0o755)
.component("/var/log", 0, 0, 0o755)
.component_file("/var/log/app.log", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("3 ancestors"));
}
}
#[test]
fn ancestor_missing_other_execute_fails() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/var", 0, 0, 0o750)
.component_file("/var/secret.txt", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_fail());
if let LayerResult::Fail {
detail,
component_index,
} = &result
{
assert!(detail.contains("/var"));
assert!(detail.contains("other"));
assert_eq!(*component_index, Some(1));
}
}
#[test]
fn subject_is_owner_of_restrictive_ancestor() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/home/user", 1000, 1000, 0o700)
.component_file("/home/user/file.txt", 1000, 1000, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_pass());
}
#[test]
fn subject_in_group_of_restrictive_ancestor() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![50])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/shared", 0, 50, 0o710)
.component_file("/shared/data.txt", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_pass());
}
#[test]
fn acl_grants_traverse_on_ancestor() {
let acl_entries = vec![
acl_entry(AclTag::UserObj, None, true, true, true),
acl_entry(AclTag::User, Some(1000), true, false, true),
acl_entry(AclTag::GroupObj, None, false, false, false),
acl_entry(AclTag::Mask, None, true, true, true),
acl_entry(AclTag::Other, None, false, false, false),
];
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_with_acl("/restricted", 0, 0, 0o700, acl_entries)
.component_file("/restricted/file.txt", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_pass());
}
#[test]
fn acl_denies_traverse_on_ancestor() {
let acl_entries = vec![
acl_entry(AclTag::UserObj, None, true, true, true),
acl_entry(AclTag::User, Some(1000), true, false, true),
acl_entry(AclTag::GroupObj, None, false, false, false),
acl_entry(AclTag::Mask, None, true, false, false),
acl_entry(AclTag::Other, None, false, false, false),
];
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_with_acl("/restricted", 0, 0, 0o700, acl_entries)
.component_file("/restricted/file.txt", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_fail());
if let LayerResult::Fail { detail, .. } = &result {
assert!(detail.contains("/restricted"));
assert!(detail.contains("ACL"));
}
}
#[test]
fn acl_grants_execute_via_named_group() {
let acl_entries = vec![
acl_entry(AclTag::UserObj, None, true, true, false),
acl_entry(AclTag::Group, Some(50), false, false, true),
acl_entry(AclTag::GroupObj, None, false, false, false),
acl_entry(AclTag::Mask, None, true, true, true),
acl_entry(AclTag::Other, None, false, false, false),
];
let state = StateBuilder::new()
.subject(1000, 1000, vec![50])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_with_acl("/groupdir", 0, 0, 0o700, acl_entries)
.component_file("/groupdir/file.txt", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_pass(), "expected pass, got: {result:?}");
}
#[test]
fn acl_mask_restricts_group_execute_to_deny() {
let acl_entries = vec![
acl_entry(AclTag::UserObj, None, true, true, true),
acl_entry(AclTag::Group, Some(50), false, false, true),
acl_entry(AclTag::GroupObj, None, false, false, false),
acl_entry(AclTag::Mask, None, true, false, false),
acl_entry(AclTag::Other, None, false, false, false),
];
let state = StateBuilder::new()
.subject(1000, 1000, vec![50])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_with_acl("/masked", 0, 0, 0o700, acl_entries)
.component_file("/masked/file.txt", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_fail(), "expected fail, got: {result:?}");
if let LayerResult::Fail { detail, .. } = &result {
assert!(detail.contains("/masked"));
}
}
#[test]
fn acl_other_entry_grants_execute() {
let acl_entries = vec![
acl_entry(AclTag::UserObj, None, true, true, true),
acl_entry(AclTag::User, Some(9999), false, false, false),
acl_entry(AclTag::GroupObj, None, false, false, false),
acl_entry(AclTag::Mask, None, true, false, false),
acl_entry(AclTag::Other, None, false, false, true),
];
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_with_acl("/world", 0, 99, 0o700, acl_entries)
.component_file("/world/file.txt", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_pass(), "expected pass, got: {result:?}");
}
#[test]
fn acl_owner_uses_user_obj_path() {
let acl_entries = vec![
acl_entry(AclTag::UserObj, None, true, true, true),
acl_entry(AclTag::User, Some(9999), false, false, false),
acl_entry(AclTag::GroupObj, None, false, false, false),
acl_entry(AclTag::Mask, None, false, false, false),
acl_entry(AclTag::Other, None, false, false, false),
];
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_with_acl("/owned", 1000, 0, 0o700, acl_entries)
.component_file("/owned/file.txt", 0, 0, 0o644)
.build();
let result = check_traversal(&state);
assert!(result.is_pass(), "expected pass, got: {result:?}");
}