rung_github/
auth.rs

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