Skip to main content

rung_github/
auth.rs

1//! Authentication handling for GitHub API.
2//!
3//! Tokens are stored using `SecretString` from the `secrecy` crate, which
4//! automatically zeroizes memory when dropped and prevents accidental logging.
5
6use std::process::Command;
7
8#[cfg(test)]
9use secrecy::ExposeSecret;
10use secrecy::SecretString;
11
12use crate::error::{Error, Result};
13
14/// Authentication method for GitHub API.
15#[derive(Debug, Clone)]
16pub enum Auth {
17    /// Use token from gh CLI.
18    GhCli,
19
20    /// Use token from environment variable.
21    EnvVar(String),
22
23    /// Use a specific token (zeroized on drop).
24    Token(SecretString),
25}
26
27impl Auth {
28    /// Create auth from the first available method.
29    ///
30    /// Tries in order: `GITHUB_TOKEN` env var, gh CLI.
31    #[must_use]
32    pub fn auto() -> Self {
33        if std::env::var("GITHUB_TOKEN").is_ok() {
34            Self::EnvVar("GITHUB_TOKEN".into())
35        } else {
36            Self::GhCli
37        }
38    }
39
40    /// Resolve the authentication to a token string.
41    ///
42    /// Returns a `SecretString` that will be zeroized when dropped.
43    ///
44    /// # Errors
45    /// Returns error if token cannot be obtained.
46    pub fn resolve(&self) -> Result<SecretString> {
47        match self {
48            Self::GhCli => get_gh_token(),
49            Self::EnvVar(var) => std::env::var(var)
50                .map(SecretString::from)
51                .map_err(|_| Error::NoToken),
52            Self::Token(t) => Ok(t.clone()),
53        }
54    }
55}
56
57impl Default for Auth {
58    fn default() -> Self {
59        Self::auto()
60    }
61}
62
63/// Get GitHub token from gh CLI.
64fn get_gh_token() -> Result<SecretString> {
65    let output = Command::new("gh").args(["auth", "token"]).output()?;
66
67    if !output.status.success() {
68        return Err(Error::NoToken);
69    }
70
71    let token = String::from_utf8_lossy(&output.stdout).trim().to_string();
72
73    if token.is_empty() {
74        return Err(Error::NoToken);
75    }
76
77    Ok(SecretString::from(token))
78}
79
80#[cfg(test)]
81#[allow(clippy::unwrap_used)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_auth_auto_prefers_env() {
87        // This test depends on environment, so just ensure it doesn't panic
88        let _auth = Auth::auto();
89    }
90
91    #[test]
92    fn test_token_auth() {
93        let auth = Auth::Token(SecretString::from("test_token"));
94        assert_eq!(auth.resolve().unwrap().expose_secret(), "test_token");
95    }
96}