diff --git a/agdb_server/openapi/schema.json b/agdb_server/openapi/schema.json index 73412b2eb..ed3cc88bf 100644 --- a/agdb_server/openapi/schema.json +++ b/agdb_server/openapi/schema.json @@ -23,7 +23,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ServerDatabaseSize" + "$ref": "#/components/schemas/ServerDatabase" } } } @@ -295,6 +295,61 @@ ] } }, + "/api/v1/admin/db/{owner}/{db}/rename": { + "post": { + "tags": [ + "crate::routes::admin::db" + ], + "operationId": "rename", + "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 renamed" + }, + "401": { + "description": "unauthorized" + }, + "404": { + "description": "user / db not found" + }, + "467": { + "description": "invalid db" + } + }, + "security": [ + { + "Token": [] + } + ] + } + }, "/api/v1/admin/db/{owner}/{db}/user/list": { "get": { "tags": [ @@ -624,7 +679,7 @@ "schema": { "type": "array", "items": { - "$ref": "#/components/schemas/ServerDatabaseWithRole" + "$ref": "#/components/schemas/ServerDatabase" } } } @@ -889,7 +944,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServerDatabaseSize" + "$ref": "#/components/schemas/ServerDatabase" } } } @@ -990,7 +1045,7 @@ } ], "responses": { - "204": { + "201": { "description": "db renamed" }, "401": { @@ -1415,22 +1470,12 @@ "$ref": "#/components/schemas/QueryResult" } }, - "ServerDatabaseRename": { - "type": "object", - "required": [ - "new_name" - ], - "properties": { - "new_name": { - "type": "string" - } - } - }, - "ServerDatabaseSize": { + "ServerDatabase": { "type": "object", "required": [ "name", "db_type", + "role", "size", "backup" ], @@ -1446,6 +1491,9 @@ "name": { "type": "string" }, + "role": { + "$ref": "#/components/schemas/DbUserRole" + }, "size": { "type": "integer", "format": "int64", @@ -1453,34 +1501,14 @@ } } }, - "ServerDatabaseWithRole": { + "ServerDatabaseRename": { "type": "object", "required": [ - "name", - "db_type", - "role", - "size", - "backup" + "new_name" ], "properties": { - "backup": { - "type": "integer", - "format": "int64", - "minimum": 0 - }, - "db_type": { - "$ref": "#/components/schemas/DbType" - }, - "name": { + "new_name": { "type": "string" - }, - "role": { - "$ref": "#/components/schemas/DbUserRole" - }, - "size": { - "type": "integer", - "format": "int64", - "minimum": 0 } } }, diff --git a/agdb_server/src/api.rs b/agdb_server/src/api.rs index 759277eda..784df300f 100644 --- a/agdb_server/src/api.rs +++ b/agdb_server/src/api.rs @@ -13,6 +13,7 @@ use utoipa::OpenApi; crate::routes::admin::db::exec, crate::routes::admin::db::list, crate::routes::admin::db::optimize, + crate::routes::admin::db::rename, crate::routes::admin::db::remove, crate::routes::admin::db::user::add, crate::routes::admin::db::user::list, @@ -42,8 +43,7 @@ use utoipa::OpenApi; crate::routes::db::DbTypeParam, crate::routes::db::Queries, crate::routes::db::QueriesResults, - crate::routes::db::ServerDatabaseSize, - crate::routes::db::ServerDatabaseWithRole, + crate::routes::db::ServerDatabase, crate::routes::db::ServerDatabaseRename, crate::routes::db::user::DbUser, crate::routes::db::user::DbUserRole, diff --git a/agdb_server/src/app.rs b/agdb_server/src/app.rs index aa590eff4..6167719e7 100644 --- a/agdb_server/src/app.rs +++ b/agdb_server/src/app.rs @@ -52,6 +52,10 @@ pub(crate) fn app(config: Config, shutdown_sender: Sender<()>, db_pool: DbPool) "/admin/db/:user/:db/remove", routing::delete(routes::admin::db::remove), ) + .route( + "/admin/db/:user/:db/rename", + routing::post(routes::admin::db::rename), + ) .route( "/admin/db/:user/:db/user/list", routing::get(routes::admin::db::user::list), diff --git a/agdb_server/src/db_pool.rs b/agdb_server/src/db_pool.rs index b24000c4c..b08ed9ebc 100644 --- a/agdb_server/src/db_pool.rs +++ b/agdb_server/src/db_pool.rs @@ -1,11 +1,16 @@ -pub(crate) mod server_db; -pub(crate) mod server_db_storage; +mod server_db; +mod server_db_storage; use crate::config::Config; +use crate::db_pool::server_db_storage::ServerDbStorage; use crate::error_code::ErrorCode; +use crate::password; use crate::password::Password; +use crate::routes::db::user::DbUser; use crate::routes::db::user::DbUserRole; use crate::routes::db::DbType; +use crate::routes::db::Queries; +use crate::routes::db::ServerDatabase; use crate::server_error::ServerError; use crate::server_error::ServerResult; use agdb::Comparison; @@ -14,9 +19,13 @@ use agdb::DbId; use agdb::DbUserValue; use agdb::DbValue; use agdb::QueryBuilder; +use agdb::QueryError; use agdb::QueryId; use agdb::QueryResult; +use agdb::QueryType; use agdb::SearchQuery; +use agdb::Transaction; +use agdb::TransactionMut; use agdb::UserValue; use axum::http::StatusCode; use server_db::ServerDb; @@ -28,6 +37,8 @@ use std::sync::Arc; use std::sync::RwLock; use std::sync::RwLockReadGuard; use std::sync::RwLockWriteGuard; +use std::time::SystemTime; +use std::time::UNIX_EPOCH; const SERVER_DB_NAME: &str = "mapped:agdb_server.agdb"; @@ -40,8 +51,8 @@ pub(crate) struct ServerUser { pub(crate) token: String, } -#[derive(Default, UserValue)] -pub(crate) struct Database { +#[derive(UserValue)] +struct Database { pub(crate) db_id: Option, pub(crate) name: String, pub(crate) db_type: DbType, @@ -123,40 +134,89 @@ impl DbPool { Ok(db_pool) } - pub(crate) fn add_db(&self, user: DbId, database: Database, config: &Config) -> ServerResult { - let db_path = Path::new(&config.data_dir).join(&database.name); - let user_dir = db_path.parent().ok_or(ErrorCode::DbInvalid)?; - std::fs::create_dir_all(user_dir)?; + pub(crate) fn add_db( + &self, + owner: &str, + db: &str, + db_type: DbType, + config: &Config, + ) -> ServerResult { + let owner_id = self.find_user_id(owner)?; + let db_name = db_name(owner, db); + + if self.find_user_db(owner_id, &db_name).is_ok() { + return Err(ErrorCode::DbExists.into()); + } + + let db_path = Path::new(&config.data_dir).join(&db_name); + std::fs::create_dir_all(Path::new(&config.data_dir).join(owner))?; let path = db_path.to_str().ok_or(ErrorCode::DbInvalid)?.to_string(); - let db = ServerDb::new(&format!("{}:{}", database.db_type, path)).map_err(|mut e| { + let server_db = ServerDb::new(&format!("{}:{}", db_type, path)).map_err(|mut e| { e.status = ErrorCode::DbInvalid.into(); e.description = format!("{}: {}", ErrorCode::DbInvalid.as_str(), e.description); e })?; - self.get_pool_mut()?.insert(database.name.clone(), db); + let backup = if db_backup_file(owner, db, config).exists() { + SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() + } else { + 0 + }; + + self.get_pool_mut()?.insert(db_name.clone(), server_db); self.db_mut()?.transaction_mut(|t| { - let db = t.exec_mut(&QueryBuilder::insert().nodes().values(&database).query())?; + let db = t.exec_mut( + &QueryBuilder::insert() + .nodes() + .values(&Database { + db_id: None, + name: db_name.clone(), + db_type, + backup, + }) + .query(), + )?; t.exec_mut( &QueryBuilder::insert() .edges() - .from(vec![QueryId::from(user), "dbs".into()]) + .from(vec![QueryId::from(owner_id), "dbs".into()]) .to(db) .values(vec![vec![("role", DbUserRole::Admin).into()], vec![]]) .query(), ) })?; + Ok(()) } - pub(crate) fn add_db_user(&self, db: DbId, user: DbId, role: DbUserRole) -> ServerResult { + pub(crate) fn add_db_user( + &self, + owner: &str, + db: &str, + username: &str, + role: DbUserRole, + user: DbId, + ) -> ServerResult { + if owner == username { + return Err(permission_denied("cannot change role of db owner")); + } + + let db_name = db_name(owner, db); + let db_id = self.find_user_db_id(user, &db_name)?; + + if !self.is_db_admin(user, db_id)? { + return Err(permission_denied("admin only")); + } + + let user_id = self.find_user_id(username)?; + self.db_mut()?.transaction_mut(|t| { let existing_role = t.exec( &QueryBuilder::search() - .from(user) - .to(db) + .from(user_id) + .to(db_id) .limit(1) .where_() .keys(vec!["role".into()]) @@ -174,8 +234,8 @@ impl DbPool { t.exec_mut( &QueryBuilder::insert() .edges() - .from(user) - .to(db) + .from(user_id) + .to(db_id) .values_uniform(vec![("role", role).into()]) .query(), )?; @@ -185,7 +245,7 @@ impl DbPool { }) } - pub(crate) fn create_user(&self, user: ServerUser) -> ServerResult { + pub(crate) fn add_user(&self, user: ServerUser) -> ServerResult { self.db_mut()?.transaction_mut(|t| { let user = t.exec_mut(&QueryBuilder::insert().nodes().values(&user).query())?; @@ -200,94 +260,160 @@ impl DbPool { Ok(()) } - pub(crate) fn delete_db(&self, db: Database, config: &Config) -> ServerResult { - let path = Path::new(&config.data_dir).join(&db.name); - let backup_file = db_backup_file(config, &db.name); - self.remove_db(db)?; - - if path.exists() { - let main_file_name = path - .file_name() - .ok_or(ErrorCode::DbInvalid)? - .to_string_lossy(); - std::fs::remove_file(&path)?; - let dot_file = path - .parent() - .ok_or(ErrorCode::DbInvalid)? - .join(format!(".{main_file_name}")); - std::fs::remove_file(dot_file)?; - - if backup_file.exists() { - std::fs::remove_file(backup_file)?; - } + pub(crate) fn backup_db( + &self, + owner: &str, + db: &str, + user: DbId, + config: &Config, + ) -> ServerResult { + let db_name = db_name(owner, db); + let mut database = self.find_user_db(user, &db_name)?; + + if !self.is_db_admin(user, database.db_id.unwrap())? { + return Err(permission_denied("admin only")); + } + + if database.db_type == DbType::Memory { + return Err(permission_denied("memory db cannot have backup")); + } + + let backup_path = db_backup_file(owner, db, config); + + if backup_path.exists() { + std::fs::remove_file(&backup_path)?; + } else { + std::fs::create_dir_all(db_backup_dir(owner, config))?; } + self.get_pool()? + .get(&db_name) + .ok_or(db_not_found(&db_name))? + .get_mut()? + .backup(backup_path.to_string_lossy().as_ref())?; + + database.backup = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + self.save_db(database)?; + Ok(()) } - pub(crate) fn find_dbs(&self) -> ServerResult> { - Ok(self - .db()? - .exec( - &QueryBuilder::select() - .ids( - QueryBuilder::search() - .from("dbs") - .where_() - .distance(CountComparison::Equal(2)) - .query(), - ) - .query(), - )? - .try_into()?) + pub(crate) fn change_password(&self, mut user: ServerUser, new_password: &str) -> ServerResult { + password::validate_password(new_password)?; + let pswd = Password::create(&user.name, new_password); + user.password = pswd.password.to_vec(); + user.salt = pswd.user_salt.to_vec(); + self.save_user(user)?; + + Ok(()) } - pub(crate) fn find_db(&self, db: &str) -> ServerResult { - Ok(self - .0 - .server_db - .get()? + pub(crate) fn delete_db( + &self, + owner: &str, + db: &str, + user: DbId, + config: &Config, + ) -> ServerResult { + self.remove_db(owner, db, user)?; + + let main_file = db_file(owner, db, config); + if main_file.exists() { + std::fs::remove_file(&main_file)?; + } + + let wal_file = db_file(owner, &format!(".{db}"), config); + if wal_file.exists() { + std::fs::remove_file(wal_file)?; + } + + let backup_file = db_backup_file(owner, db, config); + if backup_file.exists() { + std::fs::remove_file(backup_file)?; + } + + Ok(()) + } + + pub(crate) fn exec( + &self, + owner: &str, + db: &str, + user: DbId, + queries: &Queries, + ) -> ServerResult> { + let db_name = db_name(owner, db); + let role = self.find_user_db_role(user, &db_name)?; + let required_role = required_role(queries); + + if required_role == DbUserRole::Write && role == DbUserRole::Read { + return Err(permission_denied("write rights required")); + } + + let pool = self.get_pool()?; + let db = pool.get(&db_name).ok_or(db_not_found(&db_name))?; + + let results = if required_role == DbUserRole::Read { + db.get()?.transaction(|t| { + let mut results = vec![]; + + for q in &queries.0 { + results.push(t_exec(t, q)?); + } + + Ok(results) + }) + } else { + db.get_mut()?.transaction_mut(|t| { + let mut results = vec![]; + + for q in &queries.0 { + results.push(t_exec_mut(t, q)?); + } + + Ok(results) + }) + } + .map_err(|e: QueryError| ServerError { + description: e.to_string(), + status: StatusCode::from_u16(470).unwrap(), + })?; + + Ok(results) + } + + pub(crate) fn find_dbs(&self) -> ServerResult> { + let dbs: Vec = self + .db()? .exec( &QueryBuilder::select() .ids( QueryBuilder::search() - .depth_first() .from("dbs") - .limit(1) .where_() .distance(CountComparison::Equal(2)) - .and() - .key("name") - .value(Comparison::Equal(db.into())) .query(), ) .query(), )? - .elements - .get(0) - .ok_or(db_not_found(db))? - .try_into()?) - } - - pub(crate) fn find_db_id(&self, name: &str) -> ServerResult { - Ok(self - .db()? - .exec( - &QueryBuilder::search() - .depth_first() - .from("dbs") - .limit(1) - .where_() - .distance(CountComparison::Equal(2)) - .and() - .key("name") - .value(Comparison::Equal(name.into())) - .query(), - )? - .elements - .get(0) - .ok_or(db_not_found(name))? - .id) + .try_into()?; + + dbs.into_iter() + .map(|db| { + Ok(ServerDatabase { + db_type: db.db_type, + role: DbUserRole::Admin, + size: self + .get_pool()? + .get(&db.name) + .ok_or(db_not_found(&db.name))? + .get()? + .size(), + backup: db.backup, + name: db.name, + }) + }) + .collect::>>() } pub(crate) fn find_users(&self) -> ServerResult> { @@ -313,7 +439,7 @@ impl DbPool { .collect()) } - pub(crate) fn find_user_dbs(&self, user: DbId) -> ServerResult> { + pub(crate) fn find_user_dbs(&self, user: DbId) -> ServerResult> { let mut dbs = vec![]; self.db()? @@ -333,72 +459,31 @@ impl DbPool { )? .elements .into_iter() - .for_each(|e| { + .try_for_each(|e| -> ServerResult { if e.id.0 < 0 { - dbs.push((Database::default(), (&e.values[0].value).into())); + dbs.push(ServerDatabase { + role: (&e.values[0].value).into(), + ..Default::default() + }); } else { - dbs.last_mut().unwrap().0 = Database::from_db_element(&e).unwrap_or_default(); + let db = Database::from_db_element(&e)?; + let server_db = dbs.last_mut().unwrap(); + server_db.db_type = db.db_type; + server_db.backup = db.backup; + server_db.size = self + .get_pool()? + .get(&db.name) + .ok_or(db_not_found(&db.name))? + .get()? + .size(); + server_db.name = db.name; } - }); + Ok(()) + })?; Ok(dbs) } - pub(crate) fn find_user_db(&self, user: DbId, db: &str) -> ServerResult { - let db_id_query = self.find_user_db_id_query(user, db); - Ok(self - .0 - .server_db - .get()? - .transaction(|t| -> Result { - let db_id = t - .exec(&db_id_query)? - .elements - .get(0) - .ok_or(db_not_found(db))? - .id; - Ok(t.exec(&QueryBuilder::select().ids(db_id).query())?) - })? - .try_into()?) - } - - pub(crate) fn find_user_db_role(&self, user: DbId, db: &str) -> ServerResult { - let db_id_query = self.find_user_db_id_query(user, db); - Ok((&self - .0 - .server_db - .get()? - .transaction(|t| -> Result { - let db_id = t - .exec(&db_id_query)? - .elements - .get(0) - .ok_or(db_not_found(db))? - .id; - - Ok(t.exec( - &QueryBuilder::select() - .ids( - QueryBuilder::search() - .depth_first() - .from(user) - .to(db_id) - .limit(1) - .where_() - .distance(CountComparison::LessThanOrEqual(2)) - .and() - .keys(vec!["role".into()]) - .query(), - ) - .query(), - )?) - })? - .elements[0] - .values[0] - .value) - .into()) - } - pub(crate) fn find_user(&self, name: &str) -> ServerResult { let user_id = self.find_user_id(name)?; Ok(self @@ -449,27 +534,8 @@ impl DbPool { .id) } - 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(user_not_found(name))? - .id) - } - - pub(crate) fn db_users(&self, db: DbId) -> ServerResult> { + pub(crate) fn db_users(&self, owner: &str, db: &str, user: DbId) -> ServerResult> { + let db_id = self.find_user_db_id(user, &db_name(owner, db))?; let mut users = vec![]; self.db()? @@ -478,7 +544,7 @@ impl DbPool { .ids( QueryBuilder::search() .depth_first() - .to(db) + .to(db_id) .where_() .distance(CountComparison::LessThanOrEqual(2)) .and() @@ -494,44 +560,73 @@ impl DbPool { .into_iter() .for_each(|e| { if e.id.0 < 0 { - users.push((String::new(), (&e.values[0].value).into())); + users.push(DbUser { + user: String::new(), + role: (&e.values[0].value).into(), + }); } else { - users.last_mut().unwrap().0 = e.values[0].value.to_string(); + users.last_mut().unwrap().user = e.values[0].value.to_string(); } }); Ok(users) } - pub(crate) fn is_db_admin(&self, user: DbId, db: DbId) -> ServerResult { - Ok(self - .db()? - .exec( - &QueryBuilder::search() - .from(user) - .to(db) - .limit(1) - .where_() - .distance(CountComparison::LessThanOrEqual(2)) - .and() - .key("role") - .value(Comparison::Equal(DbUserRole::Admin.into())) - .query(), - )? - .result - == 1) + pub(crate) fn optimize_db( + &self, + owner: &str, + db: &str, + user: DbId, + ) -> ServerResult { + let db_name = db_name(owner, db); + let db = self.find_user_db(user, &db_name)?; + let role = self.find_user_db_role(user, &db_name)?; + + if role == DbUserRole::Read { + return Err(permission_denied("write rights required")); + } + + let pool = self.get_pool()?; + let server_db = pool.get(&db.name).ok_or(db_not_found(&db.name))?; + server_db.get_mut()?.optimize_storage()?; + let size = server_db.get()?.size(); + + Ok(ServerDatabase { + name: db.name, + db_type: db.db_type, + role, + size, + backup: db.backup, + }) } - pub(crate) fn remove_db_user(&self, db: DbId, user: DbId) -> ServerResult { + pub(crate) fn remove_db_user( + &self, + owner: &str, + db: &str, + username: &str, + user: DbId, + ) -> ServerResult { + if owner == username { + return Err(permission_denied("cannot remove owner")); + } + + let db_id = self.find_user_db_id(user, &db_name(owner, db))?; + let user_id = self.find_db_user_id(db_id, username)?; + + if user != user_id && !self.is_db_admin(user, db_id)? { + return Err(permission_denied("admin only")); + } + self.db_mut()?.exec_mut( &QueryBuilder::remove() .ids( QueryBuilder::search() - .from(user) - .to(db) + .from(user_id) + .to(db_id) .limit(1) .where_() - .edge() + .keys(vec!["role".into()]) .query(), ) .query(), @@ -539,21 +634,46 @@ impl DbPool { Ok(()) } - pub(crate) fn remove_db(&self, db: Database) -> ServerResult { + pub(crate) fn remove_db(&self, owner: &str, db: &str, user: DbId) -> ServerResult { + let user_name = self.user_name(user)?; + + if owner != user_name { + return Err(permission_denied("owner only")); + } + + let db_name = db_name(owner, db); + let db_id = self.find_user_db_id(user, &db_name)?; + self.db_mut()? - .exec_mut(&QueryBuilder::remove().ids(db.db_id.unwrap()).query())?; + .exec_mut(&QueryBuilder::remove().ids(db_id).query())?; - Ok(self.get_pool_mut()?.remove(&db.name).unwrap()) + Ok(self.get_pool_mut()?.remove(&db_name).unwrap()) } pub(crate) fn rename_db( &self, - mut db: Database, + owner: &str, + db: &str, new_name: &str, + user: DbId, config: &Config, ) -> ServerResult { - let mut pool = self.get_pool_mut()?; - let server_db = pool.remove(&db.name).unwrap(); + let (new_owner, new_db) = new_name.split_once('/').ok_or(ErrorCode::DbInvalid)?; + let username = self.user_name(user)?; + + if owner != username { + return Err(permission_denied("owner only")); + } + + let db_name = db_name(owner, db); + let mut database = self.find_user_db(user, &db_name)?; + + if new_owner != owner { + std::fs::create_dir_all(Path::new(&config.data_dir).join(new_owner))?; + self.add_db_user(owner, db, new_owner, DbUserRole::Admin, user)?; + } + + let server_db = self.get_pool_mut()?.remove(&db_name).unwrap(); server_db .get_mut()? .rename( @@ -563,10 +683,62 @@ impl DbPool { .as_ref(), ) .map_err(|_| ErrorCode::DbInvalid)?; - pool.insert(new_name.to_string(), server_db); - db.name = new_name.to_string(); + self.get_pool_mut()?.insert(new_name.to_string(), server_db); + database.name = new_name.to_string(); + + let backup_path = db_backup_file(owner, db, config); + + if backup_path.exists() { + let new_backup_path = db_backup_file(new_owner, new_db, config); + let backups_dir = new_backup_path.parent().unwrap(); + std::fs::create_dir_all(backups_dir)?; + std::fs::rename(backup_path, new_backup_path)?; + } + self.db_mut()? - .exec_mut(&QueryBuilder::insert().element(&db).query())?; + .exec_mut(&QueryBuilder::insert().element(&database).query())?; + + Ok(()) + } + + pub(crate) fn restore_db( + &self, + owner: &str, + db: &str, + user: DbId, + config: &Config, + ) -> ServerResult { + let db_name = db_name(owner, db); + let mut database = self.find_user_db(user, &db_name)?; + + if !self.is_db_admin(user, database.db_id.unwrap())? { + return Err(permission_denied("admin only")); + } + + let backup_path = db_backup_file(owner, db, config); + + if !backup_path.exists() { + return Err(ServerError { + description: "backup not found".to_string(), + status: StatusCode::NOT_FOUND, + }); + } + + let current_path = db_file(owner, db, config); + let backup_temp = db_backup_dir(owner, config).join(db); + + self.get_pool_mut()?.remove(&db_name); + std::fs::rename(¤t_path, &backup_temp)?; + std::fs::rename(&backup_path, ¤t_path)?; + std::fs::rename(backup_temp, backup_path)?; + let server_db = ServerDb::new(&format!( + "{}:{}", + database.db_type, + current_path.to_string_lossy() + ))?; + self.get_pool_mut()?.insert(db_name, server_db); + database.backup = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); + self.save_db(database)?; Ok(()) } @@ -581,12 +753,6 @@ impl DbPool { Ok(()) } - pub(crate) fn save_db(&self, db: Database) -> ServerResult { - self.db_mut()? - .exec_mut(&QueryBuilder::insert().element(&db).query())?; - Ok(()) - } - pub(crate) fn save_user(&self, user: ServerUser) -> ServerResult { self.db_mut()? .exec_mut(&QueryBuilder::insert().element(&user).query())?; @@ -608,12 +774,32 @@ impl DbPool { .to_string()) } - pub(crate) fn get_pool(&self) -> ServerResult>> { - Ok(self.0.pool.read()?) + fn db(&self) -> ServerResult> { + self.0.server_db.get() } - pub(crate) fn get_pool_mut(&self) -> ServerResult>> { - Ok(self.0.pool.write()?) + fn db_mut(&self) -> ServerResult> { + self.0.server_db.get_mut() + } + + fn find_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(user_not_found(name))? + .id) } fn find_user_db_id_query(&self, user: DbId, db: &str) -> SearchQuery { @@ -629,12 +815,99 @@ impl DbPool { .query() } - fn db(&self) -> ServerResult> { - self.0.server_db.get() + fn find_user_db(&self, user: DbId, db: &str) -> ServerResult { + let db_id_query = self.find_user_db_id_query(user, db); + Ok(self + .db()? + .transaction(|t| -> Result { + let db_id = t + .exec(&db_id_query)? + .elements + .get(0) + .ok_or(db_not_found(db))? + .id; + Ok(t.exec(&QueryBuilder::select().ids(db_id).query())?) + })? + .try_into()?) } - fn db_mut(&self) -> ServerResult> { - self.0.server_db.get_mut() + fn find_user_db_id(&self, user: DbId, db: &str) -> ServerResult { + let db_id_query = self.find_user_db_id_query(user, db); + Ok(self + .db()? + .exec(&db_id_query)? + .elements + .get(0) + .ok_or(db_not_found(db))? + .id) + } + + fn find_user_db_role(&self, user: DbId, db: &str) -> ServerResult { + let db_id_query = self.find_user_db_id_query(user, db); + Ok((&self + .db()? + .transaction(|t| -> Result { + let db_id = t + .exec(&db_id_query)? + .elements + .get(0) + .ok_or(db_not_found(db))? + .id; + + Ok(t.exec( + &QueryBuilder::select() + .ids( + QueryBuilder::search() + .depth_first() + .from(user) + .to(db_id) + .limit(1) + .where_() + .distance(CountComparison::LessThanOrEqual(2)) + .and() + .keys(vec!["role".into()]) + .query(), + ) + .query(), + )?) + })? + .elements[0] + .values[0] + .value) + .into()) + } + + fn get_pool(&self) -> ServerResult>> { + Ok(self.0.pool.read()?) + } + + fn get_pool_mut(&self) -> ServerResult>> { + Ok(self.0.pool.write()?) + } + + fn is_db_admin(&self, user: DbId, db: DbId) -> ServerResult { + Ok(self + .db()? + .exec( + &QueryBuilder::search() + .from(user) + .to(db) + .limit(1) + .where_() + .distance(CountComparison::LessThanOrEqual(2)) + .and() + .key("role") + .value(Comparison::Equal(DbUserRole::Admin.into())) + .query(), + )? + .result + == 1) + } + + fn save_db(&self, db: Database) -> ServerResult { + self.db_mut()? + .exec_mut(&QueryBuilder::insert().element(&db).query())?; + Ok(()) } } @@ -642,14 +915,100 @@ fn user_not_found(name: &str) -> ServerError { ServerError::new(StatusCode::NOT_FOUND, &format!("user not found: {name}")) } -pub(crate) fn db_not_found(name: &str) -> ServerError { +fn db_not_found(name: &str) -> ServerError { ServerError::new(StatusCode::NOT_FOUND, &format!("db not found: {name}")) } -pub(crate) fn db_backup_file(config: &Config, db: &str) -> PathBuf { - let (owner, db_name) = db.split_once('/').unwrap(); - Path::new(&config.data_dir) - .join(owner) - .join("backups") - .join(format!("{db_name}.bak")) +fn db_backup_file(owner: &str, db: &str, config: &Config) -> PathBuf { + db_backup_dir(owner, config).join(format!("{db}.bak")) +} + +fn db_backup_dir(owner: &str, config: &Config) -> PathBuf { + Path::new(&config.data_dir).join(owner).join("backups") +} + +fn db_file(owner: &str, db: &str, config: &Config) -> PathBuf { + Path::new(&config.data_dir).join(owner).join(db) +} + +fn db_name(owner: &str, db: &str) -> String { + format!("{owner}/{db}") +} + +fn permission_denied(message: &str) -> ServerError { + ServerError::new( + StatusCode::FORBIDDEN, + &format!("permission denied: {}", message), + ) +} + +fn required_role(queries: &Queries) -> DbUserRole { + for q in &queries.0 { + match q { + QueryType::InsertAlias(_) + | QueryType::InsertEdges(_) + | QueryType::InsertNodes(_) + | QueryType::InsertValues(_) + | QueryType::Remove(_) + | QueryType::RemoveAliases(_) + | QueryType::RemoveValues(_) => { + return DbUserRole::Write; + } + _ => {} + } + } + + DbUserRole::Read +} + +fn t_exec(t: &Transaction, q: &QueryType) -> Result { + match q { + QueryType::Search(q) => t.exec(q), + QueryType::Select(q) => t.exec(q), + QueryType::SelectAliases(q) => t.exec(q), + QueryType::SelectAllAliases(q) => t.exec(q), + QueryType::SelectKeys(q) => t.exec(q), + QueryType::SelectKeyCount(q) => t.exec(q), + QueryType::SelectValues(q) => t.exec(q), + _ => unreachable!(), + } +} + +fn t_exec_mut( + t: &mut TransactionMut, + q: &QueryType, +) -> Result { + match q { + QueryType::Search(q) => t.exec(q), + QueryType::Select(q) => t.exec(q), + QueryType::SelectAliases(q) => t.exec(q), + QueryType::SelectAllAliases(q) => t.exec(q), + QueryType::SelectKeys(q) => t.exec(q), + QueryType::SelectKeyCount(q) => t.exec(q), + QueryType::SelectValues(q) => t.exec(q), + QueryType::InsertAlias(q) => t.exec_mut(q), + QueryType::InsertEdges(q) => t.exec_mut(q), + QueryType::InsertNodes(q) => t.exec_mut(q), + QueryType::InsertValues(q) => t.exec_mut(q), + QueryType::Remove(q) => t.exec_mut(q), + QueryType::RemoveAliases(q) => t.exec_mut(q), + QueryType::RemoveValues(q) => t.exec_mut(q), + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::db_pool::server_db::ServerDb; + use agdb::QueryBuilder; + + #[test] + #[should_panic] + fn unreachable() { + let db = ServerDb::new("memory:test").unwrap(); + db.get() + .unwrap() + .transaction(|t| t_exec(t, &QueryType::Remove(QueryBuilder::remove().ids(1).query()))) + .unwrap(); + } } diff --git a/agdb_server/src/routes/admin/db.rs b/agdb_server/src/routes/admin/db.rs index af95c0e68..fea0e337e 100644 --- a/agdb_server/src/routes/admin/db.rs +++ b/agdb_server/src/routes/admin/db.rs @@ -1,26 +1,14 @@ pub(crate) mod user; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; - use crate::config::Config; -use crate::db_pool::db_backup_file; -use crate::db_pool::db_not_found; -use crate::db_pool::Database; use crate::db_pool::DbPool; -use crate::error_code::ErrorCode; -use crate::routes::db::required_role; -use crate::routes::db::t_exec; -use crate::routes::db::t_exec_mut; -use crate::routes::db::user::DbUserRole; use crate::routes::db::DbTypeParam; use crate::routes::db::Queries; use crate::routes::db::QueriesResults; -use crate::routes::db::ServerDatabaseSize; -use crate::server_error::ServerError; +use crate::routes::db::ServerDatabase; +use crate::routes::db::ServerDatabaseRename; use crate::server_error::ServerResponse; use crate::user_id::AdminId; -use agdb::QueryError; use axum::extract::Path; use axum::extract::Query; use axum::extract::State; @@ -49,27 +37,7 @@ pub(crate) async fn add( Path((owner, db)): Path<(String, String)>, request: Query, ) -> ServerResponse { - let user = db_pool.find_user_id(&owner)?; - let name = format!("{owner}/{db}"); - - if db_pool.find_user_db(user, &name).is_ok() { - return Err(ErrorCode::DbExists.into()); - } - - let backup = if db_backup_file(&config, &name).exists() { - SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() - } else { - 0 - }; - - let db = Database { - db_id: None, - name, - db_type: request.db_type, - backup, - }; - - db_pool.add_db(user, db, &config)?; + db_pool.add_db(&owner, &db, request.db_type, &config)?; Ok(StatusCode::CREATED) } @@ -93,9 +61,8 @@ pub(crate) async fn delete( State(config): State, Path((owner, db)): Path<(String, String)>, ) -> ServerResponse { - let db_name = format!("{owner}/{db}"); - let db = db_pool.find_db(&db_name)?; - db_pool.delete_db(db, &config)?; + let owner_id = db_pool.find_user_id(&owner)?; + db_pool.delete_db(&owner, &db, owner_id, &config)?; Ok(StatusCode::NO_CONTENT) } @@ -121,36 +88,8 @@ pub(crate) async fn exec( Path((owner, db)): Path<(String, String)>, Json(queries): Json, ) -> ServerResponse<(StatusCode, Json)> { - let pool = db_pool.get_pool()?; - let db_name = format!("{}/{}", owner, db); - let db = pool.get(&db_name).ok_or(db_not_found(&db_name))?; - let required_role = required_role(&queries); - - let results = if required_role == DbUserRole::Read { - db.get()?.transaction(|t| { - let mut results = vec![]; - - for q in &queries.0 { - results.push(t_exec(t, q)?); - } - - Ok(results) - }) - } else { - db.get_mut()?.transaction_mut(|t| { - let mut results = vec![]; - - for q in &queries.0 { - results.push(t_exec_mut(t, q)?); - } - - Ok(results) - }) - } - .map_err(|e: QueryError| ServerError { - description: e.to_string(), - status: StatusCode::from_u16(470).unwrap(), - })?; + let owner_id = db_pool.find_user_id(&owner)?; + let results = db_pool.exec(&owner, &db, owner_id, &queries)?; Ok((StatusCode::OK, Json(QueriesResults(results)))) } @@ -159,31 +98,16 @@ pub(crate) async fn exec( path = "/api/v1/admin/db/list", security(("Token" = [])), responses( - (status = 200, description = "ok", body = Vec), + (status = 200, description = "ok", body = Vec), (status = 401, description = "unauthorized"), ) )] pub(crate) async fn list( _admin: AdminId, State(db_pool): State, -) -> ServerResponse<(StatusCode, Json>)> { - let pool = db_pool.get_pool()?; - let dbs = db_pool - .find_dbs()? - .into_iter() - .map(|db| { - Ok(ServerDatabaseSize { - name: db.name.clone(), - db_type: db.db_type, - size: pool - .get(&db.name) - .ok_or(db_not_found(&db.name))? - .get()? - .size(), - backup: db.backup, - }) - }) - .collect::, ServerError>>()?; +) -> ServerResponse<(StatusCode, Json>)> { + let dbs = db_pool.find_dbs()?; + Ok((StatusCode::OK, Json(dbs))) } @@ -203,24 +127,11 @@ pub(crate) async fn optimize( _admin: AdminId, State(db_pool): State, Path((owner, db)): Path<(String, String)>, -) -> ServerResponse<(StatusCode, Json)> { - let db_name = format!("{owner}/{db}"); - let db = db_pool.find_db(&db_name)?; - let pool = db_pool.get_pool()?; - let server_db = pool.get(&db.name).ok_or(db_not_found(&db.name))?; - server_db.get_mut()?.optimize_storage()?; - let size = server_db.get()?.size(); - let backup = db.backup; +) -> ServerResponse<(StatusCode, Json)> { + let owner_id = db_pool.find_user_id(&owner)?; + let db = db_pool.optimize_db(&owner, &db, owner_id)?; - Ok(( - StatusCode::OK, - Json(ServerDatabaseSize { - name: db.name, - db_type: db.db_type, - size, - backup, - }), - )) + Ok((StatusCode::OK, Json(db))) } #[utoipa::path(delete, @@ -241,9 +152,36 @@ pub(crate) async fn remove( State(db_pool): State, Path((owner, db)): Path<(String, String)>, ) -> ServerResponse { - let name = format!("{owner}/{db}"); - let db = db_pool.find_db(&name)?; - db_pool.remove_db(db)?; + let owner_id = db_pool.find_user_id(&owner)?; + db_pool.remove_db(&owner, &db, owner_id)?; Ok(StatusCode::NO_CONTENT) } + +#[utoipa::path(post, + path = "/api/v1/admin/db/{owner}/{db}/rename", + security(("Token" = [])), + params( + ("owner" = String, Path, description = "db owner user name"), + ("db" = String, Path, description = "db name"), + ServerDatabaseRename + ), + responses( + (status = 201, description = "db renamed"), + (status = 401, description = "unauthorized"), + (status = 404, description = "user / db not found"), + (status = 467, description = "invalid db"), + ) +)] +pub(crate) async fn rename( + _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.rename_db(&owner, &db, &request.new_name, owner_id, &config)?; + + Ok(StatusCode::CREATED) +} diff --git a/agdb_server/src/routes/admin/db/user.rs b/agdb_server/src/routes/admin/db/user.rs index 7d55188e3..49caa192e 100644 --- a/agdb_server/src/routes/admin/db/user.rs +++ b/agdb_server/src/routes/admin/db/user.rs @@ -1,7 +1,6 @@ use crate::db_pool::DbPool; use crate::routes::db::user::DbUser; use crate::routes::db::user::DbUserRoleParam; -use crate::server_error::ServerError; use crate::server_error::ServerResponse; use crate::user_id::AdminId; use axum::extract::Path; @@ -32,17 +31,8 @@ pub(crate) async fn add( Path((owner, db, username)): Path<(String, String, String)>, request: Query, ) -> ServerResponse { - if owner == username { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "cannot change role of db owner", - )); - } - - let db_name = format!("{}/{}", owner, db); - let db_id = db_pool.find_db_id(&db_name)?; - let db_user = db_pool.find_user_id(&username)?; - db_pool.add_db_user(db_id, db_user, request.0.db_role)?; + let owner_id = db_pool.find_user_id(&owner)?; + db_pool.add_db_user(&owner, &db, &username, request.0.db_role, owner_id)?; Ok(StatusCode::CREATED) } @@ -65,13 +55,8 @@ pub(crate) async fn list( State(db_pool): State, Path((owner, db)): Path<(String, String)>, ) -> ServerResponse<(StatusCode, Json>)> { - let db_name = format!("{}/{}", owner, db); - let db_id = db_pool.find_db_id(&db_name)?; - let users = db_pool - .db_users(db_id)? - .into_iter() - .map(|(user, role)| DbUser { user, role }) - .collect(); + let owner_id = db_pool.find_user_id(&owner)?; + let users = db_pool.db_users(&owner, &db, owner_id)?; Ok((StatusCode::OK, Json(users))) } @@ -96,17 +81,8 @@ pub(crate) async fn remove( State(db_pool): State, Path((owner, db, username)): Path<(String, String, String)>, ) -> ServerResponse { - if owner == username { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "cannot remove db owner", - )); - } - - let db_name = format!("{}/{}", owner, db); - let db_id = db_pool.find_db_id(&db_name)?; - let db_user = db_pool.db_user_id(db_id, &username)?; - db_pool.remove_db_user(db_id, db_user)?; + let owner_id = db_pool.find_user_id(&owner)?; + db_pool.remove_db_user(&owner, &db, &username, owner_id)?; Ok(StatusCode::NO_CONTENT) } diff --git a/agdb_server/src/routes/admin/user.rs b/agdb_server/src/routes/admin/user.rs index 026b5391c..8e894fca3 100644 --- a/agdb_server/src/routes/admin/user.rs +++ b/agdb_server/src/routes/admin/user.rs @@ -18,38 +18,6 @@ pub(crate) struct UserStatus { pub(crate) name: String, } -#[utoipa::path(put, - path = "/api/v1/admin/user/{username}/change_password", - security(("Token" = [])), - params( - ("username" = String, Path, description = "user name"), - ), - request_body = UserCredentials, - responses( - (status = 201, description = "password changed"), - (status = 401, description = "unauthorized"), - (status = 461, description = "password too short (<8)"), - (status = 464, description = "user not found"), - ) -)] -pub(crate) async fn change_password( - _admin_id: AdminId, - State(db_pool): State, - Path(username): Path, - Json(request): Json, -) -> ServerResponse { - password::validate_password(&request.password)?; - - let mut user = db_pool.find_user(&username)?; - let pswd = Password::create(&username, &request.password); - user.password = pswd.password.to_vec(); - user.salt = pswd.user_salt.to_vec(); - - db_pool.save_user(user)?; - - Ok(StatusCode::CREATED) -} - #[utoipa::path(post, path = "/api/v1/admin/user/{username}/add", security(("Token" = [])), @@ -80,7 +48,7 @@ pub(crate) async fn add( let pswd = Password::create(&username, &request.password); - db_pool.create_user(ServerUser { + db_pool.add_user(ServerUser { db_id: None, name: username.clone(), password: pswd.password.to_vec(), @@ -91,6 +59,32 @@ pub(crate) async fn add( Ok(StatusCode::CREATED) } +#[utoipa::path(put, + path = "/api/v1/admin/user/{username}/change_password", + security(("Token" = [])), + params( + ("username" = String, Path, description = "user name"), + ), + request_body = UserCredentials, + responses( + (status = 201, description = "password changed"), + (status = 401, description = "unauthorized"), + (status = 461, description = "password too short (<8)"), + (status = 464, description = "user not found"), + ) +)] +pub(crate) async fn change_password( + _admin_id: AdminId, + State(db_pool): State, + Path(username): Path, + Json(request): Json, +) -> ServerResponse { + let user = db_pool.find_user(&username)?; + db_pool.change_password(user, &request.password)?; + + Ok(StatusCode::CREATED) +} + #[utoipa::path(get, path = "/api/v1/admin/user/list", security(("Token" = [])), diff --git a/agdb_server/src/routes/db.rs b/agdb_server/src/routes/db.rs index 7ba6853a9..06f33cbba 100644 --- a/agdb_server/src/routes/db.rs +++ b/agdb_server/src/routes/db.rs @@ -1,23 +1,14 @@ pub(crate) mod user; use crate::config::Config; -use crate::db_pool::db_backup_file; -use crate::db_pool::db_not_found; -use crate::db_pool::server_db::ServerDb; -use crate::db_pool::server_db_storage::ServerDbStorage; -use crate::db_pool::Database; use crate::db_pool::DbPool; -use crate::error_code::ErrorCode; use crate::routes::db::user::DbUserRole; use crate::server_error::ServerError; use crate::server_error::ServerResponse; use crate::user_id::UserId; use agdb::DbError; -use agdb::QueryError; use agdb::QueryResult; use agdb::QueryType; -use agdb::Transaction; -use agdb::TransactionMut; use axum::extract::Path; use axum::extract::Query; use axum::extract::State; @@ -26,9 +17,6 @@ use axum::Json; use serde::Deserialize; use serde::Serialize; use std::fmt::Display; -use std::path::Path as FilePath; -use std::time::SystemTime; -use std::time::UNIX_EPOCH; use utoipa::IntoParams; use utoipa::ToSchema; @@ -46,16 +34,8 @@ pub(crate) struct DbTypeParam { pub(crate) db_type: DbType, } -#[derive(Serialize, ToSchema)] -pub(crate) struct ServerDatabaseSize { - pub(crate) name: String, - pub(crate) db_type: DbType, - pub(crate) size: u64, - pub(crate) backup: u64, -} - -#[derive(Serialize, ToSchema)] -pub(crate) struct ServerDatabaseWithRole { +#[derive(Default, Serialize, ToSchema)] +pub(crate) struct ServerDatabase { pub(crate) name: String, pub(crate) db_type: DbType, pub(crate) role: DbUserRole, @@ -140,26 +120,7 @@ pub(crate) async fn add( )); } - let name = format!("{owner}/{db}"); - - if db_pool.find_user_db(user.0, &name).is_ok() { - return Err(ErrorCode::DbExists.into()); - } - - let backup = if db_backup_file(&config, &name).exists() { - SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs() - } else { - 0 - }; - - let db = Database { - db_id: None, - name, - db_type: request.db_type, - backup, - }; - - db_pool.add_db(user.0, db, &config)?; + db_pool.add_db(&owner, &db, request.db_type, &config)?; Ok(StatusCode::CREATED) } @@ -184,39 +145,7 @@ pub(crate) async fn backup( State(config): State, Path((owner, db)): Path<(String, String)>, ) -> ServerResponse { - let db_name = format!("{}/{}", owner, db); - let mut database = db_pool.find_user_db(user.0, &db_name)?; - - if database.db_type == DbType::Memory { - return Err(ServerError { - description: "memory db cannot have backup".to_string(), - status: StatusCode::FORBIDDEN, - }); - } - - if !db_pool.is_db_admin(user.0, database.db_id.unwrap())? { - return Err(ServerError { - description: "must be a db admin".to_string(), - status: StatusCode::FORBIDDEN, - }); - } - - let pool = db_pool.get_pool()?; - let server_db = pool.get(&db_name).ok_or(db_not_found(&db_name))?; - let backup_path = db_backup_file(&config, &db_name); - let backup_dir = backup_path.parent().unwrap(); - - if backup_path.exists() { - std::fs::remove_file(&backup_path)?; - } - - std::fs::create_dir_all(backup_dir)?; - - server_db - .get_mut()? - .backup(backup_path.to_string_lossy().as_ref())?; - database.backup = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - db_pool.save_db(database)?; + db_pool.backup_db(&owner, &db, user.0, &config)?; Ok(StatusCode::CREATED) } @@ -241,18 +170,7 @@ pub(crate) async fn delete( State(config): State, Path((owner, db)): Path<(String, String)>, ) -> ServerResponse { - let username = db_pool.user_name(user.0)?; - - if owner != username { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "user must be a db owner", - )); - } - - let db_name = format!("{}/{}", owner, db); - let db = db_pool.find_user_db(user.0, &db_name)?; - db_pool.delete_db(db, &config)?; + db_pool.delete_db(&owner, &db, user.0, &config)?; Ok(StatusCode::NO_CONTENT) } @@ -278,45 +196,7 @@ pub(crate) async fn exec( Path((owner, db)): Path<(String, String)>, Json(queries): Json, ) -> ServerResponse<(StatusCode, Json)> { - let db_name = format!("{}/{}", owner, db); - let role = db_pool.find_user_db_role(user.0, &db_name)?; - let required_role = required_role(&queries); - - if required_role == DbUserRole::Write && role == DbUserRole::Read { - return Err(ServerError { - description: "Permission denied: mutable queries require at least 'write' role (current role: 'read')".to_string(), - status: StatusCode::FORBIDDEN, - }); - } - - let pool = db_pool.get_pool()?; - let db = pool.get(&db_name).ok_or(db_not_found(&db_name))?; - - let results = if required_role == DbUserRole::Read { - db.get()?.transaction(|t| { - let mut results = vec![]; - - for q in &queries.0 { - results.push(t_exec(t, q)?); - } - - Ok(results) - }) - } else { - db.get_mut()?.transaction_mut(|t| { - let mut results = vec![]; - - for q in &queries.0 { - results.push(t_exec_mut(t, q)?); - } - - Ok(results) - }) - } - .map_err(|e: QueryError| ServerError { - description: e.to_string(), - status: StatusCode::from_u16(470).unwrap(), - })?; + let results = db_pool.exec(&owner, &db, user.0, &queries)?; Ok((StatusCode::OK, Json(QueriesResults(results)))) } @@ -325,32 +205,16 @@ pub(crate) async fn exec( path = "/api/v1/db/list", security(("Token" = [])), responses( - (status = 200, description = "ok", body = Vec), + (status = 200, description = "ok", body = Vec), (status = 401, description = "unauthorized"), ) )] pub(crate) async fn list( user: UserId, State(db_pool): State, -) -> ServerResponse<(StatusCode, Json>)> { - let pool = db_pool.get_pool()?; - let dbs = db_pool - .find_user_dbs(user.0)? - .into_iter() - .map(|(db, role)| { - Ok(ServerDatabaseWithRole { - name: db.name.clone(), - db_type: db.db_type, - role, - size: pool - .get(&db.name) - .ok_or(db_not_found(&db.name))? - .get()? - .size(), - backup: db.backup, - }) - }) - .collect::, ServerError>>()?; +) -> ServerResponse<(StatusCode, Json>)> { + let dbs = db_pool.find_user_dbs(user.0)?; + Ok((StatusCode::OK, Json(dbs))) } @@ -362,7 +226,7 @@ pub(crate) async fn list( ("db" = String, Path, description = "db name"), ), responses( - (status = 200, description = "ok", body = ServerDatabaseSize), + (status = 200, description = "ok", body = ServerDatabase), (status = 401, description = "unauthorized"), (status = 403, description = "must have write permissions"), ) @@ -371,33 +235,10 @@ pub(crate) async fn optimize( user: UserId, State(db_pool): State, Path((owner, db)): Path<(String, String)>, -) -> ServerResponse<(StatusCode, Json)> { - let db_name = format!("{owner}/{db}"); - let db = db_pool.find_user_db(user.0, &db_name)?; - let role = db_pool.find_user_db_role(user.0, &db_name)?; - - if role == DbUserRole::Read { - return Err(ServerError { - description: "Permission denied: optimization can only be done with write permissions" - .to_string(), - status: StatusCode::FORBIDDEN, - }); - } +) -> ServerResponse<(StatusCode, Json)> { + let db = db_pool.optimize_db(&owner, &db, user.0)?; - let pool = db_pool.get_pool()?; - let server_db = pool.get(&db.name).ok_or(db_not_found(&db.name))?; - server_db.get_mut()?.optimize_storage()?; - let size = server_db.get()?.size(); - - Ok(( - StatusCode::OK, - Json(ServerDatabaseSize { - name: db.name, - db_type: db.db_type, - size, - backup: db.backup, - }), - )) + Ok((StatusCode::OK, Json(db))) } #[utoipa::path(post, @@ -419,18 +260,7 @@ pub(crate) async fn remove( State(db_pool): State, Path((owner, db)): Path<(String, String)>, ) -> ServerResponse { - let username = db_pool.user_name(user.0)?; - - if owner != username { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "user must be a db owner", - )); - } - - let db_name = format!("{}/{}", owner, db); - let db = db_pool.find_user_db(user.0, &db_name)?; - db_pool.remove_db(db)?; + db_pool.remove_db(&owner, &db, user.0)?; Ok(StatusCode::NO_CONTENT) } @@ -444,7 +274,7 @@ pub(crate) async fn remove( ServerDatabaseRename ), responses( - (status = 204, description = "db renamed"), + (status = 201, description = "db renamed"), (status = 401, description = "unauthorized"), (status = 403, description = "user must be a db owner"), (status = 404, description = "user / db not found"), @@ -458,31 +288,9 @@ pub(crate) async fn rename( Path((owner, db)): Path<(String, String)>, request: Query, ) -> ServerResponse { - let username = db_pool.user_name(user.0)?; - - if owner != username { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "user must be a db owner", - )); - } - - let (new_owner, _new_db) = request - .new_name - .split_once('/') - .ok_or(ErrorCode::DbInvalid)?; - let db_name = format!("{}/{}", owner, db); - let db = db_pool.find_user_db(user.0, &db_name)?; - - if new_owner != owner { - let new_owner_id = db_pool.find_user_id(new_owner)?; - std::fs::create_dir_all(FilePath::new(&config.data_dir).join(new_owner))?; - db_pool.add_db_user(db.db_id.unwrap(), new_owner_id, DbUserRole::Admin)?; - } + db_pool.rename_db(&owner, &db, &request.new_name, user.0, &config)?; - db_pool.rename_db(db, &request.new_name, &config)?; - - Ok(StatusCode::NO_CONTENT) + Ok(StatusCode::CREATED) } #[utoipa::path(post, @@ -505,114 +313,7 @@ pub(crate) async fn restore( State(config): State, Path((owner, db)): Path<(String, String)>, ) -> ServerResponse { - let db_name = format!("{}/{}", owner, db); - let mut database = db_pool.find_user_db(user.0, &db_name)?; - - if !db_pool.is_db_admin(user.0, database.db_id.unwrap())? { - return Err(ServerError { - description: "must be a db admin".to_string(), - status: StatusCode::FORBIDDEN, - }); - } - - let backup_path = db_backup_file(&config, &db_name); - - if !backup_path.exists() { - return Err(ServerError { - description: "backup not found".to_string(), - status: StatusCode::NOT_FOUND, - }); - } - - let current_path = FilePath::new(&config.data_dir).join(&db_name); - let backup_temp = backup_path.parent().unwrap().join(db); - - db_pool.get_pool_mut()?.remove(&db_name); - std::fs::rename(¤t_path, &backup_temp)?; - std::fs::rename(&backup_path, ¤t_path)?; - std::fs::rename(backup_temp, backup_path)?; - let server_db = ServerDb::new(&format!( - "{}:{}", - database.db_type, - current_path.to_string_lossy() - ))?; - db_pool.get_pool_mut()?.insert(db_name, server_db); - database.backup = SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(); - db_pool.save_db(database)?; + db_pool.restore_db(&owner, &db, user.0, &config)?; Ok(StatusCode::CREATED) } - -pub(crate) fn required_role(queries: &Queries) -> DbUserRole { - for q in &queries.0 { - match q { - QueryType::InsertAlias(_) - | QueryType::InsertEdges(_) - | QueryType::InsertNodes(_) - | QueryType::InsertValues(_) - | QueryType::Remove(_) - | QueryType::RemoveAliases(_) - | QueryType::RemoveValues(_) => { - return DbUserRole::Write; - } - _ => {} - } - } - - DbUserRole::Read -} - -pub(crate) fn t_exec( - t: &Transaction, - q: &QueryType, -) -> Result { - match q { - QueryType::Search(q) => t.exec(q), - QueryType::Select(q) => t.exec(q), - QueryType::SelectAliases(q) => t.exec(q), - QueryType::SelectAllAliases(q) => t.exec(q), - QueryType::SelectKeys(q) => t.exec(q), - QueryType::SelectKeyCount(q) => t.exec(q), - QueryType::SelectValues(q) => t.exec(q), - _ => unreachable!(), - } -} - -pub(crate) fn t_exec_mut( - t: &mut TransactionMut, - q: &QueryType, -) -> Result { - match q { - QueryType::Search(q) => t.exec(q), - QueryType::Select(q) => t.exec(q), - QueryType::SelectAliases(q) => t.exec(q), - QueryType::SelectAllAliases(q) => t.exec(q), - QueryType::SelectKeys(q) => t.exec(q), - QueryType::SelectKeyCount(q) => t.exec(q), - QueryType::SelectValues(q) => t.exec(q), - QueryType::InsertAlias(q) => t.exec_mut(q), - QueryType::InsertEdges(q) => t.exec_mut(q), - QueryType::InsertNodes(q) => t.exec_mut(q), - QueryType::InsertValues(q) => t.exec_mut(q), - QueryType::Remove(q) => t.exec_mut(q), - QueryType::RemoveAliases(q) => t.exec_mut(q), - QueryType::RemoveValues(q) => t.exec_mut(q), - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::db_pool::server_db::ServerDb; - use agdb::QueryBuilder; - - #[test] - #[should_panic] - fn unreachable() { - let db = ServerDb::new("memory:test").unwrap(); - db.get() - .unwrap() - .transaction(|t| t_exec(t, &QueryType::Remove(QueryBuilder::remove().ids(1).query()))) - .unwrap(); - } -} diff --git a/agdb_server/src/routes/db/user.rs b/agdb_server/src/routes/db/user.rs index dfa6cac3d..08fc97fa4 100644 --- a/agdb_server/src/routes/db/user.rs +++ b/agdb_server/src/routes/db/user.rs @@ -1,5 +1,4 @@ use crate::db_pool::DbPool; -use crate::server_error::ServerError; use crate::server_error::ServerResponse; use crate::user_id::UserId; use axum::extract::Path; @@ -12,9 +11,10 @@ use serde::Serialize; use utoipa::IntoParams; use utoipa::ToSchema; -#[derive(Clone, Copy, Serialize, Deserialize, ToSchema, PartialEq, Eq)] +#[derive(Clone, Copy, Default, Serialize, Deserialize, ToSchema, PartialEq, Eq)] #[serde(rename_all = "snake_case")] pub(crate) enum DbUserRole { + #[default] Admin, Write, Read, @@ -53,25 +53,7 @@ pub(crate) async fn add( Path((owner, db, username)): Path<(String, String, String)>, request: Query, ) -> ServerResponse { - if owner == username { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "cannot change role of db owner", - )); - } - - let db_name = format!("{}/{}", owner, db); - let db_id = db_pool.find_db_id(&db_name)?; - - if !db_pool.is_db_admin(user.0, db_id)? { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "must be a db admin", - )); - } - - let db_user = db_pool.find_user_id(&username)?; - db_pool.add_db_user(db_id, db_user, request.0.db_role)?; + db_pool.add_db_user(&owner, &db, &username, request.0.db_role, user.0)?; Ok(StatusCode::CREATED) } @@ -94,13 +76,7 @@ pub(crate) async fn list( State(db_pool): State, Path((owner, db)): Path<(String, String)>, ) -> ServerResponse<(StatusCode, Json>)> { - let name = format!("{}/{}", owner, db); - let database = db_pool.find_user_db(user.0, &name)?; - let users = db_pool - .db_users(database.db_id.unwrap())? - .into_iter() - .map(|(user, role)| DbUser { user, role }) - .collect(); + let users = db_pool.db_users(&owner, &db, user.0)?; Ok((StatusCode::OK, Json(users))) } @@ -125,25 +101,7 @@ pub(crate) async fn remove( State(db_pool): State, Path((owner, db, username)): Path<(String, String, String)>, ) -> ServerResponse { - if owner == username { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "cannot remove db owner", - )); - } - - let db_name = format!("{}/{}", owner, db); - let db_id = db_pool.find_db_id(&db_name)?; - let db_user = db_pool.db_user_id(db_id, &username)?; - - if user.0 != db_user && !db_pool.is_db_admin(user.0, db_id)? { - return Err(ServerError::new( - StatusCode::FORBIDDEN, - "must be a db admin", - )); - } - - db_pool.remove_db_user(db_id, db_user)?; + db_pool.remove_db_user(&owner, &db, &username, user.0)?; Ok(StatusCode::NO_CONTENT) } diff --git a/agdb_server/src/routes/user.rs b/agdb_server/src/routes/user.rs index d80f9981a..fd5f70808 100644 --- a/agdb_server/src/routes/user.rs +++ b/agdb_server/src/routes/user.rs @@ -1,5 +1,4 @@ use crate::db_pool::DbPool; -use crate::password; use crate::password::Password; use crate::server_error::ServerResponse; use axum::extract::Path; @@ -71,20 +70,14 @@ pub(crate) async fn change_password( Path(username): Path, Json(request): Json, ) -> ServerResponse { - password::validate_password(&request.new_password)?; - - let mut user = db_pool.find_user(&username)?; + let user = db_pool.find_user(&username)?; let old_pswd = Password::new(&user.name, &user.password, &user.salt)?; if !old_pswd.verify_password(&request.password) { return Ok(StatusCode::UNAUTHORIZED); } - let new_pswd = Password::create(&user.name, &request.new_password); - user.password = new_pswd.password.to_vec(); - user.salt = new_pswd.user_salt.to_vec(); - - db_pool.save_user(user)?; + db_pool.change_password(user, &request.new_password)?; Ok(StatusCode::CREATED) } diff --git a/agdb_server/tests/integration.rs b/agdb_server/tests/integration.rs index 22ca40997..2f4923c71 100644 --- a/agdb_server/tests/integration.rs +++ b/agdb_server/tests/integration.rs @@ -43,15 +43,7 @@ struct DbUser { } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct DbWithSize { - pub name: String, - pub db_type: String, - pub size: u64, - pub backup: u64, -} - -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)] -pub struct DbWithRole { +pub struct Db { pub name: String, pub db_type: String, pub role: String, diff --git a/agdb_server/tests/routes/admin_db_add_test.rs b/agdb_server/tests/routes/admin_db_add_test.rs index 0bac1d1c7..09a2a26b0 100644 --- a/agdb_server/tests/routes/admin_db_add_test.rs +++ b/agdb_server/tests/routes/admin_db_add_test.rs @@ -1,4 +1,4 @@ -use crate::DbWithRole; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -46,9 +46,7 @@ async fn add_with_backup() -> anyhow::Result<()> { .0, 201 ); - let (status, list) = server - .get::>(DB_LIST_URI, &user.token) - .await?; + let (status, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(status, 200); assert_ne!(list?[0].backup, 0); Ok(()) diff --git a/agdb_server/tests/routes/admin_db_list_test.rs b/agdb_server/tests/routes/admin_db_list_test.rs index 6437029f0..3ed06a87d 100644 --- a/agdb_server/tests/routes/admin_db_list_test.rs +++ b/agdb_server/tests/routes/admin_db_list_test.rs @@ -1,4 +1,4 @@ -use crate::DbWithSize; +use crate::Db; use crate::TestServer; use crate::ADMIN_DB_LIST_URI; use crate::NO_TOKEN; @@ -11,19 +11,21 @@ async fn db_list() -> anyhow::Result<()> { let db1 = server.init_db("memory", &user1).await?; let db2 = server.init_db("memory", &user2).await?; let (status, list) = server - .get::>(ADMIN_DB_LIST_URI, &server.admin_token) + .get::>(ADMIN_DB_LIST_URI, &server.admin_token) .await?; assert_eq!(status, 200); let list = list?; - assert!(list.contains(&DbWithSize { + assert!(list.contains(&Db { name: db1, db_type: "memory".to_string(), + role: "admin".to_string(), size: 2600, backup: 0, })); - assert!(list.contains(&DbWithSize { + assert!(list.contains(&Db { name: db2, db_type: "memory".to_string(), + role: "admin".to_string(), size: 2600, backup: 0, })); @@ -40,7 +42,7 @@ async fn with_backup() -> anyhow::Result<()> { .post(&format!("/db/{db}/backup"), &String::new(), &user.token) .await?; let (status, list) = server - .get::>(ADMIN_DB_LIST_URI, &server.admin_token) + .get::>(ADMIN_DB_LIST_URI, &server.admin_token) .await?; assert_eq!(status, 200); let list = list?; @@ -53,9 +55,7 @@ async fn with_backup() -> anyhow::Result<()> { #[tokio::test] async fn no_admin_token() -> anyhow::Result<()> { let server = TestServer::new().await?; - let (status, list) = server - .get::>(ADMIN_DB_LIST_URI, NO_TOKEN) - .await?; + let (status, list) = server.get::>(ADMIN_DB_LIST_URI, NO_TOKEN).await?; assert_eq!(status, 401); assert!(list.is_err()); Ok(()) diff --git a/agdb_server/tests/routes/admin_db_optimize_test.rs b/agdb_server/tests/routes/admin_db_optimize_test.rs index 9745c48ff..989083417 100644 --- a/agdb_server/tests/routes/admin_db_optimize_test.rs +++ b/agdb_server/tests/routes/admin_db_optimize_test.rs @@ -1,5 +1,4 @@ -use crate::DbWithRole; -use crate::DbWithSize; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -15,11 +14,7 @@ async fn optimize() -> anyhow::Result<()> { server .post(&format!("/db/{db}/exec"), &queries, &user.token) .await?; - let original_size = server - .get::>(DB_LIST_URI, &user.token) - .await? - .1?[0] - .size; + let original_size = server.get::>(DB_LIST_URI, &user.token).await?.1?[0].size; let (status, response) = server .post( &format!("/admin/db/{db}/optimize"), @@ -28,7 +23,7 @@ async fn optimize() -> anyhow::Result<()> { ) .await?; assert_eq!(status, 200); - let optimized_size = serde_json::from_str::(&response)?.size; + let optimized_size = serde_json::from_str::(&response)?.size; assert!(optimized_size < original_size); Ok(()) diff --git a/agdb_server/tests/routes/admin_db_remove_test.rs b/agdb_server/tests/routes/admin_db_remove_test.rs index 8bdb28799..4f0caae61 100644 --- a/agdb_server/tests/routes/admin_db_remove_test.rs +++ b/agdb_server/tests/routes/admin_db_remove_test.rs @@ -1,4 +1,4 @@ -use crate::DbWithRole; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -15,9 +15,7 @@ async fn remove() -> anyhow::Result<()> { .await?, 204 ); - let (status, list) = server - .get::>(DB_LIST_URI, &user.token) - .await?; + let (status, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(status, 200); assert!(list?.is_empty()); assert!(Path::new(&server.data_dir).join(db).exists()); diff --git a/agdb_server/tests/routes/admin_db_rename_test.rs b/agdb_server/tests/routes/admin_db_rename_test.rs new file mode 100644 index 000000000..bb39838a0 --- /dev/null +++ b/agdb_server/tests/routes/admin_db_rename_test.rs @@ -0,0 +1,160 @@ +use crate::Db; +use crate::TestServer; +use crate::DB_LIST_URI; +use crate::NO_TOKEN; +use std::path::Path; + +#[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 status = server + .post( + &format!("/admin/db/{db}/rename?new_name={}/renamed_db", user.name), + &String::new(), + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 201); + assert!(!Path::new(&server.data_dir).join(db).exists()); + assert!(Path::new(&server.data_dir) + .join(user.name) + .join("renamed_db") + .exists()); + Ok(()) +} + +#[tokio::test] +async fn rename_with_backup() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("mapped", &user).await?; + server + .post(&format!("/db/{db}/backup"), &String::new(), &user.token) + .await?; + let status = server + .post( + &format!("/admin/db/{db}/rename?new_name={}/renamed_db", user.name), + &String::new(), + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 201); + assert!(!Path::new(&server.data_dir).join(&db).exists()); + assert!(!Path::new(&server.data_dir) + .join(&user.name) + .join("backups") + .join(format!("{}.bak", db.split_once('/').unwrap().1)) + .exists()); + assert!(Path::new(&server.data_dir) + .join(&user.name) + .join("renamed_db") + .exists()); + assert!(Path::new(&server.data_dir) + .join(user.name) + .join("backups") + .join("renamed_db.bak") + .exists()); + Ok(()) +} + +#[tokio::test] +async fn transfer() -> 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!("/admin/db/{db}/rename?new_name={}/renamed_db", other.name), + &String::new(), + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 201); + let list = server.get::>(DB_LIST_URI, &other.token).await?.1?; + assert_eq!( + list, + vec![Db { + name: format!("{}/renamed_db", other.name), + db_type: "mapped".to_string(), + role: "admin".to_string(), + size: 2600, + backup: 0, + }] + ); + assert!(!Path::new(&server.data_dir).join(db).exists()); + assert!(Path::new(&server.data_dir) + .join(other.name) + .join("renamed_db") + .exists()); + 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}/rename?new_name={}/a\0a", user.name), + &String::new(), + &server.admin_token, + ) + .await? + .0; + assert_eq!(status, 467); + assert!(Path::new(&server.data_dir).join(db).exists()); + Ok(()) +} + +#[tokio::test] +async fn db_not_found() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let status = server + .post( + "/admin/db/user/db/rename?new_name=user/renamed_db", + &String::new(), + &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/db/rename?new_name=user/renamed_db", + &String::new(), + &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/db/rename?new_name=user/renamed_db", + &String::new(), + NO_TOKEN, + ) + .await? + .0; + assert_eq!(status, 401); + Ok(()) +} diff --git a/agdb_server/tests/routes/admin_db_user_add_test.rs b/agdb_server/tests/routes/admin_db_user_add_test.rs index 8830b5a4a..742017e6f 100644 --- a/agdb_server/tests/routes/admin_db_user_add_test.rs +++ b/agdb_server/tests/routes/admin_db_user_add_test.rs @@ -1,4 +1,4 @@ -use crate::DbWithRole; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -19,13 +19,10 @@ async fn add_db_user() -> anyhow::Result<()> { .await?, 201 ); - let list = server - .get::>(DB_LIST_URI, &other.token) - .await? - .1; + let list = server.get::>(DB_LIST_URI, &other.token).await?.1; assert_eq!( list?, - vec![DbWithRole { + vec![Db { name: db, db_type: "memory".to_string(), role: "write".to_string(), @@ -62,13 +59,10 @@ async fn change_user_role() -> anyhow::Result<()> { .await?, 201 ); - let list = server - .get::>(DB_LIST_URI, &other.token) - .await? - .1; + let list = server.get::>(DB_LIST_URI, &other.token).await?.1; assert_eq!( list?, - vec![DbWithRole { + vec![Db { name: db, db_type: "memory".to_string(), role: "admin".to_string(), diff --git a/agdb_server/tests/routes/db_add_test.rs b/agdb_server/tests/routes/db_add_test.rs index 76955454b..8fc7659a8 100644 --- a/agdb_server/tests/routes/db_add_test.rs +++ b/agdb_server/tests/routes/db_add_test.rs @@ -1,4 +1,4 @@ -use crate::DbWithRole; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -46,9 +46,7 @@ async fn add_with_backup() -> anyhow::Result<()> { .0, 201 ); - let (status, list) = server - .get::>(DB_LIST_URI, &user.token) - .await?; + let (status, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(status, 200); assert_ne!(list?[0].backup, 0); Ok(()) diff --git a/agdb_server/tests/routes/db_delete_test.rs b/agdb_server/tests/routes/db_delete_test.rs index 6f7cfbe87..01be0ddad 100644 --- a/agdb_server/tests/routes/db_delete_test.rs +++ b/agdb_server/tests/routes/db_delete_test.rs @@ -1,4 +1,4 @@ -use crate::DbWithRole; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -38,9 +38,7 @@ async fn in_memory_db() -> anyhow::Result<()> { .await?, 204 ); - let (_, list) = server - .get::>(DB_LIST_URI, &user.token) - .await?; + let (_, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(list?, vec![]); Ok(()) } diff --git a/agdb_server/tests/routes/db_list_test.rs b/agdb_server/tests/routes/db_list_test.rs index 90db10f20..9a05f3fc5 100644 --- a/agdb_server/tests/routes/db_list_test.rs +++ b/agdb_server/tests/routes/db_list_test.rs @@ -1,4 +1,4 @@ -use crate::DbWithRole; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -10,14 +10,14 @@ async fn list() -> anyhow::Result<()> { let db1 = server.init_db("memory", &user).await?; let db2 = server.init_db("mapped", &user).await?; let mut expected = vec![ - DbWithRole { + Db { name: db1.clone(), db_type: "memory".to_string(), role: "admin".to_string(), size: 2600, backup: 0, }, - DbWithRole { + Db { name: db2.clone(), db_type: "mapped".to_string(), role: "admin".to_string(), @@ -26,9 +26,7 @@ async fn list() -> anyhow::Result<()> { }, ]; expected.sort(); - let (status, list) = server - .get::>(DB_LIST_URI, &user.token) - .await?; + let (status, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(status, 200); let mut list = list?; list.sort(); @@ -44,9 +42,7 @@ async fn with_backup() -> anyhow::Result<()> { server .post(&format!("/db/{db}/backup"), &String::new(), &user.token) .await?; - let (status, list) = server - .get::>(DB_LIST_URI, &user.token) - .await?; + let (status, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(status, 200); assert_ne!(list?[0].backup, 0); @@ -57,9 +53,7 @@ async fn with_backup() -> anyhow::Result<()> { async fn list_empty() -> anyhow::Result<()> { let server = TestServer::new().await?; let user = server.init_user().await?; - let (status, list) = server - .get::>(DB_LIST_URI, &user.token) - .await?; + let (status, list) = server.get::>(DB_LIST_URI, &user.token).await?; assert_eq!(status, 200); assert_eq!(list?, vec![]); Ok(()) @@ -68,7 +62,7 @@ async fn list_empty() -> anyhow::Result<()> { #[tokio::test] async fn list_no_token() -> anyhow::Result<()> { let server = TestServer::new().await?; - let (status, list) = server.get::>(DB_LIST_URI, NO_TOKEN).await?; + let (status, list) = server.get::>(DB_LIST_URI, NO_TOKEN).await?; assert_eq!(status, 401); assert!(list.is_err()); Ok(()) diff --git a/agdb_server/tests/routes/db_optimize_test.rs b/agdb_server/tests/routes/db_optimize_test.rs index 4bff1520d..46489276f 100644 --- a/agdb_server/tests/routes/db_optimize_test.rs +++ b/agdb_server/tests/routes/db_optimize_test.rs @@ -1,5 +1,4 @@ -use crate::DbWithRole; -use crate::DbWithSize; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -15,16 +14,12 @@ async fn optimize() -> anyhow::Result<()> { server .post(&format!("/db/{db}/exec"), &queries, &user.token) .await?; - let original_size = server - .get::>(DB_LIST_URI, &user.token) - .await? - .1?[0] - .size; + let original_size = server.get::>(DB_LIST_URI, &user.token).await?.1?[0].size; let (status, response) = server .post(&format!("/db/{db}/optimize"), &String::new(), &user.token) .await?; assert_eq!(status, 200); - let optimized_size = serde_json::from_str::(&response)?.size; + let optimized_size = serde_json::from_str::(&response)?.size; assert!(optimized_size < original_size); Ok(()) diff --git a/agdb_server/tests/routes/db_rename_test.rs b/agdb_server/tests/routes/db_rename_test.rs index 5684d0f12..f98f2f195 100644 --- a/agdb_server/tests/routes/db_rename_test.rs +++ b/agdb_server/tests/routes/db_rename_test.rs @@ -1,16 +1,9 @@ -use crate::DbWithRole; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; -use serde::Serialize; use std::path::Path; -#[derive(Serialize)] -struct DbRename { - db: String, - new_name: String, -} - #[tokio::test] async fn rename() -> anyhow::Result<()> { let server = TestServer::new().await?; @@ -24,7 +17,7 @@ async fn rename() -> anyhow::Result<()> { ) .await? .0; - assert_eq!(status, 204); + assert_eq!(status, 201); assert!(!Path::new(&server.data_dir).join(db).exists()); assert!(Path::new(&server.data_dir) .join(user.name) @@ -33,6 +26,41 @@ async fn rename() -> anyhow::Result<()> { Ok(()) } +#[tokio::test] +async fn rename_with_backup() -> anyhow::Result<()> { + let server = TestServer::new().await?; + let user = server.init_user().await?; + let db = server.init_db("mapped", &user).await?; + server + .post(&format!("/db/{db}/backup"), &String::new(), &user.token) + .await?; + let status = server + .post( + &format!("/db/{db}/rename?new_name={}/renamed_db", user.name), + &String::new(), + &user.token, + ) + .await? + .0; + assert_eq!(status, 201); + assert!(!Path::new(&server.data_dir).join(&db).exists()); + assert!(!Path::new(&server.data_dir) + .join(&user.name) + .join("backups") + .join(format!("{}.bak", db.split_once('/').unwrap().1)) + .exists()); + assert!(Path::new(&server.data_dir) + .join(&user.name) + .join("renamed_db") + .exists()); + assert!(Path::new(&server.data_dir) + .join(user.name) + .join("backups") + .join("renamed_db.bak") + .exists()); + Ok(()) +} + #[tokio::test] async fn transfer() -> anyhow::Result<()> { let server = TestServer::new().await?; @@ -47,14 +75,11 @@ async fn transfer() -> anyhow::Result<()> { ) .await? .0; - assert_eq!(status, 204); - let list = server - .get::>(DB_LIST_URI, &other.token) - .await? - .1?; + assert_eq!(status, 201); + let list = server.get::>(DB_LIST_URI, &other.token).await?.1?; assert_eq!( list, - vec![DbWithRole { + vec![Db { name: format!("{}/renamed_db", other.name), db_type: "mapped".to_string(), role: "admin".to_string(), @@ -122,13 +147,13 @@ async fn db_not_found() -> anyhow::Result<()> { let user = server.init_user().await?; let status = server .post( - "/db/user/db/rename?new_name=user/renamed_db", + &format!("/db/{}/db/rename?new_name=user/renamed_db", user.name), &String::new(), &user.token, ) .await? .0; - assert_eq!(status, 403); + assert_eq!(status, 404); Ok(()) } diff --git a/agdb_server/tests/routes/db_user_add_test.rs b/agdb_server/tests/routes/db_user_add_test.rs index dc1968cff..eb9d07010 100644 --- a/agdb_server/tests/routes/db_user_add_test.rs +++ b/agdb_server/tests/routes/db_user_add_test.rs @@ -1,4 +1,4 @@ -use crate::DbWithRole; +use crate::Db; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -19,13 +19,10 @@ async fn add_db_user() -> anyhow::Result<()> { .await?, 201 ); - let list = server - .get::>(DB_LIST_URI, &other.token) - .await? - .1; + let list = server.get::>(DB_LIST_URI, &other.token).await?.1; assert_eq!( list?, - vec![DbWithRole { + vec![Db { name: db, db_type: "memory".to_string(), role: "write".to_string(), @@ -62,13 +59,10 @@ async fn change_user_role() -> anyhow::Result<()> { .await?, 201 ); - let list = server - .get::>(DB_LIST_URI, &other.token) - .await? - .1; + let list = server.get::>(DB_LIST_URI, &other.token).await?.1; assert_eq!( list?, - vec![DbWithRole { + vec![Db { name: db, db_type: "memory".to_string(), role: "read".to_string(), diff --git a/agdb_server/tests/routes/db_user_remove_test.rs b/agdb_server/tests/routes/db_user_remove_test.rs index cde91ea4f..8eb78dc43 100644 --- a/agdb_server/tests/routes/db_user_remove_test.rs +++ b/agdb_server/tests/routes/db_user_remove_test.rs @@ -1,5 +1,5 @@ +use crate::Db; use crate::DbUser; -use crate::DbWithRole; use crate::TestServer; use crate::DB_LIST_URI; use crate::NO_TOKEN; @@ -129,10 +129,7 @@ async fn remove_self() -> anyhow::Result<()> { .await?, 204 ); - let list = server - .get::>(DB_LIST_URI, &other.token) - .await? - .1; + let list = server.get::>(DB_LIST_URI, &other.token).await?.1; assert_eq!(list?, vec![]); Ok(()) } diff --git a/agdb_server/tests/routes/mod.rs b/agdb_server/tests/routes/mod.rs index 67a5fbdf5..d4bb6b2b7 100644 --- a/agdb_server/tests/routes/mod.rs +++ b/agdb_server/tests/routes/mod.rs @@ -4,6 +4,7 @@ mod admin_db_exec_test; mod admin_db_list_test; mod admin_db_optimize_test; mod admin_db_remove_test; +mod admin_db_rename_test; mod admin_db_user_add_test; mod admin_db_user_list_test; mod admin_db_user_remove_test;