diff --git a/agdb_server/openapi/schema.json b/agdb_server/openapi/schema.json index 1c6fa4213..9e76a04cd 100644 --- a/agdb_server/openapi/schema.json +++ b/agdb_server/openapi/schema.json @@ -74,6 +74,40 @@ ] } }, + "/api/v1/admin/db/user/list": { + "get": { + "tags": [ + "crate::routes::admin::db::user" + ], + "operationId": "list", + "parameters": [ + { + "name": "db", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "ok" + }, + "401": { + "description": "unauthorized" + }, + "466": { + "description": "db not found" + } + }, + "security": [ + { + "Token": [] + } + ] + } + }, "/api/v1/admin/shutdown": { "get": { "tags": [ @@ -393,7 +427,7 @@ "operationId": "list", "parameters": [ { - "name": "name", + "name": "db", "in": "path", "required": true, "schema": { @@ -635,10 +669,10 @@ "ServerDatabaseName": { "type": "object", "required": [ - "name" + "db" ], "properties": { - "name": { + "db": { "type": "string" } } diff --git a/agdb_server/src/api.rs b/agdb_server/src/api.rs index 59f52f5e5..563676260 100644 --- a/agdb_server/src/api.rs +++ b/agdb_server/src/api.rs @@ -10,6 +10,7 @@ use utoipa::OpenApi; crate::routes::status, crate::routes::admin::db::list, crate::routes::admin::db::remove, + crate::routes::admin::db::user::list, 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 b5836fc68..82afec882 100644 --- a/agdb_server/src/app.rs +++ b/agdb_server/src/app.rs @@ -18,9 +18,13 @@ pub(crate) fn app(config: Config, shutdown_sender: Sender<()>, db_pool: DbPool) shutdown_sender, }; + let admin_db_user_router_v1 = + Router::new().route("/list", routing::get(routes::admin::db::user::list)); + let admin_db_router_v1 = Router::new() .route("/list", routing::get(routes::admin::db::list)) - .route("/remove", routing::post(routes::admin::db::remove)); + .route("/remove", routing::post(routes::admin::db::remove)) + .nest("/user", admin_db_user_router_v1); let admin_user_router_v1 = Router::new() .route( diff --git a/agdb_server/src/routes/admin/db.rs b/agdb_server/src/routes/admin/db.rs index 407f802c4..a9bcc9be1 100644 --- a/agdb_server/src/routes/admin/db.rs +++ b/agdb_server/src/routes/admin/db.rs @@ -1,3 +1,5 @@ +pub(crate) mod user; + use crate::db::DbPool; use crate::routes::db::ServerDatabase; use crate::routes::db::ServerDatabaseName; @@ -42,7 +44,7 @@ pub(crate) async fn remove( State(db_pool): State, Json(request): Json, ) -> ServerResponse { - let db = db_pool.find_db(&request.name)?; + let db = db_pool.find_db(&request.db)?; db_pool.remove_db(db)?; Ok(StatusCode::NO_CONTENT) diff --git a/agdb_server/src/routes/admin/db/user.rs b/agdb_server/src/routes/admin/db/user.rs new file mode 100644 index 000000000..cd6d4dd8f --- /dev/null +++ b/agdb_server/src/routes/admin/db/user.rs @@ -0,0 +1,40 @@ +use crate::db::DbPool; +use crate::routes::db::user::DbUser; +use crate::routes::db::ServerDatabaseName; +use crate::server_error::ServerResponse; +use crate::user_id::AdminId; +use axum::extract::Query; +use axum::extract::State; +use axum::http::StatusCode; +use axum::Json; + +#[utoipa::path(get, + path = "/api/v1/admin/db/user/list", + security(("Token" = [])), + params( + ServerDatabaseName, + ), + responses( + (status = 200, description = "ok"), + (status = 401, description = "unauthorized"), + (status = 466, description = "db not found"), + ) +)] +pub(crate) async fn list( + _admin: AdminId, + State(db_pool): State, + request: Query, +) -> ServerResponse<(StatusCode, Json>)> { + let db = db_pool.find_db_id(&request.db)?; + let users = db_pool + .db_users(db)? + .into_iter() + .map(|(name, role)| DbUser { + database: request.db.clone(), + user: name, + role: role.into(), + }) + .collect(); + + Ok((StatusCode::OK, Json(users))) +} diff --git a/agdb_server/src/routes/db.rs b/agdb_server/src/routes/db.rs index 71e4f490f..7ccf5e296 100644 --- a/agdb_server/src/routes/db.rs +++ b/agdb_server/src/routes/db.rs @@ -31,7 +31,7 @@ pub(crate) struct ServerDatabase { #[derive(Deserialize, ToSchema, IntoParams)] pub(crate) struct ServerDatabaseName { - pub(crate) name: String, + pub(crate) db: String, } impl Display for DbType { @@ -113,7 +113,7 @@ pub(crate) async fn delete( State(config): State, Json(request): Json, ) -> ServerResponse { - let db = db_pool.find_user_db(user.0, &request.name)?; + let db = db_pool.find_user_db(user.0, &request.db)?; if !db_pool.is_db_admin(user.0, db.db_id.unwrap())? { return Ok(StatusCode::FORBIDDEN); @@ -160,7 +160,7 @@ pub(crate) async fn remove( State(db_pool): State, Json(request): Json, ) -> ServerResponse { - let db = db_pool.find_user_db(user.0, &request.name)?; + let db = db_pool.find_user_db(user.0, &request.db)?; if !db_pool.is_db_admin(user.0, db.db_id.unwrap())? { return Ok(StatusCode::FORBIDDEN); diff --git a/agdb_server/src/routes/db/user.rs b/agdb_server/src/routes/db/user.rs index 8ebe16162..4b06c9b64 100644 --- a/agdb_server/src/routes/db/user.rs +++ b/agdb_server/src/routes/db/user.rs @@ -98,12 +98,12 @@ pub(crate) async fn list( State(db_pool): State, request: Query, ) -> ServerResponse<(StatusCode, Json>)> { - let db = db_pool.find_user_db(user.0, &request.name)?; + let db = db_pool.find_user_db(user.0, &request.db)?; let users = db_pool .db_users(db.db_id.unwrap())? .into_iter() .map(|(name, role)| DbUser { - database: request.name.clone(), + database: request.db.clone(), user: name, role: role.into(), }) diff --git a/agdb_server/tests/integration.rs b/agdb_server/tests/integration.rs index e9641d104..3d5cf76c8 100644 --- a/agdb_server/tests/integration.rs +++ b/agdb_server/tests/integration.rs @@ -18,15 +18,16 @@ use std::time::Duration; use tokio::sync::Mutex; pub const USER_CHANGE_PASSWORD_URI: &str = "/user/change_password"; -pub const ADMIN_USER_CREATE_URI: &str = "/admin/user/create"; pub const DB_ADD_URI: &str = "/db/add"; pub const DB_USER_ADD_URI: &str = "/db/user/add"; pub const DB_USER_LIST_URI: &str = "/db/user/list"; pub const DB_USER_REMOVE_URI: &str = "/db/user/remove"; pub const DB_DELETE_URI: &str = "/db/delete"; pub const DB_LIST_URI: &str = "/db/list"; +pub const ADMIN_USER_CREATE_URI: &str = "/admin/user/create"; 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_LIST_URI: &str = "/admin/db/user/list"; 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_remove_test.rs b/agdb_server/tests/routes/admin_db_remove_test.rs index 6a2f9d2ff..636296c92 100644 --- a/agdb_server/tests/routes/admin_db_remove_test.rs +++ b/agdb_server/tests/routes/admin_db_remove_test.rs @@ -9,7 +9,7 @@ use serde::Serialize; #[derive(Serialize)] struct DbName { - name: String, + db: String, } #[tokio::test] @@ -23,7 +23,7 @@ async fn remove() -> anyhow::Result<()> { name: db.clone(), db_type: "mapped".to_string(), })); - let rem = DbName { name: db.clone() }; + let rem = DbName { db: db.clone() }; assert_eq!( server .post(ADMIN_DB_REMOVE_URI, &rem, &server.admin_token) @@ -43,7 +43,7 @@ async fn db_not_found() -> anyhow::Result<()> { let server = TestServer::new().await?; let user = server.init_user().await?; let db = DbName { - name: format!("{}/some_db", user.name), + db: format!("{}/some_db", user.name), }; assert_eq!( server @@ -59,7 +59,7 @@ async fn db_not_found() -> anyhow::Result<()> { async fn user_not_found() -> anyhow::Result<()> { let server = TestServer::new().await?; let db = DbName { - name: "missing_user/some_db".to_string(), + db: "missing_user/some_db".to_string(), }; assert_eq!( server @@ -75,7 +75,7 @@ async fn user_not_found() -> anyhow::Result<()> { async fn no_admin_token() -> anyhow::Result<()> { let server = TestServer::new().await?; let db = DbName { - name: "user/some_db".to_string(), + db: "user/some_db".to_string(), }; assert_eq!( server.post(ADMIN_DB_REMOVE_URI, &db, NO_TOKEN).await?.0, diff --git a/agdb_server/tests/routes/admin_db_user_list_test.rs b/agdb_server/tests/routes/admin_db_user_list_test.rs new file mode 100644 index 000000000..42e034d69 --- /dev/null +++ b/agdb_server/tests/routes/admin_db_user_list_test.rs @@ -0,0 +1,66 @@ +use crate::AddUser; +use crate::TestServer; +use crate::ADMIN_DB_USER_LIST_URI; +use crate::DB_USER_ADD_URI; +use crate::NO_TOKEN; +use serde::Deserialize; + +#[derive(Debug, Deserialize, PartialEq, Eq, PartialOrd, Ord)] +struct DbUser { + database: String, + user: String, + role: String, +} + +#[tokio::test] +async fn list_users() -> 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 role = AddUser { + database: &db, + user: &other.name, + role: "read", + }; + assert_eq!( + server.post(DB_USER_ADD_URI, &role, &user.token).await?.0, + 201 + ); + let (_, list) = server + .get::>( + &format!("{ADMIN_DB_USER_LIST_URI}?db={}", &db), + &server.admin_token, + ) + .await?; + let mut list = list?; + list.sort(); + let mut expected = vec![ + DbUser { + database: db.clone(), + user: user.name, + role: "admin".to_string(), + }, + DbUser { + database: db, + user: other.name, + role: "read".to_string(), + }, + ]; + expected.sort(); + assert_eq!(list, expected); + Ok(()) +} + +#[tokio::test] +async fn no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + assert_eq!( + server + .get::>(&format!("{ADMIN_DB_USER_LIST_URI}?db=some_db"), NO_TOKEN) + .await? + .0, + 401 + ); + Ok(()) +} diff --git a/agdb_server/tests/routes/db_delete_test.rs b/agdb_server/tests/routes/db_delete_test.rs index aafc2469f..d0ab2745a 100644 --- a/agdb_server/tests/routes/db_delete_test.rs +++ b/agdb_server/tests/routes/db_delete_test.rs @@ -11,7 +11,7 @@ use std::path::Path; #[derive(Serialize, Deserialize)] struct DeleteDb { - name: String, + db: String, } #[tokio::test] @@ -20,7 +20,7 @@ async fn delete() -> anyhow::Result<()> { let user = server.init_user().await?; let db = server.init_db("mapped", &user).await?; assert!(Path::new(&server.data_dir).join(&db).exists()); - let del = DeleteDb { name: db.clone() }; + let del = DeleteDb { db: db.clone() }; assert_eq!(server.post(DB_DELETE_URI, &del, &user.token).await?.0, 204); let (_, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(list?, vec![]); @@ -33,7 +33,7 @@ async fn db_not_found() -> anyhow::Result<()> { let server = TestServer::new().await?; let user = server.init_user().await?; let del = DeleteDb { - name: format!("{}/delete_db_not_found", user.name), + db: format!("{}/delete_db_not_found", user.name), }; assert_eq!(server.post(DB_DELETE_URI, &del, &user.token).await?.0, 466); Ok(()) @@ -45,7 +45,7 @@ async fn other_user() -> anyhow::Result<()> { let user = server.init_user().await?; let db = server.init_db("mapped", &user).await?; let other = server.init_user().await?; - let del = DeleteDb { name: db.clone() }; + let del = DeleteDb { db: db.clone() }; assert_eq!(server.post(DB_DELETE_URI, &del, &other.token).await?.0, 466); let (_, list) = server.get::>(DB_LIST_URI, &user.token).await?; let expected = vec![Db { @@ -72,7 +72,7 @@ async fn with_read_role() -> anyhow::Result<()> { server.post(DB_USER_ADD_URI, &role, &user.token).await?.0, 201 ); - let del = DeleteDb { name: db.clone() }; + let del = DeleteDb { db: db.clone() }; assert_eq!( server.post(DB_DELETE_URI, &del, &reader.token).await?.0, 403 @@ -102,7 +102,7 @@ async fn with_write_role() -> anyhow::Result<()> { server.post(DB_USER_ADD_URI, &role, &user.token).await?.0, 201 ); - let del = DeleteDb { name: db.clone() }; + let del = DeleteDb { db: db.clone() }; assert_eq!( server.post(DB_DELETE_URI, &del, &writer.token).await?.0, 403 @@ -132,7 +132,7 @@ async fn with_admin_role() -> anyhow::Result<()> { server.post(DB_USER_ADD_URI, &role, &user.token).await?.0, 201 ); - let del = DeleteDb { name: db.clone() }; + let del = DeleteDb { db: db.clone() }; assert_eq!(server.post(DB_DELETE_URI, &del, &admin.token).await?.0, 204); let (_, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(list?, vec![]); @@ -143,9 +143,7 @@ async fn with_admin_role() -> anyhow::Result<()> { #[tokio::test] async fn no_token() -> anyhow::Result<()> { let server = TestServer::new().await?; - let del = DeleteDb { - name: String::new(), - }; + let del = DeleteDb { db: String::new() }; assert_eq!(server.post(DB_DELETE_URI, &del, NO_TOKEN).await?.0, 401); Ok(()) } diff --git a/agdb_server/tests/routes/db_remove_test.rs b/agdb_server/tests/routes/db_remove_test.rs index ff12d78bc..fc123a83d 100644 --- a/agdb_server/tests/routes/db_remove_test.rs +++ b/agdb_server/tests/routes/db_remove_test.rs @@ -11,7 +11,7 @@ use std::path::Path; #[derive(Serialize, Deserialize)] struct RemoveDb { - name: String, + db: String, } #[tokio::test] @@ -20,7 +20,7 @@ async fn remove() -> anyhow::Result<()> { let user = server.init_user().await?; let db = server.init_db("mapped", &user).await?; assert!(Path::new(&server.data_dir).join(&db).exists()); - let del = RemoveDb { name: db.clone() }; + let del = RemoveDb { db: db.clone() }; assert_eq!(server.post(DB_REMOVE_URI, &del, &user.token).await?.0, 204); let (_, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(list?, vec![]); @@ -33,7 +33,7 @@ async fn db_not_found() -> anyhow::Result<()> { let server = TestServer::new().await?; let user = server.init_user().await?; let del = RemoveDb { - name: format!("{}/db_not_found", user.name), + db: format!("{}/db_not_found", user.name), }; assert_eq!(server.post(DB_REMOVE_URI, &del, &user.token).await?.0, 466); Ok(()) @@ -45,7 +45,7 @@ async fn other_user() -> anyhow::Result<()> { let user = server.init_user().await?; let db = server.init_db("mapped", &user).await?; let other = server.init_user().await?; - let del = RemoveDb { name: db.clone() }; + let del = RemoveDb { db: db.clone() }; assert_eq!(server.post(DB_REMOVE_URI, &del, &other.token).await?.0, 466); let (_, list) = server.get::>(DB_LIST_URI, &user.token).await?; let expected = vec![Db { @@ -71,7 +71,7 @@ async fn with_read_role() -> anyhow::Result<()> { server.post(DB_USER_ADD_URI, &role, &user.token).await?.0, 201 ); - let del = RemoveDb { name: db.clone() }; + let del = RemoveDb { db: db.clone() }; assert_eq!( server.post(DB_REMOVE_URI, &del, &reader.token).await?.0, 403 @@ -100,7 +100,7 @@ async fn with_write_role() -> anyhow::Result<()> { server.post(DB_USER_ADD_URI, &role, &user.token).await?.0, 201 ); - let del = RemoveDb { name: db.clone() }; + let del = RemoveDb { db: db.clone() }; assert_eq!( server.post(DB_REMOVE_URI, &del, &writer.token).await?.0, 403 @@ -129,7 +129,7 @@ async fn with_admin_role() -> anyhow::Result<()> { server.post(DB_USER_ADD_URI, &role, &user.token).await?.0, 201 ); - let del = RemoveDb { name: db.clone() }; + let del = RemoveDb { db: db.clone() }; assert_eq!(server.post(DB_REMOVE_URI, &del, &admin.token).await?.0, 204); let (_, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(list?, vec![]); @@ -140,9 +140,7 @@ async fn with_admin_role() -> anyhow::Result<()> { #[tokio::test] async fn no_token() -> anyhow::Result<()> { let server = TestServer::new().await?; - let del = RemoveDb { - name: String::new(), - }; + let del = RemoveDb { db: String::new() }; assert_eq!(server.post(DB_REMOVE_URI, &del, NO_TOKEN).await?.0, 401); Ok(()) } diff --git a/agdb_server/tests/routes/db_user_list.rs b/agdb_server/tests/routes/db_user_list.rs index 3d6f8760f..43450c587 100644 --- a/agdb_server/tests/routes/db_user_list.rs +++ b/agdb_server/tests/routes/db_user_list.rs @@ -12,23 +12,6 @@ struct DbUser { role: String, } -#[tokio::test] -async fn list_user() -> anyhow::Result<()> { - let server = TestServer::new().await?; - let user = server.init_user().await?; - let db = server.init_db("memory", &user).await?; - let (_, list) = server - .get::>(&format!("{DB_USER_LIST_URI}?name={}", &db), &user.token) - .await?; - let expected = vec![DbUser { - database: db, - user: user.name, - role: "admin".to_string(), - }]; - assert_eq!(list?, expected); - Ok(()) -} - #[tokio::test] async fn list_users() -> anyhow::Result<()> { let server = TestServer::new().await?; @@ -45,7 +28,7 @@ async fn list_users() -> anyhow::Result<()> { 201 ); let (_, list) = server - .get::>(&format!("{DB_USER_LIST_URI}?name={}", &db), &other.token) + .get::>(&format!("{DB_USER_LIST_URI}?db={}", &db), &other.token) .await?; let mut list = list?; list.sort(); @@ -70,7 +53,7 @@ async fn no_token() -> anyhow::Result<()> { let server = TestServer::new().await?; assert_eq!( server - .get::>(&format!("{DB_USER_LIST_URI}?name=some_db"), NO_TOKEN) + .get::>(&format!("{DB_USER_LIST_URI}?db=some_db"), NO_TOKEN) .await? .0, 401 diff --git a/agdb_server/tests/routes/mod.rs b/agdb_server/tests/routes/mod.rs index 1fb624ba7..df75a0dc5 100644 --- a/agdb_server/tests/routes/mod.rs +++ b/agdb_server/tests/routes/mod.rs @@ -1,5 +1,6 @@ mod admin_db_list_test; mod admin_db_remove_test; +mod admin_db_user_list_test; mod admin_user_change_password_test; mod admin_user_create_test; mod admin_user_list_test;