docker_image_pusher/registry/
auth.rs1use crate::error::handlers::HttpErrorHandler;
4use crate::error::{PusherError, Result};
5use crate::output::OutputManager;
6use reqwest::Client;
7use serde::Deserialize;
8
9#[derive(Debug, Deserialize)]
10struct AuthChallenge {
11 realm: String,
12 service: String,
13 scope: Option<String>,
14}
15
16#[derive(Debug, Deserialize)]
17struct TokenResponse {
18 token: Option<String>,
19 access_token: Option<String>,
20}
21
22#[derive(Debug, Clone)]
23pub struct Auth {
24 client: Client,
25 registry_address: String,
26}
27
28impl Auth {
29 pub fn new(registry_address: &str, skip_tls: bool) -> Result<Self> {
30 let client_builder = if skip_tls {
31 Client::builder()
32 .danger_accept_invalid_certs(true)
33 .danger_accept_invalid_hostnames(true)
34 } else {
35 Client::builder()
36 };
37
38 let client = client_builder
39 .build()
40 .map_err(|e| PusherError::Network(format!("Failed to build auth client: {}", e)))?;
41
42 Ok(Self {
43 client,
44 registry_address: registry_address.to_string(),
45 })
46 }
47
48 pub async fn login(
49 &self,
50 username: &str,
51 password: &str,
52 output: &OutputManager,
53 ) -> Result<Option<String>> {
54 output.verbose("Attempting to authenticate with registry...");
55
56 let v2_url = format!("{}/v2/", self.registry_address);
58 let response =
59 self.client.get(&v2_url).send().await.map_err(|e| {
60 PusherError::Network(format!("Failed to access registry API: {}", e))
61 })?;
62
63 if response.status() == 401 {
64 if let Some(auth_header) = response.headers().get("www-authenticate") {
66 let auth_str = auth_header.to_str().map_err(|e| {
67 PusherError::Authentication(format!("Invalid auth header: {}", e))
68 })?;
69
70 if auth_str.starts_with("Bearer ") {
71 return self
72 .handle_bearer_auth(auth_str, username, password, output)
73 .await;
74 }
75 }
76 }
77
78 output.info("No authentication required or unsupported authentication method");
80 Ok(None)
81 }
82
83 pub async fn get_repository_token(
85 &self,
86 username: &str,
87 password: &str,
88 repository: &str,
89 output: &OutputManager,
90 ) -> Result<Option<String>> {
91 output.verbose(&format!(
92 "Getting repository-specific token for: {}",
93 repository
94 ));
95
96 let repo_url = format!("{}/v2/{}/blobs/uploads/", self.registry_address, repository);
98 let response = self.client.post(&repo_url).send().await.map_err(|e| {
99 PusherError::Network(format!("Failed to access repository endpoint: {}", e))
100 })?;
101
102 if response.status() == 401 {
103 if let Some(auth_header) = response.headers().get("www-authenticate") {
104 let auth_str = auth_header.to_str().map_err(|e| {
105 PusherError::Authentication(format!("Invalid auth header: {}", e))
106 })?;
107
108 if auth_str.starts_with("Bearer ") {
109 return self
110 .handle_bearer_auth_with_scope(
111 auth_str, username, password, repository, output,
112 )
113 .await;
114 }
115 }
116 }
117
118 self.login(username, password, output).await
120 }
121
122 async fn handle_bearer_auth(
123 &self,
124 auth_header: &str,
125 username: &str,
126 password: &str,
127 output: &OutputManager,
128 ) -> Result<Option<String>> {
129 let challenge = self.parse_auth_challenge(auth_header)?;
130
131 output.verbose(&format!(
132 "Bearer auth challenge: realm={}, service={}",
133 challenge.realm, challenge.service
134 ));
135
136 let mut token_url = format!("{}?service={}", challenge.realm, challenge.service);
138 if let Some(scope) = &challenge.scope {
139 token_url.push_str(&format!("&scope={}", scope));
140 }
141
142 self.request_token(&token_url, username, password, output)
143 .await
144 }
145
146 async fn handle_bearer_auth_with_scope(
147 &self,
148 auth_header: &str,
149 username: &str,
150 password: &str,
151 repository: &str,
152 output: &OutputManager,
153 ) -> Result<Option<String>> {
154 let challenge = self.parse_auth_challenge(auth_header)?;
155
156 output.verbose(&format!(
157 "Bearer auth challenge for {}: realm={}, service={}",
158 repository, challenge.realm, challenge.service
159 ));
160
161 let scope = format!("repository:{}:push,pull", repository);
163 let token_url = format!(
164 "{}?service={}&scope={}",
165 challenge.realm, challenge.service, scope
166 );
167
168 output.verbose(&format!("Requesting token with scope: {}", scope));
169
170 self.request_token(&token_url, username, password, output)
171 .await
172 }
173
174 async fn request_token(
175 &self,
176 token_url: &str,
177 username: &str,
178 password: &str,
179 output: &OutputManager,
180 ) -> Result<Option<String>> {
181 output.verbose(&format!("Token request URL: {}", token_url));
182
183 let response = self
184 .client
185 .get(token_url)
186 .basic_auth(username, Some(password))
187 .send()
188 .await
189 .map_err(|e| PusherError::Network(format!("Failed to request auth token: {}", e)))?;
190
191 output.verbose(&format!("Token response status: {}", response.status()));
192
193 if response.status().is_success() {
194 let token_response: TokenResponse = response.json().await.map_err(|e| {
195 PusherError::Authentication(format!("Failed to parse token response: {}", e))
196 })?;
197
198 let token = token_response
199 .token
200 .or(token_response.access_token)
201 .ok_or_else(|| {
202 PusherError::Authentication("No token in auth response".to_string())
203 })?;
204
205 output.success("Authentication token obtained");
206 output.verbose(&format!("Token prefix: {}...", &token[..10]));
207 Ok(Some(token))
208 } else {
209 let status = response.status();
210 let error_text = response
211 .text()
212 .await
213 .unwrap_or_else(|_| "Failed to read error response".to_string());
214
215 output.error(&format!(
216 "Token request failed (status {}): {}",
217 status, error_text
218 ));
219
220 Err(HttpErrorHandler::handle_auth_error(status, &error_text))
221 }
222 }
223
224 fn parse_auth_challenge(&self, auth_header: &str) -> Result<AuthChallenge> {
225 let mut realm = String::new();
227 let mut service = String::new();
228 let mut scope = None;
229
230 let params = auth_header
232 .strip_prefix("Bearer ")
233 .ok_or_else(|| PusherError::Authentication("Invalid Bearer auth header".to_string()))?;
234
235 for param in params.split(',') {
237 let param = param.trim();
238 if let Some(eq_pos) = param.find('=') {
239 let key = param[..eq_pos].trim();
240 let value = param[eq_pos + 1..].trim().trim_matches('"');
241
242 match key {
243 "realm" => realm = value.to_string(),
244 "service" => service = value.to_string(),
245 "scope" => scope = Some(value.to_string()),
246 _ => {} }
248 }
249 }
250
251 if realm.is_empty() || service.is_empty() {
252 return Err(PusherError::Authentication(
253 "Invalid auth challenge format".to_string(),
254 ));
255 }
256
257 Ok(AuthChallenge {
258 realm,
259 service,
260 scope,
261 })
262 }
263}