use super::*;
use crate::operation::Operation;
use crate::state::acl::{AclEntry, AclPerms, AclTag};
use crate::test_helpers::StateBuilder;
fn entry(tag: AclTag, qualifier: Option<u32>, r: bool, w: bool, x: bool) -> AclEntry {
AclEntry {
tag,
qualifier,
perms: AclPerms {
read: r,
write: w,
execute: x,
},
}
}
fn base_acl(extras: Vec<AclEntry>) -> Vec<AclEntry> {
let mut entries = vec![
entry(AclTag::UserObj, None, true, true, false),
entry(AclTag::GroupObj, None, true, false, false),
entry(AclTag::Mask, None, true, true, true),
entry(AclTag::Other, None, false, false, false),
];
entries.extend(extras);
entries
}
fn full_acl(named_uid: u32, r: bool, w: bool, x: bool) -> Vec<AclEntry> {
base_acl(vec![entry(AclTag::User, Some(named_uid), r, w, x)])
}
#[test]
fn no_extended_acl_entries_passes() {
let base_only = vec![
entry(AclTag::UserObj, None, true, true, false),
entry(AclTag::GroupObj, None, true, false, false),
entry(AclTag::Other, None, false, false, false),
];
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 1000, 0o640, base_only)
.build();
let result = check_acl(&state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("no extended"));
}
}
#[test]
fn named_user_acl_grants_read() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl(
"/file.txt",
1000,
1000,
0o640,
full_acl(2000, true, false, false),
)
.build();
let result = check_acl(&state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("named user"));
}
}
#[test]
fn group_acl_entry_grants_access() {
let acl = base_acl(vec![entry(AclTag::Group, Some(50), true, true, false)]);
let state = StateBuilder::new()
.subject(2000, 2000, vec![50])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 1000, 0o600, acl)
.build();
let result = check_acl(&state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("named group"));
}
}
#[test]
fn inaccessible_acl_returns_degraded() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_inaccessible("/file.txt")
.build();
let result = check_acl(&state);
assert!(result.is_degraded());
}
#[test]
fn stat_operation_with_acl_always_passes() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Stat)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl(
"/file.txt",
1000,
1000,
0o000,
full_acl(2000, false, false, false),
)
.build();
let result = check_acl(&state);
assert!(result.is_pass());
}
#[test]
fn acl_denies_where_mode_would_allow() {
let state = StateBuilder::new()
.subject(2000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl(
"/file.txt",
1000,
1000,
0o640,
full_acl(2000, false, false, false),
)
.build();
let result = check_acl(&state);
assert!(result.is_fail());
}
#[test]
fn no_group_match_falls_through_to_other() {
let acl = vec![
entry(AclTag::UserObj, None, true, true, false),
entry(AclTag::User, Some(9999), false, false, false),
entry(AclTag::GroupObj, None, false, false, false),
entry(AclTag::Mask, None, false, false, false),
entry(AclTag::Other, None, true, false, false),
];
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 1000, 0o604, acl)
.build();
let result = check_acl(&state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("Other"));
}
}
#[test]
fn group_match_via_supplementary_group() {
let acl = base_acl(vec![entry(AclTag::Group, Some(50), true, true, false)]);
let state = StateBuilder::new()
.subject(2000, 2000, vec![50])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 1000, 0o600, acl)
.build();
let result = check_acl(&state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("named group"));
}
}
#[test]
fn unknown_acl_probe_returns_degraded() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_unknown("/file.txt")
.build();
let result = check_acl(&state);
assert!(result.is_degraded());
}
#[test]
fn group_obj_matches_primary_gid_grants_access() {
let acl = vec![
entry(AclTag::UserObj, None, false, false, false),
entry(AclTag::User, Some(9999), false, false, false),
entry(AclTag::GroupObj, None, true, false, false),
entry(AclTag::Mask, None, true, true, true),
entry(AclTag::Other, None, false, false, false),
];
let state = StateBuilder::new()
.subject(2000, 50, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 50, 0o040, acl)
.build();
let result = check_acl(&state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("GroupObj"));
}
}
#[test]
fn acl_passes_for_chmod_without_consulting_acl_entries() {
let acl = full_acl(2000, false, false, false);
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Chmod)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 1000, 0o644, acl)
.build();
assert!(check_acl(&state).is_pass());
}
#[test]
fn acl_passes_for_chown_uid() {
use crate::state::acl::{AclEntry, AclPerms, AclTag};
let deny_all = vec![
AclEntry {
tag: AclTag::UserObj,
qualifier: None,
perms: AclPerms {
read: false,
write: false,
execute: false,
},
},
AclEntry {
tag: AclTag::GroupObj,
qualifier: None,
perms: AclPerms {
read: false,
write: false,
execute: false,
},
},
AclEntry {
tag: AclTag::Other,
qualifier: None,
perms: AclPerms {
read: false,
write: false,
execute: false,
},
},
AclEntry {
tag: AclTag::User,
qualifier: Some(1000),
perms: AclPerms {
read: false,
write: false,
execute: false,
},
},
AclEntry {
tag: AclTag::Mask,
qualifier: None,
perms: AclPerms {
read: false,
write: false,
execute: false,
},
},
];
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::ChownUid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 1000, 0o000, deny_all)
.build();
assert!(check_acl(&state).is_pass());
}
#[test]
fn acl_passes_for_chown_gid() {
let acl = full_acl(1000, false, false, false);
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::ChownGid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 1000, 0o644, acl)
.build();
assert!(check_acl(&state).is_pass());
}
#[test]
fn acl_passes_for_setxattr_user() {
use crate::operation::XattrNamespace;
let acl = full_acl(1000, false, false, false);
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::SetXattr {
namespace: XattrNamespace::User,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file_with_acl("/file.txt", 1000, 1000, 0o644, acl)
.build();
assert!(check_acl(&state).is_pass());
}