sudo-rs 0.2.0-dev.20230627

A memory safe implementation of sudo and su.
Documentation
use crate::cli::SudoOptions;
use crate::common::{CommandAndArguments, Context, Environment};
use crate::env::environment::get_target_environment;
use crate::system::{Group, Process, User};
use std::collections::{HashMap, HashSet};

const TESTS: &str = "
> env
    FOO=BAR
    HOME=/home/test
    HOSTNAME=test-ubuntu
    LANG=en_US.UTF-8
    LANGUAGE=en_US.UTF-8
    LC_ALL=en_US.UTF-8
    LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36:
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    PWD=/home/test
    SHLVL=0
    TERM=xterm
    _=/usr/bin/sudo
> sudo env
    HOSTNAME=test-ubuntu
    LANG=en_US.UTF-8
    LANGUAGE=en_US.UTF-8
    LC_ALL=en_US.UTF-8
    LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36:
    MAIL=/var/mail/root
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    SHELL=/bin/bash
    SUDO_COMMAND=/usr/bin/env
    SUDO_GID=1000
    SUDO_UID=1000
    SUDO_USER=test
    HOME=/root
    LOGNAME=root
    USER=root
    TERM=xterm
> sudo -u test env
    HOSTNAME=test-ubuntu
    LANG=en_US.UTF-8
    LANGUAGE=en_US.UTF-8
    LC_ALL=en_US.UTF-8
    LS_COLORS=cd=40;33;01:*.jpg=01;35:*.mp3=00;36:
    MAIL=/var/mail/test
    PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
    SHELL=/bin/sh
    SUDO_COMMAND=/usr/bin/env
    SUDO_GID=1000
    SUDO_UID=1000
    SUDO_USER=test
    HOME=/home/test
    LOGNAME=test
    USER=test
    TERM=xterm
";

fn parse_env_commands(input: &str) -> Vec<(&str, Environment)> {
    input
        .trim()
        .split("> ")
        .filter(|l| !l.is_empty())
        .map(|e| {
            let (cmd, vars) = e.split_once('\n').unwrap();

            let vars: Environment = vars
                .lines()
                .map(|line| line.trim().split_once('=').unwrap())
                .map(|(k, v)| (k.into(), v.into()))
                .collect();

            (cmd, vars)
        })
        .collect()
}

fn create_test_context(sudo_options: &SudoOptions) -> Context {
    let path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string();
    let command =
        CommandAndArguments::try_from_args(None, sudo_options.clone().args(), &path).unwrap();

    let current_user = User {
        uid: 1000,
        gid: 1000,
        name: "test".to_string(),
        gecos: String::new(),
        home: "/home/test".into(),
        shell: "/bin/sh".into(),
        passwd: String::new(),
        groups: vec![],
    };

    let current_group = Group {
        gid: 1000,
        name: "test".to_string(),
        passwd: String::new(),
        members: Vec::new(),
    };

    let root_user = User {
        uid: 0,
        gid: 0,
        name: "root".to_string(),
        gecos: String::new(),
        home: "/root".into(),
        shell: "/bin/bash".into(),
        passwd: String::new(),
        groups: vec![],
    };

    let root_group = Group {
        gid: 0,
        name: "root".to_string(),
        passwd: String::new(),
        members: Vec::new(),
    };

    Context {
        hostname: "test-ubuntu".to_string(),
        command,
        current_user: current_user.clone(),
        target_user: if sudo_options.user.as_deref() == Some("test") {
            current_user
        } else {
            root_user
        },
        target_group: if sudo_options.user.as_deref() == Some("test") {
            current_group
        } else {
            root_group
        },
        launch: crate::common::context::LaunchType::Direct,
        chdir: sudo_options.directory.clone(),
        stdin: sudo_options.stdin,
        non_interactive: sudo_options.non_interactive,
        process: Process::new(),
        use_session_records: false,
    }
}

fn environment_to_set(environment: Environment) -> HashSet<String> {
    HashSet::from_iter(
        environment
            .iter()
            .map(|(k, v)| format!("{}={}", k.to_str().unwrap(), v.to_str().unwrap())),
    )
}

#[test]
fn test_environment_variable_filtering() {
    let mut parts = parse_env_commands(TESTS);
    let initial_env = parts.remove(0).1;

    for (cmd, expected_env) in parts {
        let options = SudoOptions::try_parse_from(cmd.split_whitespace()).unwrap();
        let settings = crate::sudoers::Judgement::default();
        let context = create_test_context(&options);
        let resulting_env =
            get_target_environment(initial_env.clone(), HashMap::new(), &context, &settings);

        let resulting_env = environment_to_set(resulting_env);
        let expected_env = environment_to_set(expected_env);
        let mut diff = resulting_env
            .symmetric_difference(&expected_env)
            .collect::<Vec<_>>();

        diff.sort();

        assert!(
            diff.is_empty(),
            "\"{cmd}\" results in an environment mismatch:\n{diff:#?}",
        );
    }
}