diff --git a/agdb_server/openapi/schema.json b/agdb_server/openapi/schema.json index d27927e2c..3a73a1720 100644 --- a/agdb_server/openapi/schema.json +++ b/agdb_server/openapi/schema.json @@ -148,6 +148,46 @@ ] } }, + "/api/v1/admin/db/user/remove": { + "post": { + "tags": [ + "crate::routes::admin::db::user" + ], + "operationId": "remove", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/RemoveDbUser" + } + } + }, + "required": true + }, + "responses": { + "204": { + "description": "user removed" + }, + "401": { + "description": "unauthorized" + }, + "403": { + "description": "cannot remove last admin user" + }, + "464": { + "description": "user not found" + }, + "466": { + "description": "db not found" + } + }, + "security": [ + { + "Token": [] + } + ] + } + }, "/api/v1/admin/shutdown": { "get": { "tags": [ diff --git a/agdb_server/src/api.rs b/agdb_server/src/api.rs index fd2d7c8a0..cab70d912 100644 --- a/agdb_server/src/api.rs +++ b/agdb_server/src/api.rs @@ -12,6 +12,7 @@ use utoipa::OpenApi; crate::routes::admin::db::remove, crate::routes::admin::db::user::add, crate::routes::admin::db::user::list, + crate::routes::admin::db::user::remove, crate::routes::admin::user::change_password, crate::routes::admin::user::create, crate::routes::admin::user::list, diff --git a/agdb_server/src/app.rs b/agdb_server/src/app.rs index e409d93fe..deb7a6b2e 100644 --- a/agdb_server/src/app.rs +++ b/agdb_server/src/app.rs @@ -19,8 +19,9 @@ pub(crate) fn app(config: Config, shutdown_sender: Sender<()>, db_pool: DbPool) }; let admin_db_user_router_v1 = Router::new() + .route("/add", routing::post(routes::admin::db::user::add)) .route("/list", routing::get(routes::admin::db::user::list)) - .route("/add", routing::post(routes::admin::db::user::add)); + .route("/remove", routing::post(routes::admin::db::user::remove)); let admin_db_router_v1 = Router::new() .route("/list", routing::get(routes::admin::db::list)) diff --git a/agdb_server/src/db_pool.rs b/agdb_server/src/db_pool.rs index 19c0ba6e2..f74cc8710 100644 --- a/agdb_server/src/db_pool.rs +++ b/agdb_server/src/db_pool.rs @@ -218,6 +218,7 @@ impl DbPool { &QueryBuilder::select() .ids( QueryBuilder::search() + .depth_first() .from("dbs") .limit(1) .where_() @@ -243,6 +244,7 @@ impl DbPool { .db()? .exec( &QueryBuilder::search() + .depth_first() .from("dbs") .limit(1) .where_() @@ -324,6 +326,7 @@ impl DbPool { &QueryBuilder::select() .ids( QueryBuilder::search() + .depth_first() .from(user) .limit(1) .where_() @@ -351,6 +354,7 @@ impl DbPool { &QueryBuilder::select() .ids( QueryBuilder::search() + .depth_first() .from("users") .limit(1) .where_() @@ -376,6 +380,7 @@ impl DbPool { .db()? .exec( &QueryBuilder::search() + .depth_first() .from("users") .limit(1) .where_() @@ -399,6 +404,7 @@ impl DbPool { .db()? .exec( &QueryBuilder::search() + .depth_first() .from("users") .limit(1) .where_() @@ -434,6 +440,26 @@ impl DbPool { .ids()) } + pub(crate) fn db_user_id(&self, db: DbId, name: &str) -> ServerResult { + Ok(self + .db()? + .exec( + &QueryBuilder::search() + .depth_first() + .to(db) + .where_() + .distance(CountComparison::Equal(2)) + .and() + .key("name") + .value(Comparison::Equal(name.into())) + .query(), + )? + .elements + .get(0) + .ok_or(ErrorCode::UserNotFound)? + .id) + } + pub(crate) fn db_users(&self, db: DbId) -> ServerResult> { let mut users = vec![]; diff --git a/agdb_server/src/routes/admin/db/user.rs b/agdb_server/src/routes/admin/db/user.rs index 1bb0fe7ad..067e1540d 100644 --- a/agdb_server/src/routes/admin/db/user.rs +++ b/agdb_server/src/routes/admin/db/user.rs @@ -1,5 +1,6 @@ use crate::db_pool::DbPool; use crate::routes::db::user::DbUser; +use crate::routes::db::user::RemoveDbUser; use crate::routes::db::ServerDatabaseName; use crate::server_error::ServerResponse; use crate::user_id::AdminId; @@ -62,3 +63,33 @@ pub(crate) async fn list( Ok((StatusCode::OK, Json(users))) } + +#[utoipa::path(post, + path = "/api/v1/admin/db/user/remove", + request_body = RemoveDbUser, + security(("Token" = [])), + responses( + (status = 204, description = "user removed"), + (status = 401, description = "unauthorized"), + (status = 403, description = "cannot remove last admin user"), + (status = 464, description = "user not found"), + (status = 466, description = "db not found"), + ) +)] +pub(crate) async fn remove( + _admin: AdminId, + State(db_pool): State, + Json(request): Json, +) -> ServerResponse { + let db = db_pool.find_db_id(&request.database)?; + let db_user = db_pool.db_user_id(db, &request.user)?; + let admins = db_pool.db_admins(db)?; + + if admins == vec![db_user] { + return Ok(StatusCode::FORBIDDEN); + } + + db_pool.remove_db_user(db, db_user)?; + + Ok(StatusCode::NO_CONTENT) +} diff --git a/agdb_server/src/routes/db/user.rs b/agdb_server/src/routes/db/user.rs index aceb7f52b..4ad8a9cab 100644 --- a/agdb_server/src/routes/db/user.rs +++ b/agdb_server/src/routes/db/user.rs @@ -130,11 +130,9 @@ pub(crate) async fn remove( Json(request): Json, ) -> ServerResponse { let db = db_pool.find_db_id(&request.database)?; - let db_user = db_pool.find_user_id(&request.user)?; + let db_user = db_pool.db_user_id(db, &request.user)?; let admins = db_pool.db_admins(db)?; - println!("{:?} == {:?}", admins, vec![db_user]); - if (!admins.contains(&user.0) && user.0 != db_user) || admins == vec![db_user] { return Ok(StatusCode::FORBIDDEN); } diff --git a/agdb_server/tests/integration.rs b/agdb_server/tests/integration.rs index cc2a019f9..c0fd0a9c7 100644 --- a/agdb_server/tests/integration.rs +++ b/agdb_server/tests/integration.rs @@ -29,6 +29,7 @@ pub const ADMIN_DB_LIST_URI: &str = "/admin/db/list"; pub const ADMIN_DB_REMOVE_URI: &str = "/admin/db/remove"; pub const ADMIN_DB_USER_ADD_URI: &str = "/admin/db/user/add"; pub const ADMIN_DB_USER_LIST_URI: &str = "/admin/db/user/list"; +pub const ADMIN_DB_USER_REMOVE_URI: &str = "/admin/db/user/remove"; pub const ADMIN_USER_LIST_URI: &str = "/admin/user/list"; pub const ADMIN_CHANGE_PASSWORD_URI: &str = "/admin/user/change_password"; pub const DB_REMOVE_URI: &str = "/db/remove"; diff --git a/agdb_server/tests/routes/admin_db_user_remove_test.rs b/agdb_server/tests/routes/admin_db_user_remove_test.rs new file mode 100644 index 000000000..8e465261b --- /dev/null +++ b/agdb_server/tests/routes/admin_db_user_remove_test.rs @@ -0,0 +1,162 @@ +use crate::AddUser; +use crate::DbWithRole; +use crate::TestServer; +use crate::ADMIN_DB_USER_REMOVE_URI; +use crate::DB_LIST_URI; +use crate::DB_USER_ADD_URI; +use crate::NO_TOKEN; +use serde::Serialize; + +#[derive(Serialize)] +struct RemoveUser { + database: String, + user: String, +} + +#[tokio::test] +async fn remove() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let reader = server.init_user().await?; + let db = server.init_db("memory", &user).await?; + let role = AddUser { + database: &db, + user: &reader.name, + role: "read", + }; + let (_, list) = server + .get::>(DB_LIST_URI, &reader.token) + .await?; + assert_eq!(list?, vec![]); + assert_eq!( + server.post(DB_USER_ADD_URI, &role, &user.token).await?.0, + 201 + ); + let (_, list) = server + .get::>(DB_LIST_URI, &reader.token) + .await?; + let expected = vec![DbWithRole { + name: db.clone(), + db_type: "memory".to_string(), + role: "read".to_string(), + }]; + assert_eq!(list?, expected); + let rem = RemoveUser { + database: db, + user: reader.name, + }; + assert_eq!( + server + .post(ADMIN_DB_USER_REMOVE_URI, &rem, &server.admin_token) + .await? + .0, + 204 + ); + let (_, list) = server + .get::>(DB_LIST_URI, &reader.token) + .await?; + assert_eq!(list?, vec![]); + Ok(()) +} + +#[tokio::test] +async fn remove_last_admin() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("memory", &user).await?; + let rem = RemoveUser { + database: db.clone(), + user: user.name, + }; + assert_eq!( + server + .post(ADMIN_DB_USER_REMOVE_URI, &rem, &server.admin_token) + .await? + .0, + 403 + ); + let (_, list) = server + .get::>(DB_LIST_URI, &user.token) + .await?; + let expected = vec![DbWithRole { + name: db, + db_type: "memory".to_string(), + role: "admin".to_string(), + }]; + assert_eq!(list?, expected); + Ok(()) +} + +#[tokio::test] +async fn db_not_found() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let rem = RemoveUser { + database: format!("{}/db_not_found", user.name), + user: String::new(), + }; + assert_eq!( + server + .post(ADMIN_DB_USER_REMOVE_URI, &rem, &server.admin_token) + .await? + .0, + 466 + ); + Ok(()) +} + +#[tokio::test] +async fn user_not_found() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("memory", &user).await?; + let rem = RemoveUser { + database: db, + user: "user_not_found".to_string(), + }; + assert_eq!( + server + .post(ADMIN_DB_USER_REMOVE_URI, &rem, &server.admin_token) + .await? + .0, + 464 + ); + Ok(()) +} + +#[tokio::test] +async fn db_user_not_found() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let other = server.init_user().await?; + let db = server.init_db("memory", &user).await?; + let rem = RemoveUser { + database: db, + user: other.name, + }; + assert_eq!( + server + .post(ADMIN_DB_USER_REMOVE_URI, &rem, &server.admin_token) + .await? + .0, + 464 + ); + Ok(()) +} + +#[tokio::test] +async fn no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let rem = RemoveUser { + database: String::new(), + user: String::new(), + }; + assert_eq!( + server + .post(ADMIN_DB_USER_REMOVE_URI, &rem, NO_TOKEN) + .await? + .0, + 401 + ); + Ok(()) +} diff --git a/agdb_server/tests/routes/db_list_test.rs b/agdb_server/tests/routes/db_list_test.rs index f822c187d..4b9462736 100644 --- a/agdb_server/tests/routes/db_list_test.rs +++ b/agdb_server/tests/routes/db_list_test.rs @@ -9,7 +9,7 @@ async fn list() -> anyhow::Result<()> { let user = server.init_user().await?; let db1 = server.init_db("memory", &user).await?; let db2 = server.init_db("memory", &user).await?; - let expected = vec![ + let mut expected = vec![ DbWithRole { name: db1.clone(), db_type: "memory".to_string(), @@ -21,6 +21,7 @@ async fn list() -> anyhow::Result<()> { role: "admin".to_string(), }, ]; + expected.sort(); let (status, list) = server .get::>(DB_LIST_URI, &user.token) .await?; diff --git a/agdb_server/tests/routes/db_user_list.rs b/agdb_server/tests/routes/db_user_list.rs index 43450c587..27be56d2d 100644 --- a/agdb_server/tests/routes/db_user_list.rs +++ b/agdb_server/tests/routes/db_user_list.rs @@ -32,7 +32,7 @@ async fn list_users() -> anyhow::Result<()> { .await?; let mut list = list?; list.sort(); - let expected = vec![ + let mut expected = vec![ DbUser { database: db.clone(), user: user.name, @@ -44,6 +44,7 @@ async fn list_users() -> anyhow::Result<()> { role: "read".to_string(), }, ]; + expected.sort(); assert_eq!(list, expected); Ok(()) } diff --git a/agdb_server/tests/routes/db_user_remove_test.rs b/agdb_server/tests/routes/db_user_remove_test.rs index 92bc6ca67..5871ee5e1 100644 --- a/agdb_server/tests/routes/db_user_remove_test.rs +++ b/agdb_server/tests/routes/db_user_remove_test.rs @@ -198,6 +198,23 @@ async fn user_not_found() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn db_user_not_found() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let other = server.init_user().await?; + let db = server.init_db("memory", &user).await?; + let rem = RemoveUser { + database: db, + user: other.name, + }; + assert_eq!( + server.post(DB_USER_REMOVE_URI, &rem, &user.token).await?.0, + 464 + ); + Ok(()) +} + #[tokio::test] async fn no_token() -> anyhow::Result<()> { let server = TestServer::new().await?; diff --git a/agdb_server/tests/routes/mod.rs b/agdb_server/tests/routes/mod.rs index 0497f753e..3377f4c55 100644 --- a/agdb_server/tests/routes/mod.rs +++ b/agdb_server/tests/routes/mod.rs @@ -2,6 +2,7 @@ mod admin_db_list_test; mod admin_db_remove_test; mod admin_db_user_add_test; mod admin_db_user_list_test; +mod admin_db_user_remove_test; mod admin_user_change_password_test; mod admin_user_create_test; mod admin_user_list_test;