pam-ssh-agent 0.9.5

A PAM module that authenticates using the ssh-agent.
Documentation
use crate::environment::Environment;
use crate::pamext::PamHandleExt;
use anyhow::Result;

pub fn expand_vars<'a>(
    input: String,
    env: &'a dyn Environment,
    pam_handle: &'a dyn PamHandleExt,
) -> Result<String> {
    let get_home = |s: &str| {
        let user = if s.is_empty() {
            &pam_handle.get_calling_user()?
        } else {
            s
        };
        env.get_homedir(user)
    };
    let mut input = expand_homedir(input, get_home)?;
    input = expand_var(input, "%h", || {
        env.get_homedir(pam_handle.get_calling_user()?.as_ref())
    })?;
    input = expand_var(input, "%H", || env.get_hostname())?;
    input = expand_var(input, "%u", || pam_handle.get_calling_user())?;
    input = expand_var(input, "%f", || env.get_fqdn())?;
    input = expand_var(input, "%U", || {
        Ok(env.get_uid(&pam_handle.get_calling_user()?)?.to_string())
    })?;
    Ok(input)
}

fn expand_var<F>(input: String, pattern: &str, get_value: F) -> Result<String>
where
    F: FnOnce() -> Result<String>,
{
    let Some(idx) = input.find(pattern) else {
        return Ok(input);
    };
    let mut output = input[..idx].to_owned();
    output.push_str(&get_value()?);
    output.push_str(input[idx + pattern.len()..].into());
    Ok(output)
}

fn expand_homedir<F>(input: String, get_homedir: F) -> Result<String>
where
    F: FnOnce(&str) -> Result<String>,
{
    let Some(idx) = input.find('~') else {
        return Ok(input);
    };
    let user = get_username(&input, idx);
    let mut output = input[..idx].to_string();
    output.push_str(&get_homedir(user)?);
    output.push_str(&input[idx + 1 + user.len()..]);
    Ok(output)
}

fn get_username(input: &str, idx: usize) -> &str {
    let idx = idx + 1;
    for (offset, char) in input[idx..].bytes().enumerate() {
        if char == b'/' {
            return &input[idx..idx + offset];
        }
    }
    &input[idx..]
}

#[cfg(test)]
mod tests {
    use crate::expansions::{expand_homedir, expand_var, expand_vars, get_username};
    use crate::test::{CannedEnv, CannedHandler};
    use anyhow::Result;

    #[test]
    fn test_find_homedir() -> Result<()> {
        let result = expand_homedir("/foo/bar".into(), |s| {
            assert_eq!(s, "");
            Ok("/home/noa".into())
        })?;
        assert_eq!(result, "/foo/bar");

        let result = expand_homedir("~/.file".into(), |s| {
            assert_eq!(s, "");
            Ok("/home/noa".into())
        })?;
        assert_eq!(result, "/home/noa/.file");

        let result = expand_homedir("~bob/.file".into(), |s| {
            assert_eq!(s, "bob");
            Ok("/another/bob".into())
        })?;
        assert_eq!(result, "/another/bob/.file");

        let result = expand_homedir("~bob".into(), |s| {
            assert_eq!(s, "bob");
            Ok("/another/bob".into())
        })?;
        assert_eq!(result, "/another/bob");
        Ok(())
    }
    #[test]
    fn test_expand_var() -> Result<()> {
        let f = || Ok("hostname".to_string());
        let result = expand_var("/etc/%H/file".into(), "%H", f)?;
        assert_eq!(&result, "/etc/hostname/file");
        Ok(())
    }

    #[test]
    fn test_expand_var_uid() -> Result<()> {
        let f = || Ok("401".to_string());
        let result = expand_var("/etc/%d/file".into(), "%d", f)?;
        assert_eq!(&result, "/etc/401/file");
        Ok(())
    }

    #[test]
    fn test_expand_vars() -> Result<()> {
        let env = CannedEnv::new(vec!["/another/bob", "host"]);
        let handler = CannedHandler::new(vec!["user"]);
        let result = expand_vars("~bob/.foo/%H/%u/file".into(), &env, &handler)?;
        assert_eq!("/another/bob/.foo/host/user/file", &result);
        Ok(())
    }

    #[test]
    fn test_get_username() {
        assert_eq!("alice", get_username("~alice/foo", 0));
        assert_eq!("", get_username("~/foo", 0));
        assert_eq!("bob", get_username("~bob", 0));
    }
}