1use std::fmt;
18use std::fmt::Debug;
19
20use http::{HeaderMap, HeaderValue};
21use reqwest::{Response, StatusCode};
22use secrecy::{ExposeSecret, SecretString};
23use serde::{Deserialize, Serialize, Serializer};
24use thiserror::Error;
25
26use crate::authtoken_scope::*;
27use crate::types::*;
28use crate::{AuthError, AuthState};
29
30#[derive(Debug, Error)]
32#[non_exhaustive]
33pub enum AuthTokenError {
34 #[error("error preparing authentication request: {}", source)]
36 AuthRequest {
37 #[source]
39 source: Box<dyn std::error::Error + Send + Sync + 'static>,
40 },
41
42 #[error("token missing in the response")]
44 AuthTokenNotInResponse,
45
46 #[error("token missing cannot be converted to string")]
48 AuthTokenNotString,
49
50 #[error("header value error: {}", source)]
52 HeaderValue {
53 #[from]
54 source: http::header::InvalidHeaderValue,
55 },
56
57 #[error(
59 "AuthType `{}` is not a supported type for authenticating towards the cloud",
60 auth_type
61 )]
62 IdentityMethod { auth_type: String },
63
64 #[error(
66 "AuthType `{}` is not a supported type for authenticating towards the cloud with sync interface",
67 auth_type
68 )]
69 IdentityMethodSync { auth_type: String },
70
71 #[error("Auth data is missing")]
73 MissingAuthData,
74
75 #[error("Auth URL is missing")]
77 MissingAuthUrl,
78
79 #[error("`auth_methods` must be an array of string when `auth_type=multifactor`")]
81 MultifactorAuthMethodsList,
82
83 #[error("Scope error: {}", source)]
85 Scope {
86 #[from]
88 source: AuthTokenScopeError,
89 },
90
91 #[error("failed to deserialize response body: {}", source)]
93 Serde {
94 #[from]
96 source: serde_json::Error,
97 },
98
99 #[error("plugin error: {}", source)]
101 Plugin {
102 #[source]
104 source: Box<dyn std::error::Error + Send + Sync + 'static>,
105 },
106}
107
108impl AuthTokenError {
109 pub fn auth_request<E>(error: E) -> Self
110 where
111 E: std::error::Error + Send + Sync + 'static,
112 {
113 Self::AuthRequest {
114 source: Box::new(error),
115 }
116 }
117 pub fn plugin<E>(error: E) -> Self
118 where
119 E: std::error::Error + Send + Sync + 'static,
120 {
121 Self::Plugin {
122 source: Box::new(error),
123 }
124 }
125}
126
127impl From<AuthTokenScopeError> for AuthError {
129 fn from(source: AuthTokenScopeError) -> Self {
130 Self::AuthToken {
131 source: AuthTokenError::Scope { source },
132 }
133 }
134}
135
136type AuthResult<T> = Result<T, AuthTokenError>;
137
138#[derive(Clone, Default, Deserialize, Serialize)]
140pub struct AuthToken {
141 #[serde(serialize_with = "serialize_secret_string")]
143 pub token: SecretString,
144 pub auth_info: Option<AuthResponse>,
146}
147
148fn serialize_secret_string<S>(secret: &SecretString, serializer: S) -> Result<S::Ok, S::Error>
149where
150 S: Serializer,
151{
152 serializer.serialize_str(secret.expose_secret())
153}
154
155impl From<&str> for AuthToken {
156 fn from(value: &str) -> Self {
157 Self {
158 token: value.into(),
159 ..Default::default()
160 }
161 }
162}
163
164impl Debug for AuthToken {
165 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
166 f.debug_struct("Auth")
167 .field("data", &self.auth_info)
168 .finish()
169 }
170}
171
172impl AuthToken {
173 pub fn new<T>(token: T, auth_info: Option<AuthResponse>) -> Self
175 where
176 T: Into<SecretString>,
177 {
178 Self {
179 token: token.into(),
180 auth_info,
181 }
182 }
183 pub fn set_header<'a>(
187 &self,
188 headers: &'a mut HeaderMap<HeaderValue>,
189 ) -> AuthResult<&'a mut HeaderMap<HeaderValue>> {
190 let mut token_header_value = HeaderValue::from_str(self.token.expose_secret())?;
191 token_header_value.set_sensitive(true);
192 headers.insert("X-Auth-Token", token_header_value);
193
194 Ok(headers)
195 }
196
197 pub fn get_state(&self, expiration_offset: Option<chrono::TimeDelta>) -> AuthState {
201 let expiration = chrono::Utc::now();
202 let soon_expiration = match expiration_offset {
203 Some(offset) => expiration + offset,
204 None => expiration,
205 };
206 match &self.auth_info {
207 Some(data) => {
208 if data.token.expires_at <= expiration {
209 AuthState::Expired
210 } else if data.token.expires_at <= soon_expiration {
211 AuthState::AboutToExpire
212 } else {
213 AuthState::Valid
214 }
215 }
216 None => AuthState::Unset,
217 }
218 }
219
220 pub fn get_scope(&self) -> AuthTokenScope {
222 match &self.auth_info {
223 Some(data) => AuthTokenScope::from(data),
224 _ => AuthTokenScope::Unscoped,
225 }
226 }
227
228 pub async fn from_reqwest_response(response: Response) -> Result<Self, AuthError> {
230 if !response.status().is_success() {
231 let status = response.status();
233 if let StatusCode::UNAUTHORIZED = status
234 && let Some(receipt) = response.headers().get("openstack-auth-receipt")
235 {
236 let receipt_token = receipt
237 .to_str()
238 .map_err(|_| AuthError::AuthReceiptNotString)?
239 .into();
240 let mut receipt: AuthReceiptResponse = response.json().await?;
241 receipt.token = Some(receipt_token);
242 return Err(AuthError::AuthReceipt(receipt));
243 }
244
245 let body = response.text().await?;
246
247 if let Ok(data) = serde_json::from_str::<AuthErrorResponse>(&body) {
248 return Err(AuthError::Identity(data.error));
249 } else {
250 return Err(AuthError::UnknownAuth {
251 code: status.into(),
252 message: Some(body),
253 });
254 }
255 }
256
257 let token = response
258 .headers()
259 .get("x-subject-token")
260 .ok_or(AuthError::AuthTokenNotInResponse)?
261 .to_str()
262 .map_err(|_| AuthError::AuthTokenNotString)?
263 .to_string();
264
265 let token_info: AuthResponse = response.json::<AuthResponse>().await?;
266
267 Ok(Self {
268 token: SecretString::from(token),
269 auth_info: Some(token_info),
270 })
271 }
272}
273
274impl TryFrom<http::Response<bytes::Bytes>> for AuthToken {
275 type Error = AuthTokenError;
276 fn try_from(value: http::Response<bytes::Bytes>) -> Result<Self, Self::Error> {
277 let token = value
278 .headers()
279 .get("x-subject-token")
280 .ok_or(AuthTokenError::AuthTokenNotInResponse)?
281 .to_str()
282 .map_err(|_| AuthTokenError::AuthTokenNotString)?;
283
284 let token_info: AuthResponse = serde_json::from_slice(value.body())?;
285 Ok(Self::new(token, Some(token_info)))
286 }
287}
288
289#[cfg(test)]
290mod tests {
291 use chrono::Local;
292 use http::response::Builder;
293 use reqwest::Response;
294 use secrecy::ExposeSecret;
295 use serde_json::to_string;
296
297 use super::AuthError;
298 use super::AuthToken;
299 use crate::types::*;
300
301 #[tokio::test]
302 async fn test_from_reqwest_response_receipt() {
303 let auth_receipt = AuthReceiptResponse {
304 receipt: AuthReceipt {
305 methods: vec!["password".into()],
306 user: User {
307 id: "uid".into(),
308 name: "uname".into(),
309 ..Default::default()
310 },
311 expires_at: Local::now(),
312 ..Default::default()
313 },
314 required_auth_methods: vec![vec!["totp".into(), "password".into()]],
315 token: None,
316 };
317 let http_response = Builder::new()
318 .status(401)
319 .header("openstack-auth-receipt", "foobar")
320 .header("content-type", "application/json")
321 .body(to_string(&auth_receipt).unwrap())
322 .unwrap();
323
324 let response: Response = Response::from(http_response);
325
326 let rsp = AuthToken::from_reqwest_response(response).await;
327 match rsp {
328 Err(AuthError::AuthReceipt(receipt)) => {
329 let mut expected = auth_receipt.clone();
330 expected.token = Some("foobar".into());
331 assert_eq!(expected, receipt);
332 }
333 other => {
334 panic!("wrong response for the expected receipt error: {:?}", other);
335 }
336 }
337 }
338
339 #[tokio::test]
340 async fn test_from_reqwest_response_error() {
341 let err = AuthErrorResponse {
342 error: IdentityError {
343 code: 401,
344 message: "internal error".into(),
345 },
346 };
347 let http_response = Builder::new()
348 .status(401)
349 .header("content-type", "application/json")
350 .body(to_string(&err).unwrap())
351 .unwrap();
352
353 let response: Response = Response::from(http_response);
354
355 let rsp = AuthToken::from_reqwest_response(response).await;
356 match rsp {
357 Err(AuthError::Identity(error)) => {
358 assert_eq!(error, err.error);
359 }
360 other => {
361 panic!("wrong response: {:?}", other);
362 }
363 }
364 }
365
366 #[tokio::test]
367 async fn test_from_reqwest_response_success() {
368 let auth = AuthResponse::default();
369 let http_response = Builder::new()
370 .status(201)
371 .header("content-type", "application/json")
372 .header("x-subject-token", "foobar")
373 .body(to_string(&auth).unwrap())
374 .unwrap();
375
376 let response: Response = Response::from(http_response);
377
378 let rsp = AuthToken::from_reqwest_response(response).await;
379 match rsp {
380 Ok(rsp) => {
381 assert_eq!(auth, rsp.auth_info.unwrap());
382 assert_eq!("foobar", rsp.token.expose_secret());
383 }
384 other => {
385 panic!("wrong response: {:?}", other);
386 }
387 }
388 }
389
390 #[tokio::test]
391 async fn test_from_reqwest_response_success_no_token() {
392 let auth = AuthResponse::default();
393 let http_response = Builder::new()
394 .status(201)
395 .header("content-type", "application/json")
396 .body(to_string(&auth).unwrap())
397 .unwrap();
398
399 let response: Response = Response::from(http_response);
400
401 let rsp = AuthToken::from_reqwest_response(response).await;
402 match rsp {
403 Err(AuthError::AuthTokenNotInResponse) => {}
404 other => {
405 panic!("wrong response: {:?}", other);
406 }
407 }
408 }
409}