docker_image_pusher/registry/
auth.rs1use crate::error::{RegistryError, Result};
2use crate::logging::Logger;
3use reqwest::Client;
4use serde::{Deserialize, Serialize};
5use std::collections::HashMap;
6
7#[derive(Debug, Serialize, Deserialize)]
8pub struct AuthResponse {
9 pub token: String,
10 pub expires_in: Option<u64>,
11 pub issued_at: Option<String>,
12}
13
14#[derive(Debug, Clone)]
15pub struct AuthChallenge {
16 pub realm: String,
17 pub service: Option<String>,
18 pub scope: Option<String>,
19}
20
21#[derive(Clone)]
22pub struct Auth {
23 client: Client,
24}
25
26impl Auth {
27 pub fn new() -> Self {
28 Auth {
29 client: Client::new(),
30 }
31 }
32
33 fn parse_www_authenticate(header_value: &str) -> Result<AuthChallenge> {
35 if !header_value.starts_with("Bearer ") {
36 return Err(RegistryError::Registry(
37 "Only Bearer authentication is supported".to_string(),
38 ));
39 }
40
41 let params_str = &header_value[7..]; let mut params = HashMap::new();
43
44 for param in params_str.split(',') {
46 let param = param.trim();
47 if let Some(eq_pos) = param.find('=') {
48 let key = param[..eq_pos].trim();
49 let value = param[eq_pos + 1..].trim();
50 let value = if value.starts_with('"') && value.ends_with('"') {
52 &value[1..value.len() - 1]
53 } else {
54 value
55 };
56 params.insert(key, value);
57 }
58 }
59
60 let realm = params
61 .get("realm")
62 .ok_or_else(|| {
63 RegistryError::Registry("Missing realm in WWW-Authenticate header".to_string())
64 })?
65 .to_string();
66
67 Ok(AuthChallenge {
68 realm,
69 service: params.get("service").map(|s| s.to_string()),
70 scope: params.get("scope").map(|s| s.to_string()),
71 })
72 }
73
74 pub async fn authenticate_with_registry(
76 &self,
77 registry_url: &str,
78 repository: &str,
79 username: Option<&str>,
80 password: Option<&str>,
81 output: &Logger,
82 ) -> Result<Option<String>> {
83 output.verbose("Starting Docker Registry API v2 authentication...");
84
85 let ping_url = format!("{}/v2/", registry_url);
87 let response = self
88 .client
89 .get(&ping_url)
90 .send()
91 .await
92 .map_err(|e| RegistryError::Network(format!("Failed to ping registry: {}", e)))?;
93
94 match response.status().as_u16() {
95 200 => {
96 output.verbose("Registry does not require authentication");
97 return Ok(None);
98 }
99 401 => {
100 output.verbose("Registry requires authentication, processing challenge...");
101
102 let www_auth = response
104 .headers()
105 .get("www-authenticate")
106 .and_then(|h| h.to_str().ok())
107 .ok_or_else(|| {
108 RegistryError::Registry(
109 "Missing WWW-Authenticate header in 401 response".to_string(),
110 )
111 })?;
112
113 let challenge = Self::parse_www_authenticate(www_auth)?;
114 output.verbose(&format!(
115 "Auth challenge: realm={}, service={:?}, scope={:?}",
116 challenge.realm, challenge.service, challenge.scope
117 ));
118
119 return self
121 .request_token(challenge, repository, username, password, output)
122 .await;
123 }
124 _ => {
125 return Err(RegistryError::Registry(format!(
126 "Unexpected status {} when checking registry authentication",
127 response.status()
128 )));
129 }
130 }
131 }
132
133 async fn request_token(
134 &self,
135 challenge: AuthChallenge,
136 repository: &str,
137 username: Option<&str>,
138 password: Option<&str>,
139 output: &Logger,
140 ) -> Result<Option<String>> {
141 let mut url = reqwest::Url::parse(&challenge.realm)
142 .map_err(|e| RegistryError::Registry(format!("Invalid auth realm URL: {}", e)))?;
143
144 if let Some(service) = &challenge.service {
146 url.query_pairs_mut().append_pair("service", service);
147 }
148
149 let scope = format!("repository:{}:pull,push", repository);
151 url.query_pairs_mut().append_pair("scope", &scope);
152
153 output.verbose(&format!("Requesting token from: {}", url));
154
155 let mut request = self.client.get(url);
157
158 if let (Some(user), Some(pass)) = (username, password) {
159 output.verbose(&format!("Using basic auth for user: {}", user));
160 request = request.basic_auth(user, Some(pass));
161 }
162
163 let response = request
164 .send()
165 .await
166 .map_err(|e| RegistryError::Network(format!("Failed to request auth token: {}", e)))?;
167
168 if response.status().is_success() {
169 let auth_response: AuthResponse = response.json().await.map_err(|e| {
170 RegistryError::Registry(format!("Failed to parse auth response: {}", e))
171 })?;
172
173 output.success("Successfully obtained authentication token");
174 output.verbose(&format!(
175 "Token expires in: {:?} seconds",
176 auth_response.expires_in
177 ));
178
179 Ok(Some(auth_response.token))
180 } else {
181 let status = response.status();
182 let error_text = response
183 .text()
184 .await
185 .unwrap_or_else(|_| "Unknown error".to_string());
186 Err(RegistryError::Registry(format!(
187 "Authentication failed (status {}): {}",
188 status, error_text
189 )))
190 }
191 }
192
193 pub async fn login(
194 &self,
195 username: &str,
196 _password: &str,
197 output: &Logger,
198 ) -> Result<Option<String>> {
199 output.info(&format!("Authenticating user: {}", username));
200
201 output.warning("login() method is deprecated, use authenticate_with_registry() instead");
203 Ok(Some("dummy_token".to_string()))
204 }
205
206 pub async fn get_repository_token(
207 &self,
208 _username: &str,
209 _password: &str,
210 repository: &str,
211 output: &Logger,
212 ) -> Result<Option<String>> {
213 output.info(&format!("Getting repository token for: {}", repository));
214
215 output.warning(
217 "get_repository_token() method is deprecated, use authenticate_with_registry() instead",
218 );
219 Ok(Some("dummy_repo_token".to_string()))
220 }
221
222 pub async fn get_token(
223 &self,
224 _registry: &str,
225 _repo: &str,
226 _username: Option<&str>,
227 _password: Option<&str>,
228 ) -> Result<String> {
229 Ok("token".to_string())
231 }
232}