aion-server 0.30.0

Aion workflow server library: HTTP, gRPC, WebSocket, and worker endpoints. Run it with the `aion` binary from the aion-cli crate.
Documentation
//! The operator walk: it brings every mode across, refuses a held store, and
//! narrates each top-level entry.

use std::os::unix::fs::PermissionsExt as _;
use std::path::Path;

use super::{HardenError, HardenProgress, harden_store_tree};

type TestResult = Result<(), Box<dyn std::error::Error>>;

fn mode(path: &Path) -> std::io::Result<u32> {
    Ok(std::fs::metadata(path)?.permissions().mode() & 0o777)
}

/// A store whose files were loosened by hand comes back to 0700/0600 in one
/// walk, and the narration counts what it touched.
#[test]
fn a_loosened_store_is_brought_to_private_modes_and_narrated() -> TestResult {
    let sandbox = crate::test_support::private_tempdir()?;
    let data_dir = sandbox.path().join("data");
    std::fs::create_dir(&data_dir)?;
    for shard in 0..3 {
        let shard_dir = data_dir.join(format!("shard-{shard}"));
        std::fs::create_dir_all(shard_dir.join("store").join("ab"))?;
        std::fs::write(shard_dir.join("shard.wal"), b"wal")?;
        std::fs::write(shard_dir.join("store").join("ab").join("node"), b"node")?;
        std::fs::set_permissions(&shard_dir, std::fs::Permissions::from_mode(0o755))?;
        std::fs::set_permissions(
            shard_dir.join("shard.wal"),
            std::fs::Permissions::from_mode(0o644),
        )?;
        std::fs::set_permissions(
            shard_dir.join("store").join("ab").join("node"),
            std::fs::Permissions::from_mode(0o664),
        )?;
    }
    std::fs::write(data_dir.join("config.json"), b"{}")?;
    std::fs::set_permissions(
        data_dir.join("config.json"),
        std::fs::Permissions::from_mode(0o644),
    )?;

    let mut narrated: Vec<HardenProgress> = Vec::new();
    let report = harden_store_tree(&data_dir, |progress| narrated.push(progress.clone()))?;

    assert_eq!(report.files, 7, "3 WALs + 3 nodes + config.json");
    assert_eq!(
        report.directories,
        1 + 3 * 3,
        "root + (shard, store, ab) × 3"
    );
    assert_eq!(mode(&data_dir)?, 0o700);
    for shard in 0..3 {
        let shard_dir = data_dir.join(format!("shard-{shard}"));
        assert_eq!(mode(&shard_dir)?, 0o700);
        assert_eq!(mode(&shard_dir.join("store"))?, 0o700);
        assert_eq!(mode(&shard_dir.join("shard.wal"))?, 0o600);
        assert_eq!(
            mode(&shard_dir.join("store").join("ab").join("node"))?,
            0o600
        );
    }
    assert_eq!(mode(&data_dir.join("config.json"))?, 0o600);

    let entries: Vec<&str> = narrated.iter().map(|step| step.entry.as_str()).collect();
    assert_eq!(
        entries,
        ["config.json", "shard-0", "shard-1", "shard-2"],
        "one line per top-level entry, in name order"
    );
    assert_eq!(narrated[0].files, 1);
    assert_eq!(narrated[1].files, 2);
    assert_eq!(
        narrated[3].total_files, 7,
        "the running total ends at the report's count"
    );
    Ok(())
}

/// A store a live server holds is refused by name — the lock, not a guess.
#[test]
fn a_store_whose_writer_lock_is_held_is_refused() -> TestResult {
    let sandbox = crate::test_support::private_tempdir()?;
    let data_dir = sandbox.path().join("data");
    std::fs::create_dir(&data_dir)?;
    let lock_path = data_dir.join("writer.lock");
    let holder = std::fs::File::create(&lock_path)?;
    holder.lock()?;

    let error = harden_store_tree(&data_dir, |_| {})
        .err()
        .ok_or("a held store must refuse")?;
    let sentence = error.to_string();
    match error {
        HardenError::WriterLockHeld {
            data_dir: named,
            lock_path: named_lock,
        } => {
            assert_eq!(named, data_dir);
            assert_eq!(named_lock, lock_path);
        }
        other => return Err(format!("wrong refusal: {other}").into()),
    }
    assert!(
        sentence.contains("aion stop"),
        "the refusal tells the operator what to do: {sentence}"
    );

    drop(holder);
    harden_store_tree(&data_dir, |_| {})?;
    Ok(())
}

/// A symbolic link anywhere in the tree stops the walk and names the entry
/// it was found under; nothing is silently skipped.
#[test]
fn a_symbolic_link_in_the_tree_is_refused_by_entry() -> TestResult {
    let sandbox = crate::test_support::private_tempdir()?;
    let data_dir = sandbox.path().join("data");
    std::fs::create_dir_all(data_dir.join("shard-0"))?;
    std::os::unix::fs::symlink(sandbox.path(), data_dir.join("shard-0").join("escape"))?;

    let error = harden_store_tree(&data_dir, |_| {})
        .err()
        .ok_or("a link must refuse")?;
    match error {
        HardenError::Walk { entry, error, .. } => {
            assert_eq!(entry, "shard-0");
            assert!(error.to_string().contains("symbolic link"), "{error}");
        }
        other => return Err(format!("wrong refusal: {other}").into()),
    }
    Ok(())
}