docker_image_pusher/registry/
auth.rs1use crate::error::{Result, PusherError};
4use crate::error::handlers::HttpErrorHandler;
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(&self, username: &str, password: &str, output: &OutputManager) -> Result<Option<String>> {
49 output.verbose("Attempting to authenticate with registry...");
50
51 let v2_url = format!("{}/v2/", self.registry_address);
53 let response = self.client.get(&v2_url).send().await
54 .map_err(|e| PusherError::Network(format!("Failed to access registry API: {}", e)))?;
55
56 if response.status() == 401 {
57 if let Some(auth_header) = response.headers().get("www-authenticate") {
59 let auth_str = auth_header.to_str()
60 .map_err(|e| PusherError::Authentication(format!("Invalid auth header: {}", e)))?;
61
62 if auth_str.starts_with("Bearer ") {
63 return self.handle_bearer_auth(auth_str, username, password, output).await;
64 }
65 }
66 }
67
68 output.info("No authentication required or unsupported authentication method");
70 Ok(None)
71 }
72
73 pub async fn get_repository_token(
75 &self,
76 username: &str,
77 password: &str,
78 repository: &str,
79 output: &OutputManager,
80 ) -> Result<Option<String>> {
81 output.verbose(&format!("Getting repository-specific token for: {}", repository));
82
83 let repo_url = format!("{}/v2/{}/blobs/uploads/", self.registry_address, repository);
85 let response = self.client.post(&repo_url).send().await
86 .map_err(|e| PusherError::Network(format!("Failed to access repository endpoint: {}", e)))?;
87
88 if response.status() == 401 {
89 if let Some(auth_header) = response.headers().get("www-authenticate") {
90 let auth_str = auth_header.to_str()
91 .map_err(|e| PusherError::Authentication(format!("Invalid auth header: {}", e)))?;
92
93 if auth_str.starts_with("Bearer ") {
94 return self.handle_bearer_auth_with_scope(auth_str, username, password, repository, output).await;
95 }
96 }
97 }
98
99 self.login(username, password, output).await
101 }
102
103 async fn handle_bearer_auth(
104 &self,
105 auth_header: &str,
106 username: &str,
107 password: &str,
108 output: &OutputManager,
109 ) -> Result<Option<String>> {
110 let challenge = self.parse_auth_challenge(auth_header)?;
111
112 output.verbose(&format!("Bearer auth challenge: realm={}, service={}",
113 challenge.realm, challenge.service));
114
115 let mut token_url = format!("{}?service={}", challenge.realm, challenge.service);
117 if let Some(scope) = &challenge.scope {
118 token_url.push_str(&format!("&scope={}", scope));
119 }
120
121 self.request_token(&token_url, username, password, output).await
122 }
123
124 async fn handle_bearer_auth_with_scope(
125 &self,
126 auth_header: &str,
127 username: &str,
128 password: &str,
129 repository: &str,
130 output: &OutputManager,
131 ) -> Result<Option<String>> {
132 let challenge = self.parse_auth_challenge(auth_header)?;
133
134 output.verbose(&format!("Bearer auth challenge for {}: realm={}, service={}",
135 repository, challenge.realm, challenge.service));
136
137 let scope = format!("repository:{}:push,pull", repository);
139 let token_url = format!("{}?service={}&scope={}", challenge.realm, challenge.service, scope);
140
141 output.verbose(&format!("Requesting token with scope: {}", scope));
142
143 self.request_token(&token_url, username, password, output).await
144 }
145
146 async fn request_token(
147 &self,
148 token_url: &str,
149 username: &str,
150 password: &str,
151 output: &OutputManager,
152 ) -> Result<Option<String>> {
153 output.verbose(&format!("Token request URL: {}", token_url));
154
155 let response = self.client
156 .get(token_url)
157 .basic_auth(username, Some(password))
158 .send()
159 .await
160 .map_err(|e| PusherError::Network(format!("Failed to request auth token: {}", e)))?;
161
162 output.verbose(&format!("Token response status: {}", response.status()));
163
164 if response.status().is_success() {
165 let token_response: TokenResponse = response.json().await
166 .map_err(|e| PusherError::Authentication(format!("Failed to parse token response: {}", e)))?;
167
168 let token = token_response.token.or(token_response.access_token)
169 .ok_or_else(|| PusherError::Authentication("No token in auth response".to_string()))?;
170
171 output.success("Authentication token obtained");
172 output.verbose(&format!("Token prefix: {}...", &token[..10]));
173 Ok(Some(token))
174 } else {
175 let status = response.status();
176 let error_text = response.text().await
177 .unwrap_or_else(|_| "Failed to read error response".to_string());
178
179 output.error(&format!("Token request failed (status {}): {}", status, error_text));
180
181 Err(HttpErrorHandler::handle_auth_error(status, &error_text))
182 }
183 }
184
185 fn parse_auth_challenge(&self, auth_header: &str) -> Result<AuthChallenge> {
186 let mut realm = String::new();
188 let mut service = String::new();
189 let mut scope = None;
190
191 let params = auth_header.strip_prefix("Bearer ")
193 .ok_or_else(|| PusherError::Authentication("Invalid Bearer auth header".to_string()))?;
194
195 for param in params.split(',') {
197 let param = param.trim();
198 if let Some(eq_pos) = param.find('=') {
199 let key = param[..eq_pos].trim();
200 let value = param[eq_pos + 1..].trim().trim_matches('"');
201
202 match key {
203 "realm" => realm = value.to_string(),
204 "service" => service = value.to_string(),
205 "scope" => scope = Some(value.to_string()),
206 _ => {} }
208 }
209 }
210
211 if realm.is_empty() || service.is_empty() {
212 return Err(PusherError::Authentication("Invalid auth challenge format".to_string()));
213 }
214
215 Ok(AuthChallenge {
216 realm,
217 service,
218 scope,
219 })
220 }
221}