use std::collections::HashMap;
use std::fmt;
use serde::Deserialize;
pub(crate) const ACCESS_TOKENS_ENV: &str = "LUX_ACCESS_TOKENS";
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Kind {
Plain,
GitLabPAT,
GitLabOAuth2,
}
#[derive(Clone, PartialEq, Eq)]
pub struct AccessToken {
kind: Kind,
token: String,
}
impl fmt::Debug for AccessToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("AccessToken")
.field("kind", &self.kind)
.finish_non_exhaustive()
}
}
impl From<&str> for AccessToken {
fn from(raw: &str) -> Self {
let (kind, token) = if let Some(rest) = raw.strip_prefix("PAT:") {
(Kind::GitLabPAT, rest)
} else if let Some(rest) = raw.strip_prefix("OAuth2:") {
(Kind::GitLabOAuth2, rest)
} else {
(Kind::Plain, raw)
};
Self {
kind,
token: token.into(),
}
}
}
impl<'de> Deserialize<'de> for AccessToken {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
String::deserialize(deserializer).map(|raw| raw.as_str().into())
}
}
impl AccessToken {
pub fn kind(&self) -> Kind {
self.kind
}
pub fn username(&self) -> &str {
match self.kind {
Kind::Plain => "x-access-token",
Kind::GitLabPAT | Kind::GitLabOAuth2 => "oauth2",
}
}
pub unsafe fn password(&self) -> &str {
&self.token
}
}
pub(crate) fn env_access_tokens() -> HashMap<String, AccessToken> {
let Ok(raw) = std::env::var(ACCESS_TOKENS_ENV) else {
return HashMap::new();
};
let mut tokens = HashMap::new();
for entry in raw.split_whitespace() {
if let Some((host, token)) = entry.split_once('=') {
tokens.insert(host.to_string(), AccessToken::from(token));
}
}
tokens
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn from_strips_known_prefixes() {
let token = AccessToken::from("PAT:A123Bp_Cd..EfG");
assert_eq!(token.kind(), Kind::GitLabPAT);
assert_eq!(token.username(), "oauth2");
assert_eq!(unsafe { token.password() }, "A123Bp_Cd..EfG");
let token = AccessToken::from("OAuth2:1jklw3jk");
assert_eq!(token.kind(), Kind::GitLabOAuth2);
assert_eq!(token.username(), "oauth2");
assert_eq!(unsafe { token.password() }, "1jklw3jk");
let token = AccessToken::from("23ac...b289");
assert_eq!(token.kind(), Kind::Plain);
assert_eq!(token.username(), "x-access-token");
assert_eq!(unsafe { token.password() }, "23ac...b289");
}
#[test]
fn env_access_tokens_parses_pairs_and_ignores_bare_entries() {
std::env::set_var(
ACCESS_TOKENS_ENV,
"github.com=23ac...b289 gitlab.com=OAuth2:1jklw3jk",
);
let tokens = env_access_tokens();
assert_eq!(
tokens.get("github.com").cloned(),
Some(AccessToken::from("23ac...b289"))
);
assert_eq!(
tokens.get("gitlab.com").cloned(),
Some(AccessToken::from("OAuth2:1jklw3jk"))
);
std::env::remove_var(ACCESS_TOKENS_ENV);
std::env::set_var(ACCESS_TOKENS_ENV, "gitlab.com=tok,en");
let tokens = env_access_tokens();
assert_eq!(
tokens.get("gitlab.com").cloned(),
Some(AccessToken::from("tok,en"))
);
std::env::remove_var(ACCESS_TOKENS_ENV);
std::env::set_var(ACCESS_TOKENS_ENV, "23ac...b289");
assert!(env_access_tokens().is_empty());
std::env::remove_var(ACCESS_TOKENS_ENV);
std::env::set_var(ACCESS_TOKENS_ENV, "");
assert!(env_access_tokens().is_empty());
std::env::remove_var(ACCESS_TOKENS_ENV);
}
}