From 252bab0a31616916f253d2547f729a44260f58ca Mon Sep 17 00:00:00 2001 From: Michael Vlach Date: Sun, 24 Dec 2023 16:10:15 +0100 Subject: [PATCH] Add admin API endpoint for copying database --- agdb_server/openapi/schema.json | 58 +++++ agdb_server/src/api.rs | 1 + agdb_server/src/app.rs | 4 + agdb_server/src/db_pool.rs | 20 +- agdb_server/src/routes/admin/db.rs | 29 +++ agdb_server/src/routes/db.rs | 2 +- .../tests/routes/admin_db_copy_test.rs | 236 ++++++++++++++++++ agdb_server/tests/routes/db_copy_test.rs | 3 +- agdb_server/tests/routes/mod.rs | 1 + 9 files changed, 345 insertions(+), 9 deletions(-) create mode 100644 agdb_server/tests/routes/admin_db_copy_test.rs diff --git a/agdb_server/openapi/schema.json b/agdb_server/openapi/schema.json index d65e59a68..0623d14c9 100644 --- a/agdb_server/openapi/schema.json +++ b/agdb_server/openapi/schema.json @@ -142,6 +142,64 @@ ] } }, + "/api/v1/admin/db/{owner}/{db}/copy": { + "post": { + "tags": [ + "crate::routes::admin::db" + ], + "operationId": "copy", + "parameters": [ + { + "name": "owner", + "in": "path", + "description": "db owner user name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "db", + "in": "path", + "description": "db name", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "new_name", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "201": { + "description": "db copied" + }, + "401": { + "description": "unauthorized" + }, + "404": { + "description": "user / db not found" + }, + "465": { + "description": "target db exists" + }, + "467": { + "description": "invalid db" + } + }, + "security": [ + { + "Token": [] + } + ] + } + }, "/api/v1/admin/db/{owner}/{db}/delete": { "delete": { "tags": [ diff --git a/agdb_server/src/api.rs b/agdb_server/src/api.rs index 19d680faa..4ed2cb4e7 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::add, crate::routes::admin::db::backup, + crate::routes::admin::db::copy, crate::routes::admin::db::delete, crate::routes::admin::db::exec, crate::routes::admin::db::list, diff --git a/agdb_server/src/app.rs b/agdb_server/src/app.rs index 2ea99152c..52876a642 100644 --- a/agdb_server/src/app.rs +++ b/agdb_server/src/app.rs @@ -40,6 +40,10 @@ pub(crate) fn app(config: Config, shutdown_sender: Sender<()>, db_pool: DbPool) "/admin/db/:user/:db/backup", routing::post(routes::admin::db::backup), ) + .route( + "/admin/db/:user/:db/copy", + routing::post(routes::admin::db::copy), + ) .route( "/admin/db/:user/:db/delete", routing::delete(routes::admin::db::delete), diff --git a/agdb_server/src/db_pool.rs b/agdb_server/src/db_pool.rs index eaffa16c5..155afcc8c 100644 --- a/agdb_server/src/db_pool.rs +++ b/agdb_server/src/db_pool.rs @@ -313,18 +313,24 @@ impl DbPool { owner: &str, db: &str, new_name: &str, - user: DbId, + mut user: DbId, config: &Config, + admin: bool, ) -> ServerResult { let (new_owner, new_db) = new_name.split_once('/').ok_or(ErrorCode::DbInvalid)?; - let username = self.user_name(user)?; - - if new_owner != username { - return Err(permission_denied("cannot copy db to another user")); - } - let source_db = db_name(owner, db); let database = self.find_user_db(user, &source_db)?; + + if admin { + user = self.find_user_id(new_owner)?; + } else { + let username = self.user_name(user)?; + + if new_owner != username { + return Err(permission_denied("cannot copy db to another user")); + } + }; + let target_name = db_name(new_owner, new_db); let target_file = db_file(new_owner, new_db, config); diff --git a/agdb_server/src/routes/admin/db.rs b/agdb_server/src/routes/admin/db.rs index a79b842c6..49c231e89 100644 --- a/agdb_server/src/routes/admin/db.rs +++ b/agdb_server/src/routes/admin/db.rs @@ -68,6 +68,35 @@ pub(crate) async fn backup( Ok(StatusCode::CREATED) } +#[utoipa::path(post, + path = "/api/v1/admin/db/{owner}/{db}/copy", + security(("Token" = [])), + params( + ("owner" = String, Path, description = "db owner user name"), + ("db" = String, Path, description = "db name"), + ServerDatabaseRename + ), + responses( + (status = 201, description = "db copied"), + (status = 401, description = "unauthorized"), + (status = 404, description = "user / db not found"), + (status = 465, description = "target db exists"), + (status = 467, description = "invalid db"), + ) +)] +pub(crate) async fn copy( + _admin: AdminId, + State(db_pool): State, + State(config): State, + Path((owner, db)): Path<(String, String)>, + request: Query, +) -> ServerResponse { + let owner_id = db_pool.find_user_id(&owner)?; + db_pool.copy_db(&owner, &db, &request.new_name, owner_id, &config, true)?; + + Ok(StatusCode::CREATED) +} + #[utoipa::path(delete, path = "/api/v1/admin/db/{owner}/{db}/delete", security(("Token" = [])), diff --git a/agdb_server/src/routes/db.rs b/agdb_server/src/routes/db.rs index 237582071..ba762c53c 100644 --- a/agdb_server/src/routes/db.rs +++ b/agdb_server/src/routes/db.rs @@ -174,7 +174,7 @@ pub(crate) async fn copy( Path((owner, db)): Path<(String, String)>, request: Query, ) -> ServerResponse { - db_pool.copy_db(&owner, &db, &request.new_name, user.0, &config)?; + db_pool.copy_db(&owner, &db, &request.new_name, user.0, &config, false)?; Ok(StatusCode::CREATED) } diff --git a/agdb_server/tests/routes/admin_db_copy_test.rs b/agdb_server/tests/routes/admin_db_copy_test.rs new file mode 100644 index 000000000..ef68b9cb5 --- /dev/null +++ b/agdb_server/tests/routes/admin_db_copy_test.rs @@ -0,0 +1,236 @@ +use crate::TestServer; +use crate::NO_TOKEN; +use agdb::DbElement; +use agdb::DbId; +use agdb::QueryBuilder; +use agdb::QueryResult; +use agdb::QueryType; +use std::path::Path; + +#[tokio::test] +async fn copy() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("mapped", &user).await?; + let queries: Option> = Some(vec![QueryBuilder::insert() + .nodes() + .aliases(vec!["root"]) + .query() + .into()]); + server + .post(&format!("/db/{db}/exec"), &queries, &user.token) + .await?; + let status = server + .post::<()>( + &format!("/admin/db/{db}/copy?new_name={}/copy", user.name), + &None, + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 201); + assert!(Path::new(&server.data_dir) + .join(&user.name) + .join("copy") + .exists()); + let queries: Option> = + Some(vec![QueryBuilder::select().ids("root").query().into()]); + let responses = server + .post( + &format!("/db/{}/copy/exec", user.name), + &queries, + &user.token, + ) + .await? + .1; + let responses: Vec = serde_json::from_str(&responses)?; + assert_eq!(responses.len(), 1); + assert_eq!(responses[0].result, 1); + assert_eq!( + responses[0].elements, + vec![DbElement { + id: DbId(1), + from: None, + to: None, + values: vec![] + }] + ); + + Ok(()) +} + +#[tokio::test] +async fn copy_other() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let other = server.init_user().await?; + let db = server.init_db("mapped", &user).await?; + let queries: Option> = Some(vec![QueryBuilder::insert() + .nodes() + .aliases(vec!["root"]) + .query() + .into()]); + server + .post(&format!("/db/{db}/exec"), &queries, &user.token) + .await?; + let status = server + .post::<()>( + &format!("/admin/db/{db}/copy?new_name={}/copy_other", other.name), + &None, + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 201); + assert!(Path::new(&server.data_dir) + .join(&other.name) + .join("copy_other") + .exists()); + let queries: Option> = + Some(vec![QueryBuilder::select().ids("root").query().into()]); + let responses = server + .post( + &format!("/db/{}/copy_other/exec", other.name), + &queries, + &other.token, + ) + .await? + .1; + let responses: Vec = serde_json::from_str(&responses)?; + assert_eq!(responses.len(), 1); + assert_eq!(responses[0].result, 1); + assert_eq!( + responses[0].elements, + vec![DbElement { + id: DbId(1), + from: None, + to: None, + values: vec![] + }] + ); + + Ok(()) +} + +#[tokio::test] +async fn copy_target_exists() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("mapped", &user).await?; + let status = server + .post::<()>( + &format!("/admin/db/{db}/copy?new_name={db}"), + &None, + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 465); + + Ok(()) +} + +#[tokio::test] +async fn target_self() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("mapped", &user).await?; + let status = server + .post::<()>( + &format!("/admin/db/{db}/copy?new_name={db}"), + &None, + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 465); + Ok(()) +} + +#[tokio::test] +async fn target_exists() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("mapped", &user).await?; + let db2 = server.init_db("mapped", &user).await?; + let status = server + .post::<()>( + &format!("/admin/db/{db}/copy?new_name={db2}"), + &None, + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 465); + Ok(()) +} + +#[tokio::test] +async fn invalid() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("mapped", &user).await?; + let status = server + .post::<()>( + &format!("/admin/db/{db}/copy?new_name={}/a\0a", user.name), + &None, + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 467); + Ok(()) +} + +#[tokio::test] +async fn db_not_found() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let status = server + .post::<()>( + &format!( + "/admin/db/{}/not_found/copy?new_name={}/not_found", + user.name, user.name + ), + &None, + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 404); + + Ok(()) +} + +#[tokio::test] +async fn non_admin() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let status = server + .post::<()>( + "/admin/db/user/not_found/copy?new_name=user/not_found", + &None, + &user.token, + ) + .await? + .0; + assert_eq!(status, 401); + + Ok(()) +} + +#[tokio::test] +async fn no_token() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let status = server + .post::<()>( + "/admin/db/user/not_found/copy?new_name=user/not_found", + &None, + NO_TOKEN, + ) + .await? + .0; + assert_eq!(status, 401); + + Ok(()) +} diff --git a/agdb_server/tests/routes/db_copy_test.rs b/agdb_server/tests/routes/db_copy_test.rs index bdcac8d8c..08719d6f0 100644 --- a/agdb_server/tests/routes/db_copy_test.rs +++ b/agdb_server/tests/routes/db_copy_test.rs @@ -126,10 +126,11 @@ async fn copy_other() -> anyhow::Result<()> { async fn copy_to_other_user() -> anyhow::Result<()> { let server = TestServer::new().await?; let user = server.init_user().await?; + let other = server.init_user().await?; let db = server.init_db("mapped", &user).await?; let status = server .post::<()>( - &format!("/db/{db}/copy?new_name=other/new_name"), + &format!("/db/{db}/copy?new_name={}/new_name", other.name), &None, &user.token, ) diff --git a/agdb_server/tests/routes/mod.rs b/agdb_server/tests/routes/mod.rs index e013427da..b90f80f67 100644 --- a/agdb_server/tests/routes/mod.rs +++ b/agdb_server/tests/routes/mod.rs @@ -1,5 +1,6 @@ mod admin_db_add_test; mod admin_db_backup_restore_test; +mod admin_db_copy_test; mod admin_db_delete_test; mod admin_db_exec_test; mod admin_db_list_test;