1use serde::{Deserialize, Serialize};
2
3#[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#[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 pub auth_provider: Option<String>,
50}
51
52pub 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}