zlayer_core/auth/
resolver.rs1pub use zlayer_types::auth::{AuthConfig, AuthSource, RegistryAuthConfig};
11
12use super::DockerConfigAuth;
13use std::collections::HashMap;
14use std::path::PathBuf;
15
16pub struct AuthResolver {
18 config: AuthConfig,
19 docker_config: Option<DockerConfigAuth>,
20 registry_map: HashMap<String, AuthSource>,
21}
22
23impl AuthResolver {
24 #[must_use]
26 pub fn new(config: AuthConfig) -> Self {
27 let registry_map: HashMap<String, AuthSource> = config
29 .registries
30 .iter()
31 .map(|r| (r.registry.clone(), r.source.clone()))
32 .collect();
33
34 let needs_docker_config = config.default == AuthSource::DockerConfig
36 || registry_map
37 .values()
38 .any(|s| matches!(s, AuthSource::DockerConfig));
39
40 let docker_config = if needs_docker_config {
41 Self::load_docker_config(config.docker_config_path.as_ref())
42 } else {
43 None
44 };
45
46 Self {
47 config,
48 docker_config,
49 registry_map,
50 }
51 }
52
53 #[must_use]
58 pub fn resolve(&self, image: &str) -> oci_client::secrets::RegistryAuth {
59 let registry = Self::extract_registry(image);
60 let source = self
61 .registry_map
62 .get(®istry)
63 .unwrap_or(&self.config.default);
64
65 self.resolve_source(source, ®istry)
66 }
67
68 #[must_use]
72 pub fn source_for_registry(&self, registry: &str) -> &AuthSource {
73 self.registry_map
74 .get(registry)
75 .unwrap_or(&self.config.default)
76 }
77
78 pub fn resolve_source(
84 &self,
85 source: &AuthSource,
86 registry: &str,
87 ) -> oci_client::secrets::RegistryAuth {
88 match source {
89 AuthSource::Anonymous => oci_client::secrets::RegistryAuth::Anonymous,
90
91 AuthSource::Basic { username, password } => {
92 oci_client::secrets::RegistryAuth::Basic(username.clone(), password.clone())
93 }
94
95 AuthSource::DockerConfig => {
96 if let Some(ref docker_config) = self.docker_config {
97 if let Some((username, password)) = docker_config.get_credentials(registry) {
98 return oci_client::secrets::RegistryAuth::Basic(username, password);
99 }
100 }
101 oci_client::secrets::RegistryAuth::Anonymous
103 }
104
105 AuthSource::EnvVar {
106 username_var,
107 password_var,
108 } => {
109 let username = std::env::var(username_var).unwrap_or_default();
110 let password = std::env::var(password_var).unwrap_or_default();
111
112 if !username.is_empty() && !password.is_empty() {
113 oci_client::secrets::RegistryAuth::Basic(username, password)
114 } else {
115 oci_client::secrets::RegistryAuth::Anonymous
116 }
117 }
118
119 AuthSource::SecretStore { .. } => {
120 tracing::warn!(
121 "SecretStore auth source requires async resolver; returning Anonymous"
122 );
123 oci_client::secrets::RegistryAuth::Anonymous
124 }
125 }
126 }
127
128 fn extract_registry(image: &str) -> String {
135 let image_without_digest = image.split('@').next().unwrap_or(image);
137
138 let parts: Vec<&str> = image_without_digest.split('/').collect();
140
141 if parts.len() == 1 {
143 return "docker.io".to_string();
144 }
145
146 let first_part = parts[0];
148 if first_part.contains('.') || first_part.contains(':') || first_part == "localhost" {
149 first_part.to_string()
150 } else {
151 "docker.io".to_string()
153 }
154 }
155
156 fn load_docker_config(path: Option<&PathBuf>) -> Option<DockerConfigAuth> {
158 let config = if let Some(path) = path {
159 DockerConfigAuth::load_from_path(path).ok()
160 } else {
161 DockerConfigAuth::load().ok()
162 };
163
164 if config.is_none() {
165 tracing::debug!("Failed to load Docker config, using anonymous auth as fallback");
166 }
167
168 config
169 }
170}
171
172#[cfg(test)]
173mod tests {
174 use super::*;
175
176 #[test]
177 fn test_extract_registry() {
178 assert_eq!(AuthResolver::extract_registry("ubuntu"), "docker.io");
179 assert_eq!(AuthResolver::extract_registry("ubuntu:latest"), "docker.io");
180 assert_eq!(
181 AuthResolver::extract_registry("library/ubuntu"),
182 "docker.io"
183 );
184 assert_eq!(
185 AuthResolver::extract_registry("ghcr.io/owner/repo"),
186 "ghcr.io"
187 );
188 assert_eq!(
189 AuthResolver::extract_registry("ghcr.io/owner/repo:tag"),
190 "ghcr.io"
191 );
192 assert_eq!(
193 AuthResolver::extract_registry("localhost:5000/image"),
194 "localhost:5000"
195 );
196 assert_eq!(
197 AuthResolver::extract_registry("myregistry.com/path/to/image:v1.0"),
198 "myregistry.com"
199 );
200 }
201
202 #[test]
203 fn test_anonymous_auth() {
204 let config = AuthConfig {
205 default: AuthSource::Anonymous,
206 ..Default::default()
207 };
208
209 let resolver = AuthResolver::new(config);
210 let auth = resolver.resolve("ubuntu:latest");
211
212 assert!(matches!(auth, oci_client::secrets::RegistryAuth::Anonymous));
213 }
214
215 #[test]
216 fn test_basic_auth() {
217 let config = AuthConfig {
218 default: AuthSource::Basic {
219 username: "user".to_string(),
220 password: "pass".to_string(),
221 },
222 ..Default::default()
223 };
224
225 let resolver = AuthResolver::new(config);
226 let auth = resolver.resolve("ubuntu:latest");
227
228 match auth {
229 oci_client::secrets::RegistryAuth::Basic(username, password) => {
230 assert_eq!(username, "user");
231 assert_eq!(password, "pass");
232 }
233 _ => panic!("Expected Basic auth"),
234 }
235 }
236
237 #[test]
238 fn test_per_registry_auth() {
239 let config = AuthConfig {
240 registries: vec![RegistryAuthConfig {
241 registry: "ghcr.io".to_string(),
242 source: AuthSource::Basic {
243 username: "ghcr_user".to_string(),
244 password: "ghcr_pass".to_string(),
245 },
246 }],
247 default: AuthSource::Anonymous,
248 ..Default::default()
249 };
250
251 let resolver = AuthResolver::new(config);
252
253 let auth = resolver.resolve("ghcr.io/owner/repo:tag");
255 match auth {
256 oci_client::secrets::RegistryAuth::Basic(username, password) => {
257 assert_eq!(username, "ghcr_user");
258 assert_eq!(password, "ghcr_pass");
259 }
260 _ => panic!("Expected Basic auth for ghcr.io"),
261 }
262
263 let auth = resolver.resolve("ubuntu:latest");
265 assert!(matches!(auth, oci_client::secrets::RegistryAuth::Anonymous));
266 }
267
268 #[test]
269 fn test_env_var_auth() {
270 std::env::set_var("TEST_USERNAME", "env_user");
271 std::env::set_var("TEST_PASSWORD", "env_pass");
272
273 let config = AuthConfig {
274 default: AuthSource::EnvVar {
275 username_var: "TEST_USERNAME".to_string(),
276 password_var: "TEST_PASSWORD".to_string(),
277 },
278 ..Default::default()
279 };
280
281 let resolver = AuthResolver::new(config);
282 let auth = resolver.resolve("ubuntu:latest");
283
284 match auth {
285 oci_client::secrets::RegistryAuth::Basic(username, password) => {
286 assert_eq!(username, "env_user");
287 assert_eq!(password, "env_pass");
288 }
289 _ => panic!("Expected Basic auth from env vars"),
290 }
291
292 std::env::remove_var("TEST_USERNAME");
293 std::env::remove_var("TEST_PASSWORD");
294 }
295
296 #[test]
297 fn test_env_var_auth_fallback() {
298 let config = AuthConfig {
300 default: AuthSource::EnvVar {
301 username_var: "NONEXISTENT_USER".to_string(),
302 password_var: "NONEXISTENT_PASS".to_string(),
303 },
304 ..Default::default()
305 };
306
307 let resolver = AuthResolver::new(config);
308 let auth = resolver.resolve("ubuntu:latest");
309
310 assert!(matches!(auth, oci_client::secrets::RegistryAuth::Anonymous));
311 }
312
313 #[test]
314 fn test_secret_store_sync_fallback_returns_anonymous() {
315 let config = AuthConfig {
316 registries: vec![RegistryAuthConfig {
317 registry: "private.registry.io".to_string(),
318 source: AuthSource::SecretStore {
319 credential_id: "cred-uuid-123".to_string(),
320 },
321 }],
322 default: AuthSource::Anonymous,
323 ..Default::default()
324 };
325
326 let resolver = AuthResolver::new(config);
327
328 let auth = resolver.resolve("private.registry.io/image:latest");
330 assert!(matches!(auth, oci_client::secrets::RegistryAuth::Anonymous));
331
332 let auth = resolver.resolve("ubuntu:latest");
334 assert!(matches!(auth, oci_client::secrets::RegistryAuth::Anonymous));
335 }
336
337 #[test]
338 fn test_source_for_registry_returns_correct_source() {
339 let config = AuthConfig {
340 registries: vec![RegistryAuthConfig {
341 registry: "ghcr.io".to_string(),
342 source: AuthSource::Basic {
343 username: "user".to_string(),
344 password: "pass".to_string(),
345 },
346 }],
347 default: AuthSource::Anonymous,
348 ..Default::default()
349 };
350
351 let resolver = AuthResolver::new(config);
352
353 let source = resolver.source_for_registry("ghcr.io");
355 assert!(matches!(source, AuthSource::Basic { .. }));
356
357 let source = resolver.source_for_registry("docker.io");
359 assert!(matches!(source, AuthSource::Anonymous));
360 }
361
362 #[test]
363 fn test_secret_store_serde_roundtrip() {
364 let source = AuthSource::SecretStore {
365 credential_id: "abc-123".to_string(),
366 };
367 let json = serde_json::to_string(&source).unwrap();
368 let parsed: AuthSource = serde_json::from_str(&json).unwrap();
369 assert_eq!(source, parsed);
370 }
371}