openstack_sdk_auth_password/
lib.rs1use std::collections::HashMap;
41
42use async_trait::async_trait;
43use thiserror::Error;
44
45use secrecy::{ExposeSecret, SecretString};
46use serde_json::{Value, json};
47
48use openstack_sdk_auth_core::{
49 Auth, AuthError, AuthMethodPluginRegistration, AuthPluginRegistration, AuthToken,
50 AuthTokenScope, OpenStackAuthType, OpenStackMultifactorAuthMethod, execute_auth_request,
51};
52
53pub struct PasswordAuthenticator;
55
56static PLUGIN: PasswordAuthenticator = PasswordAuthenticator;
58inventory::submit! {
59 AuthPluginRegistration { method: &PLUGIN }
60}
61inventory::submit! {
62 AuthMethodPluginRegistration { method: &PLUGIN }
63}
64
65impl PasswordAuthenticator {
66 fn _get_supported_auth_methods(&self) -> Vec<&'static str> {
67 vec!["v3password", "password"]
68 }
69
70 fn _requirements(&self) -> Value {
71 json!({
72 "type": "object",
73 "required": ["password"],
74 "properties": {
75 "password": {
76 "type": "string",
77 "format": "password",
78 "description": "User password",
79 },
80 "user_id": {
81 "type": "string",
82 "description": "User ID",
83 },
84 "username": {
85 "type": "string",
86 "description": "User name",
87 },
88 "user_domain_id": {
89 "type": "string",
90 "description": "User domain ID",
91 },
92 "user_domain_name": {
93 "type": "string",
94 "description": "User domain name",
95 },
96 }
97 })
98 }
99
100 fn _get_auth_data(
101 &self,
102 values: &HashMap<String, SecretString>,
103 ) -> Result<(&'static str, Value), PasswordAuthError> {
104 let password = values
105 .get("password")
106 .ok_or(PasswordAuthError::MissingPassword)?
107 .expose_secret();
108
109 let mut user = json!({"password": password});
110 if let Some(user_id) = values.get("user_id") {
111 user["id"] = user_id.expose_secret().into();
112 } else if let Some(user_name) = values.get("username") {
113 user["name"] = user_name.expose_secret().into();
114 if let Some(udi) = values.get("user_domain_id") {
115 user["domain"]["id"] = udi.expose_secret().into();
116 } else if let Some(udn) = values.get("user_domain_name") {
117 user["domain"]["name"] = udn.expose_secret().into();
118 } else {
119 return Err(PasswordAuthError::MissingUserDomain)?;
120 }
121 } else {
122 return Err(PasswordAuthError::MissingUser)?;
123 }
124 let body = json!({ "password": {"user": user } });
125 Ok(("password", body))
126 }
127}
128
129impl OpenStackMultifactorAuthMethod for PasswordAuthenticator {
130 fn get_supported_auth_methods(&self) -> Vec<&'static str> {
132 self._get_supported_auth_methods()
133 }
134
135 fn requirements(
137 &self,
138 _hints: Option<&serde_json::Value>,
139 ) -> Result<serde_json::Value, AuthError> {
140 Ok(self._requirements())
141 }
142
143 fn get_auth_data(
145 &self,
146 values: &HashMap<String, SecretString>,
147 ) -> Result<(&'static str, serde_json::Value), AuthError> {
148 Ok(self._get_auth_data(values)?)
149 }
150}
151
152#[async_trait]
153impl OpenStackAuthType for PasswordAuthenticator {
154 fn get_supported_auth_methods(&self) -> Vec<&'static str> {
155 self._get_supported_auth_methods()
156 }
157
158 fn requirements(&self, _hints: Option<&Value>) -> Result<Value, AuthError> {
159 Ok(self._requirements())
160 }
161
162 fn api_version(&self) -> (u8, u8) {
163 (3, 0)
164 }
165
166 async fn auth(
167 &self,
168 http_client: &reqwest::Client,
169 identity_url: &url::Url,
170 values: HashMap<String, SecretString>,
171 scope: Option<&AuthTokenScope>,
172 _hints: Option<&serde_json::Value>,
173 ) -> Result<Auth, AuthError> {
174 let (method, data) = self._get_auth_data(&values)?;
175 let mut body = json!({ "auth": { "identity": data } });
176 body["auth"]["identity"]["methods"] = [method].into();
177 if let Some(scope) = scope {
178 body["auth"]["scope"] = serde_json::to_value(scope)?;
179 }
180
181 let endpoint = identity_url.join("auth/tokens")?;
182
183 let request = http_client.post(endpoint).json(&body).build()?;
184 let response = execute_auth_request(http_client, request).await?;
185
186 let auth_token = AuthToken::from_reqwest_response(response).await?;
187
188 Ok(Auth::AuthToken(Box::new(auth_token)))
189 }
190}
191
192#[derive(Debug, Error)]
194#[non_exhaustive]
195pub enum PasswordAuthError {
196 #[error("user password is missing")]
198 MissingPassword,
199
200 #[error("User name/id is required")]
202 MissingUser,
203
204 #[error("User domain name/id is required")]
206 MissingUserDomain,
207}
208
209impl From<PasswordAuthError> for AuthError {
210 fn from(source: PasswordAuthError) -> Self {
211 Self::plugin(source)
212 }
213}
214
215#[cfg(test)]
216mod tests {
217 use httpmock::MockServer;
218 use reqwest::Client;
219 use reqwest::StatusCode;
220 use secrecy::SecretString;
221 use serde_json::json;
222 use std::collections::HashMap;
223 use url::Url;
224
225 use openstack_sdk_auth_core::Auth;
226 use openstack_sdk_auth_core::types::*;
227
228 use super::*;
229
230 #[test]
231 fn test_get_supported_auth_methods() {
232 let authenticator = &PLUGIN;
233 assert!(
234 openstack_sdk_auth_core::OpenStackAuthType::get_supported_auth_methods(authenticator)
235 .contains(&"v3password")
236 );
237 assert!(
238 openstack_sdk_auth_core::OpenStackAuthType::get_supported_auth_methods(authenticator)
239 .contains(&"password")
240 );
241 }
242
243 #[test]
244 fn test_requirements() {
245 let authenticator = &PLUGIN;
246 assert!(
247 openstack_sdk_auth_core::OpenStackAuthType::requirements(authenticator, None).is_ok(),
248 );
249 }
250
251 #[test]
252 fn test_get_auth_data() {
253 let authenticator = &PLUGIN;
254 assert_eq!(
255 (
256 "password",
257 json!({"password": {"user": {"id": "uid", "password": "password"}}})
258 ),
259 authenticator
260 .get_auth_data(&HashMap::from([
261 ("password".into(), SecretString::from("password")),
262 ("user_id".into(), SecretString::from("uid")),
263 ]))
264 .unwrap()
265 );
266 assert!(
267 authenticator
268 .get_auth_data(&HashMap::from([
269 ("password".into(), SecretString::from("password")),
270 ("username".into(), SecretString::from("uname")),
271 ]))
272 .is_err()
273 );
274 assert_eq!(
275 (
276 "password",
277 json!({"password": {"user": {"name": "uname", "password": "password", "domain": {"name": "udname"}}}})
278 ),
279 authenticator
280 .get_auth_data(&HashMap::from([
281 ("password".into(), SecretString::from("password")),
282 ("username".into(), SecretString::from("uname")),
283 ("user_domain_name".into(), SecretString::from("udname")),
284 ]))
285 .unwrap()
286 );
287 assert_eq!(
288 (
289 "password",
290 json!({"password": {"user": {"name": "uname", "password": "password", "domain": {"id": "udid"}}}})
291 ),
292 authenticator
293 .get_auth_data(&HashMap::from([
294 ("password".into(), SecretString::from("password")),
295 ("username".into(), SecretString::from("uname")),
296 ("user_domain_id".into(), SecretString::from("udid")),
297 ]))
298 .unwrap()
299 );
300 assert_eq!(
301 (
302 "password",
303 json!({"password": {"user": {"name": "uname", "password": "password", "domain": {"id": "udid"}}}})
304 ),
305 authenticator
306 .get_auth_data(&HashMap::from([
307 ("password".into(), SecretString::from("password")),
308 ("username".into(), SecretString::from("uname")),
309 ("user_domain_id".into(), SecretString::from("udid")),
310 ("user_domain_name".into(), SecretString::from("udname")),
311 ]))
312 .unwrap()
313 );
314 }
315
316 #[tokio::test]
317 async fn test_auth() {
318 let server = MockServer::start_async().await;
319 let base_url = Url::parse(&server.base_url()).unwrap();
320
321 let mock = server
322 .mock_async(|when, then| {
323 when.method(httpmock::Method::POST)
324 .path("/auth/tokens")
325 .json_body(json!({
326 "auth": {
327 "identity": {
328 "methods": ["password"],
329 "password": {
330 "user": {
331 "id": "uid",
332 "password": "password"
333 }
334 }
335 }
336 }
337 }));
338 then.status(StatusCode::CREATED)
339 .header("x-subject-token", "foo")
340 .json_body(json!({
341 "token": {
342 "user": {
343 "id": "uid",
344 "name": "uname"
345 },
346 "expires_at": "2018-01-15T22:14:05.000000Z",
347 }
348 }));
349 })
350 .await;
351 let http_client = Client::new();
352
353 let authenticator = &PLUGIN;
354
355 match authenticator
356 .auth(
357 &http_client,
358 &base_url,
359 HashMap::from([
360 ("password".into(), SecretString::from("password")),
361 ("user_id".into(), SecretString::from("uid")),
362 ]),
363 None,
364 None,
365 )
366 .await
367 {
368 Ok(Auth::AuthToken(token)) => {
369 assert_eq!(token.token.expose_secret(), "foo");
370 }
371
372 other => {
373 panic!("success was expected, instead it is {:?}", other);
374 }
375 }
376 mock.assert_async().await;
377 }
378
379 #[tokio::test]
380 async fn test_auth_scope() {
381 let server = MockServer::start_async().await;
382 let base_url = Url::parse(&server.base_url()).unwrap();
383
384 let mock = server
385 .mock_async(|when, then| {
386 when.method(httpmock::Method::POST)
387 .path("/auth/tokens")
388 .json_body(json!({
389 "auth": {
390 "identity": {
391 "methods": ["password"],
392 "password": {
393 "user": {
394 "id": "uid",
395 "password": "password"
396 }
397 }
398 },
399 "scope": {
400 "project": {
401 "id": "pid"
402 }
403 }
404 }
405 }));
406 then.status(StatusCode::CREATED)
407 .header("x-subject-token", "foo")
408 .json_body(json!({
409 "token": {
410 "user": {
411 "id": "uid",
412 "name": "uname"
413 },
414 "expires_at": "2018-01-15T22:14:05.000000Z",
415 }
416 }));
417 })
418 .await;
419 let http_client = Client::new();
420
421 let authenticator = &PLUGIN;
422
423 match authenticator
424 .auth(
425 &http_client,
426 &base_url,
427 HashMap::from([
428 ("password".into(), SecretString::from("password")),
429 ("user_id".into(), SecretString::from("uid")),
430 ]),
431 Some(&AuthTokenScope::Project(Project {
432 id: Some("pid".into()),
433 ..Default::default()
434 })),
435 None,
436 )
437 .await
438 {
439 Ok(Auth::AuthToken(token)) => {
440 assert_eq!(token.token.expose_secret(), "foo");
441 }
442
443 other => {
444 panic!("success was expected, instead it is {:?}", other);
445 }
446 }
447 mock.assert_async().await;
448 }
449}