Skip to content

Commit caa0693

Browse files
feat(opensearch): restrict search view access based on user roles and permissions (#5932)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
1 parent 036a2d5 commit caa0693

File tree

4 files changed

+172
-42
lines changed

4 files changed

+172
-42
lines changed

config/dashboard.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,5 +35,6 @@ global_search=true
3535
dispute_analytics=true
3636
configure_pmts=false
3737
branding=false
38+
user_management_revamp=true
3839
totp=true
3940
live_users_counter=false

crates/analytics/src/opensearch.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,8 @@ pub enum OpenSearchError {
104104
IndexAccessNotPermittedError(SearchIndex),
105105
#[error("Opensearch unknown error")]
106106
UnknownError,
107+
#[error("Opensearch access forbidden error")]
108+
AccessForbiddenError,
107109
}
108110

109111
impl ErrorSwitch<OpenSearchError> for QueryBuildingError {
@@ -159,6 +161,12 @@ impl ErrorSwitch<ApiErrorResponse> for OpenSearchError {
159161
Self::UnknownError => {
160162
ApiErrorResponse::InternalServerError(ApiError::new("IR", 6, "Unknown error", None))
161163
}
164+
Self::AccessForbiddenError => ApiErrorResponse::ForbiddenCommonResource(ApiError::new(
165+
"IR",
166+
7,
167+
"Access Forbidden error",
168+
None,
169+
)),
162170
}
163171
}
164172
}

crates/router/src/analytics.rs

Lines changed: 157 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
pub use analytics::*;
22

33
pub mod routes {
4+
use std::{
5+
collections::{HashMap, HashSet},
6+
sync::Arc,
7+
};
8+
49
use actix_web::{web, Responder, Scope};
510
use analytics::{
611
api_event::api_events_core, connector_events::connector_events_core, enums::AuthInfo,
@@ -21,21 +26,21 @@ pub mod routes {
2126
GetSdkEventMetricRequest, ReportRequest,
2227
};
2328
use common_enums::EntityType;
24-
use common_utils::id_type::{MerchantId, OrganizationId};
2529
use error_stack::{report, ResultExt};
30+
use futures::{stream::FuturesUnordered, StreamExt};
2631

2732
use crate::{
28-
consts::opensearch::OPENSEARCH_INDEX_PERMISSIONS,
33+
consts::opensearch::SEARCH_INDEXES,
2934
core::{api_locking, errors::user::UserErrors, verification::utils},
30-
db::user::UserInterface,
35+
db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload},
3136
routes::AppState,
3237
services::{
3338
api,
3439
authentication::{self as auth, AuthenticationData, UserFromToken},
3540
authorization::{permissions::Permission, roles::RoleInfo},
3641
ApplicationResponse,
3742
},
38-
types::domain::UserEmail,
43+
types::{domain::UserEmail, storage::UserRole},
3944
};
4045

4146
pub struct Analytics;
@@ -1838,25 +1843,89 @@ pub mod routes {
18381843
.await
18391844
.change_context(UserErrors::InternalServerError)
18401845
.change_context(OpenSearchError::UnknownError)?;
1841-
let permissions = role_info.get_permissions_set();
1842-
let accessible_indexes: Vec<_> = OPENSEARCH_INDEX_PERMISSIONS
1846+
let permission_groups = role_info.get_permission_groups();
1847+
if !permission_groups.contains(&common_enums::PermissionGroup::OperationsView) {
1848+
return Err(OpenSearchError::AccessForbiddenError)?;
1849+
}
1850+
let user_roles: HashSet<UserRole> = state
1851+
.store
1852+
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
1853+
user_id: &auth.user_id,
1854+
org_id: Some(&auth.org_id),
1855+
merchant_id: None,
1856+
profile_id: None,
1857+
entity_id: None,
1858+
version: None,
1859+
status: None,
1860+
limit: None,
1861+
})
1862+
.await
1863+
.change_context(UserErrors::InternalServerError)
1864+
.change_context(OpenSearchError::UnknownError)?
1865+
.into_iter()
1866+
.collect();
1867+
1868+
let state = Arc::new(state);
1869+
let role_info_map: HashMap<String, RoleInfo> = user_roles
18431870
.iter()
1844-
.filter(|(_, perm)| perm.iter().any(|p| permissions.contains(p)))
1845-
.map(|(i, _)| *i)
1871+
.map(|user_role| {
1872+
let state = Arc::clone(&state);
1873+
let role_id = user_role.role_id.clone();
1874+
let org_id = user_role.org_id.clone().unwrap_or_default();
1875+
async move {
1876+
RoleInfo::from_role_id_in_org_scope(&state, &role_id, &org_id)
1877+
.await
1878+
.change_context(UserErrors::InternalServerError)
1879+
.change_context(OpenSearchError::UnknownError)
1880+
.map(|role_info| (role_id, role_info))
1881+
}
1882+
})
1883+
.collect::<FuturesUnordered<_>>()
1884+
.collect::<Vec<_>>()
1885+
.await
1886+
.into_iter()
1887+
.collect::<Result<HashMap<_, _>, _>>()?;
1888+
1889+
let filtered_user_roles: Vec<&UserRole> = user_roles
1890+
.iter()
1891+
.filter(|user_role| {
1892+
let user_role_id = &user_role.role_id;
1893+
if let Some(role_info) = role_info_map.get(user_role_id) {
1894+
let permissions = role_info.get_permission_groups();
1895+
permissions.contains(&common_enums::PermissionGroup::OperationsView)
1896+
} else {
1897+
false
1898+
}
1899+
})
18461900
.collect();
18471901

1848-
let merchant_id: MerchantId = auth.merchant_id;
1849-
let org_id: OrganizationId = auth.org_id;
1850-
let search_params: Vec<AuthInfo> = vec![AuthInfo::MerchantLevel {
1851-
org_id: org_id.clone(),
1852-
merchant_ids: vec![merchant_id.clone()],
1853-
}];
1902+
let search_params: Vec<AuthInfo> = filtered_user_roles
1903+
.iter()
1904+
.filter_map(|user_role| {
1905+
user_role
1906+
.get_entity_id_and_type()
1907+
.and_then(|(_, entity_type)| match entity_type {
1908+
EntityType::Profile => Some(AuthInfo::ProfileLevel {
1909+
org_id: user_role.org_id.clone()?,
1910+
merchant_id: user_role.merchant_id.clone()?,
1911+
profile_ids: vec![user_role.profile_id.clone()?],
1912+
}),
1913+
EntityType::Merchant => Some(AuthInfo::MerchantLevel {
1914+
org_id: user_role.org_id.clone()?,
1915+
merchant_ids: vec![user_role.merchant_id.clone()?],
1916+
}),
1917+
EntityType::Organization => Some(AuthInfo::OrgLevel {
1918+
org_id: user_role.org_id.clone()?,
1919+
}),
1920+
})
1921+
})
1922+
.collect();
18541923

18551924
analytics::search::msearch_results(
18561925
&state.opensearch_client,
18571926
req,
18581927
search_params,
1859-
accessible_indexes,
1928+
SEARCH_INDEXES.to_vec(),
18601929
)
18611930
.await
18621931
.map(ApplicationResponse::Json)
@@ -1898,20 +1967,82 @@ pub mod routes {
18981967
.await
18991968
.change_context(UserErrors::InternalServerError)
19001969
.change_context(OpenSearchError::UnknownError)?;
1901-
let permissions = role_info.get_permissions_set();
1902-
let _ = OPENSEARCH_INDEX_PERMISSIONS
1970+
let permission_groups = role_info.get_permission_groups();
1971+
if !permission_groups.contains(&common_enums::PermissionGroup::OperationsView) {
1972+
return Err(OpenSearchError::AccessForbiddenError)?;
1973+
}
1974+
let user_roles: HashSet<UserRole> = state
1975+
.store
1976+
.list_user_roles_by_user_id(ListUserRolesByUserIdPayload {
1977+
user_id: &auth.user_id,
1978+
org_id: Some(&auth.org_id),
1979+
merchant_id: None,
1980+
profile_id: None,
1981+
entity_id: None,
1982+
version: None,
1983+
status: None,
1984+
limit: None,
1985+
})
1986+
.await
1987+
.change_context(UserErrors::InternalServerError)
1988+
.change_context(OpenSearchError::UnknownError)?
1989+
.into_iter()
1990+
.collect();
1991+
let state = Arc::new(state);
1992+
let role_info_map: HashMap<String, RoleInfo> = user_roles
19031993
.iter()
1904-
.filter(|(ind, _)| *ind == index)
1905-
.find(|i| i.1.iter().any(|p| permissions.contains(p)))
1906-
.ok_or(OpenSearchError::IndexAccessNotPermittedError(index))?;
1994+
.map(|user_role| {
1995+
let state = Arc::clone(&state);
1996+
let role_id = user_role.role_id.clone();
1997+
let org_id = user_role.org_id.clone().unwrap_or_default();
1998+
async move {
1999+
RoleInfo::from_role_id_in_org_scope(&state, &role_id, &org_id)
2000+
.await
2001+
.change_context(UserErrors::InternalServerError)
2002+
.change_context(OpenSearchError::UnknownError)
2003+
.map(|role_info| (role_id, role_info))
2004+
}
2005+
})
2006+
.collect::<FuturesUnordered<_>>()
2007+
.collect::<Vec<_>>()
2008+
.await
2009+
.into_iter()
2010+
.collect::<Result<HashMap<_, _>, _>>()?;
19072011

1908-
let merchant_id: MerchantId = auth.merchant_id;
1909-
let org_id: OrganizationId = auth.org_id;
1910-
let search_params: Vec<AuthInfo> = vec![AuthInfo::MerchantLevel {
1911-
org_id: org_id.clone(),
1912-
merchant_ids: vec![merchant_id.clone()],
1913-
}];
2012+
let filtered_user_roles: Vec<&UserRole> = user_roles
2013+
.iter()
2014+
.filter(|user_role| {
2015+
let user_role_id = &user_role.role_id;
2016+
if let Some(role_info) = role_info_map.get(user_role_id) {
2017+
let permissions = role_info.get_permission_groups();
2018+
permissions.contains(&common_enums::PermissionGroup::OperationsView)
2019+
} else {
2020+
false
2021+
}
2022+
})
2023+
.collect();
19142024

2025+
let search_params: Vec<AuthInfo> = filtered_user_roles
2026+
.iter()
2027+
.filter_map(|user_role| {
2028+
user_role
2029+
.get_entity_id_and_type()
2030+
.and_then(|(_, entity_type)| match entity_type {
2031+
EntityType::Profile => Some(AuthInfo::ProfileLevel {
2032+
org_id: user_role.org_id.clone()?,
2033+
merchant_id: user_role.merchant_id.clone()?,
2034+
profile_ids: vec![user_role.profile_id.clone()?],
2035+
}),
2036+
EntityType::Merchant => Some(AuthInfo::MerchantLevel {
2037+
org_id: user_role.org_id.clone()?,
2038+
merchant_ids: vec![user_role.merchant_id.clone()?],
2039+
}),
2040+
EntityType::Organization => Some(AuthInfo::OrgLevel {
2041+
org_id: user_role.org_id.clone()?,
2042+
}),
2043+
})
2044+
})
2045+
.collect();
19152046
analytics::search::search_results(&state.opensearch_client, req, search_params)
19162047
.await
19172048
.map(ApplicationResponse::Json)
Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,12 @@
11
use api_models::analytics::search::SearchIndex;
22

3-
use crate::services::authorization::permissions::Permission;
4-
5-
pub const OPENSEARCH_INDEX_PERMISSIONS: &[(SearchIndex, &[Permission])] = &[
6-
(
3+
pub const fn get_search_indexes() -> [SearchIndex; 4] {
4+
[
75
SearchIndex::PaymentAttempts,
8-
&[Permission::PaymentRead, Permission::PaymentWrite],
9-
),
10-
(
116
SearchIndex::PaymentIntents,
12-
&[Permission::PaymentRead, Permission::PaymentWrite],
13-
),
14-
(
157
SearchIndex::Refunds,
16-
&[Permission::RefundRead, Permission::RefundWrite],
17-
),
18-
(
198
SearchIndex::Disputes,
20-
&[Permission::DisputeRead, Permission::DisputeWrite],
21-
),
22-
];
9+
]
10+
}
11+
12+
pub const SEARCH_INDEXES: [SearchIndex; 4] = get_search_indexes();

0 commit comments

Comments
 (0)