diff --git a/src/api/user.rs b/src/api/user.rs index c3c8707..4ec7bf0 100644 --- a/src/api/user.rs +++ b/src/api/user.rs @@ -11,6 +11,13 @@ pub trait UserApi { async fn list_users(&self) -> Result, RabbitMqClientError>; + async fn get_user(&self, name: String) -> Result; + + async fn create_user(&self, user: RabbitMqUserCreateRequest) + -> Result<(), RabbitMqClientError>; + + async fn delete_user(&self, name: String) -> Result<(), RabbitMqClientError>; + async fn list_users_without_permissions( &self, ) -> Result, RabbitMqClientError>; @@ -55,6 +62,50 @@ impl UserApi for RabbitMqClient { handle_response(response).await } + #[tracing::instrument(skip(self))] + async fn get_user(&self, name: String) -> Result { + let response = self + .client + .request( + reqwest::Method::GET, + format!("{}/api/users/{}", self.api_url, name), + ) + .send() + .await?; + + handle_response(response).await + } + + async fn create_user( + &self, + user: RabbitMqUserCreateRequest, + ) -> Result<(), RabbitMqClientError> { + let response = self + .client + .request( + reqwest::Method::PUT, + format!("{}/api/users/{}", self.api_url, user.name), + ) + .json(&user) + .send() + .await?; + + handle_empty_response(response).await + } + + async fn delete_user(&self, name: String) -> Result<(), RabbitMqClientError> { + let response = self + .client + .request( + reqwest::Method::DELETE, + format!("{}/api/users/{}", self.api_url, name), + ) + .send() + .await?; + + handle_empty_response(response).await + } + #[tracing::instrument(skip(self))] async fn list_users_without_permissions( &self, @@ -79,7 +130,7 @@ impl UserApi for RabbitMqClient { let response = self .client .request( - reqwest::Method::DELETE, + reqwest::Method::POST, format!("{}/api/users/bulk-delete", self.api_url), ) .json(&users) @@ -127,18 +178,49 @@ impl UserApi for RabbitMqClient { #[derive(Debug, Deserialize)] pub struct RabbitMqWhoAmI { pub name: String, - pub tags: Vec, + pub tags: Vec, } #[derive(Debug, Deserialize)] pub struct RabbitMqUser { pub name: String, pub password_hash: String, - pub hashing_algorithm: String, - pub tags: Vec, + pub hashing_algorithm: RabbitMqHashingAlgorithm, + pub tags: Vec, +} + +#[derive(Debug, Deserialize, Serialize, PartialEq)] +#[serde(rename_all = "snake_case")] +pub enum RabbitMqUserTag { + Administrator, + Management, + Monitoring, +} + +#[derive(Debug, Serialize)] +pub struct RabbitMqUserCreateRequest { + #[serde(skip_serializing)] + pub name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub password: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub password_hash: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub hashing_algorithm: Option, + pub tags: Vec, } #[derive(Debug, Serialize)] pub struct RabbitMqUsersBulkDeleteRequest { pub users: Vec, } + +#[derive(Debug, Deserialize, Serialize)] +pub enum RabbitMqHashingAlgorithm { + #[serde(rename = "rabbit_password_hashing_sha256")] + RabbitPasswordHashingSha256, + #[serde(rename = "rabbit_password_hashing_sha512")] + RabbitPasswordHashingSha512, + #[serde(rename = "rabbit_password_hashing_md5")] + RabbitPasswordHashingMd5, +} diff --git a/tests/all/main.rs b/tests/all/main.rs index 7456022..a40ac3a 100644 --- a/tests/all/main.rs +++ b/tests/all/main.rs @@ -5,4 +5,5 @@ mod exchanges; mod messages; mod nodes; mod queues; +mod users; mod vhosts; diff --git a/tests/all/users.rs b/tests/all/users.rs new file mode 100644 index 0000000..fc20285 --- /dev/null +++ b/tests/all/users.rs @@ -0,0 +1,208 @@ +use crate::context::TestContext; +use rabbitmq_management_client::api::user::{ + RabbitMqUserCreateRequest, RabbitMqUserTag, RabbitMqUsersBulkDeleteRequest, UserApi, +}; +use rabbitmq_management_client::errors::RabbitMqClientError; +use uuid::Uuid; + +#[tokio::test] +async fn can_list_users() { + let ctx = TestContext::new(); + + let users = ctx + .rabbitmq + .list_users() + .await + .expect("failed to list users"); + + assert!(!users.is_empty()); +} + +#[tokio::test] +async fn can_crud_users() { + let ctx = TestContext::new(); + + let admin_uuid = Uuid::new_v4(); + ctx.rabbitmq + .create_user(RabbitMqUserCreateRequest { + name: admin_uuid.clone().to_string(), + password: Some("fake_admin_password".to_string()), + password_hash: None, + hashing_algorithm: None, + tags: vec![RabbitMqUserTag::Administrator], + }) + .await + .expect("failed to create admin user"); + + let admin = ctx + .rabbitmq + .get_user(admin_uuid.clone().to_string()) + .await + .expect("failed to get admin user"); + + assert_eq!(admin.name, admin_uuid.clone().to_string()); + assert!(admin.tags.contains(&RabbitMqUserTag::Administrator)); + + ctx.rabbitmq + .delete_user(admin_uuid.clone().to_string()) + .await + .expect("failed to delete admin user"); + + // Getting the admin user should error + let deleted_admin = ctx.rabbitmq.get_user(admin_uuid.to_string()).await; + + assert!(matches!( + deleted_admin, + Err(RabbitMqClientError::NotFound(_)) + )); + + let mnm_uuid = Uuid::new_v4(); + ctx.rabbitmq + .create_user(RabbitMqUserCreateRequest { + name: mnm_uuid.clone().to_string(), + password: None, + password_hash: None, + hashing_algorithm: None, + tags: vec![RabbitMqUserTag::Management, RabbitMqUserTag::Monitoring], + }) + .await + .expect("failed to create the management & monitoring user"); + + let mnm = ctx + .rabbitmq + .get_user(mnm_uuid.clone().to_string()) + .await + .expect("failed to get mnm user"); + + assert_eq!(mnm.name, mnm_uuid.clone().to_string()); + assert!(mnm.tags.contains(&RabbitMqUserTag::Management)); + assert!(mnm.tags.contains(&RabbitMqUserTag::Monitoring)); + + ctx.rabbitmq + .delete_user(mnm_uuid.clone().to_string()) + .await + .expect("failed to delete the management & monitoring user"); + + // Getting the admin user should error + let deleted_mnm = ctx.rabbitmq.get_user(mnm_uuid.to_string()).await; + + assert!(matches!(deleted_mnm, Err(RabbitMqClientError::NotFound(_)))); +} + +#[tokio::test] +async fn can_create_empty_password_hash_user() { + let ctx = TestContext::new(); + + let user_uuid = Uuid::new_v4(); + ctx.rabbitmq + .create_user(RabbitMqUserCreateRequest { + name: user_uuid.clone().to_string(), + password: None, + password_hash: Some("".to_string()), + hashing_algorithm: None, + tags: vec![RabbitMqUserTag::Administrator], + }) + .await + .expect("failed to create user"); + + let user = ctx + .rabbitmq + .get_user(user_uuid.clone().to_string()) + .await + .expect("failed to get user"); + + assert_eq!(user.name, user_uuid.clone().to_string()); + assert_eq!(user.password_hash, ""); + + ctx.rabbitmq + .delete_user(user_uuid.to_string()) + .await + .expect("failed to delete user"); +} + +#[tokio::test] +async fn can_bulk_delete_users() { + let ctx = TestContext::new(); + + let original_users = ctx + .rabbitmq + .list_users() + .await + .expect("failed to list users"); + + let foo_uuid = Uuid::new_v4(); + ctx.rabbitmq + .create_user(RabbitMqUserCreateRequest { + name: foo_uuid.clone().to_string(), + password: None, + password_hash: None, + hashing_algorithm: None, + tags: vec![], + }) + .await + .expect("failed to create user"); + + let bar_uuid = Uuid::new_v4(); + ctx.rabbitmq + .create_user(RabbitMqUserCreateRequest { + name: bar_uuid.clone().to_string(), + password: None, + password_hash: None, + hashing_algorithm: None, + tags: vec![], + }) + .await + .expect("failed to create user"); + + let baz_uuid = Uuid::new_v4(); + ctx.rabbitmq + .create_user(RabbitMqUserCreateRequest { + name: baz_uuid.clone().to_string(), + password: None, + password_hash: None, + hashing_algorithm: None, + tags: vec![], + }) + .await + .expect("failed to create user"); + + let qux_uuid = Uuid::new_v4(); + ctx.rabbitmq + .create_user(RabbitMqUserCreateRequest { + name: qux_uuid.clone().to_string(), + password: None, + password_hash: None, + hashing_algorithm: None, + tags: vec![], + }) + .await + .expect("failed to create user"); + + let new_users = ctx + .rabbitmq + .list_users() + .await + .expect("failed to list users"); + + assert_eq!(original_users.len(), new_users.len() - 4); + + ctx.rabbitmq + .bulk_delete_users(RabbitMqUsersBulkDeleteRequest { + users: vec![ + foo_uuid.to_string(), + bar_uuid.to_string(), + baz_uuid.to_string(), + qux_uuid.to_string(), + ], + }) + .await + .expect("failed to bulk delete users"); + + let after_delete_users = ctx + .rabbitmq + .list_users() + .await + .expect("failed to list users"); + + assert_eq!(original_users.len(), after_delete_users.len()); +}