Skip to content

Commit

Permalink
[server] Add db rename #871 (#888)
Browse files Browse the repository at this point in the history
add db rename
  • Loading branch information
michaelvlach authored Dec 18, 2023
1 parent 7ac7f21 commit adcf692
Show file tree
Hide file tree
Showing 8 changed files with 256 additions and 2 deletions.
55 changes: 55 additions & 0 deletions agdb_server/openapi/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,46 @@
]
}
},
"/api/v1/db/rename": {
"post": {
"tags": [
"crate::routes::db"
],
"operationId": "rename",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ServerDatabaseRename"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "db renamed"
},
"401": {
"description": "unauthorized"
},
"403": {
"description": "user must be a db admin"
},
"466": {
"description": "db not found"
},
"467": {
"description": "invalid db"
}
},
"security": [
{
"Token": []
}
]
}
},
"/api/v1/db/user/add": {
"post": {
"tags": [
Expand Down Expand Up @@ -894,6 +934,21 @@
}
}
},
"ServerDatabaseRename": {
"type": "object",
"required": [
"db",
"new_name"
],
"properties": {
"db": {
"type": "string"
},
"new_name": {
"type": "string"
}
}
},
"ServerDatabaseWithRole": {
"type": "object",
"required": [
Expand Down
2 changes: 2 additions & 0 deletions agdb_server/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use utoipa::OpenApi;
crate::routes::db::exec,
crate::routes::db::list,
crate::routes::db::remove,
crate::routes::db::rename,
crate::routes::db::user::add,
crate::routes::db::user::list,
crate::routes::db::user::remove,
Expand All @@ -38,6 +39,7 @@ use utoipa::OpenApi;
crate::routes::db::ServerDatabase,
crate::routes::db::ServerDatabaseWithRole,
crate::routes::db::ServerDatabaseName,
crate::routes::db::ServerDatabaseRename,
crate::routes::db::user::DbUser,
crate::routes::db::user::RemoveDbUser,
crate::routes::db::user::DbUserRole,
Expand Down
1 change: 1 addition & 0 deletions agdb_server/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ pub(crate) fn app(config: Config, shutdown_sender: Sender<()>, db_pool: DbPool)
.route("/exec", routing::post(routes::db::exec))
.route("/list", routing::get(routes::db::list))
.route("/remove", routing::post(routes::db::remove))
.route("/rename", routing::post(routes::db::rename))
.nest("/user", db_user_router_v1);

Router::new()
Expand Down
25 changes: 25 additions & 0 deletions agdb_server/src/db_pool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -572,6 +572,31 @@ impl DbPool {
Ok(self.get_pool_mut()?.remove(&db.name).unwrap())
}

pub(crate) fn rename_db(
&self,
mut db: Database,
new_name: &str,
config: &Config,
) -> ServerResult {
let mut pool = self.get_pool_mut()?;
let server_db = pool.remove(&db.name).unwrap();
server_db
.get_mut()?
.rename(
Path::new(&config.data_dir)
.join(new_name)
.to_string_lossy()
.as_ref(),
)
.map_err(|_| ErrorCode::DbInvalid)?;
pool.insert(new_name.to_string(), server_db);
db.name = new_name.to_string();
self.db_mut()?
.exec_mut(&QueryBuilder::insert().element(&db).query())?;

Ok(())
}

pub(crate) fn save_token(&self, user: DbId, token: &str) -> ServerResult {
self.db_mut()?.exec_mut(
&QueryBuilder::insert()
Expand Down
44 changes: 44 additions & 0 deletions agdb_server/src/routes/db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ pub(crate) struct ServerDatabaseName {
pub(crate) db: String,
}

#[derive(Deserialize, ToSchema)]
pub(crate) struct ServerDatabaseRename {
pub(crate) db: String,
pub(crate) new_name: String,
}

#[derive(Deserialize, ToSchema)]
pub(crate) struct Queries(pub(crate) Vec<QueryType>);

Expand Down Expand Up @@ -265,6 +271,44 @@ pub(crate) async fn remove(
Ok(StatusCode::NO_CONTENT)
}

#[utoipa::path(post,
path = "/api/v1/db/rename",
request_body = ServerDatabaseRename,
security(("Token" = [])),
responses(
(status = 204, description = "db renamed"),
(status = 401, description = "unauthorized"),
(status = 403, description = "user must be a db admin"),
(status = 466, description = "db not found"),
(status = 467, description = "invalid db"),
)
)]
pub(crate) async fn rename(
user: UserId,
State(db_pool): State<DbPool>,
State(config): State<Config>,
Json(request): Json<ServerDatabaseRename>,
) -> ServerResponse {
let (db_user, _db) = request.db.split_once('/').ok_or(ErrorCode::DbInvalid)?;
let (new_db_user, _db) = request
.new_name
.split_once('/')
.ok_or(ErrorCode::DbInvalid)?;
let db = db_pool.find_user_db(user.0, &request.db)?;

if db_user != new_db_user {
return Err(ErrorCode::DbInvalid.into());
}

if !db_pool.is_db_admin(user.0, db.db_id.unwrap())? {
return Ok(StatusCode::FORBIDDEN);
}

db_pool.rename_db(db, &request.new_name, &config)?;

Ok(StatusCode::NO_CONTENT)
}

pub(crate) fn required_role(queries: &Queries) -> DbUserRole {
for q in &queries.0 {
match q {
Expand Down
5 changes: 3 additions & 2 deletions agdb_server/tests/integration.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,13 @@ use tokio::sync::Mutex;

pub const USER_CHANGE_PASSWORD_URI: &str = "/user/change_password";
pub const DB_ADD_URI: &str = "/db/add";
pub const DB_DELETE_URI: &str = "/db/delete";
pub const DB_EXEC_URI: &str = "/db/exec";
pub const DB_LIST_URI: &str = "/db/list";
pub const DB_RENAME_URI: &str = "/db/rename";
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_ADD_URI: &str = "/admin/db/add";
pub const ADMIN_DB_LIST_URI: &str = "/admin/db/list";
Expand Down
125 changes: 125 additions & 0 deletions agdb_server/tests/routes/db_rename_test.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
use std::path::Path;

use crate::AddUser;
use crate::DbWithRole;
use crate::TestServer;
use crate::DB_LIST_URI;
use crate::DB_RENAME_URI;
use crate::DB_USER_ADD_URI;
use crate::NO_TOKEN;
use serde::Serialize;

#[derive(Serialize)]
struct DbRename {
db: String,
new_name: String,
}

#[tokio::test]
async fn rename() -> anyhow::Result<()> {
let server = TestServer::new().await?;
let user = server.init_user().await?;
let db = server.init_db("mapped", &user).await?;
let json = DbRename {
db: db.clone(),
new_name: format!("{}/renamed_db", &user.name),
};
let (status, _response) = server.post(DB_RENAME_URI, &json, &user.token).await?;
assert_eq!(status, 204);
let (status, list) = server
.get::<Vec<DbWithRole>>(DB_LIST_URI, &user.token)
.await?;
assert_eq!(status, 200);
assert_eq!(
list?,
vec![DbWithRole {
name: json.new_name.clone(),
db_type: "mapped".to_string(),
role: "admin".to_string(),
}]
);
assert!(!Path::new(&server.data_dir).join(db).exists());
assert!(Path::new(&server.data_dir).join(json.new_name).exists());
Ok(())
}

#[tokio::test]
async fn rename_non_admin() -> 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 role = AddUser {
database: &db,
user: &other.name,
role: "read",
};
let json = DbRename {
db: db.clone(),
new_name: format!("{}/renamed_db", &user.name),
};
server.post(DB_USER_ADD_URI, &role, &user.token).await?;
let (status, _response) = server.post(DB_RENAME_URI, &json, &other.token).await?;
assert_eq!(status, 403);
Ok(())
}

#[tokio::test]
async fn rename_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 role = AddUser {
database: &db,
user: &other.name,
role: "admin",
};
let json = DbRename {
db: db.clone(),
new_name: format!("{}/renamed_db", &other.name),
};
server.post(DB_USER_ADD_URI, &role, &user.token).await?;
let (status, _response) = server.post(DB_RENAME_URI, &json, &other.token).await?;
assert_eq!(status, 467);
Ok(())
}

#[tokio::test]
async fn invalid_new_name() -> anyhow::Result<()> {
let server = TestServer::new().await?;
let user = server.init_user().await?;
let db = server.init_db("mapped", &user).await?;
let json = DbRename {
db: db.clone(),
new_name: format!("{}/\0??", &user.name),
};
let (status, _response) = server.post(DB_RENAME_URI, &json, &user.token).await?;
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 json = DbRename {
db: format!("{}/missing_db", &user.name),
new_name: format!("{}/renamed_db", &user.name),
};
let (status, _response) = server.post(DB_RENAME_URI, &json, &user.token).await?;
assert_eq!(status, 466);
Ok(())
}

#[tokio::test]
async fn no_token() -> anyhow::Result<()> {
let server = TestServer::new().await?;
let json = DbRename {
db: String::new(),
new_name: String::new(),
};
let (status, _response) = server.post(DB_RENAME_URI, &json, NO_TOKEN).await?;
assert_eq!(status, 401);
Ok(())
}
1 change: 1 addition & 0 deletions agdb_server/tests/routes/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ mod db_delete_test;
mod db_exec_test;
mod db_list_test;
mod db_remove_test;
mod db_rename_test;
mod db_user_add_test;
mod db_user_list;
mod db_user_remove_test;
Expand Down

0 comments on commit adcf692

Please sign in to comment.