use super::*;
use crate::checks::capability_modify;
use crate::operation::Operation;
use crate::test_helpers::StateBuilder;
#[test]
fn owner_read_on_0640_passes() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o640)
.build();
assert!(check_dac(&state).is_pass());
}
#[test]
fn group_read_on_0640_passes() {
let state = StateBuilder::new()
.subject(2000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o640)
.build();
assert!(check_dac(&state).is_pass());
}
#[test]
fn other_read_on_0640_fails() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o640)
.build();
let result = check_dac(&state);
assert!(result.is_fail());
if let LayerResult::Fail { detail, .. } = &result {
assert!(detail.contains("other"));
assert!(detail.contains("no r permission"));
}
}
#[test]
fn owner_write_on_0644_passes() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
assert!(check_dac(&state).is_pass());
}
#[test]
fn other_write_on_0644_fails() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Write)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o644)
.build();
assert!(check_dac(&state).is_fail());
}
#[test]
fn root_read_on_0000_passes_via_override() {
let state = StateBuilder::new()
.subject(0, 0, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/secret.txt", 1000, 1000, 0o000)
.build();
let result = capability_modify(check_dac(&state), &state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("CAP_DAC_OVERRIDE"));
}
}
#[test]
fn root_execute_on_0644_fails_no_x_anywhere() {
let state = StateBuilder::new()
.subject(0, 0, vec![])
.operation(Operation::Execute)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/script.sh", 1000, 1000, 0o644)
.build();
let result = capability_modify(check_dac(&state), &state);
assert!(result.is_fail());
if let LayerResult::Fail { detail, .. } = &result {
assert!(detail.contains("no execute bit"));
assert!(detail.contains("CAP_DAC_OVERRIDE does not apply"));
}
}
#[test]
fn delete_with_sticky_subject_owns_target_passes() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Delete)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/tmp", 0, 0, 0o1777)
.component_file("/tmp/myfile.txt", 1000, 1000, 0o644)
.build();
let result = check_dac(&state);
assert!(result.is_pass());
if let LayerResult::Pass { detail, .. } = &result {
assert!(detail.contains("sticky"));
}
}
#[test]
fn delete_with_sticky_subject_owns_neither_fails() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Delete)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/tmp", 0, 0, 0o1777)
.component_file("/tmp/otherfile.txt", 1000, 1000, 0o644)
.build();
let result = check_dac(&state);
assert!(result.is_fail());
if let LayerResult::Fail { detail, .. } = &result {
assert!(detail.contains("sticky"));
assert!(detail.contains("uid=2000"));
}
}
#[test]
fn create_parent_has_wx_passes() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Create)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/dir", 1000, 1000, 0o755)
.component_file("/dir/newfile.txt", 1000, 1000, 0o644)
.build();
assert!(check_dac(&state).is_pass());
}
#[test]
fn create_parent_missing_write_fails() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Create)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component("/dir", 1000, 1000, 0o755)
.component_file("/dir/newfile.txt", 2000, 2000, 0o644)
.build();
let result = check_dac(&state);
assert!(result.is_fail());
if let LayerResult::Fail { detail, .. } = &result {
assert!(detail.contains("w+x"));
}
}
#[test]
fn stat_always_passes_regardless_of_mode() {
let state = StateBuilder::new()
.subject(2000, 2000, vec![])
.operation(Operation::Stat)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/secret.txt", 0, 0, 0o000)
.build();
assert!(check_dac(&state).is_pass());
}
#[test]
fn unknown_stat_returns_degraded() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_unknown("/mystery")
.build();
assert!(check_dac(&state).is_degraded());
}
#[test]
fn delete_parent_with_unknown_stat_returns_degraded() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Delete)
.component_unknown("/unknown_dir")
.component_file("/unknown_dir/file.txt", 1000, 1000, 0o644)
.build();
let result = check_dac(&state);
assert!(result.is_degraded());
}
#[test]
fn read_single_component_with_unknown_stat_returns_degraded() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Read)
.component_unknown("/mystery.txt")
.build();
let result = check_dac(&state);
assert!(result.is_degraded());
}
#[test]
fn sticky_bit_target_stat_unknown_returns_degraded() {
let state = StateBuilder::new()
.subject(1000, 1000, vec![])
.operation(Operation::Delete)
.component("/sticky_dir", 0, 0, 0o1777)
.component_unknown("/sticky_dir/file.txt")
.build();
let result = check_dac(&state);
assert!(result.is_degraded());
}
#[test]
fn dac_passes_for_chmod_without_consulting_mode_bits() {
let state = StateBuilder::new()
.subject(999, 999, vec![])
.operation(Operation::Chmod)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o000)
.build();
let result = check_dac(&state);
assert!(result.is_pass(), "expected Pass for Chmod, got {result:?}");
}
#[test]
fn dac_passes_for_chown_uid() {
let state = StateBuilder::new()
.subject(999, 999, vec![])
.operation(Operation::ChownUid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o000)
.build();
assert!(check_dac(&state).is_pass());
}
#[test]
fn dac_passes_for_chown_gid() {
let state = StateBuilder::new()
.subject(999, 999, vec![])
.operation(Operation::ChownGid)
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o000)
.build();
assert!(check_dac(&state).is_pass());
}
#[test]
fn dac_passes_for_setxattr_user() {
use crate::operation::XattrNamespace;
let state = StateBuilder::new()
.subject(999, 999, vec![])
.operation(Operation::SetXattr {
namespace: XattrNamespace::User,
})
.mount("/", "ext4", "rw")
.component("/", 0, 0, 0o755)
.component_file("/file.txt", 1000, 1000, 0o000)
.build();
assert!(check_dac(&state).is_pass());
}
#[test]
fn check_bit_valid_chars_produce_correct_results() {
use crate::state::path::{FileType, StatResult};
use crate::state::subject::ResolvedSubject;
use crate::state::Probe;
let stat = StatResult {
uid: 1000,
gid: 1000,
mode: 0o777,
dev: 1,
nlink: 1,
file_type: FileType::Regular,
};
let subject = ResolvedSubject {
uid: 1000,
gid: 1000,
groups: vec![],
capabilities: Probe::Unknown,
};
assert!(
check_bit(&stat, &subject, 'r'),
"owner 'r' on 0o777 must be true"
);
assert!(
check_bit(&stat, &subject, 'w'),
"owner 'w' on 0o777 must be true"
);
assert!(
check_bit(&stat, &subject, 'x'),
"owner 'x' on 0o777 must be true"
);
let stat_zero = StatResult {
uid: 1000,
gid: 1000,
mode: 0o000,
dev: 1,
nlink: 1,
file_type: FileType::Regular,
};
assert!(
!check_bit(&stat_zero, &subject, 'r'),
"owner 'r' on 0o000 must be false"
);
assert!(
!check_bit(&stat_zero, &subject, 'w'),
"owner 'w' on 0o000 must be false"
);
assert!(
!check_bit(&stat_zero, &subject, 'x'),
"owner 'x' on 0o000 must be false"
);
}