diff --git a/config/dashboard.toml b/config/dashboard.toml index 4cafdd49ab..87c0072173 100644 --- a/config/dashboard.toml +++ b/config/dashboard.toml @@ -35,5 +35,6 @@ global_search=true dispute_analytics=true configure_pmts=false branding=false +user_management_revamp=true totp=true live_users_counter=false \ No newline at end of file diff --git a/crates/analytics/src/opensearch.rs b/crates/analytics/src/opensearch.rs index 149c77212a..3be9688d8f 100644 --- a/crates/analytics/src/opensearch.rs +++ b/crates/analytics/src/opensearch.rs @@ -104,6 +104,8 @@ pub enum OpenSearchError { IndexAccessNotPermittedError(SearchIndex), #[error("Opensearch unknown error")] UnknownError, + #[error("Opensearch access forbidden error")] + AccessForbiddenError, } impl ErrorSwitch for QueryBuildingError { @@ -159,6 +161,12 @@ impl ErrorSwitch for OpenSearchError { Self::UnknownError => { ApiErrorResponse::InternalServerError(ApiError::new("IR", 6, "Unknown error", None)) } + Self::AccessForbiddenError => ApiErrorResponse::ForbiddenCommonResource(ApiError::new( + "IR", + 7, + "Access Forbidden error", + None, + )), } } } diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index a0b6606c22..89159bcff8 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -1,6 +1,11 @@ pub use analytics::*; pub mod routes { + use std::{ + collections::{HashMap, HashSet}, + sync::Arc, + }; + use actix_web::{web, Responder, Scope}; use analytics::{ api_event::api_events_core, connector_events::connector_events_core, enums::AuthInfo, @@ -21,13 +26,13 @@ pub mod routes { GetSdkEventMetricRequest, ReportRequest, }; use common_enums::EntityType; - use common_utils::id_type::{MerchantId, OrganizationId}; use error_stack::{report, ResultExt}; + use futures::{stream::FuturesUnordered, StreamExt}; use crate::{ - consts::opensearch::OPENSEARCH_INDEX_PERMISSIONS, + consts::opensearch::SEARCH_INDEXES, core::{api_locking, errors::user::UserErrors, verification::utils}, - db::user::UserInterface, + db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload}, routes::AppState, services::{ api, @@ -35,7 +40,7 @@ pub mod routes { authorization::{permissions::Permission, roles::RoleInfo}, ApplicationResponse, }, - types::domain::UserEmail, + types::{domain::UserEmail, storage::UserRole}, }; pub struct Analytics; @@ -1838,25 +1843,89 @@ pub mod routes { .await .change_context(UserErrors::InternalServerError) .change_context(OpenSearchError::UnknownError)?; - let permissions = role_info.get_permissions_set(); - let accessible_indexes: Vec<_> = OPENSEARCH_INDEX_PERMISSIONS - .iter() - .filter(|(_, perm)| perm.iter().any(|p| permissions.contains(p))) - .map(|(i, _)| *i) + let permission_groups = role_info.get_permission_groups(); + if !permission_groups.contains(&common_enums::PermissionGroup::OperationsView) { + return Err(OpenSearchError::AccessForbiddenError)?; + } + let user_roles: HashSet = state + .store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: &auth.user_id, + org_id: Some(&auth.org_id), + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: None, + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError)? + .into_iter() .collect(); - let merchant_id: MerchantId = auth.merchant_id; - let org_id: OrganizationId = auth.org_id; - let search_params: Vec = vec![AuthInfo::MerchantLevel { - org_id: org_id.clone(), - merchant_ids: vec![merchant_id.clone()], - }]; + let state = Arc::new(state); + let role_info_map: HashMap = user_roles + .iter() + .map(|user_role| { + let state = Arc::clone(&state); + let role_id = user_role.role_id.clone(); + let org_id = user_role.org_id.clone().unwrap_or_default(); + async move { + RoleInfo::from_role_id_in_org_scope(&state, &role_id, &org_id) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError) + .map(|role_info| (role_id, role_info)) + } + }) + .collect::>() + .collect::>() + .await + .into_iter() + .collect::, _>>()?; + + let filtered_user_roles: Vec<&UserRole> = user_roles + .iter() + .filter(|user_role| { + let user_role_id = &user_role.role_id; + if let Some(role_info) = role_info_map.get(user_role_id) { + let permissions = role_info.get_permission_groups(); + permissions.contains(&common_enums::PermissionGroup::OperationsView) + } else { + false + } + }) + .collect(); + + let search_params: Vec = filtered_user_roles + .iter() + .filter_map(|user_role| { + user_role + .get_entity_id_and_type() + .and_then(|(_, entity_type)| match entity_type { + EntityType::Profile => Some(AuthInfo::ProfileLevel { + org_id: user_role.org_id.clone()?, + merchant_id: user_role.merchant_id.clone()?, + profile_ids: vec![user_role.profile_id.clone()?], + }), + EntityType::Merchant => Some(AuthInfo::MerchantLevel { + org_id: user_role.org_id.clone()?, + merchant_ids: vec![user_role.merchant_id.clone()?], + }), + EntityType::Organization => Some(AuthInfo::OrgLevel { + org_id: user_role.org_id.clone()?, + }), + }) + }) + .collect(); analytics::search::msearch_results( &state.opensearch_client, req, search_params, - accessible_indexes, + SEARCH_INDEXES.to_vec(), ) .await .map(ApplicationResponse::Json) @@ -1898,20 +1967,82 @@ pub mod routes { .await .change_context(UserErrors::InternalServerError) .change_context(OpenSearchError::UnknownError)?; - let permissions = role_info.get_permissions_set(); - let _ = OPENSEARCH_INDEX_PERMISSIONS + let permission_groups = role_info.get_permission_groups(); + if !permission_groups.contains(&common_enums::PermissionGroup::OperationsView) { + return Err(OpenSearchError::AccessForbiddenError)?; + } + let user_roles: HashSet = state + .store + .list_user_roles_by_user_id(ListUserRolesByUserIdPayload { + user_id: &auth.user_id, + org_id: Some(&auth.org_id), + merchant_id: None, + profile_id: None, + entity_id: None, + version: None, + status: None, + limit: None, + }) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError)? + .into_iter() + .collect(); + let state = Arc::new(state); + let role_info_map: HashMap = user_roles .iter() - .filter(|(ind, _)| *ind == index) - .find(|i| i.1.iter().any(|p| permissions.contains(p))) - .ok_or(OpenSearchError::IndexAccessNotPermittedError(index))?; + .map(|user_role| { + let state = Arc::clone(&state); + let role_id = user_role.role_id.clone(); + let org_id = user_role.org_id.clone().unwrap_or_default(); + async move { + RoleInfo::from_role_id_in_org_scope(&state, &role_id, &org_id) + .await + .change_context(UserErrors::InternalServerError) + .change_context(OpenSearchError::UnknownError) + .map(|role_info| (role_id, role_info)) + } + }) + .collect::>() + .collect::>() + .await + .into_iter() + .collect::, _>>()?; - let merchant_id: MerchantId = auth.merchant_id; - let org_id: OrganizationId = auth.org_id; - let search_params: Vec = vec![AuthInfo::MerchantLevel { - org_id: org_id.clone(), - merchant_ids: vec![merchant_id.clone()], - }]; + let filtered_user_roles: Vec<&UserRole> = user_roles + .iter() + .filter(|user_role| { + let user_role_id = &user_role.role_id; + if let Some(role_info) = role_info_map.get(user_role_id) { + let permissions = role_info.get_permission_groups(); + permissions.contains(&common_enums::PermissionGroup::OperationsView) + } else { + false + } + }) + .collect(); + let search_params: Vec = filtered_user_roles + .iter() + .filter_map(|user_role| { + user_role + .get_entity_id_and_type() + .and_then(|(_, entity_type)| match entity_type { + EntityType::Profile => Some(AuthInfo::ProfileLevel { + org_id: user_role.org_id.clone()?, + merchant_id: user_role.merchant_id.clone()?, + profile_ids: vec![user_role.profile_id.clone()?], + }), + EntityType::Merchant => Some(AuthInfo::MerchantLevel { + org_id: user_role.org_id.clone()?, + merchant_ids: vec![user_role.merchant_id.clone()?], + }), + EntityType::Organization => Some(AuthInfo::OrgLevel { + org_id: user_role.org_id.clone()?, + }), + }) + }) + .collect(); analytics::search::search_results(&state.opensearch_client, req, search_params) .await .map(ApplicationResponse::Json) diff --git a/crates/router/src/consts/opensearch.rs b/crates/router/src/consts/opensearch.rs index fc1c071439..277b0e946b 100644 --- a/crates/router/src/consts/opensearch.rs +++ b/crates/router/src/consts/opensearch.rs @@ -1,22 +1,12 @@ use api_models::analytics::search::SearchIndex; -use crate::services::authorization::permissions::Permission; - -pub const OPENSEARCH_INDEX_PERMISSIONS: &[(SearchIndex, &[Permission])] = &[ - ( +pub const fn get_search_indexes() -> [SearchIndex; 4] { + [ SearchIndex::PaymentAttempts, - &[Permission::PaymentRead, Permission::PaymentWrite], - ), - ( SearchIndex::PaymentIntents, - &[Permission::PaymentRead, Permission::PaymentWrite], - ), - ( SearchIndex::Refunds, - &[Permission::RefundRead, Permission::RefundWrite], - ), - ( SearchIndex::Disputes, - &[Permission::DisputeRead, Permission::DisputeWrite], - ), -]; + ] +} + +pub const SEARCH_INDEXES: [SearchIndex; 4] = get_search_indexes();