diff --git a/src/handlers/http/cluster/mod.rs b/src/handlers/http/cluster/mod.rs index aac5d91f9..7a0fc3a8d 100644 --- a/src/handlers/http/cluster/mod.rs +++ b/src/handlers/http/cluster/mod.rs @@ -222,7 +222,7 @@ pub async fn get_demo_data_from_ingestor(action: &str) -> Result<(), PostError> // forward the role update request to all ingestors to keep them in sync pub async fn sync_users_with_roles_with_ingestors( - username: &str, + userid: &str, role: &HashSet, operation: &str, ) -> Result<(), RBACError> { @@ -236,7 +236,7 @@ pub async fn sync_users_with_roles_with_ingestors( RBACError::SerdeError(err) })?; - let username = username.to_owned(); + let userid = userid.to_owned(); let op = operation.to_string(); @@ -245,7 +245,7 @@ pub async fn sync_users_with_roles_with_ingestors( "{}{}/user/{}/role/sync/{}", ingestor.domain_name, base_path_without_preceding_slash(), - username, + userid, op ); @@ -253,7 +253,7 @@ pub async fn sync_users_with_roles_with_ingestors( async move { let res = INTRA_CLUSTER_CLIENT - .put(url) + .patch(url) .header(header::AUTHORIZATION, &ingestor.token) .header(header::CONTENT_TYPE, "application/json") .body(role_data) @@ -282,15 +282,15 @@ pub async fn sync_users_with_roles_with_ingestors( } // forward the delete user request to all ingestors to keep them in sync -pub async fn sync_user_deletion_with_ingestors(username: &str) -> Result<(), RBACError> { - let username = username.to_owned(); +pub async fn sync_user_deletion_with_ingestors(userid: &str) -> Result<(), RBACError> { + let userid = userid.to_owned(); for_each_live_ingestor(move |ingestor| { let url = format!( "{}{}/user/{}/sync", ingestor.domain_name, base_path_without_preceding_slash(), - username + userid ); async move { @@ -331,21 +331,21 @@ pub async fn sync_user_creation_with_ingestors( if let Some(role) = role { user.roles.clone_from(role); } - let username = user.username(); + let userid = user.userid(); let user_data = to_vec(&user).map_err(|err| { error!("Fatal: failed to serialize user: {:?}", err); RBACError::SerdeError(err) })?; - let username = username.to_string(); + let userid = userid.to_string(); for_each_live_ingestor(move |ingestor| { let url = format!( "{}{}/user/{}/sync", ingestor.domain_name, base_path_without_preceding_slash(), - username + userid ); let user_data = user_data.clone(); diff --git a/src/handlers/http/modal/ingest/ingestor_rbac.rs b/src/handlers/http/modal/ingest/ingestor_rbac.rs index 4f1862898..8eb39cda2 100644 --- a/src/handlers/http/modal/ingest/ingestor_rbac.rs +++ b/src/handlers/http/modal/ingest/ingestor_rbac.rs @@ -18,11 +18,14 @@ use std::collections::HashSet; -use actix_web::{Responder, web}; -use tokio::sync::Mutex; +use actix_web::{HttpResponse, web}; +use http::StatusCode; use crate::{ - handlers::http::{modal::utils::rbac_utils::get_metadata, rbac::RBACError}, + handlers::http::{ + modal::utils::rbac_utils::get_metadata, + rbac::{RBACError, UPDATE_LOCK}, + }, rbac::{ Users, map::roles, @@ -31,18 +34,14 @@ use crate::{ storage, }; -// async aware lock for updating storage metadata and user map atomicically -static UPDATE_LOCK: Mutex<()> = Mutex::const_new(()); - // Handler for POST /api/v1/user/{username} // Creates a new user by username if it does not exists pub async fn post_user( username: web::Path, body: Option>, -) -> Result { +) -> Result { let username = username.into_inner(); - let generated_password = String::default(); let metadata = get_metadata().await?; if let Some(body) = body { let user: ParseableUser = serde_json::from_value(body.into_inner())?; @@ -52,37 +51,38 @@ pub async fn post_user( Users.add_roles(&username, created_role.clone()); } - Ok(generated_password) + Ok(HttpResponse::Ok().status(StatusCode::OK).finish()) } -// Handler for DELETE /api/v1/user/delete/{username} -pub async fn delete_user(username: web::Path) -> Result { - let username = username.into_inner(); - let _ = UPDATE_LOCK.lock().await; +// Handler for DELETE /api/v1/user/delete/{userid} +pub async fn delete_user(userid: web::Path) -> Result { + let userid = userid.into_inner(); + let _guard = UPDATE_LOCK.lock().await; // fail this request if the user does not exists - if !Users.contains(&username) { + if !Users.contains(&userid) { return Err(RBACError::UserDoesNotExist); }; + // delete from parseable.json first let mut metadata = get_metadata().await?; - metadata.users.retain(|user| user.username() != username); + metadata.users.retain(|user| user.userid() != userid); let _ = storage::put_staging_metadata(&metadata); // update in mem table - Users.delete_user(&username); - Ok(format!("deleted user: {username}")) + Users.delete_user(&userid); + Ok(HttpResponse::Ok().status(StatusCode::OK).finish()) } -// Handler PATCH /user/{username}/role/sync/add => Add roles to a user +// Handler PATCH /user/{userid}/role/sync/add => Add roles to a user pub async fn add_roles_to_user( - username: web::Path, + userid: web::Path, roles_to_add: web::Json>, -) -> Result { - let username = username.into_inner(); +) -> Result { + let userid = userid.into_inner(); let roles_to_add = roles_to_add.into_inner(); - if !Users.contains(&username) { + if !Users.contains(&userid) { return Err(RBACError::UserDoesNotExist); }; @@ -103,7 +103,7 @@ pub async fn add_roles_to_user( if let Some(user) = metadata .users .iter_mut() - .find(|user| user.username() == username) + .find(|user| user.userid() == userid) { user.roles.extend(roles_to_add.clone()); } else { @@ -113,20 +113,19 @@ pub async fn add_roles_to_user( let _ = storage::put_staging_metadata(&metadata); // update in mem table - Users.add_roles(&username.clone(), roles_to_add.clone()); - - Ok(format!("Roles updated successfully for {username}")) + Users.add_roles(&userid.clone(), roles_to_add.clone()); + Ok(HttpResponse::Ok().status(StatusCode::OK).finish()) } -// Handler PATCH /user/{username}/role/sync/add => Add roles to a user +// Handler PATCH /user/{userid}/role/sync/remove => Remove roles to a user pub async fn remove_roles_from_user( - username: web::Path, + userid: web::Path, roles_to_remove: web::Json>, -) -> Result { - let username = username.into_inner(); +) -> Result { + let userid = userid.into_inner(); let roles_to_remove = roles_to_remove.into_inner(); - if !Users.contains(&username) { + if !Users.contains(&userid) { return Err(RBACError::UserDoesNotExist); }; @@ -143,7 +142,7 @@ pub async fn remove_roles_from_user( } // check that user actually has these roles - let user_roles: HashSet = HashSet::from_iter(Users.get_role(&username)); + let user_roles: HashSet = HashSet::from_iter(Users.get_role(&userid)); let roles_not_with_user: HashSet = HashSet::from_iter(roles_to_remove.difference(&user_roles).cloned()); @@ -153,12 +152,12 @@ pub async fn remove_roles_from_user( ))); } - // update parseable.json first + // update parseable.json in staging first let mut metadata = get_metadata().await?; if let Some(user) = metadata .users .iter_mut() - .find(|user| user.username() == username) + .find(|user| user.userid() == userid) { let diff: HashSet = HashSet::from_iter(user.roles.difference(&roles_to_remove).cloned()); @@ -170,14 +169,14 @@ pub async fn remove_roles_from_user( let _ = storage::put_staging_metadata(&metadata); // update in mem table - Users.remove_roles(&username.clone(), roles_to_remove.clone()); + Users.remove_roles(&userid.clone(), roles_to_remove.clone()); - Ok(format!("Roles updated successfully for {username}")) + Ok(HttpResponse::Ok().status(StatusCode::OK).finish()) } // Handler for POST /api/v1/user/{username}/generate-new-password // Resets password for the user to a newly generated one and returns it -pub async fn post_gen_password(username: web::Path) -> Result { +pub async fn post_gen_password(username: web::Path) -> Result { let username = username.into_inner(); let mut new_hash = String::default(); let mut metadata = get_metadata().await?; @@ -197,6 +196,5 @@ pub async fn post_gen_password(username: web::Path) -> Result = HashSet::new(); - for user_group in read_user_groups().values().cloned() { + for user_group in read_user_groups().values() { if user_group.roles.contains(&name) { - session_refresh_users.extend(user_group.users); + session_refresh_users.extend(user_group.users.iter().map(|u| u.userid().to_string())); } } // iterate over all users to see if they have this role - for user in users().values().cloned() { + for user in users().values() { if user.roles.contains(&name) { - session_refresh_users.insert(user.username().to_string()); + session_refresh_users.insert(user.userid().to_string()); } } - for username in session_refresh_users { - mut_sessions().remove_user(&username); + for userid in session_refresh_users { + mut_sessions().remove_user(&userid); } Ok(HttpResponse::Ok().finish()) diff --git a/src/handlers/http/modal/ingest_server.rs b/src/handlers/http/modal/ingest_server.rs index 04ba8f981..f939d6db1 100644 --- a/src/handlers/http/modal/ingest_server.rs +++ b/src/handlers/http/modal/ingest_server.rs @@ -184,13 +184,13 @@ impl IngestServer { web::scope("/user") .service( web::resource("/{username}/sync") - // PUT /user/{username}/sync => Sync creation of a new user + // POST /user/{username}/sync => Sync creation of a new user .route( web::post() .to(ingestor_rbac::post_user) .authorize(Action::PutUser), ) - // DELETE /user/{username} => Sync deletion of a user + // DELETE /user/{userid} => Sync deletion of a user .route( web::delete() .to(ingestor_rbac::delete_user) @@ -199,8 +199,8 @@ impl IngestServer { .wrap(DisAllowRootUser), ) .service( - web::resource("/{username}/role/sync/add") - // PATCH /user/{username}/role/sync/add => Add roles to a user + web::resource("/{userid}/role/sync/add") + // PATCH /user/{userid}/role/sync/add => Add roles to a user .route( web::patch() .to(ingestor_rbac::add_roles_to_user) @@ -209,8 +209,8 @@ impl IngestServer { ), ) .service( - web::resource("/{username}/role/sync/remove") - // PATCH /user/{username}/role/sync/remove => Remove roles from a user + web::resource("/{userid}/role/sync/remove") + // PATCH /user/{userid}/role/sync/remove => Remove roles from a user .route( web::patch() .to(ingestor_rbac::remove_roles_from_user) diff --git a/src/handlers/http/modal/query/querier_rbac.rs b/src/handlers/http/modal/query/querier_rbac.rs index c28a3c9f3..a74346418 100644 --- a/src/handlers/http/modal/query/querier_rbac.rs +++ b/src/handlers/http/modal/query/querier_rbac.rs @@ -18,8 +18,7 @@ use std::collections::HashSet; -use actix_web::{Responder, web}; -use tokio::sync::Mutex; +use actix_web::{HttpResponse, Responder, web}; use crate::{ handlers::http::{ @@ -28,19 +27,16 @@ use crate::{ sync_user_deletion_with_ingestors, sync_users_with_roles_with_ingestors, }, modal::utils::rbac_utils::{get_metadata, put_metadata}, - rbac::RBACError, + rbac::{RBACError, UPDATE_LOCK}, }, rbac::{ Users, - map::{roles, write_user_groups}, - user, + map::{roles, users, write_user_groups}, + user::{self, UserType}, }, validator, }; -// async aware lock for updating storage metadata and user map atomically -static UPDATE_LOCK: Mutex<()> = Mutex::const_new(()); - // Handler for POST /api/v1/user/{username} // Creates a new user by username if it does not exists pub async fn post_user( @@ -74,12 +70,12 @@ pub async fn post_user( return Err(RBACError::RolesDoNotExist(non_existant_roles)); } } - let _ = UPDATE_LOCK.lock().await; + let _guard = UPDATE_LOCK.lock().await; if Users.contains(&username) || metadata .users .iter() - .any(|user| user.username() == username) + .any(|user| matches!(&user.ty, UserType::Native(basic) if basic.username == username)) { return Err(RBACError::UserExists); } @@ -103,54 +99,83 @@ pub async fn post_user( Ok(password) } -// Handler for DELETE /api/v1/user/delete/{username} -pub async fn delete_user(username: web::Path) -> Result { - let username = username.into_inner(); - let _ = UPDATE_LOCK.lock().await; +// Handler for DELETE /api/v1/user/{userid} +pub async fn delete_user(userid: web::Path) -> Result { + let userid = userid.into_inner(); + + let _guard = UPDATE_LOCK.lock().await; // fail this request if the user does not exists - if !Users.contains(&username) { + if !Users.contains(&userid) { + return Err(RBACError::UserDoesNotExist); + }; + + // find username by userid, for native users, username is userid, for oauth users, we need to look up + let username = if let Some(user) = users().get(&userid) { + user.username_by_userid() + } else { return Err(RBACError::UserDoesNotExist); }; + // delete from parseable.json first let mut metadata = get_metadata().await?; - metadata.users.retain(|user| user.username() != username); + metadata.users.retain(|user| user.userid() != userid); // also delete from user groups - let user_groups = Users.get_user_groups(&username); + let user_groups = Users.get_user_groups(&userid); let mut groups_to_update = Vec::new(); for user_group in user_groups { - if let Some(ug) = write_user_groups().get_mut(&user_group) { - ug.remove_users(HashSet::from_iter([username.clone()]))?; + if let Some(ug) = write_user_groups().get_mut(&user_group) + && let Some(user) = users().get(&userid) + { + let userid = match &user.ty { + UserType::Native(basic) => basic.username.clone(), + UserType::OAuth(oauth) => oauth.userid.clone(), + }; + ug.remove_users_by_user_ids(HashSet::from_iter([userid]))?; groups_to_update.push(ug.clone()); - // ug.update_in_metadata().await?; } else { + // User not found, skip or log as needed continue; - }; + } } - // update in metadata user group - metadata - .user_groups - .retain(|x| !groups_to_update.contains(x)); - metadata.user_groups.extend(groups_to_update); + // For each updated group, replace in place if found; otherwise push + for updated_group in &groups_to_update { + if let Some(existing) = metadata + .user_groups + .iter_mut() + .find(|ug| ug.name == updated_group.name) + { + existing.clone_from(updated_group); + } else { + metadata.user_groups.push(updated_group.clone()); + } + } put_metadata(&metadata).await?; - sync_user_deletion_with_ingestors(&username).await?; + sync_user_deletion_with_ingestors(&userid).await?; // update in mem table - Users.delete_user(&username); - Ok(format!("deleted user: {username}")) + Users.delete_user(&userid); + Ok(HttpResponse::Ok().json(format!("deleted user: {username}"))) } -// Handler PATCH /user/{username}/role/add => Add roles to a user +// Handler PATCH /user/{userid}/role/add => Add roles to a user pub async fn add_roles_to_user( - username: web::Path, + userid: web::Path, roles_to_add: web::Json>, ) -> Result { - let username = username.into_inner(); + let userid = userid.into_inner(); let roles_to_add = roles_to_add.into_inner(); - if !Users.contains(&username) { + if !Users.contains(&userid) { + return Err(RBACError::UserDoesNotExist); + }; + + // find username by userid, for native users, username is userid, for oauth users, we need to look up + let username = if let Some(user) = users().get(&userid) { + user.username_by_userid() + } else { return Err(RBACError::UserDoesNotExist); }; @@ -172,7 +197,7 @@ pub async fn add_roles_to_user( if let Some(user) = metadata .users .iter_mut() - .find(|user| user.username() == username) + .find(|user| user.userid() == userid) { user.roles.extend(roles_to_add.clone()); } else { @@ -182,22 +207,31 @@ pub async fn add_roles_to_user( put_metadata(&metadata).await?; // update in mem table - Users.add_roles(&username.clone(), roles_to_add.clone()); + Users.add_roles(&userid.clone(), roles_to_add.clone()); - sync_users_with_roles_with_ingestors(&username, &roles_to_add, "add").await?; + sync_users_with_roles_with_ingestors(&userid, &roles_to_add, "add").await?; Ok(format!("Roles updated successfully for {username}")) } -// Handler PATCH /user/{username}/role/remove => Remove roles from a user +// Handler PATCH /user/{userid}/role/remove => Remove roles from a user pub async fn remove_roles_from_user( - username: web::Path, + userid: web::Path, roles_to_remove: web::Json>, -) -> Result { - let username = username.into_inner(); +) -> Result { + let userid = userid.into_inner(); let roles_to_remove = roles_to_remove.into_inner(); - if !Users.contains(&username) { + let _guard = UPDATE_LOCK.lock().await; + + if !Users.contains(&userid) { + return Err(RBACError::UserDoesNotExist); + }; + + // find username by userid, for native users, username is userid, for oauth users, we need to look up + let username = if let Some(user) = users().get(&userid) { + user.username_by_userid() + } else { return Err(RBACError::UserDoesNotExist); }; @@ -215,7 +249,7 @@ pub async fn remove_roles_from_user( } // check for role not present with user - let user_roles: HashSet = HashSet::from_iter(Users.get_role(&username)); + let user_roles: HashSet = HashSet::from_iter(Users.get_role(&userid)); let roles_not_with_user: HashSet = HashSet::from_iter(roles_to_remove.difference(&user_roles).cloned()); if !roles_not_with_user.is_empty() { @@ -229,7 +263,7 @@ pub async fn remove_roles_from_user( if let Some(user) = metadata .users .iter_mut() - .find(|user| user.username() == username) + .find(|user| user.userid() == userid) { let diff: HashSet = HashSet::from_iter(user.roles.difference(&roles_to_remove).cloned()); @@ -241,11 +275,11 @@ pub async fn remove_roles_from_user( put_metadata(&metadata).await?; // update in mem table - Users.remove_roles(&username.clone(), roles_to_remove.clone()); + Users.remove_roles(&userid.clone(), roles_to_remove.clone()); - sync_users_with_roles_with_ingestors(&username, &roles_to_remove, "remove").await?; + sync_users_with_roles_with_ingestors(&userid, &roles_to_remove, "remove").await?; - Ok(format!("Roles updated successfully for {username}")) + Ok(HttpResponse::Ok().json(format!("Roles updated successfully for {username}"))) } // Handler for POST /api/v1/user/{username}/generate-new-password @@ -256,7 +290,7 @@ pub async fn post_gen_password(username: web::Path) -> Result = HashSet::new(); - for user_group in read_user_groups().values().cloned() { + for user_group in read_user_groups().values() { if user_group.roles.contains(&name) { - session_refresh_users.extend(user_group.users); + session_refresh_users.extend(user_group.users.iter().map(|u| u.userid().to_string())); } } // iterate over all users to see if they have this role - for user in users().values().cloned() { + for user in users().values() { if user.roles.contains(&name) { - session_refresh_users.insert(user.username().to_string()); + session_refresh_users.insert(user.userid().to_string()); } } - for username in session_refresh_users { - mut_sessions().remove_user(&username); + for userid in session_refresh_users { + mut_sessions().remove_user(&userid); } sync_role_update_with_ingestors(name.clone(), privileges.clone()).await?; diff --git a/src/handlers/http/modal/query_server.rs b/src/handlers/http/modal/query_server.rs index 8533f392b..60c0b155b 100644 --- a/src/handlers/http/modal/query_server.rs +++ b/src/handlers/http/modal/query_server.rs @@ -200,7 +200,7 @@ impl QueryServer { .to(querier_rbac::post_user) .authorize(Action::PutUser), ) - // DELETE /user/{username} => Delete a user + // DELETE /user/{userid} => Delete a user .route( web::delete() .to(querier_rbac::delete_user) @@ -209,15 +209,15 @@ impl QueryServer { .wrap(DisAllowRootUser), ) .service( - web::resource("/{username}/role").route( + web::resource("/{userid}/role").route( web::get() .to(rbac::get_role) .authorize_for_user(Action::GetUserRoles), ), ) .service( - web::resource("/{username}/role/add") - // PATCH /user/{username}/role/add => Add roles to a user + web::resource("/{userid}/role/add") + // PATCH /user/{userid}/role/add => Add roles to a user .route( web::patch() .to(rbac::add_roles_to_user) @@ -226,8 +226,8 @@ impl QueryServer { ), ) .service( - web::resource("/{username}/role/remove") - // PATCH /user/{username}/role/remove => Remove roles from a user + web::resource("/{userid}/role/remove") + // PATCH /user/{userid}/role/remove => Remove roles from a user .route( web::patch() .to(rbac::remove_roles_from_user) diff --git a/src/handlers/http/oidc.rs b/src/handlers/http/oidc.rs index d54712670..84a7b79b7 100644 --- a/src/handlers/http/oidc.rs +++ b/src/handlers/http/oidc.rs @@ -38,7 +38,7 @@ use crate::{ rbac::{ self, Users, map::{DEFAULT_ROLE, SessionKey}, - user::{self, User, UserType}, + user::{self, GroupUser, User, UserType}, }, storage::{self, ObjectStorageError, StorageMetadata}, utils::actix::extract_session_key_from_req, @@ -367,7 +367,7 @@ pub async fn request_token( // put new user in metadata if does not exits // update local cache pub async fn put_user( - username: &str, + userid: &str, group: HashSet, user_info: user::UserInfo, ) -> Result { @@ -376,10 +376,10 @@ pub async fn put_user( let user = metadata .users .iter() - .find(|user| user.username() == username) + .find(|user| user.userid() == userid) .cloned() .unwrap_or_else(|| { - let user = User::new_oauth(username.to_owned(), group, user_info); + let user = User::new_oauth(userid.to_owned(), group, user_info); metadata.users.push(user.clone()); user }); @@ -395,7 +395,7 @@ pub async fn update_user_if_changed( user_info: user::UserInfo, ) -> Result { // Store the old username before modifying the user object - let old_username = user.username().to_string(); + let old_username = user.userid().to_string(); let User { ty, roles, .. } = &mut user; let UserType::OAuth(oauth_user) = ty else { unreachable!() @@ -427,17 +427,16 @@ pub async fn update_user_if_changed( if let Some(entry) = metadata .users .iter_mut() - .find(|x| x.username() == old_username) + .find(|x| x.userid() == old_username) { entry.clone_from(&user); // migrate user references inside user groups for group in metadata.user_groups.iter_mut() { - if group.users.remove(&old_username) { - group.users.insert(user.username().to_string()); - } + group.users.retain(|u| u.userid() != old_username); + group.users.insert(GroupUser::from_user(&user)); } - put_metadata(&metadata).await?; } + put_metadata(&metadata).await?; Users.delete_user(&old_username); Users.put_user(user.clone()); Ok(user) diff --git a/src/handlers/http/rbac.rs b/src/handlers/http/rbac.rs index 3bb320f4a..acf7f912b 100644 --- a/src/handlers/http/rbac.rs +++ b/src/handlers/http/rbac.rs @@ -21,16 +21,16 @@ use std::collections::{HashMap, HashSet}; use crate::{ rbac::{ self, Users, - map::{read_user_groups, roles}, + map::{read_user_groups, roles, users}, role::model::DefaultPrivilege, - user, + user::{self, UserType}, utils::to_prism_user, }, storage::ObjectStorageError, validator::{self, error::UsernameValidationError}, }; use actix_web::{ - Responder, + HttpResponse, Responder, http::header::ContentType, web::{self, Path}, }; @@ -42,8 +42,8 @@ use tokio::sync::Mutex; use super::modal::utils::rbac_utils::{get_metadata, put_metadata}; -// async aware lock for updating storage metadata and user map atomicically -static UPDATE_LOCK: Mutex<()> = Mutex::const_new(()); +// async aware lock for updating storage metadata and user map atomically +pub(crate) static UPDATE_LOCK: Mutex<()> = Mutex::const_new(()); #[derive(serde::Serialize)] struct User { @@ -59,20 +59,20 @@ impl From<&user::User> for User { }; User { - id: user.username().to_owned(), + id: user.userid().to_owned(), method, } } } // Handler for GET /api/v1/user -// returns list of all registerd users +// returns list of all registered users pub async fn list_users() -> impl Responder { web::Json(Users.collect_user::()) } /// Handler for GET /api/v1/users -/// returns list of all registerd users along with their roles and other info +/// returns list of all registered users along with their roles and other info pub async fn list_users_prism() -> impl Responder { // get all users let prism_users = rbac::map::users().values().map(to_prism_user).collect_vec(); @@ -124,12 +124,12 @@ pub async fn post_user( return Err(RBACError::RolesDoNotExist(non_existent_roles)); } } - let _ = UPDATE_LOCK.lock().await; - if Users.contains(&username) - || metadata - .users - .iter() - .any(|user| user.username() == username) + let _guard = UPDATE_LOCK.lock().await; + if Users.contains(&username) && Users.contains(&username) + || metadata.users.iter().any(|user| match &user.ty { + UserType::Native(basic) => basic.username == username, + UserType::OAuth(_) => false, // OAuth users should be created differently + }) { return Err(RBACError::UserExists); } @@ -159,7 +159,7 @@ pub async fn post_gen_password(username: web::Path) -> Result) -> Result) -> Result { - if !Users.contains(&username) { +pub async fn get_role(userid: web::Path) -> Result { + let userid = userid.into_inner(); + if !Users.contains(&userid) { return Err(RBACError::UserDoesNotExist); }; let direct_roles: HashMap> = Users - .get_role(&username) + .get_role(&userid) .iter() .filter_map(|role_name| { roles() @@ -200,7 +201,7 @@ pub async fn get_role(username: web::Path) -> Result>> = HashMap::new(); // user might be part of some user groups, fetch the roles from there as well - for user_group in Users.get_user_groups(&username) { + for user_group in Users.get_user_groups(&userid) { if let Some(group) = read_user_groups().get(&user_group) { let ug_roles: HashMap> = group .roles @@ -221,41 +222,54 @@ pub async fn get_role(username: web::Path) -> Result) -> Result { - let username = username.into_inner(); - +// Handler for DELETE /api/v1/user/delete/{userid} +pub async fn delete_user(userid: web::Path) -> Result { + let userid = userid.into_inner(); + let _guard = UPDATE_LOCK.lock().await; // if user is a part of any groups then don't allow deletion - if !Users.get_user_groups(&username).is_empty() { + if !Users.get_user_groups(&userid).is_empty() { return Err(RBACError::InvalidDeletionRequest(format!( - "User: {username} should not be a part of any groups" + "User: {userid} should not be a part of any groups" ))); } // fail this request if the user does not exists - if !Users.contains(&username) { + if !Users.contains(&userid) { + return Err(RBACError::UserDoesNotExist); + }; + + // find username by userid, for native users, username is userid, for oauth users, we need to look up + let username = if let Some(user) = users().get(&userid) { + user.username_by_userid() + } else { return Err(RBACError::UserDoesNotExist); }; - let _ = UPDATE_LOCK.lock().await; // delete from parseable.json first let mut metadata = get_metadata().await?; - metadata.users.retain(|user| user.username() != username); + metadata.users.retain(|user| user.userid() != userid); put_metadata(&metadata).await?; // update in mem table - Users.delete_user(&username); - Ok(format!("deleted user: {username}")) + Users.delete_user(&userid); + Ok(HttpResponse::Ok().json(format!("deleted user: {username}"))) } -// Handler PATCH /user/{username}/role/add => Add roles to a user +// Handler PATCH /user/{userid}/role/add => Add roles to a user pub async fn add_roles_to_user( - username: web::Path, + userid: web::Path, roles_to_add: web::Json>, -) -> Result { - let username = username.into_inner(); +) -> Result { + let userid = userid.into_inner(); let roles_to_add = roles_to_add.into_inner(); - if !Users.contains(&username) { + if !Users.contains(&userid) { + return Err(RBACError::UserDoesNotExist); + }; + + // find username by userid, for native users, username is userid, for oauth users, we need to look up + let username = if let Some(user) = users().get(&userid) { + user.username_by_userid() + } else { return Err(RBACError::UserDoesNotExist); }; @@ -277,7 +291,7 @@ pub async fn add_roles_to_user( if let Some(user) = metadata .users .iter_mut() - .find(|user| user.username() == username) + .find(|user| user.userid() == userid) { user.roles.extend(roles_to_add.clone()); } else { @@ -287,20 +301,27 @@ pub async fn add_roles_to_user( put_metadata(&metadata).await?; // update in mem table - Users.add_roles(&username.clone(), roles_to_add); + Users.add_roles(&userid.clone(), roles_to_add); - Ok(format!("Roles updated successfully for {username}")) + Ok(HttpResponse::Ok().json(format!("Roles updated successfully for {username}"))) } -// Handler PATCH /user/{username}/role/remove => Remove roles from a user +// Handler PATCH /user/{userid}/role/remove => Remove roles from a user pub async fn remove_roles_from_user( - username: web::Path, + userid: web::Path, roles_to_remove: web::Json>, -) -> Result { - let username = username.into_inner(); +) -> Result { + let userid = userid.into_inner(); let roles_to_remove = roles_to_remove.into_inner(); - if !Users.contains(&username) { + if !Users.contains(&userid) { + return Err(RBACError::UserDoesNotExist); + }; + + // find username by userid, for native users, username is userid, for oauth users, we need to look up + let username = if let Some(user) = users().get(&userid) { + user.username_by_userid() + } else { return Err(RBACError::UserDoesNotExist); }; @@ -318,7 +339,7 @@ pub async fn remove_roles_from_user( } // check for role not present with user - let user_roles: HashSet = HashSet::from_iter(Users.get_role(&username)); + let user_roles: HashSet = HashSet::from_iter(Users.get_role(&userid)); let roles_not_with_user: HashSet = HashSet::from_iter(roles_to_remove.difference(&user_roles).cloned()); if !roles_not_with_user.is_empty() { @@ -332,7 +353,7 @@ pub async fn remove_roles_from_user( if let Some(user) = metadata .users .iter_mut() - .find(|user| user.username() == username) + .find(|user| user.userid() == userid) { let diff: HashSet = HashSet::from_iter(user.roles.difference(&roles_to_remove).cloned()); @@ -344,13 +365,13 @@ pub async fn remove_roles_from_user( put_metadata(&metadata).await?; // update in mem table - Users.remove_roles(&username.clone(), roles_to_remove); + Users.remove_roles(&userid.clone(), roles_to_remove); - Ok(format!("Roles updated successfully for {username}")) + Ok(HttpResponse::Ok().json(format!("Roles updated successfully for {username}"))) } #[derive(Debug, Serialize)] -#[serde(rename = "camelCase")] +#[serde(rename_all = "camelCase")] pub struct InvalidUserGroupError { pub valid_name: bool, pub non_existent_roles: Vec, @@ -390,7 +411,7 @@ pub enum RBACError { InvalidUserGroupRequest(Box), #[error("{0}")] InvalidSyncOperation(String), - #[error("User group `{0}` is still being used")] + #[error("UserGroup `{0}` is still in use")] UserGroupNotEmpty(String), #[error("Resource `{0}` is still in use")] ResourceInUse(String), diff --git a/src/handlers/http/role.rs b/src/handlers/http/role.rs index b2462dc08..3db3a6f42 100644 --- a/src/handlers/http/role.rs +++ b/src/handlers/http/role.rs @@ -50,21 +50,21 @@ pub async fn put( // refresh the sessions of all users using this role // for this, iterate over all user_groups and users and create a hashset of users let mut session_refresh_users: HashSet = HashSet::new(); - for user_group in read_user_groups().values().cloned() { + for user_group in read_user_groups().values() { if user_group.roles.contains(&name) { - session_refresh_users.extend(user_group.users); + session_refresh_users.extend(user_group.users.iter().map(|u| u.userid().to_string())); } } // iterate over all users to see if they have this role - for user in users().values().cloned() { + for user in users().values() { if user.roles.contains(&name) { - session_refresh_users.insert(user.username().to_string()); + session_refresh_users.insert(user.userid().to_string()); } } - for username in session_refresh_users { - mut_sessions().remove_user(&username); + for userid in session_refresh_users { + mut_sessions().remove_user(&userid); } Ok(HttpResponse::Ok().finish()) diff --git a/src/rbac/map.rs b/src/rbac/map.rs index 7fd5ed703..5377d10d7 100644 --- a/src/rbac/map.rs +++ b/src/rbac/map.rs @@ -123,7 +123,7 @@ pub fn init(metadata: &StorageMetadata) { let mut users = Users::from(users); let admin = user::get_admin_user(); - let admin_username = admin.username().to_owned(); + let admin_username = admin.userid().to_owned(); users.insert(admin); let mut sessions = Sessions::default(); @@ -274,8 +274,8 @@ impl Sessions { }) } - pub fn get_username(&self, key: &SessionKey) -> Option<&String> { - self.active_sessions.get(key).map(|(username, _)| username) + pub fn get_userid(&self, key: &SessionKey) -> Option<&String> { + self.active_sessions.get(key).map(|(userid, _)| userid) } } @@ -286,7 +286,7 @@ pub struct Users(HashMap); impl Users { pub fn insert(&mut self, user: User) { - self.0.insert(user.username().to_owned(), user); + self.0.insert(user.userid().to_owned(), user); } } @@ -296,7 +296,7 @@ impl From> for Users { map.extend( users .into_iter() - .map(|user| (user.username().to_owned(), user)), + .map(|user| (user.userid().to_owned(), user)), ); map } diff --git a/src/rbac/mod.rs b/src/rbac/mod.rs index 256564b77..4eb115778 100644 --- a/src/rbac/mod.rs +++ b/src/rbac/mod.rs @@ -50,80 +50,80 @@ pub struct Users; impl Users { pub fn put_user(&self, user: User) { - mut_sessions().remove_user(user.username()); + mut_sessions().remove_user(user.userid()); mut_users().insert(user); } - pub fn get_user_groups(&self, username: &str) -> HashSet { + pub fn get_user_groups(&self, userid: &str) -> HashSet { users() - .get(username) + .get(userid) .map(|user| user.user_groups.clone()) .unwrap_or_default() } - pub fn get_user(&self, username: &str) -> Option { - users().get(username).cloned() + pub fn get_user(&self, userid: &str) -> Option { + users().get(userid).cloned() } - pub fn is_oauth(&self, username: &str) -> Option { - users().get(username).map(|user| user.is_oauth()) + pub fn is_oauth(&self, userid: &str) -> Option { + users().get(userid).map(|user| user.is_oauth()) } pub fn collect_user From<&'a User> + 'static>(&self) -> Vec { users().values().map(|user| user.into()).collect_vec() } - pub fn get_role(&self, username: &str) -> Vec { + pub fn get_role(&self, userid: &str) -> Vec { users() - .get(username) + .get(userid) .map(|user| user.roles.iter().cloned().collect()) .unwrap_or_default() } - pub fn delete_user(&self, username: &str) { - mut_users().remove(username); - mut_sessions().remove_user(username); + pub fn delete_user(&self, userid: &str) { + mut_users().remove(userid); + mut_sessions().remove_user(userid); } // caller ensures that this operation is valid for the user - pub fn change_password_hash(&self, username: &str, hash: &String) { + pub fn change_password_hash(&self, userid: &str, hash: &String) { if let Some(User { ty: UserType::Native(user), .. - }) = mut_users().get_mut(username) + }) = mut_users().get_mut(userid) { user.password_hash.clone_from(hash); - mut_sessions().remove_user(username); + mut_sessions().remove_user(userid); }; } - pub fn add_roles(&self, username: &str, roles: HashSet) { - if let Some(user) = mut_users().get_mut(username) { + pub fn add_roles(&self, userid: &str, roles: HashSet) { + if let Some(user) = mut_users().get_mut(userid) { user.roles.extend(roles); - mut_sessions().remove_user(username) + mut_sessions().remove_user(userid) }; } - pub fn remove_roles(&self, username: &str, roles: HashSet) { - if let Some(user) = mut_users().get_mut(username) { + pub fn remove_roles(&self, userid: &str, roles: HashSet) { + if let Some(user) = mut_users().get_mut(userid) { let diff = HashSet::from_iter(user.roles.difference(&roles).cloned()); user.roles = diff; - mut_sessions().remove_user(username) + mut_sessions().remove_user(userid) }; } - pub fn contains(&self, username: &str) -> bool { - users().contains_key(username) + pub fn contains(&self, userid: &str) -> bool { + users().contains_key(userid) } pub fn get_permissions(&self, session: &SessionKey) -> Vec { let mut permissions = sessions().get(session).cloned().unwrap_or_default(); - let Some(username) = self.get_username_from_session(session) else { + let Some(userid) = self.get_userid_from_session(session) else { return permissions.into_iter().collect_vec(); }; - let user_groups = self.get_user_groups(&username); + let user_groups = self.get_user_groups(&userid); for group in user_groups { if let Some(group) = read_user_groups().get(&group) { let group_roles = &group.roles; @@ -149,7 +149,7 @@ impl Users { pub fn new_session(&self, user: &User, session: SessionKey) { mut_sessions().track_new( - user.username().to_owned(), + user.userid().to_owned(), session, Utc::now() + Days::new(7), roles_to_permission(user.roles()), @@ -199,8 +199,8 @@ impl Users { Response::UnAuthorized } - pub fn get_username_from_session(&self, session: &SessionKey) -> Option { - sessions().get_username(session).cloned() + pub fn get_userid_from_session(&self, session: &SessionKey) -> Option { + sessions().get_userid(session).cloned() } } diff --git a/src/rbac/user.rs b/src/rbac/user.rs index f901d25e9..300bf90d9 100644 --- a/src/rbac/user.rs +++ b/src/rbac/user.rs @@ -66,14 +66,10 @@ impl User { ) } - pub fn new_oauth(username: String, roles: HashSet, user_info: UserInfo) -> Self { + pub fn new_oauth(userid: String, roles: HashSet, user_info: UserInfo) -> Self { Self { ty: UserType::OAuth(OAuth { - userid: user_info - .sub - .clone() - .or_else(|| user_info.name.clone()) - .unwrap_or(username), + userid: user_info.sub.clone().unwrap_or(userid), user_info, }), roles, @@ -81,13 +77,25 @@ impl User { } } - pub fn username(&self) -> &str { + pub fn userid(&self) -> &str { match self.ty { UserType::Native(Basic { ref username, .. }) => username, - UserType::OAuth(OAuth { - userid: ref username, - .. - }) => username, + UserType::OAuth(OAuth { ref userid, .. }) => userid, + } + } + + pub fn username_by_userid(&self) -> String { + match &self.ty { + UserType::Native(basic) => basic.username.clone(), + UserType::OAuth(oauth) => { + let user_info = oauth.user_info.clone(); + user_info.name.clone().unwrap_or_else(|| { + user_info + .email + .clone() + .unwrap_or_else(|| oauth.userid.clone()) + }) + } } } @@ -203,6 +211,68 @@ impl From for UserInfo { } } +/// Represents a user in a UserGroup - simplified structure for both user types +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct GroupUser { + pub userid: String, + pub username: String, + pub method: String, +} + +impl PartialEq for GroupUser { + fn eq(&self, other: &Self) -> bool { + self.userid == other.userid + } +} +impl Eq for GroupUser {} +impl std::hash::Hash for GroupUser { + fn hash(&self, state: &mut H) { + self.userid.hash(state) + } +} + +impl GroupUser { + pub fn username(&self) -> &str { + &self.username + } + + pub fn userid(&self) -> &str { + &self.userid + } + + pub fn is_oauth(&self) -> bool { + self.method == "oauth" + } + + pub fn user_type(&self) -> &str { + if self.is_oauth() { "oauth" } else { "native" } + } + + pub fn from_user(user: &User) -> Self { + match &user.ty { + UserType::Native(Basic { username, .. }) => GroupUser { + userid: username.clone(), // Same value for basic users + username: username.clone(), + method: "native".to_string(), + }, + UserType::OAuth(OAuth { userid, user_info }) => { + // For OAuth users, derive the display username from user_info + let display_username = user_info + .name + .clone() + .or_else(|| user_info.email.clone()) + .unwrap_or_else(|| userid.clone()); // fallback to userid if nothing else available + + GroupUser { + userid: userid.clone(), + username: display_username, + method: "oauth".to_string(), + } + } + } + } +} + /// Logically speaking, UserGroup is a collection of roles and is applied to a collection of users. /// /// The users present in a group inherit all the roles present in the group for as long as they are a part of the group. @@ -212,7 +282,7 @@ pub struct UserGroup { // #[serde(default = "crate::utils::uid::gen")] // pub id: Ulid, pub roles: HashSet, - pub users: HashSet, + pub users: HashSet, } fn is_valid_group_name(name: &str) -> bool { @@ -239,9 +309,9 @@ impl UserGroup { let mut non_existent_users = Vec::new(); if !self.users.is_empty() { // validate that the users exist - for user in &self.users { - if !users().contains_key(user) { - non_existent_users.push(user.clone()); + for group_user in &self.users { + if !users().contains_key(group_user.userid()) { + non_existent_users.push(group_user.userid().to_string()); } } } @@ -266,7 +336,7 @@ impl UserGroup { Ok(()) } } - pub fn new(name: String, roles: HashSet, users: HashSet) -> Self { + pub fn new(name: String, roles: HashSet, users: HashSet) -> Self { UserGroup { name, roles, users } } @@ -276,20 +346,20 @@ impl UserGroup { } self.roles.extend(roles); // also refresh all user sessions - for username in &self.users { - mut_sessions().remove_user(username); + for group_user in &self.users { + mut_sessions().remove_user(group_user.userid()); } Ok(()) } - pub fn add_users(&mut self, users: HashSet) -> Result<(), RBACError> { + pub fn add_users(&mut self, users: HashSet) -> Result<(), RBACError> { if users.is_empty() { return Ok(()); } self.users.extend(users.clone()); // also refresh all user sessions - for username in &users { - mut_sessions().remove_user(username); + for group_user in &users { + mut_sessions().remove_user(group_user.userid()); } Ok(()) } @@ -307,30 +377,58 @@ impl UserGroup { self.roles.clone_from(&new_roles); // also refresh all user sessions - for username in &self.users { - mut_sessions().remove_user(username); + for group_user in &self.users { + mut_sessions().remove_user(group_user.userid()); } Ok(()) } - pub fn remove_users(&mut self, users: HashSet) -> Result<(), RBACError> { + pub fn remove_users(&mut self, users: HashSet) -> Result<(), RBACError> { if users.is_empty() { return Ok(()); } let new_users = HashSet::from_iter(self.users.difference(&users).cloned()); - let removed_users: HashSet = self.users.intersection(&users).cloned().collect(); + let removed_users: HashSet = self.users.intersection(&users).cloned().collect(); if removed_users.is_empty() { return Ok(()); } // also refresh all user sessions - for username in &removed_users { - mut_sessions().remove_user(username); + for group_user in &removed_users { + mut_sessions().remove_user(group_user.userid()); } self.users.clone_from(&new_users); Ok(()) } + /// Get all user IDs in this group + pub fn get_userids(&self) -> Vec { + self.users.iter().map(|u| u.userid().to_string()).collect() + } + + /// Add users by converting from User references + pub fn add_users_from_user_refs(&mut self, user_refs: &[&User]) -> Result<(), RBACError> { + let group_users: HashSet = + user_refs.iter().map(|u| GroupUser::from_user(u)).collect(); + self.add_users(group_users) + } + + /// Remove users by user ID + pub fn remove_users_by_user_ids(&mut self, user_ids: HashSet) -> Result<(), RBACError> { + if user_ids.is_empty() { + return Ok(()); + } + + let users_to_remove: HashSet = self + .users + .iter() + .filter(|u| user_ids.contains(u.userid())) + .cloned() + .collect(); + + self.remove_users(users_to_remove) + } + pub async fn update_in_metadata(&self) -> Result<(), RBACError> { let mut metadata = get_metadata().await?; metadata.user_groups.retain(|x| x.name != self.name); diff --git a/src/rbac/utils.rs b/src/rbac/utils.rs index 55223f691..83ac17e26 100644 --- a/src/rbac/utils.rs +++ b/src/rbac/utils.rs @@ -30,12 +30,12 @@ use super::{ pub fn to_prism_user(user: &User) -> UsersPrism { let (id, username, method, email, picture) = match &user.ty { - UserType::Native(_) => (user.username(), user.username(), "native", None, None), + UserType::Native(_) => (user.userid(), user.userid(), "native", None, None), UserType::OAuth(oauth) => { - let username = user.username(); - let display_name = oauth.user_info.name.as_deref().unwrap_or(username); + let userid = user.userid(); + let display_name = oauth.user_info.name.as_deref().unwrap_or(userid); ( - username, + userid, display_name, "oauth", oauth.user_info.email.clone(), @@ -56,7 +56,7 @@ pub fn to_prism_user(user: &User) -> UsersPrism { let mut group_roles: HashMap>> = HashMap::new(); let mut user_groups = HashSet::new(); // user might be part of some user groups, fetch the roles from there as well - for user_group in Users.get_user_groups(user.username()) { + for user_group in Users.get_user_groups(user.userid()) { if let Some(group) = read_user_groups().get(&user_group) { let ug_roles: HashMap> = group .roles diff --git a/src/utils/mod.rs b/src/utils/mod.rs index bf24e0277..fc3290ab4 100644 --- a/src/utils/mod.rs +++ b/src/utils/mod.rs @@ -59,7 +59,7 @@ pub fn extract_datetime(path: &str) -> Option { pub fn get_user_from_request(req: &HttpRequest) -> Result { let session_key = extract_session_key_from_req(req).unwrap(); - let user_id = Users.get_username_from_session(&session_key); + let user_id = Users.get_userid_from_session(&session_key); if user_id.is_none() { return Err(RBACError::UserDoesNotExist); }