mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-11-03 05:17:02 +08:00
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>
This commit is contained in:
@ -35,5 +35,6 @@ global_search=true
|
|||||||
dispute_analytics=true
|
dispute_analytics=true
|
||||||
configure_pmts=false
|
configure_pmts=false
|
||||||
branding=false
|
branding=false
|
||||||
|
user_management_revamp=true
|
||||||
totp=true
|
totp=true
|
||||||
live_users_counter=false
|
live_users_counter=false
|
||||||
@ -104,6 +104,8 @@ pub enum OpenSearchError {
|
|||||||
IndexAccessNotPermittedError(SearchIndex),
|
IndexAccessNotPermittedError(SearchIndex),
|
||||||
#[error("Opensearch unknown error")]
|
#[error("Opensearch unknown error")]
|
||||||
UnknownError,
|
UnknownError,
|
||||||
|
#[error("Opensearch access forbidden error")]
|
||||||
|
AccessForbiddenError,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ErrorSwitch<OpenSearchError> for QueryBuildingError {
|
impl ErrorSwitch<OpenSearchError> for QueryBuildingError {
|
||||||
@ -159,6 +161,12 @@ impl ErrorSwitch<ApiErrorResponse> for OpenSearchError {
|
|||||||
Self::UnknownError => {
|
Self::UnknownError => {
|
||||||
ApiErrorResponse::InternalServerError(ApiError::new("IR", 6, "Unknown error", None))
|
ApiErrorResponse::InternalServerError(ApiError::new("IR", 6, "Unknown error", None))
|
||||||
}
|
}
|
||||||
|
Self::AccessForbiddenError => ApiErrorResponse::ForbiddenCommonResource(ApiError::new(
|
||||||
|
"IR",
|
||||||
|
7,
|
||||||
|
"Access Forbidden error",
|
||||||
|
None,
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,11 @@
|
|||||||
pub use analytics::*;
|
pub use analytics::*;
|
||||||
|
|
||||||
pub mod routes {
|
pub mod routes {
|
||||||
|
use std::{
|
||||||
|
collections::{HashMap, HashSet},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
use actix_web::{web, Responder, Scope};
|
use actix_web::{web, Responder, Scope};
|
||||||
use analytics::{
|
use analytics::{
|
||||||
api_event::api_events_core, connector_events::connector_events_core, enums::AuthInfo,
|
api_event::api_events_core, connector_events::connector_events_core, enums::AuthInfo,
|
||||||
@ -21,13 +26,13 @@ pub mod routes {
|
|||||||
GetSdkEventMetricRequest, ReportRequest,
|
GetSdkEventMetricRequest, ReportRequest,
|
||||||
};
|
};
|
||||||
use common_enums::EntityType;
|
use common_enums::EntityType;
|
||||||
use common_utils::id_type::{MerchantId, OrganizationId};
|
|
||||||
use error_stack::{report, ResultExt};
|
use error_stack::{report, ResultExt};
|
||||||
|
use futures::{stream::FuturesUnordered, StreamExt};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
consts::opensearch::OPENSEARCH_INDEX_PERMISSIONS,
|
consts::opensearch::SEARCH_INDEXES,
|
||||||
core::{api_locking, errors::user::UserErrors, verification::utils},
|
core::{api_locking, errors::user::UserErrors, verification::utils},
|
||||||
db::user::UserInterface,
|
db::{user::UserInterface, user_role::ListUserRolesByUserIdPayload},
|
||||||
routes::AppState,
|
routes::AppState,
|
||||||
services::{
|
services::{
|
||||||
api,
|
api,
|
||||||
@ -35,7 +40,7 @@ pub mod routes {
|
|||||||
authorization::{permissions::Permission, roles::RoleInfo},
|
authorization::{permissions::Permission, roles::RoleInfo},
|
||||||
ApplicationResponse,
|
ApplicationResponse,
|
||||||
},
|
},
|
||||||
types::domain::UserEmail,
|
types::{domain::UserEmail, storage::UserRole},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Analytics;
|
pub struct Analytics;
|
||||||
@ -1838,25 +1843,89 @@ pub mod routes {
|
|||||||
.await
|
.await
|
||||||
.change_context(UserErrors::InternalServerError)
|
.change_context(UserErrors::InternalServerError)
|
||||||
.change_context(OpenSearchError::UnknownError)?;
|
.change_context(OpenSearchError::UnknownError)?;
|
||||||
let permissions = role_info.get_permissions_set();
|
let permission_groups = role_info.get_permission_groups();
|
||||||
let accessible_indexes: Vec<_> = OPENSEARCH_INDEX_PERMISSIONS
|
if !permission_groups.contains(&common_enums::PermissionGroup::OperationsView) {
|
||||||
.iter()
|
return Err(OpenSearchError::AccessForbiddenError)?;
|
||||||
.filter(|(_, perm)| perm.iter().any(|p| permissions.contains(p)))
|
}
|
||||||
.map(|(i, _)| *i)
|
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();
|
.collect();
|
||||||
|
|
||||||
let merchant_id: MerchantId = auth.merchant_id;
|
let state = Arc::new(state);
|
||||||
let org_id: OrganizationId = auth.org_id;
|
let role_info_map: HashMap<String, RoleInfo> = user_roles
|
||||||
let search_params: Vec<AuthInfo> = vec![AuthInfo::MerchantLevel {
|
.iter()
|
||||||
org_id: org_id.clone(),
|
.map(|user_role| {
|
||||||
merchant_ids: vec![merchant_id.clone()],
|
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 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(
|
analytics::search::msearch_results(
|
||||||
&state.opensearch_client,
|
&state.opensearch_client,
|
||||||
req,
|
req,
|
||||||
search_params,
|
search_params,
|
||||||
accessible_indexes,
|
SEARCH_INDEXES.to_vec(),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map(ApplicationResponse::Json)
|
.map(ApplicationResponse::Json)
|
||||||
@ -1898,20 +1967,82 @@ pub mod routes {
|
|||||||
.await
|
.await
|
||||||
.change_context(UserErrors::InternalServerError)
|
.change_context(UserErrors::InternalServerError)
|
||||||
.change_context(OpenSearchError::UnknownError)?;
|
.change_context(OpenSearchError::UnknownError)?;
|
||||||
let permissions = role_info.get_permissions_set();
|
let permission_groups = role_info.get_permission_groups();
|
||||||
let _ = OPENSEARCH_INDEX_PERMISSIONS
|
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()
|
.iter()
|
||||||
.filter(|(ind, _)| *ind == index)
|
.map(|user_role| {
|
||||||
.find(|i| i.1.iter().any(|p| permissions.contains(p)))
|
let state = Arc::clone(&state);
|
||||||
.ok_or(OpenSearchError::IndexAccessNotPermittedError(index))?;
|
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 filtered_user_roles: Vec<&UserRole> = user_roles
|
||||||
let org_id: OrganizationId = auth.org_id;
|
.iter()
|
||||||
let search_params: Vec<AuthInfo> = vec![AuthInfo::MerchantLevel {
|
.filter(|user_role| {
|
||||||
org_id: org_id.clone(),
|
let user_role_id = &user_role.role_id;
|
||||||
merchant_ids: vec![merchant_id.clone()],
|
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)
|
analytics::search::search_results(&state.opensearch_client, req, search_params)
|
||||||
.await
|
.await
|
||||||
.map(ApplicationResponse::Json)
|
.map(ApplicationResponse::Json)
|
||||||
|
|||||||
@ -1,22 +1,12 @@
|
|||||||
use api_models::analytics::search::SearchIndex;
|
use api_models::analytics::search::SearchIndex;
|
||||||
|
|
||||||
use crate::services::authorization::permissions::Permission;
|
pub const fn get_search_indexes() -> [SearchIndex; 4] {
|
||||||
|
[
|
||||||
pub const OPENSEARCH_INDEX_PERMISSIONS: &[(SearchIndex, &[Permission])] = &[
|
|
||||||
(
|
|
||||||
SearchIndex::PaymentAttempts,
|
SearchIndex::PaymentAttempts,
|
||||||
&[Permission::PaymentRead, Permission::PaymentWrite],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SearchIndex::PaymentIntents,
|
SearchIndex::PaymentIntents,
|
||||||
&[Permission::PaymentRead, Permission::PaymentWrite],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SearchIndex::Refunds,
|
SearchIndex::Refunds,
|
||||||
&[Permission::RefundRead, Permission::RefundWrite],
|
|
||||||
),
|
|
||||||
(
|
|
||||||
SearchIndex::Disputes,
|
SearchIndex::Disputes,
|
||||||
&[Permission::DisputeRead, Permission::DisputeWrite],
|
]
|
||||||
),
|
}
|
||||||
];
|
|
||||||
|
pub const SEARCH_INDEXES: [SearchIndex; 4] = get_search_indexes();
|
||||||
|
|||||||
Reference in New Issue
Block a user