Skip to main content

xbp_oci/
auth.rs

1use serde::{Deserialize, Serialize};
2
3/// Resolved registry credentials (never log `token`).
4#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
5pub struct OciAuth {
6    pub username: Option<String>,
7    pub token: Option<String>,
8    pub source: String,
9    pub anonymous: bool,
10}
11
12impl OciAuth {
13    pub fn anonymous() -> Self {
14        Self {
15            username: None,
16            token: None,
17            source: "anonymous".into(),
18            anonymous: true,
19        }
20    }
21
22    pub fn basic(username: impl Into<String>, token: impl Into<String>, source: impl Into<String>) -> Self {
23        Self {
24            username: Some(username.into()),
25            token: Some(token.into()),
26            source: source.into(),
27            anonymous: false,
28        }
29    }
30
31    pub fn mask_token(&self) -> String {
32        match &self.token {
33            None => "(none)".into(),
34            Some(t) if t.trim().is_empty() => "(empty)".into(),
35            Some(t) if t.len() <= 8 => "****".into(),
36            Some(t) => format!("{}…{}", &t[..4], &t[t.len() - 4..]),
37        }
38    }
39}
40
41/// Inputs for auth resolution (caller supplies env/global lookups).
42#[derive(Debug, Clone, Default)]
43pub struct OciAuthRequest {
44    pub registry: String,
45    pub username: Option<String>,
46    pub token: Option<String>,
47    pub anonymous: bool,
48    /// Hint: "github", "anonymous", provider name.
49    pub auth_provider: Option<String>,
50}
51
52/// Pure resolution given explicit token candidates (no env/fs I/O).
53///
54/// Priority:
55/// 1. anonymous flag / provider
56/// 2. explicit username/token
57/// 3. first non-empty candidate from `token_candidates` (label, username?, token)
58/// 4. anonymous fallback
59pub fn resolve_auth_from_candidates(
60    request: &OciAuthRequest,
61    token_candidates: &[(String, Option<String>, String)],
62) -> OciAuth {
63    if request.anonymous
64        || request
65            .auth_provider
66            .as_deref()
67            .map(|s| s.eq_ignore_ascii_case("anonymous"))
68            .unwrap_or(false)
69    {
70        return OciAuth::anonymous();
71    }
72
73    if request.token.as_ref().map(|t| !t.trim().is_empty()).unwrap_or(false)
74        || request.username.is_some()
75    {
76        return OciAuth {
77            username: request.username.clone(),
78            token: request.token.clone(),
79            source: "cli".into(),
80            anonymous: false,
81        };
82    }
83
84    for (source, username, token) in token_candidates {
85        if token.trim().is_empty() {
86            continue;
87        }
88        return OciAuth {
89            username: username.clone().or_else(|| Some("x-access-token".into())),
90            token: Some(token.clone()),
91            source: source.clone(),
92            anonymous: false,
93        };
94    }
95
96    OciAuth {
97        username: None,
98        token: None,
99        source: "anonymous-fallback".into(),
100        anonymous: true,
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107
108    #[test]
109    fn cli_wins() {
110        let auth = resolve_auth_from_candidates(
111            &OciAuthRequest {
112                registry: "ghcr.io".into(),
113                username: Some("u".into()),
114                token: Some("t".into()),
115                ..Default::default()
116            },
117            &[("env:GH_TOKEN".into(), None, "other".into())],
118        );
119        assert_eq!(auth.source, "cli");
120    }
121
122    #[test]
123    fn candidate_order() {
124        let auth = resolve_auth_from_candidates(
125            &OciAuthRequest {
126                registry: "ghcr.io".into(),
127                ..Default::default()
128            },
129            &[
130                ("env:GH_TOKEN".into(), Some("x".into()), "aaa".into()),
131                ("global".into(), None, "bbb".into()),
132            ],
133        );
134        assert_eq!(auth.source, "env:GH_TOKEN");
135        assert_eq!(auth.token.as_deref(), Some("aaa"));
136    }
137}