1use std::collections::HashMap;
18use std::convert::TryFrom;
19use std::str::FromStr;
20use std::sync::Arc;
21
22use reqwest::Url;
23use serde::{Deserialize, Serialize};
24
25use super::config::from_config;
26use super::env::from_env;
27use crate::client::AuthenticatedClient;
28use crate::common::IdOrName;
29use crate::identity::{ApplicationCredential, Password, Scope, Token};
30use crate::{AuthType, BasicAuth, Error, ErrorKind, InterfaceType, NoAuth, Session};
31
32#[derive(Debug, Clone, Default, Deserialize, Serialize)]
33#[cfg_attr(test, derive(PartialEq, Eq))]
34pub(crate) struct Auth {
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub(crate) auth_url: Option<String>,
37 #[serde(skip_serializing_if = "Option::is_none")]
38 pub(crate) endpoint: Option<String>,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub(crate) password: Option<String>,
41 #[serde(skip_serializing_if = "Option::is_none")]
42 pub(crate) project_id: Option<String>,
43 #[serde(skip_serializing_if = "Option::is_none")]
44 pub(crate) project_name: Option<String>,
45 #[serde(skip_serializing_if = "Option::is_none")]
46 pub(crate) project_domain_id: Option<String>,
47 #[serde(skip_serializing_if = "Option::is_none")]
48 pub(crate) project_domain_name: Option<String>,
49 #[serde(skip_serializing_if = "Option::is_none")]
50 pub(crate) token: Option<String>,
51 #[serde(skip_serializing_if = "Option::is_none")]
52 pub(crate) username: Option<String>,
53 #[serde(skip_serializing_if = "Option::is_none")]
54 pub(crate) user_domain_name: Option<String>,
55 #[serde(skip_serializing_if = "Option::is_none")]
56 pub(crate) user_id: Option<String>,
57 #[serde(skip_serializing_if = "Option::is_none")]
58 pub(crate) application_credential_id: Option<String>,
59 #[serde(skip_serializing_if = "Option::is_none")]
60 pub(crate) application_credential_secret: Option<String>,
61 #[serde(skip_serializing_if = "Option::is_none")]
62 pub(crate) application_credential_name: Option<String>,
63}
64
65#[derive(Debug, Clone, Default, Deserialize, Serialize)]
71#[cfg_attr(test, derive(PartialEq, Eq))]
72pub struct CloudConfig {
73 #[serde(skip_serializing_if = "Option::is_none")]
74 pub(crate) auth: Option<Auth>,
75 #[serde(skip_serializing_if = "Option::is_none")]
76 pub(crate) auth_type: Option<String>,
77 #[serde(skip_serializing_if = "Option::is_none")]
78 pub(crate) cacert: Option<String>,
79 #[serde(skip_serializing_if = "Option::is_none")]
80 pub(crate) interface: Option<String>,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 pub(crate) region_name: Option<String>,
83 #[serde(flatten)]
84 pub(crate) options: HashMap<String, serde_yaml::Value>,
85}
86
87#[inline]
88fn require(value: Option<String>, message: &str) -> Result<String, Error> {
89 value.ok_or_else(|| Error::new(ErrorKind::InvalidConfig, message))
90}
91
92fn project_scope(
93 project_id: Option<String>,
94 project_name: Option<String>,
95 project_domain_id: Option<String>,
96 project_domain_name: Option<String>,
97) -> Option<Scope> {
98 let project_domain = project_domain_id
99 .map(IdOrName::Id)
100 .or_else(|| project_domain_name.map(IdOrName::Name))
101 .unwrap_or_else(|| IdOrName::from_name("Default"));
102 project_id
103 .map(IdOrName::Id)
104 .or_else(|| project_name.map(IdOrName::Name))
105 .map(|project| Scope::Project {
106 project,
107 domain: Some(project_domain),
108 })
109}
110
111impl Auth {
112 fn create_basic_auth(self) -> Result<BasicAuth, Error> {
113 let endpoint = require(
114 self.endpoint,
115 "HTTP basic authentication requires an endpoint",
116 )?;
117 let username = require(
118 self.username,
119 "HTTP basic authentication requires a username",
120 )?;
121 let password = require(
122 self.password,
123 "HTTP basic authentication requires a password",
124 )?;
125 BasicAuth::new(endpoint, username, password)
126 }
127
128 fn create_none_auth(self) -> Result<NoAuth, Error> {
129 if let Some(endpoint) = self.endpoint {
130 NoAuth::new(endpoint)
131 } else {
132 Ok(NoAuth::new_without_endpoint())
133 }
134 }
135
136 fn create_password_auth(self) -> Result<Password, Error> {
137 let auth_url = require(
138 self.auth_url,
139 "Password authentication requires an authentication URL",
140 )?;
141 let username = require(self.username, "Password authentication requires a username")?;
142 let password = require(self.password, "Password authentication requires a password")?;
143 let user_domain = self
144 .user_domain_name
145 .unwrap_or_else(|| String::from("Default"));
146 let mut id = Password::new(auth_url, username, password, user_domain)?;
147
148 if let Some(scope) = project_scope(
149 self.project_id,
150 self.project_name,
151 self.project_domain_id,
152 self.project_domain_name,
153 ) {
154 id.set_scope(scope);
155 }
156
157 Ok(id)
158 }
159
160 fn create_token_auth(self) -> Result<Token, Error> {
161 let auth_url = require(
162 self.auth_url,
163 "Token authentication requires an authentication URL",
164 )?;
165 let token = require(self.token, "Token authentication requires a token")?;
166 let mut id = Token::new(auth_url, token)?;
167
168 if let Some(scope) = project_scope(
169 self.project_id,
170 self.project_name,
171 self.project_domain_id,
172 self.project_domain_name,
173 ) {
174 id.set_scope(scope);
175 }
176
177 Ok(id)
178 }
179
180 fn create_application_credential(self) -> Result<ApplicationCredential, Error> {
181 let auth_url = require(
182 self.auth_url,
183 "Password authentication requires an authentication URL",
184 )?;
185 let app_secret = require(
186 self.application_credential_secret,
187 "Application credential requires a secret",
188 )?;
189
190 if let Some(app_id) = self.application_credential_id {
191 ApplicationCredential::new(auth_url, app_id, app_secret)
192 } else if let Some(app_name) = self.application_credential_name {
193 if self.username.is_some() && self.user_id.is_none() {
194 return Err(Error::new(
195 ErrorKind::InvalidConfig,
196 "Specifying Application Credential by name currently requires specifying the user ID",
197 ));
198 }
199 let user_id = require(
200 self.user_id,
201 "Application Credential authentication by name requires the user ID",
202 )?;
203 ApplicationCredential::with_user_id(auth_url, app_name, app_secret, user_id)
204 } else {
205 Err(Error::new(
206 ErrorKind::InvalidConfig,
207 "Application Credential requires an id or a name",
208 ))
209 }
210 }
211
212 fn create_auth(self, auth_type: Option<String>) -> Result<Arc<dyn AuthType>, Error> {
213 let auth_type = auth_type.unwrap_or_else(|| {
214 if self.token.is_some() {
215 "v3token"
216 } else {
217 "password"
218 }
219 .into()
220 });
221
222 Ok(if auth_type == "password" {
223 Arc::new(self.create_password_auth()?)
224 } else if auth_type == "v3token" {
225 Arc::new(self.create_token_auth()?)
226 } else if auth_type == "http_basic" {
227 Arc::new(self.create_basic_auth()?)
228 } else if auth_type == "v3applicationcredential" {
229 Arc::new(self.create_application_credential()?)
230 } else if auth_type == "none" {
231 Arc::new(self.create_none_auth()?)
232 } else {
233 return Err(Error::new(
234 ErrorKind::InvalidInput,
235 format!("Unsupported authentication type: {}", auth_type),
236 ));
237 })
238 }
239}
240
241#[derive(Debug)]
243pub(crate) struct SessionConfig {
244 pub(crate) client: AuthenticatedClient,
245 pub(crate) endpoint_overrides: HashMap<String, Url>,
246 pub(crate) interface: Option<InterfaceType>,
247 pub(crate) region_name: Option<String>,
248}
249
250impl CloudConfig {
251 pub fn from_config<S: AsRef<str>>(cloud_name: S) -> Result<CloudConfig, Error> {
253 from_config(cloud_name.as_ref())
254 }
255
256 pub fn from_env() -> Result<CloudConfig, Error> {
258 from_env()
259 }
260
261 fn create_endpoint_overrides(&self) -> Result<HashMap<String, Url>, Error> {
262 let mut result = HashMap::with_capacity(self.options.len());
263 for (ref key, ref value) in &self.options {
264 if let Some(service_type) = key.strip_suffix("_endpoint_override") {
265 if let serde_yaml::Value::String(value) = value {
266 let url = Url::parse(value).map_err(|e| {
267 Error::new(
268 ErrorKind::InvalidConfig,
269 format!("Invalid {} `{}`: {}", key, value, e),
270 )
271 })?;
272 let _ = result.insert(service_type.to_string(), url.clone());
273 let with_dashes = service_type.replace('_', "-");
275 let _ = result.insert(with_dashes, url);
276 } else {
277 return Err(Error::new(
278 ErrorKind::InvalidConfig,
279 format!("{} must be a string, got {:?}", key, value),
280 ));
281 }
282 }
283 }
284 Ok(result)
285 }
286
287 #[inline]
288 pub(crate) fn create_session_config(self) -> Result<SessionConfig, Error> {
289 let endpoint_overrides = self.create_endpoint_overrides()?;
290 let auth = if let Some(auth_info) = self.auth {
291 auth_info.create_auth(self.auth_type)?
292 } else if self.auth_type.map(|x| x == "none").unwrap_or(false) {
293 Arc::new(NoAuth::new_without_endpoint())
294 } else {
295 return Err(Error::new(
296 ErrorKind::InvalidInput,
297 "Credentials can be missing only for none authentication",
298 ));
299 };
300 let client = AuthenticatedClient::new_internal(super::get_client(self.cacert)?, auth);
301 let interface = if let Some(interface) = self.interface {
302 Some(InterfaceType::from_str(&interface)?)
303 } else {
304 None
305 };
306
307 Ok(SessionConfig {
308 client,
309 endpoint_overrides,
310 interface,
311 region_name: self.region_name,
312 })
313 }
314
315 pub async fn create_session(self) -> Result<Session, Error> {
317 let mut config = self.create_session_config()?;
318 config.client.refresh().await?;
319 let mut result = Session::new_with_authenticated_client(config.client)
320 .with_endpoint_overrides(config.endpoint_overrides);
321 result.endpoint_filters_mut().region = config.region_name;
322 if let Some(interface) = config.interface {
323 result.endpoint_filters_mut().set_interfaces(interface);
324 }
325 Ok(result)
326 }
327
328 fn check_auth_type(&self, expected: &str) -> Result<(), Error> {
329 if let Some(ref auth_type) = self.auth_type {
330 if auth_type != expected {
331 return Err(Error::new(
332 ErrorKind::InvalidInput,
333 format!(
334 "Invalid authentication type, excepted {}, got {}",
335 expected, auth_type
336 ),
337 ));
338 }
339 }
340 Ok(())
341 }
342}
343
344impl TryFrom<CloudConfig> for NoAuth {
345 type Error = Error;
346
347 fn try_from(value: CloudConfig) -> Result<NoAuth, Error> {
348 value.check_auth_type("none")?;
349 if let Some(auth) = value.auth {
350 auth.create_none_auth()
351 } else {
352 Ok(NoAuth::new_without_endpoint())
353 }
354 }
355}
356
357impl TryFrom<CloudConfig> for BasicAuth {
358 type Error = Error;
359
360 fn try_from(value: CloudConfig) -> Result<BasicAuth, Error> {
361 value.check_auth_type("http_basic")?;
362 if let Some(auth) = value.auth {
363 auth.create_basic_auth()
364 } else {
365 Err(Error::new(
366 ErrorKind::InvalidInput,
367 "Credentials can be missing only for none authentication",
368 ))
369 }
370 }
371}
372
373impl TryFrom<CloudConfig> for Password {
374 type Error = Error;
375
376 fn try_from(value: CloudConfig) -> Result<Password, Error> {
377 value.check_auth_type("password")?;
378 if let Some(auth) = value.auth {
379 auth.create_password_auth()
380 } else {
381 Err(Error::new(
382 ErrorKind::InvalidInput,
383 "Credentials can be missing only for none authentication",
384 ))
385 }
386 }
387}
388
389impl TryFrom<CloudConfig> for Token {
390 type Error = Error;
391
392 fn try_from(value: CloudConfig) -> Result<Token, Error> {
393 value.check_auth_type("v3token")?;
394 if let Some(auth) = value.auth {
395 auth.create_token_auth()
396 } else {
397 Err(Error::new(
398 ErrorKind::InvalidInput,
399 "Credentials can be missing only for none authentication",
400 ))
401 }
402 }
403}
404
405#[cfg(test)]
406mod test_cloud_config {
407 #[cfg(any(feature = "native-tls", feature = "rustls"))]
408 use std::io::Write;
409
410 use maplit::hashmap;
411 use reqwest::Url;
412
413 use super::{Auth, CloudConfig};
414
415 #[test]
416 fn test_endpoint_overrides_empty() {
417 let cfg = CloudConfig::default();
418 let result = cfg.create_endpoint_overrides().unwrap();
419 assert!(result.is_empty());
420 }
421
422 #[test]
423 fn test_endpoint_overrides_valid() {
424 let options = hashmap! {
425 "baremetal_endpoint_override".into() => "http://127.0.0.1/baremetal".into(),
426 "baremetal_introspection_endpoint_override".into() => "http://127.0.0.1:5050/".into(),
427 "something unrelated".into() => "banana".into(),
428 };
429 let cfg = CloudConfig {
430 options,
431 ..CloudConfig::default()
432 };
433 let result = cfg.create_endpoint_overrides().unwrap();
434 assert_eq!(
435 result,
436 hashmap! {
437 "baremetal".into() => Url::parse("http://127.0.0.1/baremetal").unwrap(),
438 "baremetal_introspection".into() => Url::parse("http://127.0.0.1:5050/").unwrap(),
439 "baremetal-introspection".into() => Url::parse("http://127.0.0.1:5050/").unwrap(),
440 }
441 );
442 }
443
444 #[test]
445 fn test_endpoint_overrides_wrong_type() {
446 let options = hashmap! {
447 "baremetal_endpoint_override".into() => "http://127.0.0.1/baremetal".into(),
448 "baremetal_introspection_endpoint_override".into() => 42.into(),
449 };
450 let cfg = CloudConfig {
451 options,
452 ..CloudConfig::default()
453 };
454 assert!(cfg.create_endpoint_overrides().is_err());
455 }
456
457 #[test]
458 fn test_endpoint_overrides_wrong_url() {
459 let options = hashmap! {
460 "baremetal_endpoint_override".into() => "http://127.0.0.1/baremetal".into(),
461 "baremetal_introspection_endpoint_override".into() => "?! banana".into(),
462 };
463 let cfg = CloudConfig {
464 options,
465 ..CloudConfig::default()
466 };
467 assert!(cfg.create_endpoint_overrides().is_err());
468 }
469
470 #[test]
471 fn test_create_session_config_no_auth() {
472 let cfg = CloudConfig::default();
473 assert!(cfg.create_session_config().is_err());
474 }
475
476 #[tokio::test]
477 async fn test_create_session_config_none_auth() {
478 let options = hashmap! {
479 "baremetal_endpoint_override".into() => "http://127.0.0.1/baremetal".into(),
480 };
481 let cfg = CloudConfig {
482 auth_type: Some("none".into()),
483 options,
484 ..CloudConfig::default()
485 };
486 let sscfg = cfg.create_session_config().unwrap();
487 assert!(sscfg
488 .client
489 .get_endpoint("baremetal", &Default::default())
490 .await
491 .is_err());
492 }
493
494 #[tokio::test]
495 async fn test_create_session_config_basic_auth() {
496 let cfg = CloudConfig {
497 auth_type: Some("http_basic".into()),
498 auth: Some(Auth {
499 username: Some("vasya".into()),
500 password: Some("hacker".into()),
501 endpoint: Some("http://127.0.0.1".into()),
502 ..Auth::default()
503 }),
504 ..CloudConfig::default()
505 };
506 let sscfg = cfg.create_session_config().unwrap();
507 assert_eq!(
508 sscfg
509 .client
510 .get_endpoint("baremetal", &Default::default())
511 .await
512 .unwrap()
513 .as_str(),
514 "http://127.0.0.1/"
515 );
516 }
517
518 #[test]
519 #[cfg(any(feature = "native-tls", feature = "rustls"))]
520 fn test_create_session_config_with_region_and_cacert() {
521 let mut cacert = tempfile::NamedTempFile::new().unwrap();
522 write!(
523 cacert,
524 r#"-----BEGIN CERTIFICATE-----
525MIIBYzCCAQqgAwIBAgIUJcTlPhsFyWG9S0pAAElKuSFEPBYwCgYIKoZIzj0EAwIw
526FDESMBAGA1UEAwwJbG9jYWxob3N0MB4XDTIwMTAwMjExNTU1NloXDTIwMTEwMTEx
527NTU1NlowFDESMBAGA1UEAwwJbG9jYWxob3N0MFkwEwYHKoZIzj0CAQYIKoZIzj0D
528AQcDQgAEsfpkV9dAThk54U1K+rXUnNbpwuNo5wCRrKpk+cNR/2HBO8VydNj7dkxs
529VBUvI7M9hY8dgg1jBVoPcCf0GSOvuqM6MDgwFAYDVR0RBA0wC4IJbG9jYWxob3N0
530MAsGA1UdDwQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDATAKBggqhkjOPQQDAgNH
531ADBEAiAdjF7484kjb3XJoLbgqnZh4V1yHKs57eBVuil9/V0YugIgLwb/vSUAPowb
532hK9jLBzNvo8qzKqaGfnGieuLeXCqFDA=
533-----END CERTIFICATE-----"#
534 )
535 .unwrap();
536 cacert.flush().unwrap();
537
538 let cfg = CloudConfig {
539 auth_type: Some("password".into()),
540 auth: Some(Auth {
541 auth_url: Some("http://127.0.0.1".into()),
542 username: Some("vasya".into()),
543 password: Some("hacker".into()),
544 project_name: Some("admin".into()),
545 ..Auth::default()
546 }),
547 cacert: Some(cacert.path().to_str().unwrap().into()),
548 region_name: Some("Lapland".into()),
549 ..CloudConfig::default()
550 };
551 let sscfg = cfg.create_session_config().unwrap();
552 assert_eq!(sscfg.region_name.as_ref().unwrap(), "Lapland");
553 }
554
555 #[test]
556 #[cfg(any(feature = "native-tls", feature = "rustls"))]
557 fn test_create_session_config_cacert_not_found() {
558 let cfg = CloudConfig {
559 auth_type: Some("password".into()),
560 auth: Some(Auth {
561 auth_url: Some("http://127.0.0.1".into()),
562 username: Some("vasya".into()),
563 password: Some("hacker".into()),
564 project_name: Some("admin".into()),
565 ..Auth::default()
566 }),
567 cacert: Some("/I/do/not/exist".into()),
568 region_name: Some("Lapland".into()),
569 ..CloudConfig::default()
570 };
571 assert!(cfg.create_session_config().is_err());
572 }
573}