Skip to main content

zlayer_core/auth/
docker_config.rs

1//! Docker config.json authentication parser
2//!
3//! This module parses the Docker config file format used by Docker and other container tools
4//! to store registry credentials. The config file is typically located at ~/.docker/config.json.
5
6use crate::error::Result;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs;
10use std::path::{Path, PathBuf};
11
12/// Docker config.json authentication manager
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct DockerConfigAuth {
15    #[serde(default)]
16    auths: HashMap<String, AuthEntry>,
17}
18
19/// Authentication entry in Docker config
20#[derive(Debug, Clone, Serialize, Deserialize)]
21struct AuthEntry {
22    /// Base64-encoded "username:password"
23    auth: Option<String>,
24    /// Plain username (alternative to auth field)
25    username: Option<String>,
26    /// Plain password (alternative to auth field)
27    password: Option<String>,
28}
29
30impl DockerConfigAuth {
31    /// Load Docker config from the default location (~/.docker/config.json)
32    pub fn load() -> Result<Self> {
33        let path = Self::default_config_path()?;
34        Self::load_from_path(&path)
35    }
36
37    /// Load Docker config from a specific path
38    pub fn load_from_path(path: &Path) -> Result<Self> {
39        if !path.exists() {
40            return Ok(Self {
41                auths: HashMap::new(),
42            });
43        }
44
45        let contents = fs::read_to_string(path).map_err(|e| {
46            crate::error::Error::config(format!("Failed to read Docker config: {}", e))
47        })?;
48
49        let config: DockerConfigAuth = serde_json::from_str(&contents).map_err(|e| {
50            crate::error::Error::config(format!("Failed to parse Docker config: {}", e))
51        })?;
52
53        Ok(config)
54    }
55
56    /// Get credentials for a specific registry
57    ///
58    /// Returns (username, password) if credentials are found for the registry.
59    /// The registry parameter should match the registry hostname (e.g., "docker.io", "ghcr.io").
60    pub fn get_credentials(&self, registry: &str) -> Option<(String, String)> {
61        // Try exact match first
62        if let Some(entry) = self.auths.get(registry) {
63            return Self::extract_credentials(entry);
64        }
65
66        // Try with https:// prefix
67        let https_registry = format!("https://{}", registry);
68        if let Some(entry) = self.auths.get(&https_registry) {
69            return Self::extract_credentials(entry);
70        }
71
72        // Try index.docker.io for docker.io
73        if registry == "docker.io" || registry == "registry-1.docker.io" {
74            if let Some(entry) = self.auths.get("https://index.docker.io/v1/") {
75                return Self::extract_credentials(entry);
76            }
77        }
78
79        None
80    }
81
82    /// Extract credentials from an auth entry
83    fn extract_credentials(entry: &AuthEntry) -> Option<(String, String)> {
84        // If username and password are provided directly
85        if let (Some(username), Some(password)) = (&entry.username, &entry.password) {
86            return Some((username.clone(), password.clone()));
87        }
88
89        // If auth field is provided (base64 encoded "username:password")
90        if let Some(auth) = &entry.auth {
91            return Self::decode_auth(auth);
92        }
93
94        None
95    }
96
97    /// Decode base64-encoded "username:password" auth string
98    fn decode_auth(auth: &str) -> Option<(String, String)> {
99        use base64::Engine;
100        let decoded = base64::engine::general_purpose::STANDARD
101            .decode(auth)
102            .ok()?;
103
104        let decoded_str = String::from_utf8(decoded).ok()?;
105        let parts: Vec<&str> = decoded_str.splitn(2, ':').collect();
106
107        if parts.len() == 2 {
108            Some((parts[0].to_string(), parts[1].to_string()))
109        } else {
110            None
111        }
112    }
113
114    /// Get the default Docker config path (~/.docker/config.json)
115    fn default_config_path() -> Result<PathBuf> {
116        let home = dirs::home_dir().ok_or_else(|| {
117            crate::error::Error::config("Cannot determine home directory".to_string())
118        })?;
119
120        Ok(home.join(".docker").join("config.json"))
121    }
122
123    /// Get all configured registry hostnames
124    pub fn registries(&self) -> Vec<String> {
125        self.auths.keys().cloned().collect()
126    }
127}
128
129#[cfg(test)]
130mod tests {
131    use super::*;
132
133    #[test]
134    fn test_parse_docker_config() {
135        let config_json = r#"
136        {
137            "auths": {
138                "ghcr.io": {
139                    "auth": "dXNlcm5hbWU6cGFzc3dvcmQ="
140                },
141                "docker.io": {
142                    "username": "myuser",
143                    "password": "mypass"
144                }
145            }
146        }
147        "#;
148
149        let config: DockerConfigAuth = serde_json::from_str(config_json).unwrap();
150
151        // Test base64 auth field
152        let (username, password) = config.get_credentials("ghcr.io").unwrap();
153        assert_eq!(username, "username");
154        assert_eq!(password, "password");
155
156        // Test plain username/password
157        let (username, password) = config.get_credentials("docker.io").unwrap();
158        assert_eq!(username, "myuser");
159        assert_eq!(password, "mypass");
160    }
161
162    #[test]
163    fn test_registry_normalization() {
164        let config_json = r#"
165        {
166            "auths": {
167                "https://ghcr.io": {
168                    "auth": "dXNlcm5hbWU6cGFzc3dvcmQ="
169                },
170                "https://index.docker.io/v1/": {
171                    "auth": "ZG9ja2VyOnBhc3M="
172                }
173            }
174        }
175        "#;
176
177        let config: DockerConfigAuth = serde_json::from_str(config_json).unwrap();
178
179        // Should find with or without https://
180        assert!(config.get_credentials("ghcr.io").is_some());
181
182        // Should find docker.io credentials from index.docker.io
183        assert!(config.get_credentials("docker.io").is_some());
184    }
185
186    #[test]
187    fn test_decode_auth() {
188        // "username:password" in base64
189        let auth = "dXNlcm5hbWU6cGFzc3dvcmQ=";
190        let (username, password) = DockerConfigAuth::decode_auth(auth).unwrap();
191        assert_eq!(username, "username");
192        assert_eq!(password, "password");
193    }
194
195    #[test]
196    fn test_empty_config() {
197        let config = DockerConfigAuth {
198            auths: HashMap::new(),
199        };
200
201        assert!(config.get_credentials("docker.io").is_none());
202    }
203}