whyno-cli 0.5.0

Linux permission debugger
use whyno_core::operation::Operation;
use whyno_core::state::acl::{AclEntry, AclPerms, AclTag};
use whyno_core::state::fsflags::FsFlags;
use whyno_core::test_helpers::StateBuilder;

fn render(state: &whyno_core::state::SystemState) -> String {
    let mut buf = Vec::new();
    super::write_path_walk(state, &mut buf).expect("render should succeed");
    String::from_utf8(buf).expect("output should be valid UTF-8")
}

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 single_known_component() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .component_file("/file.txt", 1000, 1000, 0o644)
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

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

#[test]
fn inaccessible_component_shows_inaccessible_markers() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component_inaccessible("/secret")
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn all_three_probe_states_in_one_walk() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component_unknown("/vanished")
        .component_inaccessible("/locked")
        .component_file("/locked/file.txt", 0, 0, 0o644)
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn acl_empty_entries_shows_none() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component_file_with_acl("/file.txt", 1000, 1000, 0o644, vec![])
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn acl_all_six_tag_types() {
    let entries = vec![
        acl_entry(AclTag::UserObj, None, true, true, true),
        acl_entry(AclTag::User, Some(2000), true, false, false),
        acl_entry(AclTag::GroupObj, None, true, false, true),
        acl_entry(AclTag::Group, Some(50), false, false, true),
        acl_entry(AclTag::Mask, None, true, false, true),
        acl_entry(AclTag::Other, None, false, false, false),
    ];
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component_file_with_acl("/file.txt", 1000, 1000, 0o640, entries)
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn flags_immutable_true_append_false() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .component("/", 0, 0, 0o755)
        .component_file_with_flags(
            "/locked.txt",
            1000,
            1000,
            0o644,
            FsFlags {
                immutable: true,
                append_only: false,
            },
        )
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn flags_immutable_false_append_true() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .component("/", 0, 0, 0o755)
        .component_file_with_flags(
            "/append.txt",
            1000,
            1000,
            0o644,
            FsFlags {
                immutable: false,
                append_only: true,
            },
        )
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn flags_both_true() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .component("/", 0, 0, 0o755)
        .component_file_with_flags(
            "/both.txt",
            1000,
            1000,
            0o644,
            FsFlags {
                immutable: true,
                append_only: true,
            },
        )
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn mount_ro_option() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Write)
        .component("/mnt", 0, 0, 0o755)
        .component_file("/mnt/data.txt", 1000, 1000, 0o644)
        .mount("/mnt", "ext4", "ro")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn mount_noexec_option() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Execute)
        .component("/mnt", 0, 0, 0o755)
        .component_file("/mnt/script.sh", 1000, 1000, 0o755)
        .mount("/mnt", "tmpfs", "noexec")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn mount_nosuid_option() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Execute)
        .component("/mnt", 0, 0, 0o755)
        .component_file("/mnt/suid_bin", 0, 0, 0o4755)
        .mount("/mnt", "ext4", "nosuid")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn component_with_no_mount_resolved() {
    // component without a mount (no mount entry covering it)
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .component_file("/unmounted.txt", 1000, 1000, 0o644)
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}

#[test]
fn deep_path_four_components() {
    let state = StateBuilder::new()
        .subject(1000, 1000, vec![])
        .operation(Operation::Read)
        .component("/", 0, 0, 0o755)
        .component("/a", 0, 0, 0o755)
        .component("/a/b", 0, 0, 0o755)
        .component_file("/a/b/file.txt", 1000, 1000, 0o644)
        .mount("/", "ext4", "rw")
        .build();
    let output = render(&state);
    insta::assert_snapshot!(output);
}