From 3ddfe53838c6b039dc5f669ccd23d3035521d691 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Tue, 24 Sep 2024 19:48:39 +0530 Subject: [PATCH] feat(users): Add entity type filter in list users and list roles API (#5997) --- crates/api_models/src/events/user_role.rs | 12 ++-- crates/api_models/src/user_role.rs | 5 ++ crates/api_models/src/user_role/role.rs | 5 ++ crates/router/src/core/user_role.rs | 75 ++++++++++---------- crates/router/src/core/user_role/role.rs | 85 +++++++++++++---------- crates/router/src/routes/user_role.rs | 24 +++++-- crates/router/src/utils/user_role.rs | 60 +++++++++++++++- 7 files changed, 180 insertions(+), 86 deletions(-) diff --git a/crates/api_models/src/events/user_role.rs b/crates/api_models/src/events/user_role.rs index 81b38198cf..eb09abd471 100644 --- a/crates/api_models/src/events/user_role.rs +++ b/crates/api_models/src/events/user_role.rs @@ -2,12 +2,12 @@ use common_utils::events::{ApiEventMetric, ApiEventsType}; use crate::user_role::{ role::{ - CreateRoleRequest, GetRoleRequest, ListRolesAtEntityLevelRequest, ListRolesResponse, - RoleInfoResponseNew, RoleInfoWithGroupsResponse, RoleInfoWithPermissionsResponse, - UpdateRoleRequest, + CreateRoleRequest, GetRoleRequest, ListRolesAtEntityLevelRequest, ListRolesRequest, + ListRolesResponse, RoleInfoResponseNew, RoleInfoWithGroupsResponse, + RoleInfoWithPermissionsResponse, UpdateRoleRequest, }, AcceptInvitationRequest, AuthorizationInfoResponse, DeleteUserRoleRequest, - MerchantSelectRequest, UpdateUserRoleRequest, + ListUsersInEntityRequest, MerchantSelectRequest, UpdateUserRoleRequest, }; common_utils::impl_api_event_type!( @@ -25,6 +25,8 @@ common_utils::impl_api_event_type!( ListRolesResponse, ListRolesAtEntityLevelRequest, RoleInfoResponseNew, - RoleInfoWithGroupsResponse + RoleInfoWithGroupsResponse, + ListUsersInEntityRequest, + ListRolesRequest ) ); diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 8e1a0483c0..f2743d6a31 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -159,3 +159,8 @@ pub struct Entity { pub entity_id: String, pub entity_type: common_enums::EntityType, } + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ListUsersInEntityRequest { + pub entity_type: Option, +} diff --git a/crates/api_models/src/user_role/role.rs b/crates/api_models/src/user_role/role.rs index 828dfeb20f..be467421e6 100644 --- a/crates/api_models/src/user_role/role.rs +++ b/crates/api_models/src/user_role/role.rs @@ -35,6 +35,11 @@ pub struct RoleInfoWithGroupsResponse { pub role_scope: RoleScope, } +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct ListRolesRequest { + pub entity_type: Option, +} + #[derive(Debug, serde::Serialize)] pub struct RoleInfoResponseNew { pub role_id: String, diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index d42f71695a..fd9c8c8903 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -673,6 +673,7 @@ pub async fn delete_user_role( pub async fn list_users_in_lineage( state: SessionState, user_from_token: auth::UserFromToken, + request: user_role_api::ListUsersInEntityRequest, ) -> UserResponse> { let requestor_role_info = roles::RoleInfo::from_role_id_in_merchant_scope( &state, @@ -683,51 +684,55 @@ pub async fn list_users_in_lineage( .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(), + let user_roles_set: HashSet<_> = match utils::user_role::get_min_entity( + requestor_role_info.get_entity_type(), + request.entity_type, + )? { + EntityType::Organization => { + utils::user_role::fetch_user_roles_by_payload( + &state, + ListUserRolesByOrgIdPayload { + user_id: None, + org_id: &user_from_token.org_id, + merchant_id: None, + profile_id: None, + version: None, + }, + request.entity_type, + ) + .await? + } + EntityType::Merchant => { + utils::user_role::fetch_user_roles_by_payload( + &state, + ListUserRolesByOrgIdPayload { + user_id: None, + org_id: &user_from_token.org_id, + merchant_id: Some(&user_from_token.merchant_id), + profile_id: None, + version: None, + }, + request.entity_type, + ) + .await? + } 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 { + utils::user_role::fetch_user_roles_by_payload( + &state, + 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() + }, + request.entity_type, + ) + .await? } EntityType::Internal => HashSet::new(), }; diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index 571ae7f995..ffc18214c2 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -217,6 +217,7 @@ pub async fn update_role( pub async fn list_roles_with_info( state: SessionState, user_from_token: UserFromToken, + request: role_api::ListRolesRequest, ) -> UserResponse> { let user_role_info = user_from_token .get_role_info_from_db(&state) @@ -229,49 +230,57 @@ pub async fn list_roles_with_info( .collect::>(); let user_role_entity = user_role_info.get_entity_type(); - let custom_roles = match user_role_entity { - EntityType::Organization => state - .store - .list_roles_for_org_by_parameters(&user_from_token.org_id, None, None, None) - .await - .change_context(UserErrors::InternalServerError) - .attach_printable("Failed to get roles")?, - EntityType::Merchant => state - .store - .list_roles_for_org_by_parameters( - &user_from_token.org_id, - Some(&user_from_token.merchant_id), - None, - None, - ) - .await - .change_context(UserErrors::InternalServerError) - .attach_printable("Failed to get roles")?, - // TODO: Populate this from Db function when support for profile id and profile level custom roles is added - EntityType::Profile => Vec::new(), - EntityType::Internal => { - return Err(UserErrors::InvalidRoleOperationWithMessage( - "Internal roles are not allowed for this operation".to_string(), - ) - .into()); - } - }; + let custom_roles = + match utils::user_role::get_min_entity(user_role_entity, request.entity_type)? { + EntityType::Organization => state + .store + .list_roles_for_org_by_parameters( + &user_from_token.org_id, + None, + request.entity_type, + None, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get roles")?, + EntityType::Merchant => state + .store + .list_roles_for_org_by_parameters( + &user_from_token.org_id, + Some(&user_from_token.merchant_id), + request.entity_type, + None, + ) + .await + .change_context(UserErrors::InternalServerError) + .attach_printable("Failed to get roles")?, + // TODO: Populate this from Db function when support for profile id and profile level custom roles is added + EntityType::Profile => Vec::new(), + EntityType::Internal => { + return Err(UserErrors::InvalidRoleOperationWithMessage( + "Internal roles are not allowed for this operation".to_string(), + ) + .into()); + } + }; role_info_vec.extend(custom_roles.into_iter().map(roles::RoleInfo::from)); + let list_role_info_response = role_info_vec .into_iter() .filter_map(|role_info| { - if user_role_entity >= role_info.get_entity_type() { - Some(role_api::RoleInfoResponseNew { - role_id: role_info.get_role_id().to_string(), - role_name: role_info.get_role_name().to_string(), - groups: role_info.get_permission_groups().to_vec(), - entity_type: role_info.get_entity_type(), - scope: role_info.get_scope(), - }) - } else { - None - } + let is_lower_entity = user_role_entity >= role_info.get_entity_type(); + let request_filter = request.entity_type.map_or(true, |entity_type| { + entity_type == role_info.get_entity_type() + }); + + (is_lower_entity && request_filter).then_some(role_api::RoleInfoResponseNew { + role_id: role_info.get_role_id().to_string(), + role_name: role_info.get_role_name().to_string(), + groups: role_info.get_permission_groups().to_vec(), + entity_type: role_info.get_entity_type(), + scope: role_info.get_scope(), + }) }) .collect::>(); diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index b1260b5d7a..922e7612b1 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -291,16 +291,20 @@ pub async fn get_role_information( .await } -pub async fn list_users_in_lineage(state: web::Data, req: HttpRequest) -> HttpResponse { +pub async fn list_users_in_lineage( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> 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) + query.into_inner(), + |state, user_from_token, request, _| { + user_role_core::list_users_in_lineage(state, user_from_token, request) }, &auth::DashboardNoPermissionAuth, api_locking::LockAction::NotApplicable, @@ -308,15 +312,21 @@ pub async fn list_users_in_lineage(state: web::Data, req: HttpRequest) .await } -pub async fn list_roles_with_info(state: web::Data, req: HttpRequest) -> HttpResponse { +pub async fn list_roles_with_info( + state: web::Data, + req: HttpRequest, + query: web::Query, +) -> HttpResponse { let flow = Flow::ListRolesV2; Box::pin(api::server_wrap( flow, state.clone(), &req, - (), - |state, user_from_token, _, _| role_core::list_roles_with_info(state, user_from_token), + query.into_inner(), + |state, user_from_token, request, _| { + role_core::list_roles_with_info(state, user_from_token, request) + }, &auth::JWTAuth { permission: Permission::UsersRead, minimum_entity_level: EntityType::Profile, diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index b4008bd000..9141b91bdd 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -14,7 +14,7 @@ use storage_impl::errors::StorageError; use crate::{ consts, core::errors::{UserErrors, UserResult}, - db::user_role::ListUserRolesByUserIdPayload, + db::user_role::{ListUserRolesByOrgIdPayload, ListUserRolesByUserIdPayload}, routes::SessionState, services::authorization::{self as authz, permissions::Permission, roles}, types::domain, @@ -398,3 +398,61 @@ pub async fn get_single_merchant_id_and_profile_id( Ok((merchant_id, profile_id)) } + +pub async fn fetch_user_roles_by_payload( + state: &SessionState, + payload: ListUserRolesByOrgIdPayload<'_>, + request_entity_type: Option, +) -> UserResult> { + Ok(state + .store + .list_user_roles_by_org_id(payload) + .await + .change_context(UserErrors::InternalServerError)? + .into_iter() + .filter_map(|user_role| { + let (_entity_id, entity_type) = user_role.get_entity_id_and_type()?; + request_entity_type + .map_or(true, |req_entity_type| entity_type == req_entity_type) + .then_some(user_role) + }) + .collect::>()) +} + +pub fn get_min_entity( + user_entity: EntityType, + filter_entity: Option, +) -> UserResult { + match (user_entity, filter_entity) { + (EntityType::Organization, None) + | (EntityType::Organization, Some(EntityType::Organization)) => { + Ok(EntityType::Organization) + } + + (EntityType::Merchant, None) + | (EntityType::Organization, Some(EntityType::Merchant)) + | (EntityType::Merchant, Some(EntityType::Merchant)) => Ok(EntityType::Merchant), + + (EntityType::Profile, None) + | (EntityType::Organization, Some(EntityType::Profile)) + | (EntityType::Merchant, Some(EntityType::Profile)) + | (EntityType::Profile, Some(EntityType::Profile)) => Ok(EntityType::Profile), + + (EntityType::Internal, _) => Ok(EntityType::Internal), + + (EntityType::Organization, Some(EntityType::Internal)) + | (EntityType::Merchant, Some(EntityType::Internal)) + | (EntityType::Profile, Some(EntityType::Internal)) => { + Err(UserErrors::InvalidRoleOperation.into()) + } + + (EntityType::Merchant, Some(EntityType::Organization)) + | (EntityType::Profile, Some(EntityType::Organization)) + | (EntityType::Profile, Some(EntityType::Merchant)) => { + Err(report!(UserErrors::InvalidRoleOperation)).attach_printable(format!( + "{} level user requesting data for {:?} level", + user_entity, filter_entity + )) + } + } +}