Bug: feat(opensearch): restrict search view access based on user roles and permissions



To fix the issue raised here: [https://github.com/juspay/hyperswitch-cloud/issues/6759](https://github.com/juspay/hyperswitch-cloud/issues/6759)

- Currently, a profile level user is able to see the other profile payments list when using global-search, irrespective of whether he has permissions or not. But the user will not be able to view the details related to the payments upon clicking the particular payment.

- This behaviour should be fixed by restricting the profiles / merchants to be searched based on the user roles associated with the role_id and the permissions associated with the user role

- Now, only if the use role has the necessary READ permissions to access the indexes, he would be able to search the payments related to that particular profile/merchant.
- 
The search_params should now be constructed with only those ProfileLevel / MerchantLevel / OrgLevel entities which will be searched through the opensearch query.
diff --git a/config/dashboard.toml b/config/dashboard.toml
index 4cafdd49ab3..87c00721736 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 149c77212a0..3be9688d8f7 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<OpenSearchError> for QueryBuildingError {
@@ -159,6 +161,12 @@ impl ErrorSwitch<ApiErrorResponse> 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 a0b6606c227..89159bcff86 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
+                let permission_groups = role_info.get_permission_groups();
+                if !permission_groups.contains(&common_enums::PermissionGroup::OperationsView) {
+                    return Err(OpenSearchError::AccessForbiddenError)?;
+                }
+                let user_roles: HashSet<UserRole> = 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<String, RoleInfo> = user_roles
                     .iter()
-                    .filter(|(_, perm)| perm.iter().any(|p| permissions.contains(p)))
-                    .map(|(i, _)| *i)
+                    .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::<FuturesUnordered<_>>()
+                    .collect::<Vec<_>>()
+                    .await
+                    .into_iter()
+                    .collect::<Result<HashMap<_, _>, _>>()?;
+
+                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 merchant_id: MerchantId = auth.merchant_id;
-                let org_id: OrganizationId = auth.org_id;
-                let search_params: Vec<AuthInfo> = vec![AuthInfo::MerchantLevel {
-                    org_id: org_id.clone(),
-                    merchant_ids: vec![merchant_id.clone()],
-                }];
+                let search_params: Vec<AuthInfo> = 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<UserRole> = 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<String, RoleInfo> = 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::<FuturesUnordered<_>>()
+                    .collect::<Vec<_>>()
+                    .await
+                    .into_iter()
+                    .collect::<Result<HashMap<_, _>, _>>()?;
 
-                let merchant_id: MerchantId = auth.merchant_id;
-                let org_id: OrganizationId = auth.org_id;
-                let search_params: Vec<AuthInfo> = 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<AuthInfo> = 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 fc1c071439a..277b0e946ba 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();

## Type of Change
<!-- Put an `x` in the boxes that apply -->

- [x] Bugfix
- [ ] New feature
- [ ] Enhancement
- [ ] Refactoring
- [ ] Dependency updates
- [ ] Documentation
- [ ] CI/CD

## Description
<!-- Describe your changes in detail -->
Fixes the issue raised here: [https://github.com/juspay/hyperswitch-cloud/issues/6759](https://github.com/juspay/hyperswitch-cloud/issues/6759)

- Currently, a profile level user is able to see the other profile payments list when using global-search, irrespective of whether he has permissions or not. But the user will not be able to view the details related to the payments upon clicking the particular payment.

- This PR fixes this behaviour by restricting the profiles / merchants to be searched based on the user roles associated with the role_id and the permissions associated with the user role

- Now, only if the use role has the necessary READ permissions to access the indexes, he would be able to search the payments related to that particular profile/merchant.

- The `search_params` will now be constructed with only those ProfileLevel / MerchantLevel / OrgLevel entities which will be searched through the opensearch query.
### Additional Changes

- [ ] This PR modifies the API contract
- [ ] This PR modifies the database schema
- [ ] This PR modifies application configuration/environment variables

<!--
Provide links to the files with corresponding changes.

Following are the paths where you can find config files:
1. `config`
2. `crates/router/src/configs`
3. `loadtest/config`
-->


## Motivation and Context
<!--
Why is this change required? What problem does it solve?
If it fixes an open issue, please link to the issue here.

If you don't have an issue, we'd recommend starting with one first so the PR
can focus on the implementation (unless it is an obvious bug or documentation fix
that will have little conversation).
-->
To preserve confidentiality of the payment details

## How did you test it?
<!--
Did you write an integration/unit/API test to verify the code changes?
Or did you test this change manually (provide relevant screenshots)?
-->
Try to search payments of a profile from another profile which doesn't have access to the profile where the payments are present. Results should not show up while using global search.


## Checklist
<!-- Put an `x` in the boxes that apply -->

- [x] I formatted the code `cargo +nightly fmt --all`
- [x] I addressed lints thrown by `cargo clippy`
- [x] I reviewed the submitted code
- [ ] I added unit tests for my changes where possible
