1use 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#[derive(Debug, Clone)]
14pub struct GitLabAuthConfig {
15 pub token: String,
17 pub source: AuthSource,
19 pub host: String,
21}
22
23pub 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 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 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
88pub 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
111pub 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}