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