doas 0.1.0

A Rust implementation of doas, a minimal sudo alternative with PAM authentication
Documentation
use std::collections::HashMap;
use std::env;
use std::ffi::{CStr, CString};
use std::fs;
use std::process::{self, Command};

use libc::{getuid, setgid, setuid};
use pam::Authenticator;
use std::os::unix::process::CommandExt;

#[derive(Debug)]
pub struct Rule {
    pub permit: bool,
    pub nopass: bool,
    pub keepenv: bool,
    #[allow(dead_code)]
    pub nolog: bool,
    pub identity: String,
    pub target: String,
}

pub fn get_current_user() -> String {
    unsafe {
        let uid = getuid();
        let passwd = libc::getpwuid(uid);
        if passwd.is_null() {
            panic!("Failed to get current user");
        }
        CStr::from_ptr((*passwd).pw_name).to_string_lossy().to_string()
    }
}

pub fn parse_config(path: &str) -> Result<Vec<Rule>, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;
    let mut rules = Vec::new();

    for line in content.lines() {
        let line = line.trim();
        if line.is_empty() || line.starts_with('#') {
            continue;
        }

        let mut parts = line.split_whitespace().peekable();
        let Some(action) = parts.next() else { continue };
        let permit = match action {
            "permit" => true,
            "deny" => false,
            _ => continue,
        };

        let mut nopass = false;
        let mut keepenv = false;
        let mut nolog = false;
        let mut identity: Option<String> = None;

        while let Some(token) = parts.next() {
            match token {
                "nopass" => nopass = true,
                "keepenv" => keepenv = true,
                "nolog" => nolog = true,
                _ => {
                    identity = Some(token.to_string());
                    break;
                }
            }
        }

        let Some(identity) = identity else { continue };
        if parts.next() != Some("as") {
            continue;
        }
        let Some(target) = parts.next() else { continue };

        rules.push(Rule {
            permit,
            nopass,
            keepenv,
            nolog,
            identity,
            target: target.to_string(),
        });
    }
    Ok(rules)
}

pub fn check_permission(rules: &[Rule], identity: &str, target: &str) -> bool {
    for rule in rules {
        if rule.identity == identity && rule.target == target {
            return rule.permit;
        }
    }
    false
}

pub fn find_rule<'a>(rules: &'a [Rule], identity: &str, target: &str) -> Option<&'a Rule> {
    rules.iter().find(|r| r.identity == identity && r.target == target)
}

pub fn authenticate(_user: &str) -> bool {
    let mut auth = Authenticator::with_password("doas").unwrap();
    auth.authenticate().is_ok() && auth.open_session().is_ok()
}

pub fn run_command(command: &[String], target_user: &str, keepenv: bool) {
    let uid = get_user_uid(target_user);
    let gid = get_user_gid(target_user);

    unsafe {
        if setgid(gid) != 0 {
            eprintln!("doas: failed to set gid");
            process::exit(1);
        }
        if setuid(uid) != 0 {
            eprintln!("doas: failed to set uid");
            process::exit(1);
        }
    }

    let mut env_vars = if keepenv {
        env::vars().collect::<HashMap<_, _>>()
    } else {
        HashMap::new()
    };
    env_vars.insert(
        "PATH".to_string(),
        "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin".to_string(),
    );
    env_vars.insert("USER".to_string(), target_user.to_string());
    env_vars.insert("HOME".to_string(), get_user_home(target_user));
    env_vars.insert("SHELL".to_string(), get_user_shell(target_user));

    let mut cmd = Command::new(&command[0]);
    cmd.args(&command[1..]);
    cmd.env_clear();
    for (k, v) in env_vars {
        cmd.env(k, v);
    }
    let err = cmd.exec();
    eprintln!("doas: failed to exec {}: {}", command[0], err);
    process::exit(1);
}

fn get_user_uid(user: &str) -> libc::uid_t {
    unsafe {
        let c_user = CString::new(user).unwrap();
        let passwd = libc::getpwnam(c_user.as_ptr());
        if passwd.is_null() {
            panic!("User {} not found", user);
        }
        (*passwd).pw_uid
    }
}

fn get_user_gid(user: &str) -> libc::gid_t {
    unsafe {
        let c_user = CString::new(user).unwrap();
        let passwd = libc::getpwnam(c_user.as_ptr());
        if passwd.is_null() {
            panic!("User {} not found", user);
        }
        (*passwd).pw_gid
    }
}

fn get_user_home(user: &str) -> String {
    unsafe {
        let c_user = CString::new(user).unwrap();
        let passwd = libc::getpwnam(c_user.as_ptr());
        if passwd.is_null() {
            "/".to_string()
        } else {
            CStr::from_ptr((*passwd).pw_dir).to_string_lossy().to_string()
        }
    }
}

fn get_user_shell(user: &str) -> String {
    unsafe {
        let c_user = CString::new(user).unwrap();
        let passwd = libc::getpwnam(c_user.as_ptr());
        if passwd.is_null() {
            "/bin/sh".to_string()
        } else {
            CStr::from_ptr((*passwd).pw_shell).to_string_lossy().to_string()
        }
    }
}