Skip to main content

containerregistry_auth/
resolver.rs

1//! Credential resolution with configurable sources.
2//!
3//! The resolver follows this order of precedence:
4//! 1. Explicit credentials (if provided)
5//! 2. Docker config `auths` section
6//! 3. Registry-specific credential helper (`credHelpers`)
7//! 4. Default credential store (`credsStore`)
8//! 5. Anonymous access
9
10use crate::config::DockerConfig;
11use crate::helper::{CredentialHelper, normalize_server_url};
12use crate::{Credential, Result};
13
14/// A resolver for container registry credentials.
15///
16/// The resolver checks multiple sources in order of precedence to find
17/// credentials for a given registry.
18#[derive(Clone, Debug)]
19pub struct AuthResolver {
20    /// Docker config (if loaded).
21    config: Option<DockerConfig>,
22
23    /// Explicit credentials to use (overrides all other sources).
24    explicit: Option<Credential>,
25}
26
27impl AuthResolver {
28    /// Creates a new resolver with the default Docker config.
29    ///
30    /// Loads the config from `DOCKER_CONFIG` or `~/.docker/config.json`.
31    /// If the config doesn't exist, uses an empty config.
32    pub fn new() -> Self {
33        let config = DockerConfig::load().ok();
34        Self {
35            config,
36            explicit: None,
37        }
38    }
39
40    /// Creates a new resolver with a specific Docker config.
41    pub fn with_config(config: DockerConfig) -> Self {
42        Self {
43            config: Some(config),
44            explicit: None,
45        }
46    }
47
48    /// Creates a new resolver that always returns anonymous credentials.
49    pub fn anonymous() -> Self {
50        Self {
51            config: None,
52            explicit: Some(Credential::Anonymous),
53        }
54    }
55
56    /// Sets explicit credentials that override all other sources.
57    pub fn with_explicit(mut self, credential: Credential) -> Self {
58        self.explicit = Some(credential);
59        self
60    }
61
62    /// Resolves credentials for a registry.
63    ///
64    /// Returns the credential to use, following the resolution order:
65    /// 1. Explicit credentials
66    /// 2. Docker config auths
67    /// 3. Registry-specific helper
68    /// 4. Default credential store
69    /// 5. Anonymous
70    pub fn resolve(&self, registry: &str) -> Result<Credential> {
71        // 1. Check explicit credentials
72        if let Some(ref cred) = self.explicit {
73            return Ok(cred.clone());
74        }
75
76        let config = match &self.config {
77            Some(c) => c,
78            None => return Ok(Credential::Anonymous),
79        };
80
81        // 2. Check Docker config auths
82        if let Some(cred) = config.get_auth(registry) {
83            return Ok(cred);
84        }
85
86        // 3 & 4. Check credential helpers
87        if let Some(helper_name) = config.get_credential_helper(registry) {
88            let helper = CredentialHelper::new(helper_name);
89            let server_url = normalize_server_url(registry);
90
91            match helper.get(&server_url) {
92                Ok(cred) if !cred.is_anonymous() => return Ok(cred),
93                Ok(_) => {} // Anonymous means helper didn't find credentials, continue
94                Err(crate::Error::HelperNotFound(_)) => {} // Helper not installed, continue
95                Err(e) => return Err(e), // Real error
96            }
97        }
98
99        // 5. Fall back to anonymous
100        Ok(Credential::Anonymous)
101    }
102
103    /// Resolves credentials for a registry, returning anonymous on any error.
104    ///
105    /// This is useful when you want to try authenticated access but fall back
106    /// to anonymous if there are any issues with credential resolution.
107    pub fn resolve_or_anonymous(&self, registry: &str) -> Credential {
108        self.resolve(registry).unwrap_or(Credential::Anonymous)
109    }
110}
111
112impl Default for AuthResolver {
113    fn default() -> Self {
114        Self::new()
115    }
116}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use crate::test_util::ENV_LOCK;
122
123    #[test]
124    fn test_resolver_explicit_credentials() {
125        let resolver = AuthResolver::anonymous().with_explicit(Credential::basic("user", "pass"));
126
127        let cred = resolver.resolve("any-registry.io").unwrap();
128        assert_eq!(cred.username(), Some("user"));
129    }
130
131    #[test]
132    fn test_resolver_anonymous() {
133        let resolver = AuthResolver::anonymous();
134        let cred = resolver.resolve("any-registry.io").unwrap();
135        assert!(cred.is_anonymous());
136    }
137
138    #[test]
139    fn test_resolver_from_config_auths() {
140        let config = DockerConfig::from_json(
141            r#"{
142            "auths": {
143                "gcr.io": {
144                    "auth": "dXNlcjpwYXNz"
145                }
146            }
147        }"#,
148        )
149        .unwrap();
150
151        let resolver = AuthResolver::with_config(config);
152        let cred = resolver.resolve("gcr.io").unwrap();
153
154        assert_eq!(cred.username(), Some("user"));
155        assert_eq!(cred.password(), Some("pass"));
156    }
157
158    #[test]
159    fn test_resolver_falls_back_to_anonymous() {
160        let config = DockerConfig::from_json(r#"{}"#).unwrap();
161
162        let resolver = AuthResolver::with_config(config);
163        let cred = resolver.resolve("unknown-registry.io").unwrap();
164
165        assert!(cred.is_anonymous());
166    }
167
168    #[test]
169    fn test_resolver_resolve_or_anonymous() {
170        let resolver = AuthResolver::anonymous();
171        let cred = resolver.resolve_or_anonymous("any-registry.io");
172        assert!(cred.is_anonymous());
173    }
174
175    #[test]
176    fn test_resolver_auths_precede_helpers() {
177        let config = DockerConfig::from_json(
178            r#"{
179            "auths": {
180                "example.com": { "username": "user", "password": "pass" }
181            },
182            "credHelpers": {
183                "example.com": "fake"
184            }
185        }"#,
186        )
187        .unwrap();
188
189        let resolver = AuthResolver::with_config(config);
190        let cred = resolver.resolve("example.com").unwrap();
191        assert_eq!(cred.username(), Some("user"));
192    }
193
194    #[test]
195    fn test_resolver_uses_credential_helper_when_no_auths() {
196        let _guard = ENV_LOCK.lock().unwrap();
197        let temp = tempfile::tempdir().unwrap();
198        let helper_path = temp.path().join("docker-credential-fake");
199
200        std::fs::write(
201            &helper_path,
202            r#"#!/bin/sh
203read server
204if [ "$server" = "https://example.com" ]; then
205  echo '{"Username":"helper","Secret":"secret"}'
206  exit 0
207fi
208echo "credentials not found" 1>&2
209exit 1
210"#,
211        )
212        .unwrap();
213        #[cfg(unix)]
214        {
215            use std::os::unix::fs::PermissionsExt;
216            let mut perms = std::fs::metadata(&helper_path).unwrap().permissions();
217            perms.set_mode(0o755);
218            std::fs::set_permissions(&helper_path, perms).unwrap();
219        }
220
221        let prev_path = std::env::var("PATH").ok();
222        let new_path = format!(
223            "{}:{}",
224            temp.path().to_string_lossy(),
225            prev_path.clone().unwrap_or_default()
226        );
227        unsafe {
228            std::env::set_var("PATH", new_path);
229        }
230
231        let config = DockerConfig::from_json(
232            r#"{
233            "credHelpers": {
234                "example.com": "fake"
235            }
236        }"#,
237        )
238        .unwrap();
239
240        let resolver = AuthResolver::with_config(config);
241        let cred = resolver.resolve("example.com").unwrap();
242        assert_eq!(cred.username(), Some("helper"));
243        assert_eq!(cred.password(), Some("secret"));
244
245        if let Some(value) = prev_path {
246            unsafe {
247                std::env::set_var("PATH", value);
248            }
249        } else {
250            unsafe {
251                std::env::remove_var("PATH");
252            }
253        }
254    }
255}