whyno-core 0.5.0

Permission check pipeline, fix engine, and state types
Documentation
//! Edge-case tests for the traversal check layer — unknown stat, inaccessible, root bypass.
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 acl_group_obj_match_grants_execute() {
    // subject is in the owning group, GroupObj grants execute via ACL
    let acl_entries = vec![
        acl_entry(AclTag::UserObj, None, true, true, false),
        acl_entry(AclTag::User, Some(9999), false, false, false),
        acl_entry(AclTag::GroupObj, None, false, false, true),
        acl_entry(AclTag::Mask, None, true, true, true),
        acl_entry(AclTag::Other, None, false, false, false),
    ];
    let state = StateBuilder::new()
        .subject(1000, 50, vec![])
        .operation(Operation::Read)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_with_acl("/grouponly", 0, 50, 0o700, acl_entries)
        .component_file("/grouponly/file.txt", 0, 0, 0o644)
        .build();

    let result = check_traversal(&state);
    assert!(result.is_pass(), "expected pass, got: {result:?}");
}

#[test]
fn root_subject_always_passes_traversal() {
    let state = StateBuilder::new()
        .subject(0, 0, vec![])
        .operation(Operation::Read)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o000)
        .component("/locked", 0, 0, 0o000)
        .component_file("/locked/file.txt", 0, 0, 0o000)
        .build();

    let result = check_traversal(&state);
    assert!(result.is_pass());
}

#[test]
fn inaccessible_ancestor_returns_degraded() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_inaccessible("/secret")
        .component_file("/secret/file.txt", 0, 0, 0o644)
        .build();

    let result = check_traversal(&state);
    assert!(result.is_degraded());
    if let LayerResult::Degraded { reason } = &result {
        assert!(reason.contains("/secret"));
    }
}

#[test]
fn unknown_ancestor_stat_returns_degraded() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_unknown("/mystery")
        .component_file("/mystery/file.txt", 0, 0, 0o644)
        .build();

    let result = check_traversal(&state);
    assert!(result.is_degraded());
    if let LayerResult::Degraded { reason } = &result {
        assert!(reason.contains("/mystery"));
    }
}

#[test]
fn single_component_walk_passes() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .build();

    let result = check_traversal(&state);
    assert!(result.is_pass());
    if let LayerResult::Pass { detail, .. } = &result {
        assert!(detail.contains("single component"));
    }
}

#[test]
fn acl_probe_inaccessible_returns_degraded() {
    // component_inaccessible sets acl=Probe::Inaccessible and stat=Probe::Inaccessible
    // check_ancestor returns Degraded before reaching check_acl_execute
    // To test the "ACL inaccessible for traversal" branch, we need a component
    // that has known stat but inaccessible ACL with extended entries.
    // has_extended_acl returns false for Probe::Inaccessible,
    // so the code path for "ACL inaccessible for traversal" is triggered only
    // when acl is Probe::Known with extended entries then the inner Probe fails.
    // check_acl_execute is only called when has_extended_acl returns true,
    // which requires Probe::Known(a) where a.has_extended_entries() is true.
    // The inaccessible ACL branch inside check_acl_execute requires the acl
    // to NOT be Probe::Known -- this happens e.g. if state is mutated externally.
    // degraded_for path tested via inaccessible stat instead.
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .mount("/", "ext4", "rw")
        .component("/", 0, 0, 0o755)
        .component_inaccessible("/locked_dir")
        .component_file("/locked_dir/file.txt", 0, 0, 0o644)
        .build();

    let result = check_traversal(&state);
    assert!(result.is_degraded());
}