1#![deny(dead_code, unused_imports, unused_mut)]
18
19use bytes::Bytes;
20use chrono::TimeDelta;
21use http::{Response as HttpResponse, StatusCode};
22use reqwest::{
23 blocking::{Client, Request, Response},
24 Certificate, Url,
25};
26use std::convert::TryInto;
27use std::fmt::{self, Debug};
28use std::time::SystemTime;
29use std::{fs::File, io::Read};
30use tokio::runtime::Runtime;
31use tracing::{debug, error, event, info, instrument, trace, warn, Level};
32
33use crate::api;
34use crate::api::query;
35use crate::api::query::RawQuery;
36use crate::auth::{
37 self,
38 auth_helper::{AuthHelper, Dialoguer, Noop},
39 authtoken,
40 authtoken::{AuthTokenError, AuthType},
41 Auth, AuthError, AuthState,
42};
43use crate::catalog::{Catalog, CatalogError, ServiceEndpoint};
44use crate::config::CloudConfig;
45use crate::config::{get_config_identity_hash, ConfigFile};
46use crate::error::{OpenStackError, OpenStackResult, RestError};
47use crate::state;
48use crate::types::identity::v3::{AuthReceiptResponse, AuthResponse, Project};
49use crate::types::{ApiVersion, ServiceType};
50use crate::utils::expand_tilde;
51
52#[allow(dead_code)]
56#[derive(Clone)]
57enum ClientCert {
58 None,
59 #[cfg(feature = "client_der")]
60 Der(Vec<u8>, String),
61 #[cfg(feature = "client_pem")]
62 Pem(Vec<u8>),
63}
64
65#[derive(Clone)]
101pub struct OpenStack {
102 client: Client,
104 config: CloudConfig,
106 auth: Auth,
108 catalog: Catalog,
110 state: state::State,
116}
117
118impl Debug for OpenStack {
119 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
120 f.debug_struct("OpenStack")
121 .field("service_endpoints", &self.catalog)
122 .finish()
123 }
124}
125
126#[allow(dead_code)]
129#[derive(Debug, Clone)]
130enum CertPolicy {
131 Default,
132 Insecure,
133}
134
135impl OpenStack {
136 fn new_impl(config: &CloudConfig, auth: Auth) -> OpenStackResult<Self> {
138 let mut client_builder = Client::builder();
139
140 if let Some(cacert) = &config.cacert {
141 let mut buf = Vec::new();
142 File::open(expand_tilde(cacert).unwrap_or(cacert.into()))
143 .map_err(|e| OpenStackError::IOWithPath {
144 source: e,
145 path: cacert.into(),
146 })?
147 .read_to_end(&mut buf)
148 .map_err(|e| OpenStackError::IOWithPath {
149 source: e,
150 path: cacert.into(),
151 })?;
152 for cert in Certificate::from_pem_bundle(&buf)? {
153 client_builder = client_builder.add_root_certificate(cert);
154 }
155 }
156 if let Some(false) = &config.verify {
157 warn!(
158 "SSL Verification is disabled! Please consider using `cacert` for adding custom certificate instead."
159 );
160 client_builder = client_builder.danger_accept_invalid_certs(true);
161 }
162
163 let mut session = OpenStack {
164 client: client_builder.build()?,
165 config: config.clone(),
166 auth,
167 catalog: Catalog::default(),
168 state: state::State::new(),
169 };
170
171 let auth_data = session
172 .config
173 .auth
174 .as_ref()
175 .ok_or(AuthTokenError::MissingAuthData)?;
176
177 let identity_service_url = auth_data
178 .auth_url
179 .as_ref()
180 .ok_or(AuthTokenError::MissingAuthUrl)?;
181
182 session.catalog.register_catalog_endpoint(
183 "identity",
184 identity_service_url,
185 config.region_name.as_ref(),
186 Some("public"),
187 )?;
188
189 session.catalog.configure(config)?;
190
191 session
192 .state
193 .set_auth_hash_key(get_config_identity_hash(config))
194 .enable_auth_cache(ConfigFile::new()?.is_auth_cache_enabled());
195
196 Ok(session)
197 }
198
199 #[instrument(name = "connect", level = "trace", skip(config))]
201 pub fn new(config: &CloudConfig) -> OpenStackResult<Self> {
202 let mut session = Self::new_impl(config, Auth::None)?;
203
204 session.discover_service_endpoint(&ServiceType::Identity)?;
206
207 session.authorize(None, false, false)?;
208
209 Ok(session)
210 }
211
212 fn set_auth(&mut self, auth: auth::Auth, skip_cache_update: bool) -> &mut Self {
214 self.auth = auth;
215 if !skip_cache_update {
216 if let Auth::AuthToken(auth) = &self.auth {
217 let scope = match &auth.auth_info {
223 Some(info) => {
224 if info.token.application_credential.is_some() {
225 authtoken::AuthTokenScope::Unscoped
226 } else {
227 auth.get_scope()
228 }
229 }
230 _ => auth.get_scope(),
231 };
232 self.state.set_scope_auth(&scope, auth);
233 }
234 }
235 self
236 }
237
238 fn set_token_auth(&mut self, token: String, token_info: Option<AuthResponse>) -> &mut Self {
240 let token_auth = authtoken::AuthToken {
241 token,
242 auth_info: token_info,
243 };
244 self.set_auth(auth::Auth::AuthToken(Box::new(token_auth.clone())), false);
245 self
246 }
247
248 pub fn authorize(
250 &mut self,
251 scope: Option<authtoken::AuthTokenScope>,
252 interactive: bool,
253 renew_auth: bool,
254 ) -> Result<(), OpenStackError> {
255 if interactive {
256 self.authorize_with_auth_helper(scope, &mut Dialoguer::default(), renew_auth)
257 } else {
258 self.authorize_with_auth_helper(scope, &mut Noop::default(), renew_auth)
259 }
260 }
261
262 pub fn authorize_with_auth_helper<A>(
264 &mut self,
265 scope: Option<authtoken::AuthTokenScope>,
266 auth_helper: &mut A,
267 renew_auth: bool,
268 ) -> Result<(), OpenStackError>
269 where
270 A: AuthHelper,
271 {
272 let rt = Runtime::new()?;
274 let requested_scope = scope.unwrap_or(authtoken::AuthTokenScope::try_from(&self.config)?);
275
276 if let (Some(auth), false) = (self.state.get_scope_auth(&requested_scope), renew_auth) {
277 trace!("Auth already available");
279 self.auth = auth::Auth::AuthToken(Box::new(auth.clone()));
280 } else {
281 let auth_type = AuthType::from_cloud_config(&self.config)?;
284 let mut force_new_auth = renew_auth;
285 if let AuthType::V3ApplicationCredential = auth_type {
286 force_new_auth = true;
290 }
291 let mut rsp;
292 if let (Some(available_auth), false) = (self.state.get_any_valid_auth(), force_new_auth)
293 {
294 trace!("Valid Auth is available for reauthz: {:?}", available_auth);
298 let auth_ep = authtoken::build_reauth_request(&available_auth, &requested_scope)?;
299 rsp = auth_ep.raw_query(self)?;
300 } else {
301 trace!("No Auth already available. Proceeding with new login");
303
304 match AuthType::from_cloud_config(&self.config)? {
305 AuthType::V3ApplicationCredential
306 | AuthType::V3Password
307 | AuthType::V3Token
308 | AuthType::V3Totp
309 | AuthType::V3Multifactor => {
310 let identity = rt.block_on(authtoken::build_identity_data_from_config(
311 &self.config,
312 auth_helper,
313 ))?;
314 let auth_ep = authtoken::build_auth_request_with_identity_and_scope(
315 &identity,
316 &requested_scope,
317 )?;
318 rsp = auth_ep.raw_query(self)?;
319
320 if let StatusCode::UNAUTHORIZED = rsp.status() {
322 if let Some(receipt) = rsp.headers().get("openstack-auth-receipt") {
323 let receipt_data: AuthReceiptResponse =
324 serde_json::from_slice(rsp.body())?;
325 let auth_endpoint =
326 rt.block_on(authtoken::build_auth_request_from_receipt(
327 &self.config,
328 receipt.clone(),
329 &receipt_data,
330 &requested_scope,
331 auth_helper,
332 ))?;
333 rsp = auth_endpoint.raw_query(self)?;
334 }
335 }
336 api::check_response_error::<Self>(&rsp, None)?;
337 }
338 AuthType::V3OidcAccessToken => {
339 let auth_ep = rt.block_on(auth::v3oidcaccesstoken::get_auth_ep(
340 &self.config,
341 auth_helper,
342 ))?;
343 rsp = auth_ep.raw_query(self)?;
344
345 let token = rsp
346 .headers()
347 .get("x-subject-token")
348 .ok_or(AuthError::AuthTokenNotInResponse)?
349 .to_str()
350 .map_err(|_| AuthError::AuthTokenNotString)?;
351
352 let token_info: AuthResponse = serde_json::from_slice(rsp.body())?;
354 let token_auth = authtoken::AuthToken {
355 token: token.to_string(),
356 auth_info: Some(token_info),
357 };
358 self.set_auth(Auth::AuthToken(Box::new(token_auth.clone())), false);
359
360 let auth_ep =
362 authtoken::build_reauth_request(&token_auth, &requested_scope)?;
363 rsp = auth_ep.raw_query(self)?;
364 }
365 other => {
366 return Err(AuthTokenError::IdentityMethodSync {
367 auth_type: other.as_str().into(),
368 })?;
369 }
370 }
371 };
372
373 let data: AuthResponse = serde_json::from_slice(rsp.body())?;
374 debug!("Auth token is {:?}", data);
375
376 let token = rsp
377 .headers()
378 .get("x-subject-token")
379 .ok_or(AuthError::AuthTokenNotInResponse)?
380 .to_str()
381 .map_err(|_| AuthError::AuthTokenNotString)?;
382
383 self.set_token_auth(token.into(), Some(data));
384 }
385
386 if let auth::Auth::AuthToken(token_data) = &self.auth {
387 match &token_data.auth_info {
388 Some(auth_data) => {
389 if let Some(project) = &auth_data.token.project {
390 self.catalog.set_project_id(project.id.clone());
391 self.catalog.configure(&self.config)?;
393 }
394 if let Some(endpoints) = &auth_data.token.catalog {
395 self.catalog
396 .process_catalog_endpoints(endpoints, Some("public"))?;
397 } else {
398 error!("No catalog information");
399 }
400 }
401 _ => return Err(OpenStackError::NoAuth),
402 }
403 }
404 Ok(())
406 }
407
408 #[instrument(skip(self))]
409 pub fn discover_service_endpoint(
410 &mut self,
411 service_type: &ServiceType,
412 ) -> Result<(), OpenStackError> {
413 if let Ok(ep) = self.catalog.get_service_endpoint(
414 service_type.to_string(),
415 None,
416 self.config.region_name.as_ref(),
417 ) {
418 if self.catalog.discovery_allowed(service_type.to_string()) {
419 info!("Performing `{}` endpoint version discovery", service_type);
420
421 let orig_url = ep.url().clone();
422 let mut try_url = ep.url().clone();
423 try_url
426 .path_segments_mut()
427 .map_err(|_| CatalogError::cannot_be_base(ep.url()))?
428 .pop_if_empty()
429 .push("");
430 let mut max_depth = 10;
431 loop {
432 let req = http::Request::builder()
433 .method(http::Method::GET)
434 .uri(query::url_to_http_uri(try_url.clone())?);
435
436 match self.rest_with_auth(req, Vec::new(), &self.auth) {
437 Ok(rsp) => {
438 if rsp.status() != StatusCode::NOT_FOUND
439 && self
440 .catalog
441 .process_endpoint_discovery(
442 service_type,
443 &try_url,
444 rsp.body(),
445 self.config.region_name.as_ref(),
446 )
447 .is_ok()
448 {
449 debug!(
450 "Finished service version discovery at {}",
451 try_url.as_str()
452 );
453 return Ok(());
454 }
455 }
456 Err(err) => {
457 error!(
458 "Error querying {} for the version discovery. It is most likely a misconfiguration on the cloud side. {}",
459 try_url.as_str(),
460 err
461 );
462 }
463 };
464 if try_url.path() != "/" {
465 try_url
468 .path_segments_mut()
469 .map_err(|_| CatalogError::cannot_be_base(&orig_url))?
470 .pop();
471 } else {
472 return Err(OpenStackError::Discovery {
473 service: service_type.to_string(),
474 url: orig_url.into(),
475 msg: match service_type {
476 ServiceType::Identity => "Service is not working.".into(),
477 _ => "No Version document found. Either service is not supporting version discovery, or API is not working".into(),
478 }
479 });
480 }
481
482 max_depth -= 1;
483 if max_depth == 0 {
484 break;
485 }
486 }
487 return Err(OpenStackError::Discovery {
488 service: service_type.to_string(),
489 url: orig_url.into(),
490 msg: "Unknown".into(),
491 });
492 }
493 return Ok(());
494 }
495 Ok(())
496 }
497
498 pub fn get_auth_token(&self) -> Option<String> {
500 if let Auth::AuthToken(token) = &self.auth {
501 return Some(token.token.clone());
502 }
503 None
504 }
505
506 pub fn get_auth_state(&self, offset: Option<TimeDelta>) -> Option<AuthState> {
510 if let Auth::AuthToken(token) = &self.auth {
511 return Some(token.get_state(offset));
512 }
513 None
514 }
515
516 #[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(""))))]
518 fn execute_request(&self, request: Request) -> Result<Response, reqwest::Error> {
519 info!("Sending request {:?}", request);
520 let url: Url = request.url().clone();
521 let method = request.method().clone();
522
523 let start = SystemTime::now();
524 let rsp = self.client.execute(request)?;
525 let elapsed = SystemTime::now().duration_since(start).unwrap_or_default();
526 event!(
527 name: "http_request",
528 Level::INFO,
529 url=url.as_str(),
530 duration_ms=elapsed.as_millis(),
531 status=rsp.status().as_u16(),
532 method=method.as_str(),
533 request_id=rsp.headers().get("x-openstack-request-id").map(|v| v.to_str().unwrap_or("")),
534 "Request completed with status {}",
535 rsp.status(),
536 );
537 Ok(rsp)
538 }
539
540 fn rest_with_auth(
542 &self,
543 mut request: http::request::Builder,
544 body: Vec<u8>,
545 auth: &Auth,
546 ) -> Result<HttpResponse<Bytes>, api::ApiError<<Self as api::RestClient>::Error>> {
547 let call = || -> Result<_, RestError> {
548 if let Some(headers) = request.headers_mut() {
549 auth.set_header(headers)?;
550 }
551 let http_request = request.body(body)?;
552 let request = http_request.try_into()?;
553
554 let rsp = self.execute_request(request)?;
555
556 let mut http_rsp = HttpResponse::builder()
557 .status(rsp.status())
558 .version(rsp.version());
559
560 if let Some(headers) = http_rsp.headers_mut() {
561 headers.extend(rsp.headers().clone())
562 }
563
564 Ok(http_rsp.body(rsp.bytes()?)?)
565 };
566 call().map_err(api::ApiError::client)
567 }
568}
569
570impl api::RestClient for OpenStack {
571 type Error = RestError;
572
573 fn get_service_endpoint(
575 &self,
576 service_type: &ServiceType,
577 version: Option<&ApiVersion>,
578 ) -> Result<&ServiceEndpoint, api::ApiError<Self::Error>> {
579 Ok(self
580 .catalog
581 .get_service_endpoint(service_type.to_string(), version, None::<String>)?)
582 }
583
584 fn get_current_project(&self) -> Option<Project> {
585 if let Auth::AuthToken(token) = &self.auth {
586 return token.auth_info.clone().and_then(|x| x.token.project);
587 }
588 None
589 }
590}
591
592impl api::Client for OpenStack {
593 fn rest(
595 &self,
596 request: http::request::Builder,
597 body: Vec<u8>,
598 ) -> Result<HttpResponse<Bytes>, api::ApiError<Self::Error>> {
599 self.rest_with_auth(request, body, &self.auth)
600 }
601}