Skip to main content

jj_ryu/auth/
gitlab.rs

1//! GitLab authentication
2
3use crate::auth::AuthSource;
4use crate::error::{Error, Result};
5use crate::types::normalize_host;
6use reqwest::Client;
7use serde::Deserialize;
8use std::env;
9use tokio::process::Command;
10use tracing::debug;
11
12/// GitLab authentication configuration
13#[derive(Debug, Clone)]
14pub struct GitLabAuthConfig {
15    /// Authentication token
16    pub token: String,
17    /// Where the token was obtained from
18    pub source: AuthSource,
19    /// GitLab host (e.g., "gitlab.com")
20    pub host: String,
21}
22
23/// Get GitLab authentication
24///
25/// Priority:
26/// 1. glab CLI (`glab auth status --show-token`)
27/// 2. `GITLAB_TOKEN` environment variable
28/// 3. `GL_TOKEN` environment variable
29pub async fn get_gitlab_auth(host: Option<&str>) -> Result<GitLabAuthConfig> {
30    let host = host
31        .map(normalize_host)
32        .or_else(|| env::var("GITLAB_HOST").ok().map(|h| normalize_host(&h)))
33        .unwrap_or_else(|| "gitlab.com".to_string());
34
35    // Try glab CLI first
36    debug!(host = %host, "attempting to get GitLab token via glab CLI");
37    if let Some(token) = get_glab_cli_token(&host).await {
38        debug!("obtained GitLab token from glab CLI");
39        return Ok(GitLabAuthConfig {
40            token,
41            source: AuthSource::Cli,
42            host,
43        });
44    }
45
46    // Try environment variables
47    debug!("glab CLI token not available, checking env vars");
48    if let Ok(token) = env::var("GITLAB_TOKEN") {
49        debug!("obtained GitLab token from GITLAB_TOKEN env var");
50        return Ok(GitLabAuthConfig {
51            token,
52            source: AuthSource::EnvVar,
53            host,
54        });
55    }
56
57    if let Ok(token) = env::var("GL_TOKEN") {
58        debug!("obtained GitLab token from GL_TOKEN env var");
59        return Ok(GitLabAuthConfig {
60            token,
61            source: AuthSource::EnvVar,
62            host,
63        });
64    }
65
66    debug!("no GitLab authentication found");
67    Err(Error::Auth(
68        "No GitLab authentication found. Run `glab auth login` or set GITLAB_TOKEN".to_string(),
69    ))
70}
71
72async fn get_glab_cli_token(host: &str) -> Option<String> {
73    let output = Command::new("glab")
74        .args(["auth", "status", "--show-token", "--hostname", host])
75        .output()
76        .await
77        .ok()?;
78
79    if !output.status.success() {
80        return None;
81    }
82
83    let stdout = String::from_utf8_lossy(&output.stdout);
84    let stderr = String::from_utf8_lossy(&output.stderr);
85    parse_glab_token_from_status(&format!("{stdout}\n{stderr}"))
86}
87
88/// Parse an unmasked token from current or older `glab auth status --show-token` output.
89pub fn parse_glab_token_from_status(output: &str) -> Option<String> {
90    output.lines().find_map(|line| {
91        let token = line
92            .split_once("Token found in ")
93            .and_then(|(_, source_and_token)| source_and_token.split_once(':'))
94            .map(|(_, token)| token)
95            .or_else(|| line.split_once("Token found:").map(|(_, token)| token))?
96            .trim();
97
98        if token.is_empty() || token.chars().all(|character| character == '*') {
99            None
100        } else {
101            Some(token.to_string())
102        }
103    })
104}
105
106#[derive(Deserialize)]
107struct GitLabUser {
108    username: String,
109}
110
111/// Test GitLab authentication
112pub async fn test_gitlab_auth(config: &GitLabAuthConfig) -> Result<String> {
113    let url = format!("https://{}/api/v4/user", config.host);
114
115    let client = Client::builder()
116        .timeout(std::time::Duration::from_secs(30))
117        .build()
118        .map_err(|e| Error::GitLabApi(format!("failed to create HTTP client: {e}")))?;
119
120    let user: GitLabUser = client
121        .get(&url)
122        .header("PRIVATE-TOKEN", &config.token)
123        .send()
124        .await?
125        .error_for_status()
126        .map_err(|e| Error::Auth(format!("Invalid token: {e}")))?
127        .json()
128        .await?;
129
130    Ok(user.username)
131}