Skip to main content

git_same/auth/
mod.rs

1//! Authentication management for gisa.
2//!
3//! This module handles authentication with Git hosting providers
4//! using the GitHub CLI (`gh auth token`).
5
6pub mod gh_cli;
7pub mod ssh;
8
9use crate::config::WorkspaceProvider;
10use crate::errors::AppError;
11use tracing::{debug, warn};
12
13/// Authentication result containing the token and metadata.
14#[derive(Debug, Clone)]
15pub struct AuthResult {
16    /// The authentication token
17    pub token: String,
18    /// Method used to obtain the token
19    pub method: ResolvedAuthMethod,
20    /// The authenticated username (if available)
21    pub username: Option<String>,
22}
23
24/// The actual method used for authentication.
25#[derive(Debug, Clone, PartialEq, Eq)]
26pub enum ResolvedAuthMethod {
27    /// Used GitHub CLI
28    GhCli,
29}
30
31impl std::fmt::Display for ResolvedAuthMethod {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        match self {
34            ResolvedAuthMethod::GhCli => write!(f, "GitHub CLI"),
35        }
36    }
37}
38
39/// Get authentication using the GitHub CLI.
40///
41/// Requires `gh` to be installed and authenticated.
42pub fn get_auth() -> Result<AuthResult, AppError> {
43    debug!("Resolving authentication via gh CLI");
44
45    let gh_installed = gh_cli::is_installed();
46    let gh_authenticated = gh_installed && gh_cli::is_authenticated();
47    debug!(gh_installed, gh_authenticated, "Checking GitHub CLI status");
48
49    if gh_installed && gh_authenticated {
50        match gh_cli::get_token() {
51            Ok(token) => {
52                let username = gh_cli::get_username().ok();
53                debug!(
54                    username = username.as_deref().unwrap_or("<unknown>"),
55                    "Authenticated via GitHub CLI"
56                );
57                return Ok(AuthResult {
58                    token,
59                    method: ResolvedAuthMethod::GhCli,
60                    username,
61                });
62            }
63            Err(e) => {
64                warn!(error = %e, "gh CLI token retrieval failed");
65            }
66        }
67    }
68
69    let ssh_note = if ssh::has_ssh_keys() {
70        "\n\nNote: SSH keys detected. While SSH keys work for git clone/push,\n\
71         you still need a provider API token for repository discovery.\n\
72         The SSH keys will be used automatically for cloning."
73    } else {
74        ""
75    };
76
77    Err(AppError::auth(format!(
78        "No authentication found.\n\n\
79         Please authenticate using the GitHub CLI:\n\n\
80         For GitHub.com:       gh auth login\n\
81         For GitHub Enterprise: gh auth login --hostname <your-host>\n\
82         {}\n\
83         Install from: https://cli.github.com/",
84        ssh_note
85    )))
86}
87
88/// Get authentication for a specific workspace provider configuration.
89pub fn get_auth_for_provider(provider: &WorkspaceProvider) -> Result<AuthResult, AppError> {
90    debug!(
91        api_url = provider.api_url.as_deref().unwrap_or("default"),
92        "Resolving authentication for provider"
93    );
94
95    // For GitHub Enterprise, try to get token for specific host
96    if let Some(api_url) = &provider.api_url {
97        if let Some(host) = extract_host(api_url) {
98            if host != "api.github.com" {
99                debug!(host, "Attempting GitHub Enterprise authentication");
100                if let Ok(token) = gh_cli::get_token_for_host(&host) {
101                    debug!(host, "Authenticated via gh CLI for enterprise host");
102                    return Ok(AuthResult {
103                        token,
104                        method: ResolvedAuthMethod::GhCli,
105                        username: None,
106                    });
107                }
108            }
109        }
110    }
111
112    if !gh_cli::is_installed() {
113        debug!("gh CLI not installed");
114        return Err(AppError::auth(
115            "GitHub CLI is not installed. Install from https://cli.github.com/",
116        ));
117    }
118    if !gh_cli::is_authenticated() {
119        debug!("gh CLI not authenticated");
120        return Err(AppError::auth(
121            "GitHub CLI is not authenticated. Run: gh auth login",
122        ));
123    }
124
125    let token = gh_cli::get_token()?;
126    let username = gh_cli::get_username().ok();
127    debug!(
128        username = username.as_deref().unwrap_or("<unknown>"),
129        "Authenticated via gh CLI"
130    );
131
132    Ok(AuthResult {
133        token,
134        method: ResolvedAuthMethod::GhCli,
135        username,
136    })
137}
138
139/// Extract hostname from an API URL.
140fn extract_host(url: &str) -> Option<String> {
141    let without_scheme = url.split_once("://").map(|(_, rest)| rest).unwrap_or(url);
142    let host = without_scheme.split('/').next()?;
143    if host.is_empty() {
144        return None;
145    }
146    Some(host.to_string())
147}
148
149#[cfg(test)]
150#[path = "mod_tests.rs"]
151mod tests;