1use std::collections::HashMap;
18use std::convert::TryInto;
19use std::fmt::{self, Debug};
20use std::time::{Duration, SystemTime};
21use std::{fs::File, io::Read};
22
23use async_trait::async_trait;
24use bytes::Bytes;
25use chrono::TimeDelta;
26use futures::io::{Error as IoError, ErrorKind as IoErrorKind};
27use futures::stream::TryStreamExt;
28use http::{HeaderMap, HeaderValue, Response as HttpResponse, StatusCode, header};
29use reqwest::{Body, Certificate, Client as AsyncClient, Request, Response};
30use secrecy::{ExposeSecret, SecretString};
31use tokio_util::codec;
32use tokio_util::compat::FuturesAsyncReadCompatExt;
33use tracing::{Level, debug, enabled, error, event, info, instrument, trace, warn};
34
35use openstack_sdk_auth_core::{
36 Auth, AuthError, AuthPluginRegistration, AuthToken, OpenStackAuthType,
37 authtoken::AuthTokenError,
38 authtoken_scope::AuthTokenScope,
39 types::{AuthResponse, Project, ServiceEndpoints},
40};
41
42use openstack_sdk_auth_applicationcredential as _;
44#[cfg(feature = "keystone_ng")]
45use openstack_sdk_auth_federation as _;
46#[cfg(feature = "keystone_ng")]
47use openstack_sdk_auth_jwt as _;
48use openstack_sdk_auth_oidcaccesstoken as _;
49#[cfg(feature = "passkey")]
50use openstack_sdk_auth_passkey as _;
51use openstack_sdk_auth_password as _;
52use openstack_sdk_auth_receipt as token_receipt;
53use openstack_sdk_auth_token as token_auth;
54use openstack_sdk_auth_totp as _;
55use openstack_sdk_auth_websso as _;
56
57use crate::auth::authtoken::{AuthType, build_token_info_endpoint};
58
59use openstack_sdk_core::api::{
60 self, RestClient,
61 query::{self, RawQueryAsync},
62};
63use openstack_sdk_core::auth::{
64 AuthState,
65 auth_helper::{AuthHelper, Dialoguer, Noop},
66 gather_auth_data,
67};
68use openstack_sdk_core::catalog::{Catalog, CatalogError, ServiceEndpoint};
69use openstack_sdk_core::config::{CloudConfig, ConfigFile, get_config_identity_hash};
70use openstack_sdk_core::error::{OpenStackError, OpenStackResult, RestError};
71use openstack_sdk_core::state;
72use openstack_sdk_core::types::{ApiVersion, BoxedAsyncRead, ServiceType};
73use openstack_sdk_core::utils::expand_tilde;
74
75#[derive(Clone)]
110pub struct AsyncOpenStack {
111 client: reqwest::Client,
113 config: CloudConfig,
115 auth: Auth,
117 catalog: Catalog,
119 state: state::State,
125}
126
127impl Debug for AsyncOpenStack {
128 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
129 f.debug_struct("OpenStack")
130 .field("service_endpoints", &self.catalog)
131 .finish()
132 }
133}
134
135#[async_trait]
136impl api::RestClient for AsyncOpenStack {
137 type Error = RestError;
138
139 fn get_service_endpoint(
141 &self,
142 service_type: &ServiceType,
143 version: Option<&ApiVersion>,
144 ) -> Result<&ServiceEndpoint, api::ApiError<Self::Error>> {
145 Ok(self.catalog.get_service_endpoint(
146 service_type.to_string(),
147 version,
148 self.config.region_name.as_ref(),
149 self.config.interface.as_ref(),
150 )?)
151 }
152
153 fn get_current_project(&self) -> Option<Project> {
155 if let Auth::AuthToken(token) = &self.auth {
156 return token.auth_info.clone().and_then(|x| x.token.project);
157 }
158 None
159 }
160}
161
162#[async_trait]
163impl api::AsyncClient for AsyncOpenStack {
164 async fn rest_async(
166 &self,
167 request: http::request::Builder,
168 body: Vec<u8>,
169 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
170 self.rest_with_auth_async(request, body, &self.auth).await
171 }
172
173 async fn rest_read_body_async(
175 &self,
176 request: http::request::Builder,
177 body: BoxedAsyncRead,
178 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
179 self.rest_with_auth_read_body_async(request, body, &self.auth)
180 .await
181 }
182
183 async fn download_async(
185 &self,
186 request: http::request::Builder,
187 body: Vec<u8>,
188 ) -> Result<(HeaderMap, BoxedAsyncRead), api::ApiError<<Self as api::RestClient>::Error>> {
189 self.download_with_auth_async(request, body, &self.auth)
190 .await
191 }
192}
193
194impl AsyncOpenStack {
195 fn new_impl(config: &CloudConfig, auth: Auth) -> OpenStackResult<Self> {
197 let mut client_builder = AsyncClient::builder();
198
199 if let Some(cacert) = &config.cacert {
200 let mut buf = Vec::new();
201 File::open(expand_tilde(cacert).unwrap_or(cacert.into()))
202 .map_err(|e| OpenStackError::IOWithPath {
203 source: e,
204 path: cacert.into(),
205 })?
206 .read_to_end(&mut buf)
207 .map_err(|e| OpenStackError::IOWithPath {
208 source: e,
209 path: cacert.into(),
210 })?;
211 for cert in Certificate::from_pem_bundle(&buf)? {
212 client_builder = client_builder.add_root_certificate(cert);
213 }
214 }
215 if let Some(false) = &config.verify {
216 warn!(
217 "SSL Verification is disabled! Please consider using `cacert` for adding custom certificate instead."
218 );
219 client_builder = client_builder.danger_accept_invalid_certs(true);
220 }
221 client_builder = client_builder.pool_max_idle_per_host(10);
222 client_builder = client_builder.pool_idle_timeout(Duration::from_secs(30));
223 client_builder = client_builder.timeout(Duration::from_secs(
224 config
225 .options
226 .get("api_timeout")
227 .and_then(|val| val.clone().into_uint().ok())
228 .unwrap_or(30),
229 ));
230 client_builder = client_builder.connect_timeout(Duration::from_secs(5));
231 client_builder = client_builder.tcp_keepalive(Duration::from_secs(60));
232 client_builder = client_builder.gzip(true);
233 client_builder = client_builder.deflate(true);
234
235 let mut session = AsyncOpenStack {
236 client: client_builder.build()?,
237 config: config.clone(),
238 auth,
239 catalog: Catalog::default(),
240 state: state::State::new(),
241 };
242
243 let auth_data = session
244 .config
245 .auth
246 .as_ref()
247 .ok_or(AuthTokenError::MissingAuthData)?;
248
249 let identity_service_url = auth_data
250 .auth_url
251 .as_ref()
252 .ok_or(AuthTokenError::MissingAuthUrl)?;
253
254 session.catalog.register_catalog_endpoint(
255 "identity",
256 identity_service_url,
257 config.region_name.as_ref(),
258 Some("public"),
259 )?;
260
261 session.catalog.configure(config)?;
262
263 session
264 .state
265 .set_auth_hash_key(get_config_identity_hash(config))
266 .enable_auth_cache(ConfigFile::new()?.is_auth_cache_enabled());
267
268 Ok(session)
269 }
270
271 #[instrument(name = "connect", level = "trace", skip(config))]
273 pub async fn new(config: &CloudConfig) -> OpenStackResult<Self> {
274 let mut session = Self::new_impl(config, Auth::None)?;
275
276 session
278 .discover_service_endpoint(&ServiceType::Identity)
279 .await?;
280
281 session.authorize(None, false, false).await?;
282
283 Ok(session)
284 }
285
286 #[instrument(name = "connect", level = "trace", skip(config, auth_helper))]
288 pub async fn new_with_authentication_helper<A>(
289 config: &CloudConfig,
290 auth_helper: &mut A,
291 renew_auth: bool,
292 ) -> OpenStackResult<Self>
293 where
294 A: AuthHelper + Sync + Send,
295 {
296 let mut session = Self::new_impl(config, Auth::None)?;
297
298 session
300 .discover_service_endpoint(&ServiceType::Identity)
301 .await?;
302
303 session
304 .authorize_with_auth_helper(None, auth_helper, renew_auth)
305 .await?;
306
307 Ok(session)
308 }
309
310 #[instrument(name = "connect", level = "trace", skip(config))]
312 #[deprecated(
313 since = "0.22.0",
314 note = "please use `new_with_authentication_helper` instead"
315 )]
316 pub async fn new_interactive(config: &CloudConfig, renew_auth: bool) -> OpenStackResult<Self> {
317 Self::new_with_authentication_helper(config, &mut Dialoguer::default(), renew_auth).await
318 }
319
320 fn set_auth(&mut self, auth: Auth, skip_cache_update: bool) -> &mut Self {
322 self.auth = auth;
323 if !skip_cache_update && let Auth::AuthToken(auth) = &self.auth {
324 let scope = match &auth.auth_info {
330 Some(info) => {
331 if info.token.application_credential.is_some() {
332 AuthTokenScope::Unscoped
333 } else {
334 auth.get_scope()
335 }
336 }
337 _ => auth.get_scope(),
338 };
339 self.state.set_scope_auth(&scope, auth);
340 }
341 self
342 }
343
344 pub async fn authorize(
346 &mut self,
347 scope: Option<AuthTokenScope>,
348 interactive: bool,
349 renew_auth: bool,
350 ) -> Result<(), OpenStackError> {
351 if interactive {
352 self.authorize_with_auth_helper(scope, &mut Dialoguer::default(), renew_auth)
353 .await
354 } else {
355 self.authorize_with_auth_helper(scope, &mut Noop::default(), renew_auth)
356 .await
357 }
358 }
359
360 async fn reauth(
362 &self,
363 auth: &AuthToken,
364 scope: &AuthTokenScope,
365 ) -> Result<Auth, OpenStackError> {
366 Ok(token_auth::PLUGIN
367 .auth(
368 &self.client,
369 self.get_service_endpoint(&ServiceType::Identity, Some(&ApiVersion::from((3, 0))))?
370 .url(),
371 HashMap::from([("token".into(), auth.token.clone())]),
372 Some(scope),
373 None,
374 )
375 .await?)
376 }
377
378 pub async fn authorize_with_auth_helper<A>(
381 &mut self,
382 scope: Option<AuthTokenScope>,
383 auth_helper: &mut A,
384 renew_auth: bool,
385 ) -> Result<(), OpenStackError>
386 where
387 A: AuthHelper + Sync + Send,
388 {
389 let requested_scope =
390 scope.map_or_else(|| AuthTokenScope::try_from(&self.config), |v| Ok(v.clone()))?;
391
392 if let (Some(auth), false) = (self.state.get_scope_auth(&requested_scope), renew_auth) {
393 trace!("Auth already available");
395 self.set_auth(Auth::AuthToken(Box::new(auth.clone())), true);
396 } else {
397 let auth_type = AuthType::from_cloud_config(&self.config)?;
400 let mut force_new_auth = renew_auth;
401 if let AuthType::V3ApplicationCredential = auth_type {
402 force_new_auth = true;
405 }
406 if let (Some(available_auth), false) = (self.state.get_any_valid_auth(), force_new_auth)
407 {
408 trace!("Valid Auth is available for reauthz: {:?}", available_auth);
411 let token_auth = self.reauth(&available_auth, &requested_scope).await?;
412 self.set_auth(token_auth.clone(), false);
413 } else {
414 trace!("No Auth already available. Proceeding with new login");
416
417 let auth_type = auth_type.as_str();
418 if let Some(authenticator) = inventory::iter::<AuthPluginRegistration>
420 .into_iter()
421 .find(|x| x.method.get_supported_auth_methods().contains(&auth_type))
422 .map(|x| x.method)
423 {
424 let auth_hints = self
426 .config
427 .auth_methods
428 .as_ref()
429 .map(|methods| serde_json::json!({"auth_methods": methods}));
430 match authenticator
431 .auth(
432 &self.client,
433 self.get_service_endpoint(
434 &ServiceType::Identity,
435 Some(&ApiVersion::from(authenticator.api_version())),
436 )?
437 .url(),
438 gather_auth_data(
439 &authenticator.requirements(auth_hints.as_ref())?,
440 &self.config,
441 auth_helper,
442 )
443 .await?,
444 Some(&requested_scope),
445 auth_hints.as_ref(),
446 )
447 .await
448 {
449 Ok(token_auth) => {
450 self.set_auth(token_auth.clone(), false);
451 }
452 Err(AuthError::AuthReceipt(receipt)) => {
453 let auth_hints = serde_json::to_value(&receipt)?;
457 let token_auth = token_receipt::PLUGIN
459 .auth(
460 &self.client,
461 self.get_service_endpoint(
462 &ServiceType::Identity,
463 Some(&ApiVersion::from(authenticator.api_version())),
464 )?
465 .url(),
466 gather_auth_data(
467 &token_receipt::PLUGIN.requirements(Some(&auth_hints))?,
468 &self.config,
469 auth_helper,
470 )
471 .await?,
472 Some(&requested_scope),
473 Some(&auth_hints),
474 )
475 .await?;
476 self.set_auth(token_auth.clone(), false);
477 }
478 Err(other) => {
479 return Err(other.into());
480 }
481 }
482 } else {
483 return Err(AuthTokenError::IdentityMethod {
484 auth_type: auth_type.into(),
485 })?;
486 }
487 }
488 }
489
490 if let Auth::AuthToken(token_auth) = &self.auth {
491 if token_auth.auth_info.is_none() {
498 let mut resolved_token = token_auth.clone();
499 let token_info = self.fetch_token_info(token_auth.token.clone()).await?;
500 resolved_token.auth_info = Some(token_info.clone());
501 let scope = AuthTokenScope::from(&token_info);
502
503 self.state.set_scope_auth(&scope, &resolved_token);
505 }
506
507 if requested_scope != AuthTokenScope::Unscoped
508 && !token_auth
509 .auth_info
510 .as_ref()
511 .map(AuthTokenScope::from)
512 .is_some_and(|scope| requested_scope == scope)
513 {
514 let token_auth = self.reauth(token_auth, &requested_scope).await?;
516 self.set_auth(token_auth.clone(), false);
520 } else {
521 self.state
525 .set_scope_auth(&AuthTokenScope::Unscoped, token_auth);
526 }
527 } else {
528 return Err(AuthError::AuthTokenNotInResponse)?;
529 }
530
531 if let Auth::AuthToken(token_data) = &self.auth {
532 match &token_data.auth_info {
533 Some(auth_data) => {
534 if let Some(project) = &auth_data.token.project {
535 self.catalog.set_project_id(project.id.clone());
536 self.catalog.configure(&self.config)?;
538 }
539 if let Some(endpoints) = &auth_data.token.catalog {
540 self.catalog.process_catalog_endpoints(endpoints)?;
541 } else {
542 error!("No catalog information");
543 }
544 }
545 _ => return Err(OpenStackError::NoAuth),
546 }
547 }
548 Ok(())
550 }
551
552 #[instrument(skip(self))]
554 pub async fn discover_service_endpoint(
555 &mut self,
556 service_type: &ServiceType,
557 ) -> Result<(), OpenStackError> {
558 if let Ok(ep) = self.catalog.get_service_endpoint(
559 service_type.to_string(),
560 None,
561 self.config.region_name.as_ref(),
562 self.config.interface.as_ref(),
563 ) {
564 if self.catalog.discovery_allowed(service_type.to_string()) {
565 info!("Performing `{}` endpoint version discovery", service_type);
566
567 let orig_url = ep.url().clone();
568 let mut try_url = ep.url().clone();
569 try_url
572 .path_segments_mut()
573 .map_err(|_| CatalogError::cannot_be_base(ep.url()))?
574 .pop_if_empty()
575 .push("");
576 let mut max_depth = 10;
577 loop {
578 let req = http::Request::builder()
579 .method(http::Method::GET)
580 .uri(query::url_to_http_uri(try_url.clone())?);
581
582 match self.rest_with_auth_async(req, Vec::new(), &self.auth).await {
583 Ok(rsp) => {
584 if rsp.status() != StatusCode::NOT_FOUND
585 && self
586 .catalog
587 .process_endpoint_discovery(
588 service_type,
589 &try_url,
590 rsp.body(),
591 self.config.region_name.as_ref(),
592 self.config.interface.as_ref(),
593 )
594 .is_ok()
595 {
596 debug!(
597 "Finished service version discovery at {}",
598 try_url.as_str()
599 );
600 debug!("catalog {:?}", self.catalog);
601 return Ok(());
602 }
603 }
604 Err(err) => {
605 error!(
606 "Error querying {} for the version discovery. It is most likely a misconfiguration on the cloud side. {}",
607 try_url.as_str(),
608 err
609 );
610 }
611 };
612 if try_url.path() != "/" {
613 try_url
616 .path_segments_mut()
617 .map_err(|_| CatalogError::cannot_be_base(&orig_url))?
618 .pop();
619 } else {
620 return Err(OpenStackError::Discovery {
621 service: service_type.to_string(),
622 url: orig_url.into(),
623 msg: match service_type {
624 ServiceType::Identity => "Service is not working.".into(),
625 _ => "No Version document found. Either service is not supporting version discovery, or API is not working".into(),
626 }
627 });
628 }
629
630 max_depth -= 1;
631 if max_depth == 0 {
632 break;
633 }
634 }
635 return Err(OpenStackError::Discovery {
636 service: service_type.to_string(),
637 url: orig_url.into(),
638 msg: "Unknown".into(),
639 });
640 }
641 return Ok(());
642 }
643 Ok(())
644 }
645
646 pub fn get_token_catalog(&self) -> Option<Vec<ServiceEndpoints>> {
649 self.catalog.get_token_catalog()
650 }
651
652 pub fn get_auth_info(&self) -> Option<AuthResponse> {
654 if let Auth::AuthToken(token) = &self.auth {
655 return token.auth_info.clone();
656 }
657 None
658 }
659
660 pub fn get_auth_state(&self, offset: Option<TimeDelta>) -> Option<AuthState> {
664 if let Auth::AuthToken(token) = &self.auth {
665 return Some(token.get_state(offset));
666 }
667 None
668 }
669
670 pub fn get_auth_token(&self) -> Option<SecretString> {
672 if let Auth::AuthToken(token) = &self.auth {
673 return Some(token.token.clone());
674 }
675 None
676 }
677
678 pub async fn fetch_token_info(
680 &self,
681 token: SecretString,
682 ) -> Result<AuthResponse, OpenStackError> {
683 let auth_ep = build_token_info_endpoint(token.expose_secret())?;
684 let rsp = auth_ep.raw_query_async(self).await?;
685 let data: AuthResponse = serde_json::from_slice(rsp.body())?;
686 Ok(data)
687 }
688
689 #[instrument(name="request", skip_all, fields(http.uri = request.url().as_str(), http.method = request.method().as_str(), openstack.ver=request.headers().get("openstack-api-version").map(|v| v.to_str().unwrap_or(""))))]
691 async fn execute_request(&self, request: Request) -> Result<Response, reqwest::Error> {
692 info!("Sending request {:?}", request);
693 let url = request.url().clone();
694 let method = request.method().clone();
695
696 if enabled!(Level::TRACE)
697 && request.headers().get(header::CONTENT_TYPE)
698 == Some(&HeaderValue::from_static("application/json"))
699 {
700 request
702 .body()
703 .and_then(|body| body.as_bytes())
704 .and_then(|bytes| String::from_utf8(bytes.to_vec()).ok())
705 .inspect(|rq| {
706 let censored = self
707 .config
708 .get_sensitive_values()
709 .iter()
710 .fold(rq.clone(), |sanitized, &secret| {
711 sanitized.replace(secret, "<CENSORED>")
712 });
713 trace!("Request Body: {:?}", censored);
714 });
715 }
716
717 let start = SystemTime::now();
718 let rsp = self.client.execute(request).await?;
719 let elapsed = SystemTime::now().duration_since(start).unwrap_or_default();
720 event!(
721 name: "http_request",
722 Level::INFO,
723 url=url.as_str(),
724 duration_ms=elapsed.as_millis(),
725 status=rsp.status().as_u16(),
726 method=method.as_str(),
727 request_id=rsp.headers().get("x-openstack-request-id").map(|v| v.to_str().unwrap_or("")),
728 "Request completed with status {}",
729 rsp.status(),
730 );
731 Ok(rsp)
732 }
733
734 async fn rest_with_auth_async(
736 &self,
737 mut request: http::request::Builder,
738 body: Vec<u8>,
739 auth: &Auth,
740 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
741 use futures_util::TryFutureExt;
742 let call = || async {
743 if let Some(headers) = request.headers_mut() {
744 auth.set_header(headers)?;
745 }
746 let http_request = request.body(body)?;
747 let request = http_request.try_into()?;
748
749 let rsp = self.execute_request(request).await?;
750
751 let mut http_rsp = HttpResponse::builder()
752 .status(rsp.status())
753 .version(rsp.version());
754
755 if let Some(headers) = http_rsp.headers_mut() {
756 headers.extend(rsp.headers().clone())
757 }
758
759 Ok(http_rsp.body(rsp.bytes().await?)?)
760 };
761 call().map_err(api::ApiError::client).await
762 }
763
764 async fn rest_with_auth_read_body_async(
766 &self,
767 mut request: http::request::Builder,
768 body_read: BoxedAsyncRead,
769 auth: &Auth,
770 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
771 use futures_util::TryFutureExt;
772 let call = || async {
773 if let Some(headers) = request.headers_mut() {
774 auth.set_header(headers)?;
775 }
776 let stream = codec::FramedRead::new(body_read.compat(), codec::BytesCodec::new())
777 .map_ok(|b| b.freeze());
778 let http_request = request.body(Body::wrap_stream(stream))?;
779 let request = http_request.try_into()?;
780
781 let rsp = self.execute_request(request).await?;
782
783 let mut http_rsp = HttpResponse::builder()
784 .status(rsp.status())
785 .version(rsp.version());
786
787 if let Some(headers) = http_rsp.headers_mut() {
788 headers.extend(rsp.headers().clone())
789 }
790
791 Ok(http_rsp.body(rsp.bytes().await?)?)
792 };
793 call().map_err(api::ApiError::client).await
794 }
795
796 async fn download_with_auth_async(
798 &self,
799 mut request: http::request::Builder,
800 body: Vec<u8>,
801 auth: &Auth,
802 ) -> Result<(HeaderMap, BoxedAsyncRead), api::ApiError<<Self as api::RestClient>::Error>> {
803 use futures_util::TryFutureExt;
804 let call = || async {
805 if let Some(headers) = request.headers_mut() {
806 auth.set_header(headers)?;
807 }
808 let http_request = request.body(body)?;
809 let request = http_request.try_into()?;
810 let rsp = self.execute_request(request).await?;
811
812 let mut headers = HeaderMap::new();
813 for (key, value) in rsp.headers() {
814 headers.insert(key, value.clone());
815 }
816
817 let boxed_async_read = BoxedAsyncRead::new(
818 rsp.bytes_stream()
819 .map_err(|orig| {
820 let kind = if orig.is_timeout() {
821 IoErrorKind::TimedOut
822 } else {
823 IoErrorKind::Other
824 };
825 IoError::new(kind, orig)
826 })
827 .into_async_read(),
828 );
829 Ok((headers, boxed_async_read))
830 };
831 call().map_err(api::ApiError::client).await
832 }
833}