containerregistry_auth/
config.rs1use std::collections::HashMap;
7use std::path::{Path, PathBuf};
8
9use serde::Deserialize;
10
11use crate::{Credential, Error, Result};
12
13#[derive(Clone, Debug, Default, Deserialize)]
18#[serde(rename_all = "camelCase")]
19pub struct DockerConfig {
20 #[serde(default)]
22 pub auths: HashMap<String, AuthEntry>,
23
24 #[serde(default, rename = "credsStore")]
26 pub creds_store: Option<String>,
27
28 #[serde(default, rename = "credHelpers")]
30 pub cred_helpers: HashMap<String, String>,
31}
32
33#[derive(Clone, Debug, Default, Deserialize)]
35pub struct AuthEntry {
36 #[serde(default)]
38 pub auth: Option<String>,
39
40 #[serde(default)]
42 pub username: Option<String>,
43
44 #[serde(default)]
46 pub password: Option<String>,
47
48 #[serde(default, rename = "identitytoken")]
50 pub identity_token: Option<String>,
51
52 #[serde(default, rename = "registrytoken")]
54 pub registry_token: Option<String>,
55}
56
57impl DockerConfig {
58 pub fn load() -> Result<Self> {
63 let path = Self::default_config_path()?;
64 Self::load_from(&path)
65 }
66
67 pub fn load_from(path: &Path) -> Result<Self> {
69 let contents = std::fs::read_to_string(path).map_err(|e| {
70 if e.kind() == std::io::ErrorKind::NotFound {
71 return Error::Io(e);
73 }
74 Error::Io(e)
75 })?;
76
77 Self::from_json(&contents)
78 }
79
80 pub fn from_json(json: &str) -> Result<Self> {
82 serde_json::from_str(json).map_err(|e| Error::ConfigParse(e.to_string()))
83 }
84
85 pub fn default_config_path() -> Result<PathBuf> {
90 if let Ok(docker_config) = std::env::var("DOCKER_CONFIG") {
92 let path = PathBuf::from(docker_config);
93 return Ok(path.join("config.json"));
94 }
95
96 let home = std::env::var("HOME")
98 .or_else(|_| std::env::var("USERPROFILE"))
99 .map_err(|_| Error::ConfigParse("could not determine home directory".to_string()))?;
100
101 Ok(PathBuf::from(home).join(".docker").join("config.json"))
102 }
103
104 pub fn get_credential_helper(&self, registry: &str) -> Option<&str> {
109 if let Some(helper) = self.cred_helpers.get(registry) {
111 return Some(helper.as_str());
112 }
113
114 let normalized = normalize_registry(registry);
116 if let Some(helper) = self.cred_helpers.get(&normalized) {
117 return Some(helper.as_str());
118 }
119
120 self.creds_store.as_deref()
122 }
123
124 pub fn get_auth(&self, registry: &str) -> Option<Credential> {
126 if let Some(entry) = self.auths.get(registry)
128 && let Some(cred) = entry.to_credential()
129 {
130 return Some(cred);
131 }
132
133 let normalized = normalize_registry(registry);
135
136 for (key, entry) in &self.auths {
138 let normalized_key = normalize_registry(key);
139 if normalized_key == normalized
140 && let Some(cred) = entry.to_credential()
141 {
142 return Some(cred);
143 }
144 }
145
146 None
147 }
148}
149
150impl AuthEntry {
151 pub fn to_credential(&self) -> Option<Credential> {
153 if let Some(ref token) = self.identity_token
155 && !token.is_empty()
156 {
157 return Some(Credential::identity_token(token));
158 }
159
160 if let Some(ref token) = self.registry_token
162 && !token.is_empty()
163 {
164 return Some(Credential::bearer(token));
165 }
166
167 if let Some(ref auth) = self.auth
169 && !auth.is_empty()
170 {
171 return Self::decode_auth(auth);
172 }
173
174 if let (Some(username), Some(password)) = (&self.username, &self.password)
176 && !username.is_empty()
177 {
178 return Some(Credential::basic(username, password));
179 }
180
181 None
182 }
183
184 fn decode_auth(auth: &str) -> Option<Credential> {
186 use base64::Engine;
187
188 let decoded = base64::engine::general_purpose::STANDARD
189 .decode(auth)
190 .ok()?;
191 let decoded_str = String::from_utf8(decoded).ok()?;
192
193 let (username, password) = decoded_str.split_once(':')?;
195
196 if username.is_empty() {
197 None
198 } else {
199 Some(Credential::basic(username, password))
200 }
201 }
202}
203
204fn normalize_registry(registry: &str) -> String {
209 let registry = registry
210 .trim_start_matches("https://")
211 .trim_start_matches("http://");
212 let registry = registry.trim_end_matches('/');
213
214 let registry = registry.trim_end_matches("/v1").trim_end_matches("/v2");
216
217 match registry {
219 "docker.io" | "registry-1.docker.io" => "index.docker.io".to_string(),
220 other => other.to_string(),
221 }
222}
223
224#[cfg(test)]
225mod tests {
226 use super::*;
227 use crate::test_util::ENV_LOCK;
228
229 #[test]
230 fn test_parse_empty_config() {
231 let config: DockerConfig = serde_json::from_str("{}").unwrap();
232 assert!(config.auths.is_empty());
233 assert!(config.creds_store.is_none());
234 assert!(config.cred_helpers.is_empty());
235 }
236
237 #[test]
238 fn test_parse_auths_base64() {
239 let json = r#"{
240 "auths": {
241 "https://index.docker.io/v1/": {
242 "auth": "dXNlcjpwYXNz"
243 }
244 }
245 }"#;
246
247 let config: DockerConfig = serde_json::from_str(json).unwrap();
248 let entry = config.auths.get("https://index.docker.io/v1/").unwrap();
249 let cred = entry.to_credential().unwrap();
250
251 assert_eq!(cred.username(), Some("user"));
252 assert_eq!(cred.password(), Some("pass"));
253 }
254
255 #[test]
256 fn test_parse_auths_plaintext() {
257 let json = r#"{
258 "auths": {
259 "gcr.io": {
260 "username": "myuser",
261 "password": "mypass"
262 }
263 }
264 }"#;
265
266 let config: DockerConfig = serde_json::from_str(json).unwrap();
267 let entry = config.auths.get("gcr.io").unwrap();
268 let cred = entry.to_credential().unwrap();
269
270 assert_eq!(cred.username(), Some("myuser"));
271 assert_eq!(cred.password(), Some("mypass"));
272 }
273
274 #[test]
275 fn test_parse_creds_store() {
276 let json = r#"{
277 "credsStore": "osxkeychain"
278 }"#;
279
280 let config: DockerConfig = serde_json::from_str(json).unwrap();
281 assert_eq!(config.creds_store.as_deref(), Some("osxkeychain"));
282 }
283
284 #[test]
285 fn test_parse_cred_helpers() {
286 let json = r#"{
287 "credHelpers": {
288 "gcr.io": "gcloud",
289 "123456789.dkr.ecr.us-east-1.amazonaws.com": "ecr-login"
290 }
291 }"#;
292
293 let config: DockerConfig = serde_json::from_str(json).unwrap();
294 assert_eq!(
295 config.cred_helpers.get("gcr.io"),
296 Some(&"gcloud".to_string())
297 );
298 }
299
300 #[test]
301 fn test_get_credential_helper() {
302 let json = r#"{
303 "credsStore": "osxkeychain",
304 "credHelpers": {
305 "gcr.io": "gcloud"
306 }
307 }"#;
308
309 let config: DockerConfig = serde_json::from_str(json).unwrap();
310
311 assert_eq!(config.get_credential_helper("gcr.io"), Some("gcloud"));
313
314 assert_eq!(
316 config.get_credential_helper("docker.io"),
317 Some("osxkeychain")
318 );
319 }
320
321 #[test]
322 fn test_normalize_registry() {
323 assert_eq!(normalize_registry("docker.io"), "index.docker.io");
324 assert_eq!(
325 normalize_registry("registry-1.docker.io"),
326 "index.docker.io"
327 );
328 assert_eq!(normalize_registry("gcr.io"), "gcr.io");
329 assert_eq!(normalize_registry("https://gcr.io/"), "gcr.io");
330 }
331
332 #[test]
333 fn test_get_auth_normalized() {
334 let json = r#"{
335 "auths": {
336 "https://index.docker.io/v1/": {
337 "auth": "dXNlcjpwYXNz"
338 }
339 }
340 }"#;
341
342 let config: DockerConfig = serde_json::from_str(json).unwrap();
343
344 assert!(config.get_auth("index.docker.io").is_some());
346 }
347
348 #[test]
349 fn test_decode_auth_with_colon_in_password() {
350 let auth = "dXNlcjpwYXNzOndpdGg6Y29sb25z";
352 let cred = AuthEntry::decode_auth(auth).unwrap();
353 assert_eq!(cred.username(), Some("user"));
354 assert_eq!(cred.password(), Some("pass:with:colons"));
355 }
356
357 #[test]
358 fn test_load_uses_docker_config_env() {
359 let _guard = ENV_LOCK.lock().unwrap();
360 let temp = tempfile::tempdir().unwrap();
361 let config_path = temp.path().join("config.json");
362
363 std::fs::write(
364 &config_path,
365 r#"{"auths":{"example.com":{"username":"user","password":"pass"}}}"#,
366 )
367 .unwrap();
368
369 let prev = std::env::var("DOCKER_CONFIG").ok();
370 unsafe {
371 std::env::set_var("DOCKER_CONFIG", temp.path());
372 }
373
374 let config = DockerConfig::load().unwrap();
375 let cred = config.get_auth("example.com").unwrap();
376 assert_eq!(cred.username(), Some("user"));
377 assert_eq!(cred.password(), Some("pass"));
378
379 if let Some(value) = prev {
380 unsafe {
381 std::env::set_var("DOCKER_CONFIG", value);
382 }
383 } else {
384 unsafe {
385 std::env::remove_var("DOCKER_CONFIG");
386 }
387 }
388 }
389}