Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
148 changes: 148 additions & 0 deletions src/gitlab.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ use heck::ToKebabCase;
use reqwest::IntoUrl;
use reqwest::header::{HeaderMap, HeaderValue};
use serde_derive::{Deserialize, Serialize};
use serde_yaml::Value;
use std::collections::HashMap;
use std::fmt;
use std::path::PathBuf;
Expand Down Expand Up @@ -387,6 +388,7 @@ fn read_glab_hosts() -> Option<HashMap<String, String>> {
return None;
}
};
warn_glab_expired_tokens(&contents);
match tokens::yaml_hosts_to_tokens(&contents) {
Some(tokens) => Some(tokens),
None => {
Expand All @@ -396,6 +398,50 @@ fn read_glab_hosts() -> Option<HashMap<String, String>> {
}
}

/// Warn if any glab OAuth2 tokens are expired.
///
/// glab stores `oauth2_expiry_date` alongside `oauth2_refresh_token`. Current glab
/// versions write RFC3339; older versions used RFC822. We only check RFC3339 since
/// that is the correct format going forward--old tokens will simply not trigger the
/// warning. mise cannot refresh OAuth2 tokens itself, so we warn the user to run a
/// glab command (e.g. `glab api user`) which will trigger a silent token refresh.
fn warn_glab_expired_tokens(contents: &str) {
for (host, expiry_str) in find_expired_glab_tokens(contents) {
warn!(
"glab OAuth2 token for {host} expired at {expiry_str}. Run a glab command (e.g. `glab api user`) to refresh it."
);
}
}

/// Returns `(host, expiry_str)` pairs for every glab host whose OAuth2 token is expired.
fn find_expired_glab_tokens(contents: &str) -> Vec<(String, String)> {
let Ok(yaml) = serde_yaml::from_str::<Value>(contents) else {
return vec![];
};
let Some(hosts) = yaml.get("hosts").and_then(Value::as_mapping) else {
return vec![];
};

let mut expired = vec![];
let now = chrono::Utc::now();
for (k, entry) in hosts {
let Some(host) = k.as_str() else { continue };
if entry.get("oauth2_refresh_token").is_none() {
continue;
}
let Some(expiry_str) = entry.get("oauth2_expiry_date").and_then(Value::as_str) else {
continue;
};
let Ok(expiry_date) = chrono::DateTime::parse_from_rfc3339(expiry_str) else {
continue;
};
if expiry_date < now {
expired.push((host.to_string(), expiry_str.to_string()));
}
}
expired
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -435,4 +481,106 @@ something_else = "value"
let result = tokens::parse_tokens_toml(toml).unwrap();
assert!(result.is_empty());
}

#[test]
fn test_find_expired_glab_tokens_expired() {
let yaml = r#"
hosts:
gitlab.com:
oauth_token: gloas-abc123
oauth2_refresh_token: refresh_token
oauth2_expiry_date: "2023-03-13T15:47:00Z"
"#;
let expired = find_expired_glab_tokens(yaml);
assert_eq!(expired.len(), 1);
assert_eq!(expired[0].0, "gitlab.com");
assert_eq!(expired[0].1, "2023-03-13T15:47:00Z");
}

#[test]
fn test_find_expired_glab_tokens_not_expired() {
let yaml = r#"
hosts:
gitlab.com:
oauth_token: gloas-abc123
oauth2_refresh_token: refresh_token
oauth2_expiry_date: "2050-01-01T00:00:00Z"
"#;
let expired = find_expired_glab_tokens(yaml);
assert!(expired.is_empty());
}

#[test]
fn test_find_expired_glab_tokens_no_expiry_field() {
// PATs have no expiry date--should not be flagged
let yaml = r#"
hosts:
gitlab.com:
token: glpat-abc123
"#;
let expired = find_expired_glab_tokens(yaml);
assert!(expired.is_empty());
}

#[test]
fn test_find_expired_glab_tokens_multiple_hosts() {
let yaml = r#"
hosts:
gitlab.com:
oauth_token: gloas-abc123
oauth2_refresh_token: refresh1
oauth2_expiry_date: "2023-03-13T15:47:00Z"
gitlab.mycompany.com:
oauth_token: gloas-def456
oauth2_refresh_token: refresh2
oauth2_expiry_date: "2050-01-01T00:00:00Z"
"#;
let expired = find_expired_glab_tokens(yaml);
assert_eq!(expired.len(), 1);
assert_eq!(expired[0].0, "gitlab.com");
}

#[test]
fn test_find_expired_glab_tokens_old_format_skipped() {
// Old RFC822 format is not parsed--no false positives
let yaml = r#"
hosts:
gitlab.com:
oauth_token: gloas-abc123
oauth2_expiry_date: "13 Mar 23 15:47 GMT"
"#;
let expired = find_expired_glab_tokens(yaml);
assert!(expired.is_empty());
}

#[test]
fn test_find_expired_glab_tokens_invalid_date() {
let yaml = r#"
hosts:
gitlab.com:
oauth_token: gloas-abc123
oauth2_expiry_date: "not-a-date"
"#;
let expired = find_expired_glab_tokens(yaml);
assert!(expired.is_empty());
}

#[test]
fn test_find_expired_glab_tokens_no_refresh_token_skipped() {
// No oauth2_refresh_token means reauthentication is needed, not a refresh—don't warn.
let yaml = r#"
hosts:
gitlab.com:
oauth_token: gloas-abc123
oauth2_expiry_date: "2023-03-13T15:47:00Z"
"#;
let expired = find_expired_glab_tokens(yaml);
assert!(expired.is_empty());
}

#[test]
fn test_find_expired_glab_tokens_empty() {
assert!(find_expired_glab_tokens("").is_empty());
assert!(find_expired_glab_tokens("hosts: {}").is_empty());
}
}
Loading