sudo-rs 0.2.13

A memory safe implementation of sudo and su.
Documentation
use crate::common::resolve::CurrentUser;
use crate::common::{CommandAndArguments, Context};
use crate::sudo::{
    cli::{SudoAction, SudoRunOptions},
    env::environment::{Environment, get_target_environment},
};
use crate::system::interface::{GroupId, UserId};
use crate::system::{Group, Hostname, 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:
    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
    SUDO_HOME=/home/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:
    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
    SUDO_HOME=/home/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 = 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: SudoRunOptions) -> Context {
    let path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string();
    let command = CommandAndArguments::build_from_args(None, sudo_options.positional_args, &path);

    let current_user = CurrentUser::fake(User {
        uid: UserId::new(1000),
        gid: GroupId::new(1000),

        name: "test".into(),
        home: "/home/test".into(),
        shell: "/bin/sh".into(),
        groups: vec![],
    });

    let current_group = Group {
        gid: GroupId::new(1000),
        name: Some("test".to_string()),
    };

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

    let root_group = Group {
        gid: GroupId::new(0),
        name: Some("root".to_string()),
    };

    Context {
        hostname: Hostname::fake("test-ubuntu"),
        command,
        current_user: current_user.clone(),
        target_user: if sudo_options.user.as_deref() == Some("test") {
            current_user.into()
        } 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.chdir,
        askpass: sudo_options.askpass,
        stdin: sudo_options.stdin,
        prompt: sudo_options.prompt,
        non_interactive: sudo_options.non_interactive,
        use_session_records: false,
        bell: false,
        background: false,
        files_to_edit: vec![],
    }
}

fn environment_to_set(environment: Environment) -> HashSet<String> {
    HashSet::from_iter(
        environment
            .into_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 = SudoAction::try_parse_from(cmd.split_whitespace())
            .unwrap()
            .try_into_run()
            .ok()
            .unwrap();
        let settings = crate::defaults::Settings::default();
        let context = create_test_context(options);
        let resulting_env = get_target_environment(
            initial_env.clone(),
            HashMap::new(),
            Vec::new(),
            &context,
            &crate::sudoers::Restrictions {
                env_keep: settings.env_keep(),
                env_check: settings.env_check(),
                path: settings.secure_path(),
                use_pty: true,
                chdir: crate::sudoers::DirChange::Strict(None),
                trust_environment: false,
                umask: crate::exec::Umask::Preserve,
                #[cfg(feature = "apparmor")]
                apparmor_profile: None,
                noexec: false,
            },
        )
        .unwrap();

        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:#?}",
        );
    }
}