use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct OciAuth {
pub username: Option<String>,
pub token: Option<String>,
pub source: String,
pub anonymous: bool,
}
impl OciAuth {
pub fn anonymous() -> Self {
Self {
username: None,
token: None,
source: "anonymous".into(),
anonymous: true,
}
}
pub fn basic(username: impl Into<String>, token: impl Into<String>, source: impl Into<String>) -> Self {
Self {
username: Some(username.into()),
token: Some(token.into()),
source: source.into(),
anonymous: false,
}
}
pub fn mask_token(&self) -> String {
match &self.token {
None => "(none)".into(),
Some(t) if t.trim().is_empty() => "(empty)".into(),
Some(t) if t.len() <= 8 => "****".into(),
Some(t) => format!("{}…{}", &t[..4], &t[t.len() - 4..]),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct OciAuthRequest {
pub registry: String,
pub username: Option<String>,
pub token: Option<String>,
pub anonymous: bool,
pub auth_provider: Option<String>,
}
pub fn resolve_auth_from_candidates(
request: &OciAuthRequest,
token_candidates: &[(String, Option<String>, String)],
) -> OciAuth {
if request.anonymous
|| request
.auth_provider
.as_deref()
.map(|s| s.eq_ignore_ascii_case("anonymous"))
.unwrap_or(false)
{
return OciAuth::anonymous();
}
if request.token.as_ref().map(|t| !t.trim().is_empty()).unwrap_or(false)
|| request.username.is_some()
{
return OciAuth {
username: request.username.clone(),
token: request.token.clone(),
source: "cli".into(),
anonymous: false,
};
}
for (source, username, token) in token_candidates {
if token.trim().is_empty() {
continue;
}
return OciAuth {
username: username.clone().or_else(|| Some("x-access-token".into())),
token: Some(token.clone()),
source: source.clone(),
anonymous: false,
};
}
OciAuth {
username: None,
token: None,
source: "anonymous-fallback".into(),
anonymous: true,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cli_wins() {
let auth = resolve_auth_from_candidates(
&OciAuthRequest {
registry: "ghcr.io".into(),
username: Some("u".into()),
token: Some("t".into()),
..Default::default()
},
&[("env:GH_TOKEN".into(), None, "other".into())],
);
assert_eq!(auth.source, "cli");
}
#[test]
fn candidate_order() {
let auth = resolve_auth_from_candidates(
&OciAuthRequest {
registry: "ghcr.io".into(),
..Default::default()
},
&[
("env:GH_TOKEN".into(), Some("x".into()), "aaa".into()),
("global".into(), None, "bbb".into()),
],
);
assert_eq!(auth.source, "env:GH_TOKEN");
assert_eq!(auth.token.as_deref(), Some("aaa"));
}
}