diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index 2547f08203..97d0c63fdf 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -418,3 +418,15 @@ pub struct ListProfilesForUserInOrgAndMerchantAccountResponse { pub profile_id: id_type::ProfileId, pub profile_name: String, } + +#[derive(Debug, serde::Serialize)] +pub struct ListUsersInEntityResponse { + pub email: pii::Email, + pub roles: Vec, +} + +#[derive(Debug, serde::Serialize, Clone)] +pub struct MinimalRoleInfo { + pub role_id: String, + pub role_name: String, +} diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index dd935b9a50..d221a95c25 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -3094,6 +3094,7 @@ pub enum ApiVersion { strum::Display, strum::EnumString, ToSchema, + Hash, )] #[router_derive::diesel_enum(storage_type = "text")] #[strum(serialize_all = "snake_case")] diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 1f4353f25d..f59fedf5c4 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -142,3 +142,10 @@ pub const MAX_ALLOWED_MERCHANT_NAME_LENGTH: usize = 64; /// Default locale pub const DEFAULT_LOCALE: &str = "en"; + +/// Role ID for Org Admin +pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; +/// Role ID for Internal View Only +pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only"; +/// Role ID for Internal Admin +pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin"; diff --git a/crates/diesel_models/src/query/user_role.rs b/crates/diesel_models/src/query/user_role.rs index 7904d3c264..85f866f2fc 100644 --- a/crates/diesel_models/src/query/user_role.rs +++ b/crates/diesel_models/src/query/user_role.rs @@ -185,7 +185,7 @@ impl UserRole { .await } - pub async fn generic_user_roles_list( + pub async fn generic_user_roles_list_for_user( conn: &PgPooledConn, user_id: String, org_id: Option, @@ -235,4 +235,50 @@ impl UserRole { }, } } + + pub async fn generic_user_roles_list_for_org_and_extra( + conn: &PgPooledConn, + user_id: Option, + org_id: id_type::OrganizationId, + merchant_id: Option, + profile_id: Option, + version: Option, + ) -> StorageResult> { + let mut query = ::table() + .filter(dsl::org_id.eq(org_id)) + .into_boxed(); + + if let Some(user_id) = user_id { + query = query.filter(dsl::user_id.eq(user_id)); + } + + if let Some(merchant_id) = merchant_id { + query = query.filter(dsl::merchant_id.eq(merchant_id)); + } + + if let Some(profile_id) = profile_id { + query = query.filter(dsl::profile_id.eq(profile_id)); + } + + if let Some(version) = version { + query = query.filter(dsl::version.eq(version)); + } + + router_env::logger::debug!(query = %debug_query::(&query).to_string()); + + match generics::db_metrics::track_database_call::( + query.get_results_async(conn), + generics::db_metrics::DatabaseOperation::Filter, + ) + .await + { + Ok(value) => Ok(value), + Err(err) => match err { + DieselError::NotFound => { + Err(report!(err)).change_context(errors::DatabaseError::NotFound) + } + _ => Err(report!(err)).change_context(errors::DatabaseError::Others), + }, + } + } } diff --git a/crates/diesel_models/src/user_role.rs b/crates/diesel_models/src/user_role.rs index b508c86211..ac7def5ec2 100644 --- a/crates/diesel_models/src/user_role.rs +++ b/crates/diesel_models/src/user_role.rs @@ -1,11 +1,13 @@ +use std::hash::Hash; + use common_enums::EntityType; -use common_utils::id_type; +use common_utils::{consts, id_type}; use diesel::{AsChangeset, Identifiable, Insertable, Queryable, Selectable}; use time::PrimitiveDateTime; use crate::{enums, schema::user_roles}; -#[derive(Clone, Debug, Identifiable, Queryable, Selectable)] +#[derive(Clone, Debug, Identifiable, Queryable, Selectable, Eq)] #[diesel(table_name = user_roles, check_for_backend(diesel::pg::Pg))] pub struct UserRole { pub id: i32, @@ -24,6 +26,55 @@ pub struct UserRole { pub version: enums::UserRoleVersion, } +fn get_entity_id_and_type(user_role: &UserRole) -> (Option, Option) { + match (user_role.version, user_role.role_id.as_str()) { + (enums::UserRoleVersion::V1, consts::ROLE_ID_ORGANIZATION_ADMIN) => ( + user_role + .org_id + .clone() + .map(|org_id| org_id.get_string_repr().to_string()), + Some(EntityType::Organization), + ), + (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER) + | (enums::UserRoleVersion::V1, consts::ROLE_ID_INTERNAL_ADMIN) => ( + user_role + .merchant_id + .clone() + .map(|merchant_id| merchant_id.get_string_repr().to_string()), + Some(EntityType::Internal), + ), + (enums::UserRoleVersion::V1, _) => ( + user_role + .merchant_id + .clone() + .map(|merchant_id| merchant_id.get_string_repr().to_string()), + Some(EntityType::Merchant), + ), + (enums::UserRoleVersion::V2, _) => (user_role.entity_id.clone(), user_role.entity_type), + } +} + +impl Hash for UserRole { + fn hash(&self, state: &mut H) { + let (entity_id, entity_type) = get_entity_id_and_type(self); + + self.user_id.hash(state); + entity_id.hash(state); + entity_type.hash(state); + } +} + +impl PartialEq for UserRole { + fn eq(&self, other: &Self) -> bool { + let (self_entity_id, self_entity_type) = get_entity_id_and_type(self); + let (other_entity_id, other_entity_type) = get_entity_id_and_type(other); + + self.user_id == other.user_id + && self_entity_id == other_entity_id + && self_entity_type == other_entity_type + } +} + #[derive(router_derive::Setter, Clone, Debug, Insertable, router_derive::DebugAsDisplay)] #[diesel(table_name = user_roles)] pub struct UserRoleNew { diff --git a/crates/router/src/consts/user_role.rs b/crates/router/src/consts/user_role.rs index 0a5d6556a1..672e583001 100644 --- a/crates/router/src/consts/user_role.rs +++ b/crates/router/src/consts/user_role.rs @@ -1,8 +1,5 @@ // User Roles -pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only"; -pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin"; pub const ROLE_ID_MERCHANT_ADMIN: &str = "merchant_admin"; -pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin"; pub const ROLE_ID_MERCHANT_VIEW_ONLY: &str = "merchant_view_only"; pub const ROLE_ID_MERCHANT_IAM_ADMIN: &str = "merchant_iam_admin"; pub const ROLE_ID_MERCHANT_DEVELOPER: &str = "merchant_developer"; diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 1cd37679ad..e3c4bedab7 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -88,6 +88,8 @@ pub enum UserErrors { AuthConfigParsingError, #[error("Invalid SSO request")] SSOFailed, + #[error("profile_id missing in JWT")] + JwtProfileIdMissing, } impl common_utils::errors::ErrorSwitch for UserErrors { @@ -224,6 +226,9 @@ impl common_utils::errors::ErrorSwitch { AER::BadRequest(ApiError::new(sub_code, 46, self.get_error_message(), None)) } + Self::JwtProfileIdMissing => { + AER::Unauthorized(ApiError::new(sub_code, 47, self.get_error_message(), None)) + } } } } @@ -271,6 +276,7 @@ impl UserErrors { Self::InvalidUserAuthMethodOperation => "Invalid user auth method operation", Self::AuthConfigParsingError => "Auth config parsing error", Self::SSOFailed => "Invalid SSO request", + Self::JwtProfileIdMissing => "profile_id missing in JWT", } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 9510ddd632..17ebd00a99 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -62,7 +62,7 @@ pub async fn signup_with_merchant_id( let user_role = new_user .insert_user_role_in_db( state.clone(), - consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), UserStatus::Active, ) .await?; @@ -134,7 +134,7 @@ pub async fn signup( let user_role = new_user .insert_user_role_in_db( state.clone(), - consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), UserStatus::Active, ) .await?; @@ -165,7 +165,7 @@ pub async fn signup_token_only_flow( let user_role = new_user .insert_user_role_in_db( state.clone(), - consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), UserStatus::Active, ) .await?; @@ -316,7 +316,7 @@ pub async fn connect_account( let user_role = new_user .insert_user_role_in_db( state.clone(), - consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), UserStatus::Active, ) .await?; @@ -1310,7 +1310,7 @@ pub async fn create_internal_user( new_user .insert_user_role_in_db( state, - consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), + common_utils::consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), UserStatus::Active, ) .await?; @@ -1389,7 +1389,7 @@ pub async fn switch_merchant_id( } else { let user_roles = state .store - .list_user_roles_by_user_id(&user_from_token.user_id, UserRoleVersion::V1) + .list_user_roles_by_user_id_and_version(&user_from_token.user_id, UserRoleVersion::V1) .await .change_context(UserErrors::InternalServerError)?; @@ -1450,7 +1450,7 @@ pub async fn create_merchant_account( let role_insertion_res = new_user .insert_user_role_in_db( state.clone(), - consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), UserStatus::Active, ) .await; @@ -1471,7 +1471,10 @@ pub async fn list_merchants_for_user( ) -> UserResponse> { let user_roles = state .store - .list_user_roles_by_user_id(user_from_token.user_id.as_str(), UserRoleVersion::V1) + .list_user_roles_by_user_id_and_version( + user_from_token.user_id.as_str(), + UserRoleVersion::V1, + ) .await .change_context(UserErrors::InternalServerError)?; @@ -2572,7 +2575,7 @@ pub async fn list_orgs_for_user( ) -> UserResponse> { let orgs = state .store - .list_user_roles( + .list_user_roles_by_user_id( user_from_token.user_id.as_str(), None, None, @@ -2638,7 +2641,7 @@ pub async fn list_merchants_for_user_in_org( } else { let merchant_ids = state .store - .list_user_roles( + .list_user_roles_by_user_id( user_from_token.user_id.as_str(), Some(&user_from_token.org_id), None, @@ -2721,7 +2724,7 @@ pub async fn list_profiles_for_user_in_org_and_merchant_account( } else { let profile_ids = state .store - .list_user_roles( + .list_user_roles_by_user_id( user_from_token.user_id.as_str(), Some(&user_from_token.org_id), Some(&user_from_token.merchant_id), @@ -2793,7 +2796,7 @@ pub async fn switch_org_for_user( let user_role = state .store - .list_user_roles( + .list_user_roles_by_user_id( &user_from_token.user_id, Some(&request.org_id), None, @@ -3012,7 +3015,7 @@ pub async fn switch_merchant_for_user_in_org( EntityType::Merchant | EntityType::Profile => { let user_role = state .store - .list_user_roles( + .list_user_roles_by_user_id( &user_from_token.user_id, Some(&user_from_token.org_id), Some(&request.merchant_id), @@ -3152,7 +3155,7 @@ pub async fn switch_profile_for_user_in_org_and_merchant( EntityType::Profile => { let user_role = state .store - .list_user_roles( + .list_user_roles_by_user_id( &user_from_token.user_id, Some(&user_from_token.org_id), Some(&user_from_token.merchant_id), diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index b599a26fdf..971b34b066 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use api_models::{user as user_api, user_role as user_role_api}; use diesel_models::{ @@ -10,6 +10,7 @@ use once_cell::sync::Lazy; use crate::{ core::errors::{StorageErrorExt, UserErrors, UserResponse}, + db::user_role::ListUserRolesByOrgIdPayload, routes::{app::ReqState, SessionState}, services::{ authentication as auth, @@ -20,7 +21,7 @@ use crate::{ utils, }; pub mod role; -use common_enums::PermissionGroup; +use common_enums::{EntityType, PermissionGroup}; use strum::IntoEnumIterator; // TODO: To be deprecated once groups are stable @@ -618,13 +619,13 @@ pub async fn delete_user_role( // Check if user has any more role associations let user_roles_v2 = state .store - .list_user_roles_by_user_id(user_from_db.get_user_id(), UserRoleVersion::V2) + .list_user_roles_by_user_id_and_version(user_from_db.get_user_id(), UserRoleVersion::V2) .await .change_context(UserErrors::InternalServerError)?; let user_roles_v1 = state .store - .list_user_roles_by_user_id(user_from_db.get_user_id(), UserRoleVersion::V1) + .list_user_roles_by_user_id_and_version(user_from_db.get_user_id(), UserRoleVersion::V1) .await .change_context(UserErrors::InternalServerError)?; @@ -641,3 +642,135 @@ pub async fn delete_user_role( auth::blacklist::insert_user_in_blacklist(&state, user_from_db.get_user_id()).await?; Ok(ApplicationResponse::StatusOk) } + +pub async fn list_users_in_lineage( + state: SessionState, + user_from_token: auth::UserFromToken, +) -> UserResponse> { + let requestor_role_info = roles::RoleInfo::from_role_id( + &state, + &user_from_token.role_id, + &user_from_token.merchant_id, + &user_from_token.org_id, + ) + .await + .change_context(UserErrors::InternalServerError)?; + + let user_roles_set: HashSet<_> = match requestor_role_info.get_entity_type() { + EntityType::Organization => state + .store + .list_user_roles_by_org_id(ListUserRolesByOrgIdPayload { + user_id: None, + org_id: &user_from_token.org_id, + merchant_id: None, + profile_id: None, + version: None, + }) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .collect(), + EntityType::Merchant => state + .store + .list_user_roles_by_org_id(ListUserRolesByOrgIdPayload { + user_id: None, + org_id: &user_from_token.org_id, + merchant_id: Some(&user_from_token.merchant_id), + profile_id: None, + version: None, + }) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .collect(), + EntityType::Profile => { + let Some(profile_id) = user_from_token.profile_id.as_ref() else { + return Err(UserErrors::JwtProfileIdMissing.into()); + }; + + state + .store + .list_user_roles_by_org_id(ListUserRolesByOrgIdPayload { + user_id: None, + org_id: &user_from_token.org_id, + merchant_id: Some(&user_from_token.merchant_id), + profile_id: Some(profile_id), + version: None, + }) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .collect() + } + EntityType::Internal => HashSet::new(), + }; + + let mut email_map = state + .global_store + .find_users_by_user_ids( + user_roles_set + .iter() + .map(|user_role| user_role.user_id.clone()) + .collect(), + ) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .map(|user| (user.user_id.clone(), user.email)) + .collect::>(); + + let role_info_map = + futures::future::try_join_all(user_roles_set.iter().map(|user_role| async { + roles::RoleInfo::from_role_id( + &state, + &user_role.role_id, + &user_from_token.merchant_id, + &user_from_token.org_id, + ) + .await + .map(|role_info| { + ( + user_role.role_id.clone(), + user_api::MinimalRoleInfo { + role_id: user_role.role_id.clone(), + role_name: role_info.get_role_name().to_string(), + }, + ) + }) + })) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .collect::>(); + + let user_role_map = user_roles_set + .into_iter() + .fold(HashMap::new(), |mut map, user_role| { + map.entry(user_role.user_id) + .or_insert(Vec::with_capacity(1)) + .push(user_role.role_id); + map + }); + + Ok(ApplicationResponse::Json( + user_role_map + .into_iter() + .map(|(user_id, role_id_vec)| { + Ok::<_, error_stack::Report>(user_api::ListUsersInEntityResponse { + email: email_map + .remove(&user_id) + .ok_or(UserErrors::InternalServerError)?, + roles: role_id_vec + .into_iter() + .map(|role_id| { + role_info_map + .get(&role_id) + .cloned() + .ok_or(UserErrors::InternalServerError) + }) + .collect::, _>>()?, + }) + }) + .collect::, _>>()?, + )) +} diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index a286180e20..d79b313642 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -5,7 +5,6 @@ use diesel_models::role::{RoleNew, RoleUpdate}; use error_stack::{report, ResultExt}; use crate::{ - consts, core::errors::{StorageErrorExt, UserErrors, UserResponse}, routes::{app::ReqState, SessionState}, services::{ @@ -72,7 +71,7 @@ pub async fn create_role( .await?; if matches!(req.role_scope, RoleScope::Organization) - && user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN + && user_from_token.role_id != common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN { return Err(report!(UserErrors::InvalidRoleOperation)) .attach_printable("Non org admin user creating org level role"); @@ -292,7 +291,7 @@ pub async fn update_role( .to_not_found_response(UserErrors::InvalidRoleOperation)?; if matches!(role_info.get_scope(), RoleScope::Organization) - && user_from_token.role_id != consts::user_role::ROLE_ID_ORGANIZATION_ADMIN + && user_from_token.role_id != common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN { return Err(report!(UserErrors::InvalidRoleOperation)) .attach_printable("Non org admin user changing org level role"); diff --git a/crates/router/src/db/kafka_store.rs b/crates/router/src/db/kafka_store.rs index 403b79a037..0d8b753463 100644 --- a/crates/router/src/db/kafka_store.rs +++ b/crates/router/src/db/kafka_store.rs @@ -35,7 +35,7 @@ use super::{ user::{sample_data::BatchSampleDataInterface, UserInterface}, user_authentication_method::UserAuthenticationMethodInterface, user_key_store::UserKeyStoreInterface, - user_role::UserRoleInterface, + user_role::{ListUserRolesByOrgIdPayload, UserRoleInterface}, }; #[cfg(feature = "payouts")] use crate::services::kafka::payout::KafkaPayout; @@ -2792,13 +2792,13 @@ impl UserRoleInterface for KafkaStore { .await } - async fn list_user_roles_by_user_id( + async fn list_user_roles_by_user_id_and_version( &self, user_id: &str, version: enums::UserRoleVersion, ) -> CustomResult, errors::StorageError> { self.diesel_store - .list_user_roles_by_user_id(user_id, version) + .list_user_roles_by_user_id_and_version(user_id, version) .await } @@ -2871,7 +2871,7 @@ impl UserRoleInterface for KafkaStore { .await } - async fn list_user_roles( + async fn list_user_roles_by_user_id( &self, user_id: &str, org_id: Option<&id_type::OrganizationId>, @@ -2881,9 +2881,23 @@ impl UserRoleInterface for KafkaStore { version: Option, ) -> CustomResult, errors::StorageError> { self.diesel_store - .list_user_roles(user_id, org_id, merchant_id, profile_id, entity_id, version) + .list_user_roles_by_user_id( + user_id, + org_id, + merchant_id, + profile_id, + entity_id, + version, + ) .await } + + async fn list_user_roles_by_org_id<'a>( + &self, + payload: ListUserRolesByOrgIdPayload<'a>, + ) -> CustomResult, errors::StorageError> { + self.diesel_store.list_user_roles_by_org_id(payload).await + } } #[async_trait::async_trait] diff --git a/crates/router/src/db/user_role.rs b/crates/router/src/db/user_role.rs index 524eaaaab7..8d07f53e12 100644 --- a/crates/router/src/db/user_role.rs +++ b/crates/router/src/db/user_role.rs @@ -30,7 +30,7 @@ pub trait UserRoleInterface { version: enums::UserRoleVersion, ) -> CustomResult; - async fn list_user_roles_by_user_id( + async fn list_user_roles_by_user_id_and_version( &self, user_id: &str, version: enums::UserRoleVersion, @@ -70,7 +70,7 @@ pub trait UserRoleInterface { version: enums::UserRoleVersion, ) -> CustomResult; - async fn list_user_roles( + async fn list_user_roles_by_user_id( &self, user_id: &str, org_id: Option<&id_type::OrganizationId>, @@ -79,6 +79,19 @@ pub trait UserRoleInterface { entity_id: Option<&String>, version: Option, ) -> CustomResult, errors::StorageError>; + + async fn list_user_roles_by_org_id<'a>( + &self, + payload: ListUserRolesByOrgIdPayload<'a>, + ) -> CustomResult, errors::StorageError>; +} + +pub struct ListUserRolesByOrgIdPayload<'a> { + pub user_id: Option<&'a String>, + pub org_id: &'a id_type::OrganizationId, + pub merchant_id: Option<&'a id_type::MerchantId>, + pub profile_id: Option<&'a id_type::ProfileId>, + pub version: Option, } #[async_trait::async_trait] @@ -126,7 +139,7 @@ impl UserRoleInterface for Store { } #[instrument(skip_all)] - async fn list_user_roles_by_user_id( + async fn list_user_roles_by_user_id_and_version( &self, user_id: &str, version: enums::UserRoleVersion, @@ -217,7 +230,7 @@ impl UserRoleInterface for Store { .map_err(|error| report!(errors::StorageError::from(error))) } - async fn list_user_roles( + async fn list_user_roles_by_user_id( &self, user_id: &str, org_id: Option<&id_type::OrganizationId>, @@ -227,7 +240,7 @@ impl UserRoleInterface for Store { version: Option, ) -> CustomResult, errors::StorageError> { let conn = connection::pg_connection_read(self).await?; - storage::UserRole::generic_user_roles_list( + storage::UserRole::generic_user_roles_list_for_user( &conn, user_id.to_owned(), org_id.cloned(), @@ -239,6 +252,23 @@ impl UserRoleInterface for Store { .await .map_err(|error| report!(errors::StorageError::from(error))) } + + async fn list_user_roles_by_org_id<'a>( + &self, + payload: ListUserRolesByOrgIdPayload<'a>, + ) -> CustomResult, errors::StorageError> { + let conn = connection::pg_connection_read(self).await?; + storage::UserRole::generic_user_roles_list_for_org_and_extra( + &conn, + payload.user_id.cloned(), + payload.org_id.to_owned(), + payload.merchant_id.cloned(), + payload.profile_id.cloned(), + payload.version, + ) + .await + .map_err(|error| report!(errors::StorageError::from(error))) + } } #[async_trait::async_trait] @@ -328,7 +358,7 @@ impl UserRoleInterface for MockDb { .into()) } - async fn list_user_roles_by_user_id( + async fn list_user_roles_by_user_id_and_version( &self, user_id: &str, version: enums::UserRoleVersion, @@ -505,7 +535,7 @@ impl UserRoleInterface for MockDb { } } - async fn list_user_roles( + async fn list_user_roles_by_user_id( &self, user_id: &str, org_id: Option<&id_type::OrganizationId>, @@ -551,4 +581,48 @@ impl UserRoleInterface for MockDb { Ok(filtered_roles) } + + async fn list_user_roles_by_org_id<'a>( + &self, + payload: ListUserRolesByOrgIdPayload<'a>, + ) -> CustomResult, errors::StorageError> { + let user_roles = self.user_roles.lock().await; + + let mut filtered_roles = Vec::new(); + + for role in user_roles.iter() { + let role_org_id = role + .org_id + .as_ref() + .ok_or(report!(errors::StorageError::MockDbError))?; + + let mut filter_condition = role_org_id == payload.org_id; + + if let Some(user_id) = payload.user_id { + filter_condition = filter_condition && user_id == &role.user_id + } + + role.merchant_id.as_ref().zip(payload.merchant_id).inspect( + |(role_merchant_id, merchant_id)| { + filter_condition = filter_condition && role_merchant_id == merchant_id + }, + ); + + role.profile_id.as_ref().zip(payload.profile_id).inspect( + |(role_profile_id, profile_id)| { + filter_condition = filter_condition && role_profile_id == profile_id + }, + ); + + payload + .version + .inspect(|ver| filter_condition = filter_condition && ver == &role.version); + + if filter_condition { + filtered_roles.push(role.clone()) + } + } + + Ok(filtered_roles) + } } diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 97042485d6..32bcc34d6b 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -1799,6 +1799,7 @@ impl User { .service( web::resource("/list").route(web::get().to(list_users_for_merchant_account)), ) + .service(web::resource("/v2/list").route(web::get().to(list_users_in_lineage))) .service( web::resource("/invite_multiple").route(web::post().to(invite_multiple_user)), ) diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index f64fab1f40..f2a1d7e345 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -263,7 +263,8 @@ impl From for ApiIdentifier { | Flow::DeleteUserRole | Flow::CreateRole | Flow::UpdateRole - | Flow::UserFromEmail => Self::UserRole, + | Flow::UserFromEmail + | Flow::ListUsersInLineage => Self::UserRole, Flow::GetActionUrl | Flow::SyncOnboardingStatus | Flow::ResetTrackingId => { Self::ConnectorOnboarding diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index 526722af98..a730321e19 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -269,3 +269,20 @@ pub async fn get_role_information( )) .await } + +pub async fn list_users_in_lineage(state: web::Data, req: HttpRequest) -> HttpResponse { + let flow = Flow::ListUsersInLineage; + + Box::pin(api::server_wrap( + flow, + state.clone(), + &req, + (), + |state, user_from_token, _, _| { + user_role_core::list_users_in_lineage(state, user_from_token) + }, + &auth::DashboardNoPermissionAuth, + api_locking::LockAction::NotApplicable, + )) + .await +} diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs index 2f67af9f23..f5320c4133 100644 --- a/crates/router/src/services/authorization/roles/predefined_roles.rs +++ b/crates/router/src/services/authorization/roles/predefined_roles.rs @@ -9,7 +9,7 @@ use crate::consts; pub static PREDEFINED_ROLES: Lazy> = Lazy::new(|| { let mut roles = HashMap::new(); roles.insert( - consts::user_role::ROLE_ID_INTERNAL_ADMIN, + common_utils::consts::ROLE_ID_INTERNAL_ADMIN, RoleInfo { groups: vec![ PermissionGroup::OperationsView, @@ -25,7 +25,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsManage, PermissionGroup::OrganizationManage, ], - role_id: consts::user_role::ROLE_ID_INTERNAL_ADMIN.to_string(), + role_id: common_utils::consts::ROLE_ID_INTERNAL_ADMIN.to_string(), role_name: "internal_admin".to_string(), scope: RoleScope::Organization, entity_type: EntityType::Internal, @@ -36,7 +36,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| }, ); roles.insert( - consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER, + common_utils::consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER, RoleInfo { groups: vec![ PermissionGroup::OperationsView, @@ -46,7 +46,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::UsersView, PermissionGroup::MerchantDetailsView, ], - role_id: consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), + role_id: common_utils::consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(), role_name: "internal_view_only".to_string(), scope: RoleScope::Organization, entity_type: EntityType::Internal, @@ -58,7 +58,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| ); roles.insert( - consts::user_role::ROLE_ID_ORGANIZATION_ADMIN, + common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN, RoleInfo { groups: vec![ PermissionGroup::OperationsView, @@ -74,7 +74,7 @@ pub static PREDEFINED_ROLES: Lazy> = Lazy::new(| PermissionGroup::MerchantDetailsManage, PermissionGroup::OrganizationManage, ], - role_id: consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(), + role_id: common_utils::consts::ROLE_ID_ORGANIZATION_ADMIN.to_string(), role_name: "organization_admin".to_string(), scope: RoleScope::Organization, entity_type: EntityType::Organization, diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index 3df0a11af0..b252e61853 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -868,7 +868,7 @@ impl UserFromStorage { pub async fn get_roles_from_db(&self, state: &SessionState) -> UserResult> { state .store - .list_user_roles_by_user_id(&self.0.user_id, UserRoleVersion::V1) + .list_user_roles_by_user_id_and_version(&self.0.user_id, UserRoleVersion::V1) .await .change_context(UserErrors::InternalServerError) } @@ -949,7 +949,7 @@ impl UserFromStorage { } else { state .store - .list_user_roles_by_user_id(&self.0.user_id, UserRoleVersion::V1) + .list_user_roles_by_user_id_and_version(&self.0.user_id, UserRoleVersion::V1) .await? .into_iter() .find(|role| role.status == UserStatus::Active) diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index 88eec0f791..538ae888cd 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -466,6 +466,8 @@ pub enum Flow { ListMerchantsForUserInOrg, /// List Profile for user in org and merchant ListProfileForUserInOrgAndMerchant, + /// List Users in Org + ListUsersInLineage, /// List initial webhook delivery attempts WebhookEventInitialDeliveryAttemptList, /// List delivery attempts for a webhook event