mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-29 00:49:42 +08:00
feat(authz): Add custom role checks in authorization (#3719)
Co-authored-by: Apoorv Dixit <apoorv.dixit@juspay.in> Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -1,3 +1,4 @@
|
||||
use common_enums::RoleScope;
|
||||
use common_utils::pii;
|
||||
|
||||
use crate::user::DashboardEntryResponse;
|
||||
@ -7,9 +8,10 @@ pub struct ListRolesResponse(pub Vec<RoleInfoResponse>);
|
||||
|
||||
#[derive(Debug, serde::Serialize)]
|
||||
pub struct RoleInfoResponse {
|
||||
pub role_id: &'static str,
|
||||
pub role_id: String,
|
||||
pub permissions: Vec<Permission>,
|
||||
pub role_name: &'static str,
|
||||
pub role_name: String,
|
||||
pub role_scope: RoleScope,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
|
||||
@ -497,7 +497,7 @@ pub mod routes {
|
||||
.await
|
||||
.map(ApplicationResponse::Json)
|
||||
},
|
||||
&auth::JWTAuth(Permission::Analytics),
|
||||
&auth::JWTAuth(Permission::PaymentWrite),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
|
||||
@ -18,10 +18,8 @@ use crate::services::email::types as email_types;
|
||||
use crate::{
|
||||
consts,
|
||||
routes::AppState,
|
||||
services::{
|
||||
authentication as auth, authorization::predefined_permissions, ApplicationResponse,
|
||||
},
|
||||
types::domain,
|
||||
services::{authentication as auth, authorization::roles, ApplicationResponse},
|
||||
types::{domain, transformers::ForeignInto},
|
||||
utils,
|
||||
};
|
||||
pub mod dashboard_metadata;
|
||||
@ -444,7 +442,16 @@ pub async fn invite_user(
|
||||
.into());
|
||||
}
|
||||
|
||||
if !predefined_permissions::is_role_invitable(request.role_id.as_str())? {
|
||||
let role_info = roles::get_role_info_from_role_id(
|
||||
&state,
|
||||
&request.role_id,
|
||||
&user_from_token.merchant_id,
|
||||
&user_from_token.org_id,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidRoleId)?;
|
||||
|
||||
if !role_info.is_invitable() {
|
||||
return Err(UserErrors::InvalidRoleId.into())
|
||||
.attach_printable(format!("role_id = {} is not invitable", request.role_id));
|
||||
}
|
||||
@ -652,7 +659,16 @@ async fn handle_invitation(
|
||||
.into());
|
||||
}
|
||||
|
||||
if !predefined_permissions::is_role_invitable(request.role_id.as_str())? {
|
||||
let role_info = roles::get_role_info_from_role_id(
|
||||
state,
|
||||
&request.role_id,
|
||||
&user_from_token.merchant_id,
|
||||
&user_from_token.org_id,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidRoleId)?;
|
||||
|
||||
if !role_info.is_invitable() {
|
||||
return Err(UserErrors::InvalidRoleId.into())
|
||||
.attach_printable(format!("role_id = {} is not invitable", request.role_id));
|
||||
}
|
||||
@ -1030,20 +1046,18 @@ pub async fn switch_merchant_id(
|
||||
.into());
|
||||
}
|
||||
|
||||
let user_roles = state
|
||||
.store
|
||||
.list_user_roles_by_user_id(&user_from_token.user_id)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
let active_user_roles = user_roles
|
||||
.into_iter()
|
||||
.filter(|role| role.status == UserStatus::Active)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let user = user_from_token.get_user_from_db(&state).await?;
|
||||
|
||||
let (token, role_id) = if utils::user_role::is_internal_role(&user_from_token.role_id) {
|
||||
let role_info = roles::get_role_info_from_role_id(
|
||||
&state,
|
||||
&user_from_token.role_id,
|
||||
&user_from_token.merchant_id,
|
||||
&user_from_token.org_id,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InternalServerError)?;
|
||||
|
||||
let (token, role_id) = if role_info.is_internal() {
|
||||
let key_store = state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
@ -1082,6 +1096,17 @@ pub async fn switch_merchant_id(
|
||||
.await?;
|
||||
(token, user_from_token.role_id)
|
||||
} else {
|
||||
let user_roles = state
|
||||
.store
|
||||
.list_user_roles_by_user_id(&user_from_token.user_id)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
let active_user_roles = user_roles
|
||||
.into_iter()
|
||||
.filter(|role| role.status == UserStatus::Active)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let user_role = active_user_roles
|
||||
.iter()
|
||||
.find(|role| role.merchant_id == request.merchant_id)
|
||||
@ -1166,17 +1191,47 @@ pub async fn get_users_for_merchant_account(
|
||||
state: AppState,
|
||||
user_from_token: auth::UserFromToken,
|
||||
) -> UserResponse<user_api::GetUsersResponse> {
|
||||
let users = state
|
||||
let users_and_user_roles = state
|
||||
.store
|
||||
.find_users_and_roles_by_merchant_id(user_from_token.merchant_id.as_str())
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("No users for given merchant id")?
|
||||
.attach_printable("No users for given merchant id")?;
|
||||
|
||||
let users_user_roles_and_roles =
|
||||
futures::future::try_join_all(users_and_user_roles.into_iter().map(
|
||||
|(user, user_role)| async {
|
||||
roles::get_role_info_from_role_id(
|
||||
&state,
|
||||
&user_role.role_id,
|
||||
&user_role.merchant_id,
|
||||
&user_role.org_id,
|
||||
)
|
||||
.await
|
||||
.map(|role_info| (user, user_role, role_info))
|
||||
.to_not_found_response(UserErrors::InternalServerError)
|
||||
},
|
||||
))
|
||||
.await?;
|
||||
|
||||
let user_details_vec = users_user_roles_and_roles
|
||||
.into_iter()
|
||||
.filter_map(|(user, role)| domain::UserAndRoleJoined(user, role).try_into().ok())
|
||||
.map(|(user, user_role, role_info)| {
|
||||
let user = domain::UserFromStorage::from(user);
|
||||
user_api::UserDetails {
|
||||
email: user.get_email(),
|
||||
name: user.get_name(),
|
||||
role_id: user_role.role_id,
|
||||
role_name: role_info.get_role_name().to_string(),
|
||||
status: user_role.status.foreign_into(),
|
||||
last_modified_at: user_role.last_modified,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok(ApplicationResponse::Json(user_api::GetUsersResponse(users)))
|
||||
Ok(ApplicationResponse::Json(user_api::GetUsersResponse(
|
||||
user_details_vec,
|
||||
)))
|
||||
}
|
||||
|
||||
#[cfg(feature = "email")]
|
||||
|
||||
@ -10,7 +10,7 @@ use crate::{
|
||||
routes::AppState,
|
||||
services::{
|
||||
authentication::{self as auth},
|
||||
authorization::{info, predefined_permissions},
|
||||
authorization::{info, roles},
|
||||
ApplicationResponse,
|
||||
},
|
||||
types::domain,
|
||||
@ -30,58 +30,96 @@ pub async fn get_authorization_info(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn list_roles(_state: AppState) -> UserResponse<user_role_api::ListRolesResponse> {
|
||||
pub async fn list_invitable_roles(
|
||||
state: AppState,
|
||||
user_from_token: auth::UserFromToken,
|
||||
) -> UserResponse<user_role_api::ListRolesResponse> {
|
||||
let predefined_roles_map = roles::predefined_roles::PREDEFINED_ROLES
|
||||
.iter()
|
||||
.filter(|(_, role_info)| role_info.is_invitable())
|
||||
.map(|(role_id, role_info)| user_role_api::RoleInfoResponse {
|
||||
permissions: role_info
|
||||
.get_permissions_set()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
role_id: role_id.to_string(),
|
||||
role_name: role_info.get_role_name().to_string(),
|
||||
role_scope: role_info.get_scope(),
|
||||
});
|
||||
|
||||
let custom_roles_map = state
|
||||
.store
|
||||
.list_all_roles(&user_from_token.merchant_id, &user_from_token.org_id)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?
|
||||
.into_iter()
|
||||
.map(roles::RoleInfo::from)
|
||||
.filter(|role_info| role_info.is_invitable())
|
||||
.map(|role_info| user_role_api::RoleInfoResponse {
|
||||
permissions: role_info
|
||||
.get_permissions_set()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect(),
|
||||
role_id: role_info.get_role_id().to_string(),
|
||||
role_name: role_info.get_role_name().to_string(),
|
||||
role_scope: role_info.get_scope(),
|
||||
});
|
||||
|
||||
Ok(ApplicationResponse::Json(user_role_api::ListRolesResponse(
|
||||
predefined_permissions::PREDEFINED_PERMISSIONS
|
||||
.iter()
|
||||
.filter(|(_, role_info)| role_info.is_invitable())
|
||||
.filter_map(|(role_id, role_info)| {
|
||||
utils::user_role::get_role_name_and_permission_response(role_info).map(
|
||||
|(permissions, role_name)| user_role_api::RoleInfoResponse {
|
||||
permissions,
|
||||
role_id,
|
||||
role_name,
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
predefined_roles_map.chain(custom_roles_map).collect(),
|
||||
)))
|
||||
}
|
||||
|
||||
pub async fn get_role(
|
||||
_state: AppState,
|
||||
state: AppState,
|
||||
user_from_token: auth::UserFromToken,
|
||||
role: user_role_api::GetRoleRequest,
|
||||
) -> UserResponse<user_role_api::RoleInfoResponse> {
|
||||
let info = predefined_permissions::PREDEFINED_PERMISSIONS
|
||||
.get_key_value(role.role_id.as_str())
|
||||
.and_then(|(role_id, role_info)| {
|
||||
utils::user_role::get_role_name_and_permission_response(role_info).map(
|
||||
|(permissions, role_name)| user_role_api::RoleInfoResponse {
|
||||
permissions,
|
||||
role_id,
|
||||
role_name,
|
||||
},
|
||||
)
|
||||
})
|
||||
.ok_or(UserErrors::InvalidRoleId)?;
|
||||
let role_info = roles::get_role_info_from_role_id(
|
||||
&state,
|
||||
&role.role_id,
|
||||
&user_from_token.merchant_id,
|
||||
&user_from_token.org_id,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidRoleId)?;
|
||||
|
||||
Ok(ApplicationResponse::Json(info))
|
||||
if role_info.is_internal() {
|
||||
return Err(UserErrors::InvalidRoleId.into());
|
||||
}
|
||||
|
||||
let permissions = role_info
|
||||
.get_permissions_set()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok(ApplicationResponse::Json(user_role_api::RoleInfoResponse {
|
||||
permissions,
|
||||
role_id: role.role_id,
|
||||
role_name: role_info.get_role_name().to_string(),
|
||||
role_scope: role_info.get_scope(),
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn get_role_from_token(
|
||||
_state: AppState,
|
||||
user: auth::UserFromToken,
|
||||
state: AppState,
|
||||
user_from_token: auth::UserFromToken,
|
||||
) -> UserResponse<Vec<user_role_api::Permission>> {
|
||||
Ok(ApplicationResponse::Json(
|
||||
predefined_permissions::PREDEFINED_PERMISSIONS
|
||||
.get(user.role_id.as_str())
|
||||
.ok_or(UserErrors::InternalServerError.into())
|
||||
.attach_printable("Invalid Role Id in JWT")?
|
||||
.get_permissions()
|
||||
.iter()
|
||||
.map(|&per| per.into())
|
||||
.collect(),
|
||||
))
|
||||
let role_info = user_from_token
|
||||
.get_role_info_from_db(&state)
|
||||
.await
|
||||
.attach_printable("Invalid role_id in JWT")?;
|
||||
|
||||
let permissions = role_info
|
||||
.get_permissions_set()
|
||||
.into_iter()
|
||||
.map(Into::into)
|
||||
.collect();
|
||||
|
||||
Ok(ApplicationResponse::Json(permissions))
|
||||
}
|
||||
|
||||
pub async fn update_user_role(
|
||||
@ -89,7 +127,16 @@ pub async fn update_user_role(
|
||||
user_from_token: auth::UserFromToken,
|
||||
req: user_role_api::UpdateUserRoleRequest,
|
||||
) -> UserResponse<()> {
|
||||
if !predefined_permissions::is_role_updatable(&req.role_id)? {
|
||||
let role_info = roles::get_role_info_from_role_id(
|
||||
&state,
|
||||
&req.role_id,
|
||||
&user_from_token.merchant_id,
|
||||
&user_from_token.org_id,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidRoleId)?;
|
||||
|
||||
if !role_info.is_updatable() {
|
||||
return Err(UserErrors::InvalidRoleOperation.into())
|
||||
.attach_printable(format!("User role cannot be updated to {}", req.role_id));
|
||||
}
|
||||
@ -110,10 +157,19 @@ pub async fn update_user_role(
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidRoleOperation)?;
|
||||
|
||||
if !predefined_permissions::is_role_updatable(&user_role_to_be_updated.role_id)? {
|
||||
let role_to_be_updated = roles::get_role_info_from_role_id(
|
||||
&state,
|
||||
&user_role_to_be_updated.role_id,
|
||||
&user_from_token.merchant_id,
|
||||
&user_from_token.org_id,
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
|
||||
if !role_to_be_updated.is_updatable() {
|
||||
return Err(UserErrors::InvalidRoleOperation.into()).attach_printable(format!(
|
||||
"User role cannot be updated from {}",
|
||||
user_role_to_be_updated.role_id
|
||||
role_to_be_updated.get_role_id()
|
||||
));
|
||||
}
|
||||
|
||||
@ -270,7 +326,15 @@ pub async fn delete_user_role(
|
||||
.find(|&role| role.merchant_id == user_from_token.merchant_id.as_str())
|
||||
{
|
||||
Some(user_role) => {
|
||||
if !predefined_permissions::is_role_deletable(&user_role.role_id)? {
|
||||
let role_info = roles::get_role_info_from_role_id(
|
||||
&state,
|
||||
&user_role.role_id,
|
||||
&user_from_token.merchant_id,
|
||||
&user_from_token.org_id,
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
if !role_info.is_deletable() {
|
||||
return Err(UserErrors::InvalidDeleteOperation.into())
|
||||
.attach_printable(format!("role_id = {} is not deletable", user_role.role_id));
|
||||
}
|
||||
|
||||
@ -291,7 +291,7 @@ pub async fn delete_sample_data(
|
||||
&http_req,
|
||||
payload.into_inner(),
|
||||
sample_data::delete_sample_data_for_user,
|
||||
&auth::JWTAuth(Permission::PaymentWrite),
|
||||
&auth::JWTAuth(Permission::MerchantAccountWrite),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
|
||||
@ -36,7 +36,7 @@ pub async fn list_all_roles(state: web::Data<AppState>, req: HttpRequest) -> Htt
|
||||
state.clone(),
|
||||
&req,
|
||||
(),
|
||||
|state, _: (), _| user_role_core::list_roles(state),
|
||||
|state, user, _| user_role_core::list_invitable_roles(state, user),
|
||||
&auth::JWTAuth(Permission::UsersRead),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
@ -57,7 +57,7 @@ pub async fn get_role(
|
||||
state.clone(),
|
||||
&req,
|
||||
request_payload,
|
||||
|state, _: (), req| user_role_core::get_role(state, req),
|
||||
user_role_core::get_role,
|
||||
&auth::JWTAuth(Permission::UsersRead),
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
|
||||
@ -503,8 +503,8 @@ where
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
let permissions = authorization::get_permissions(state, &payload).await?;
|
||||
authorization::check_authorization(&self.0, &permissions)?;
|
||||
|
||||
Ok((
|
||||
(),
|
||||
@ -532,8 +532,8 @@ where
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
let permissions = authorization::get_permissions(state, &payload).await?;
|
||||
authorization::check_authorization(&self.0, &permissions)?;
|
||||
|
||||
Ok((
|
||||
UserFromToken {
|
||||
@ -570,8 +570,8 @@ where
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.required_permission, permissions)?;
|
||||
let permissions = authorization::get_permissions(state, &payload).await?;
|
||||
authorization::check_authorization(&self.required_permission, &permissions)?;
|
||||
|
||||
// Check if token has access to MerchantId that has been requested through query param
|
||||
if payload.merchant_id != self.merchant_id {
|
||||
@ -613,8 +613,8 @@ where
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
let permissions = authorization::get_permissions(state, &payload).await?;
|
||||
authorization::check_authorization(&self.0, &permissions)?;
|
||||
|
||||
let key_store = state
|
||||
.store()
|
||||
@ -663,8 +663,8 @@ where
|
||||
return Err(errors::ApiErrorResponse::InvalidJwtToken.into());
|
||||
}
|
||||
|
||||
let permissions = authorization::get_permissions(&payload.role_id)?;
|
||||
authorization::check_authorization(&self.0, permissions)?;
|
||||
let permissions = authorization::get_permissions(state, &payload).await?;
|
||||
authorization::check_authorization(&self.0, &permissions)?;
|
||||
|
||||
let key_store = state
|
||||
.store()
|
||||
|
||||
@ -1,14 +1,50 @@
|
||||
use crate::core::errors::{ApiErrorResponse, RouterResult};
|
||||
use common_enums::PermissionGroup;
|
||||
|
||||
use super::authentication::AuthToken;
|
||||
use crate::{
|
||||
core::errors::{ApiErrorResponse, RouterResult, StorageErrorExt},
|
||||
routes::app::AppStateInfo,
|
||||
};
|
||||
|
||||
pub mod info;
|
||||
pub mod permission_groups;
|
||||
pub mod permissions;
|
||||
pub mod predefined_permissions;
|
||||
pub mod roles;
|
||||
|
||||
pub fn get_permissions(role: &str) -> RouterResult<&Vec<permissions::Permission>> {
|
||||
predefined_permissions::PREDEFINED_PERMISSIONS
|
||||
.get(role)
|
||||
.map(|role_info| role_info.get_permissions())
|
||||
.ok_or(ApiErrorResponse::InvalidJwtToken.into())
|
||||
pub async fn get_permissions<A>(
|
||||
state: &A,
|
||||
token: &AuthToken,
|
||||
) -> RouterResult<Vec<permissions::Permission>>
|
||||
where
|
||||
A: AppStateInfo + Sync,
|
||||
{
|
||||
if let Some(role_info) = roles::predefined_roles::PREDEFINED_ROLES.get(token.role_id.as_str()) {
|
||||
Ok(get_permissions_from_groups(
|
||||
role_info.get_permission_groups(),
|
||||
))
|
||||
} else {
|
||||
state
|
||||
.store()
|
||||
.find_role_by_role_id_in_merchant_scope(
|
||||
&token.role_id,
|
||||
&token.merchant_id,
|
||||
&token.org_id,
|
||||
)
|
||||
.await
|
||||
.map(|role| get_permissions_from_groups(&role.groups))
|
||||
.to_not_found_response(ApiErrorResponse::InvalidJwtToken)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_permissions_from_groups(groups: &[PermissionGroup]) -> Vec<permissions::Permission> {
|
||||
groups
|
||||
.iter()
|
||||
.flat_map(|group| {
|
||||
permission_groups::get_permissions_vec(group)
|
||||
.iter()
|
||||
.cloned()
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn check_authorization(
|
||||
|
||||
@ -0,0 +1,86 @@
|
||||
use common_enums::PermissionGroup;
|
||||
|
||||
use super::permissions::Permission;
|
||||
|
||||
pub fn get_permissions_vec(permission_group: &PermissionGroup) -> &[Permission] {
|
||||
match permission_group {
|
||||
PermissionGroup::OperationsView => &OPERATIONS_VIEW,
|
||||
PermissionGroup::OperationsManage => &OPERATIONS_MANAGE,
|
||||
PermissionGroup::ConnectorsView => &CONNECTORS_VIEW,
|
||||
PermissionGroup::ConnectorsManage => &CONNECTORS_MANAGE,
|
||||
PermissionGroup::WorkflowsView => &WORKFLOWS_VIEW,
|
||||
PermissionGroup::WorkflowsManage => &WORKFLOWS_MANAGE,
|
||||
PermissionGroup::AnalyticsView => &ANALYTICS_VIEW,
|
||||
PermissionGroup::UsersView => &USERS_VIEW,
|
||||
PermissionGroup::UsersManage => &USERS_MANAGE,
|
||||
PermissionGroup::MerchantDetailsView => &MERCHANT_DETAILS_VIEW,
|
||||
PermissionGroup::MerchantDetailsManage => &MERCHANT_DETAILS_MANAGE,
|
||||
PermissionGroup::OrganizationManage => &ORGANIZATION_MANAGE,
|
||||
}
|
||||
}
|
||||
|
||||
pub static OPERATIONS_VIEW: [Permission; 6] = [
|
||||
Permission::PaymentRead,
|
||||
Permission::RefundRead,
|
||||
Permission::MandateRead,
|
||||
Permission::DisputeRead,
|
||||
Permission::CustomerRead,
|
||||
Permission::MerchantAccountRead,
|
||||
];
|
||||
|
||||
pub static OPERATIONS_MANAGE: [Permission; 6] = [
|
||||
Permission::PaymentWrite,
|
||||
Permission::RefundWrite,
|
||||
Permission::MandateWrite,
|
||||
Permission::DisputeWrite,
|
||||
Permission::CustomerWrite,
|
||||
Permission::MerchantAccountRead,
|
||||
];
|
||||
|
||||
pub static CONNECTORS_VIEW: [Permission; 2] = [
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::MerchantAccountRead,
|
||||
];
|
||||
|
||||
pub static CONNECTORS_MANAGE: [Permission; 2] = [
|
||||
Permission::MerchantConnectorAccountWrite,
|
||||
Permission::MerchantAccountRead,
|
||||
];
|
||||
|
||||
pub static WORKFLOWS_VIEW: [Permission; 5] = [
|
||||
Permission::RoutingRead,
|
||||
Permission::ThreeDsDecisionManagerRead,
|
||||
Permission::SurchargeDecisionManagerRead,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::MerchantAccountRead,
|
||||
];
|
||||
|
||||
pub static WORKFLOWS_MANAGE: [Permission; 5] = [
|
||||
Permission::RoutingWrite,
|
||||
Permission::ThreeDsDecisionManagerWrite,
|
||||
Permission::SurchargeDecisionManagerWrite,
|
||||
Permission::MerchantConnectorAccountRead,
|
||||
Permission::MerchantAccountRead,
|
||||
];
|
||||
|
||||
pub static ANALYTICS_VIEW: [Permission; 2] =
|
||||
[Permission::Analytics, Permission::MerchantAccountRead];
|
||||
|
||||
pub static USERS_VIEW: [Permission; 2] = [Permission::UsersRead, Permission::MerchantAccountRead];
|
||||
|
||||
pub static USERS_MANAGE: [Permission; 2] =
|
||||
[Permission::UsersWrite, Permission::MerchantAccountRead];
|
||||
|
||||
pub static MERCHANT_DETAILS_VIEW: [Permission; 1] = [Permission::MerchantAccountRead];
|
||||
|
||||
pub static MERCHANT_DETAILS_MANAGE: [Permission; 4] = [
|
||||
Permission::MerchantAccountWrite,
|
||||
Permission::ApiKeyRead,
|
||||
Permission::ApiKeyWrite,
|
||||
Permission::MerchantAccountRead,
|
||||
];
|
||||
|
||||
pub static ORGANIZATION_MANAGE: [Permission; 2] = [
|
||||
Permission::MerchantAccountCreate,
|
||||
Permission::MerchantAccountRead,
|
||||
];
|
||||
@ -1,6 +1,6 @@
|
||||
use strum::Display;
|
||||
|
||||
#[derive(PartialEq, Display, Clone, Debug, Copy)]
|
||||
#[derive(PartialEq, Display, Clone, Debug, Copy, Eq, Hash)]
|
||||
pub enum Permission {
|
||||
PaymentRead,
|
||||
PaymentWrite,
|
||||
|
||||
100
crates/router/src/services/authorization/roles.rs
Normal file
100
crates/router/src/services/authorization/roles.rs
Normal file
@ -0,0 +1,100 @@
|
||||
use std::collections::HashSet;
|
||||
|
||||
use common_enums::{PermissionGroup, RoleScope};
|
||||
use common_utils::errors::CustomResult;
|
||||
|
||||
use super::{permission_groups::get_permissions_vec, permissions::Permission};
|
||||
use crate::{core::errors, routes::AppState};
|
||||
|
||||
pub mod predefined_roles;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RoleInfo {
|
||||
role_id: String,
|
||||
role_name: String,
|
||||
groups: Vec<PermissionGroup>,
|
||||
scope: RoleScope,
|
||||
is_invitable: bool,
|
||||
is_deletable: bool,
|
||||
is_updatable: bool,
|
||||
is_internal: bool,
|
||||
}
|
||||
|
||||
impl RoleInfo {
|
||||
pub fn get_role_id(&self) -> &str {
|
||||
&self.role_id
|
||||
}
|
||||
|
||||
pub fn get_role_name(&self) -> &str {
|
||||
&self.role_name
|
||||
}
|
||||
|
||||
pub fn get_permission_groups(&self) -> &Vec<PermissionGroup> {
|
||||
&self.groups
|
||||
}
|
||||
|
||||
pub fn get_scope(&self) -> RoleScope {
|
||||
self.scope
|
||||
}
|
||||
|
||||
pub fn is_invitable(&self) -> bool {
|
||||
self.is_invitable
|
||||
}
|
||||
|
||||
pub fn is_deletable(&self) -> bool {
|
||||
self.is_deletable
|
||||
}
|
||||
|
||||
pub fn is_internal(&self) -> bool {
|
||||
self.is_internal
|
||||
}
|
||||
|
||||
pub fn is_updatable(&self) -> bool {
|
||||
self.is_updatable
|
||||
}
|
||||
|
||||
pub fn get_permissions_set(&self) -> HashSet<Permission> {
|
||||
self.groups
|
||||
.iter()
|
||||
.flat_map(|group| get_permissions_vec(group).iter().copied())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn check_permission_exists(&self, required_permission: &Permission) -> bool {
|
||||
self.groups
|
||||
.iter()
|
||||
.any(|group| get_permissions_vec(group).contains(required_permission))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn get_role_info_from_role_id(
|
||||
state: &AppState,
|
||||
role_id: &str,
|
||||
merchant_id: &str,
|
||||
org_id: &str,
|
||||
) -> CustomResult<RoleInfo, errors::StorageError> {
|
||||
if let Some(role) = predefined_roles::PREDEFINED_ROLES.get(role_id) {
|
||||
Ok(role.clone())
|
||||
} else {
|
||||
state
|
||||
.store
|
||||
.find_role_by_role_id_in_merchant_scope(role_id, merchant_id, org_id)
|
||||
.await
|
||||
.map(RoleInfo::from)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel_models::role::Role> for RoleInfo {
|
||||
fn from(role: diesel_models::role::Role) -> Self {
|
||||
Self {
|
||||
role_id: role.role_id,
|
||||
role_name: role.role_name,
|
||||
groups: role.groups.into_iter().map(Into::into).collect(),
|
||||
scope: role.scope,
|
||||
is_invitable: true,
|
||||
is_deletable: true,
|
||||
is_updatable: true,
|
||||
is_internal: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,210 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
use common_enums::{PermissionGroup, RoleScope};
|
||||
use once_cell::sync::Lazy;
|
||||
|
||||
use super::RoleInfo;
|
||||
use crate::consts;
|
||||
|
||||
pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|| {
|
||||
let mut roles = HashMap::new();
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_INTERNAL_ADMIN,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::OperationsManage,
|
||||
PermissionGroup::ConnectorsView,
|
||||
PermissionGroup::ConnectorsManage,
|
||||
PermissionGroup::WorkflowsView,
|
||||
PermissionGroup::WorkflowsManage,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::UsersManage,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
PermissionGroup::MerchantDetailsManage,
|
||||
PermissionGroup::OrganizationManage,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_INTERNAL_ADMIN.to_string(),
|
||||
role_name: "Internal Admin".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: false,
|
||||
is_deletable: false,
|
||||
is_updatable: false,
|
||||
is_internal: true,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::ConnectorsView,
|
||||
PermissionGroup::WorkflowsView,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(),
|
||||
role_name: "Internal View Only".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: false,
|
||||
is_deletable: false,
|
||||
is_updatable: false,
|
||||
is_internal: true,
|
||||
},
|
||||
);
|
||||
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_ORGANIZATION_ADMIN,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::OperationsManage,
|
||||
PermissionGroup::ConnectorsView,
|
||||
PermissionGroup::ConnectorsManage,
|
||||
PermissionGroup::WorkflowsView,
|
||||
PermissionGroup::WorkflowsManage,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::UsersManage,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
PermissionGroup::MerchantDetailsManage,
|
||||
PermissionGroup::OrganizationManage,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_ORGANIZATION_ADMIN.to_string(),
|
||||
role_name: "Organization Admin".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: false,
|
||||
is_deletable: false,
|
||||
is_updatable: false,
|
||||
is_internal: false,
|
||||
},
|
||||
);
|
||||
|
||||
// MERCHANT ROLES
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_ADMIN,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::OperationsManage,
|
||||
PermissionGroup::ConnectorsView,
|
||||
PermissionGroup::ConnectorsManage,
|
||||
PermissionGroup::WorkflowsView,
|
||||
PermissionGroup::WorkflowsManage,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::UsersManage,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
PermissionGroup::MerchantDetailsManage,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_MERCHANT_ADMIN.to_string(),
|
||||
role_name: "Admin".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: true,
|
||||
is_deletable: true,
|
||||
is_updatable: true,
|
||||
is_internal: false,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::ConnectorsView,
|
||||
PermissionGroup::WorkflowsView,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_MERCHANT_VIEW_ONLY.to_string(),
|
||||
role_name: "View Only".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: true,
|
||||
is_deletable: true,
|
||||
is_updatable: true,
|
||||
is_internal: false,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::UsersManage,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_MERCHANT_IAM_ADMIN.to_string(),
|
||||
role_name: "IAM".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: true,
|
||||
is_deletable: true,
|
||||
is_updatable: true,
|
||||
is_internal: false,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_DEVELOPER,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::ConnectorsView,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
PermissionGroup::MerchantDetailsManage,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_MERCHANT_DEVELOPER.to_string(),
|
||||
role_name: "Developer".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: true,
|
||||
is_deletable: true,
|
||||
is_updatable: true,
|
||||
is_internal: false,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_OPERATOR,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::OperationsManage,
|
||||
PermissionGroup::ConnectorsView,
|
||||
PermissionGroup::WorkflowsView,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_MERCHANT_OPERATOR.to_string(),
|
||||
role_name: "Operator".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: true,
|
||||
is_deletable: true,
|
||||
is_updatable: true,
|
||||
is_internal: false,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
],
|
||||
role_id: consts::user_role::ROLE_ID_MERCHANT_CUSTOMER_SUPPORT.to_string(),
|
||||
role_name: "Customer Support".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
is_invitable: true,
|
||||
is_deletable: true,
|
||||
is_updatable: true,
|
||||
is_internal: false,
|
||||
},
|
||||
);
|
||||
roles
|
||||
});
|
||||
@ -25,11 +25,7 @@ use crate::{
|
||||
},
|
||||
db::StorageInterface,
|
||||
routes::AppState,
|
||||
services::{
|
||||
authentication as auth,
|
||||
authentication::UserFromToken,
|
||||
authorization::{info, predefined_permissions},
|
||||
},
|
||||
services::{authentication as auth, authentication::UserFromToken, authorization::info},
|
||||
types::transformers::ForeignFrom,
|
||||
utils::{self, user::password},
|
||||
};
|
||||
@ -824,32 +820,6 @@ impl From<info::PermissionInfo> for user_role_api::PermissionInfo {
|
||||
}
|
||||
}
|
||||
|
||||
pub struct UserAndRoleJoined(pub storage_user::User, pub UserRole);
|
||||
|
||||
impl TryFrom<UserAndRoleJoined> for user_api::UserDetails {
|
||||
type Error = ();
|
||||
fn try_from(user_and_role: UserAndRoleJoined) -> Result<Self, Self::Error> {
|
||||
let status = match user_and_role.1.status {
|
||||
UserStatus::Active => user_role_api::UserStatus::Active,
|
||||
UserStatus::InvitationSent => user_role_api::UserStatus::InvitationSent,
|
||||
};
|
||||
|
||||
let role_id = user_and_role.1.role_id;
|
||||
let role_name = predefined_permissions::get_role_name_from_id(role_id.as_str())
|
||||
.ok_or(())?
|
||||
.to_string();
|
||||
|
||||
Ok(Self {
|
||||
email: user_and_role.0.email,
|
||||
name: user_and_role.0.name,
|
||||
role_id,
|
||||
status,
|
||||
role_name,
|
||||
last_modified_at: user_and_role.0.last_modified_at,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub enum SignInWithRoleStrategyType {
|
||||
SingleRole(SignInWithSingleRoleStrategy),
|
||||
MultipleRoles(SignInWithMultipleRolesStrategy),
|
||||
@ -947,3 +917,12 @@ impl SignInWithMultipleRolesStrategy {
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
impl ForeignFrom<UserStatus> for user_role_api::UserStatus {
|
||||
fn foreign_from(value: UserStatus) -> Self {
|
||||
match value {
|
||||
UserStatus::Active => Self::Active,
|
||||
UserStatus::InvitationSent => Self::InvitationSent,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -9,7 +9,10 @@ use masking::{ExposeInterface, Secret};
|
||||
use crate::{
|
||||
core::errors::{StorageError, UserErrors, UserResult},
|
||||
routes::AppState,
|
||||
services::authentication::{AuthToken, UserFromToken},
|
||||
services::{
|
||||
authentication::{AuthToken, UserFromToken},
|
||||
authorization::roles::{self, RoleInfo},
|
||||
},
|
||||
types::domain::{self, MerchantAccount, UserFromStorage},
|
||||
};
|
||||
|
||||
@ -19,7 +22,10 @@ pub mod password;
|
||||
pub mod sample_data;
|
||||
|
||||
impl UserFromToken {
|
||||
pub async fn get_merchant_account(&self, state: AppState) -> UserResult<MerchantAccount> {
|
||||
pub async fn get_merchant_account_from_db(
|
||||
&self,
|
||||
state: AppState,
|
||||
) -> UserResult<MerchantAccount> {
|
||||
let key_store = state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
@ -56,6 +62,12 @@ impl UserFromToken {
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
Ok(user.into())
|
||||
}
|
||||
|
||||
pub async fn get_role_info_from_db(&self, state: &AppState) -> UserResult<RoleInfo> {
|
||||
roles::get_role_info_from_role_id(state, &self.role_id, &self.merchant_id, &self.org_id)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn generate_jwt_auth_token(
|
||||
|
||||
@ -1,29 +1,6 @@
|
||||
use api_models::user_role as user_role_api;
|
||||
|
||||
use crate::{
|
||||
consts,
|
||||
services::authorization::{permissions::Permission, predefined_permissions::RoleInfo},
|
||||
};
|
||||
|
||||
pub fn is_internal_role(role_id: &str) -> bool {
|
||||
role_id == consts::user_role::ROLE_ID_INTERNAL_ADMIN
|
||||
|| role_id == consts::user_role::ROLE_ID_INTERNAL_VIEW_ONLY_USER
|
||||
}
|
||||
|
||||
pub fn get_role_name_and_permission_response(
|
||||
role_info: &RoleInfo,
|
||||
) -> Option<(Vec<user_role_api::Permission>, &'static str)> {
|
||||
role_info.get_name().map(|name| {
|
||||
(
|
||||
role_info
|
||||
.get_permissions()
|
||||
.iter()
|
||||
.map(|&per| per.into())
|
||||
.collect::<Vec<user_role_api::Permission>>(),
|
||||
name,
|
||||
)
|
||||
})
|
||||
}
|
||||
use crate::services::authorization::permissions::Permission;
|
||||
|
||||
impl From<Permission> for user_role_api::Permission {
|
||||
fn from(value: Permission) -> Self {
|
||||
|
||||
Reference in New Issue
Block a user