Skip to main content

containerregistry_auth/
credential.rs

1//! Credential types for registry authentication.
2
3use serde::{Deserialize, Serialize};
4
5/// A credential for authenticating to a container registry.
6#[derive(Clone, Debug, Default, PartialEq, Eq)]
7pub enum Credential {
8    /// Anonymous access (no authentication).
9    #[default]
10    Anonymous,
11
12    /// Basic authentication with username and password.
13    Basic { username: String, password: String },
14
15    /// Bearer token authentication.
16    Bearer(String),
17
18    /// Identity token (used by some credential helpers).
19    IdentityToken(String),
20}
21
22impl Credential {
23    /// Creates an anonymous credential.
24    pub fn anonymous() -> Self {
25        Credential::Anonymous
26    }
27
28    /// Creates a basic auth credential.
29    pub fn basic(username: impl Into<String>, password: impl Into<String>) -> Self {
30        Credential::Basic {
31            username: username.into(),
32            password: password.into(),
33        }
34    }
35
36    /// Creates a bearer token credential.
37    pub fn bearer(token: impl Into<String>) -> Self {
38        Credential::Bearer(token.into())
39    }
40
41    /// Creates an identity token credential.
42    pub fn identity_token(token: impl Into<String>) -> Self {
43        Credential::IdentityToken(token.into())
44    }
45
46    /// Returns true if this is anonymous (no auth).
47    pub fn is_anonymous(&self) -> bool {
48        matches!(self, Credential::Anonymous)
49    }
50
51    /// Returns the username if this is basic auth.
52    pub fn username(&self) -> Option<&str> {
53        match self {
54            Credential::Basic { username, .. } => Some(username),
55            _ => None,
56        }
57    }
58
59    /// Returns the password if this is basic auth.
60    pub fn password(&self) -> Option<&str> {
61        match self {
62            Credential::Basic { password, .. } => Some(password),
63            _ => None,
64        }
65    }
66
67    /// Returns the HTTP Authorization header value for this credential.
68    pub fn authorization_header(&self) -> Option<String> {
69        match self {
70            Credential::Anonymous => None,
71            Credential::Basic { username, password } => {
72                use base64::Engine;
73                let encoded = base64::engine::general_purpose::STANDARD
74                    .encode(format!("{}:{}", username, password));
75                Some(format!("Basic {}", encoded))
76            }
77            Credential::Bearer(token) => Some(format!("Bearer {}", token)),
78            Credential::IdentityToken(token) => Some(format!("Bearer {}", token)),
79        }
80    }
81}
82
83/// Response from a credential helper's "get" command.
84#[derive(Clone, Debug, Deserialize, Serialize)]
85#[serde(rename_all = "PascalCase")]
86pub struct HelperCredential {
87    /// The username.
88    #[serde(default, skip_serializing_if = "Option::is_none")]
89    pub username: Option<String>,
90
91    /// The password or secret.
92    #[serde(default, rename = "Secret", skip_serializing_if = "Option::is_none")]
93    pub secret: Option<String>,
94
95    /// The server URL (echoed back by some helpers).
96    #[serde(default, rename = "ServerURL", skip_serializing_if = "Option::is_none")]
97    pub server_url: Option<String>,
98}
99
100impl HelperCredential {
101    /// Converts this helper credential to a Credential.
102    pub fn into_credential(self) -> Credential {
103        match (self.username, self.secret) {
104            (Some(username), Some(secret)) if !username.is_empty() => {
105                Credential::basic(username, secret)
106            }
107            (_, Some(secret)) if !secret.is_empty() => {
108                // No username but has secret - treat as identity token
109                Credential::identity_token(secret)
110            }
111            _ => Credential::Anonymous,
112        }
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_credential_anonymous() {
122        let cred = Credential::anonymous();
123        assert!(cred.is_anonymous());
124        assert_eq!(cred.authorization_header(), None);
125    }
126
127    #[test]
128    fn test_credential_basic() {
129        let cred = Credential::basic("user", "pass");
130        assert!(!cred.is_anonymous());
131        assert_eq!(cred.username(), Some("user"));
132        assert_eq!(cred.password(), Some("pass"));
133
134        let header = cred.authorization_header().unwrap();
135        assert!(header.starts_with("Basic "));
136    }
137
138    #[test]
139    fn test_credential_bearer() {
140        let cred = Credential::bearer("my-token");
141        assert!(!cred.is_anonymous());
142        assert_eq!(
143            cred.authorization_header(),
144            Some("Bearer my-token".to_string())
145        );
146    }
147
148    #[test]
149    fn test_helper_credential_to_credential() {
150        let helper = HelperCredential {
151            username: Some("user".to_string()),
152            secret: Some("pass".to_string()),
153            server_url: None,
154        };
155        let cred = helper.into_credential();
156        assert_eq!(cred.username(), Some("user"));
157        assert_eq!(cred.password(), Some("pass"));
158    }
159
160    #[test]
161    fn test_helper_credential_identity_token() {
162        let helper = HelperCredential {
163            username: None,
164            secret: Some("token".to_string()),
165            server_url: None,
166        };
167        let cred = helper.into_credential();
168        assert!(matches!(cred, Credential::IdentityToken(_)));
169    }
170}