From ada6a3227616b556a0fb710302434027ff2276f4 Mon Sep 17 00:00:00 2001 From: Mani Chandra <84711804+ThisIsMani@users.noreply.github.com> Date: Wed, 21 Feb 2024 19:14:36 +0530 Subject: [PATCH] feat(authz): Add custom role checks in authorization (#3719) Co-authored-by: Apoorv Dixit Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- crates/api_models/src/user_role.rs | 6 +- crates/router/src/analytics.rs | 2 +- crates/router/src/core/user.rs | 99 +++++++-- crates/router/src/core/user_role.rs | 154 +++++++++---- crates/router/src/routes/user.rs | 2 +- crates/router/src/routes/user_role.rs | 4 +- crates/router/src/services/authentication.rs | 20 +- crates/router/src/services/authorization.rs | 50 ++++- .../authorization/permission_groups.rs | 86 +++++++ .../src/services/authorization/permissions.rs | 2 +- .../src/services/authorization/roles.rs | 100 +++++++++ .../authorization/roles/predefined_roles.rs | 210 ++++++++++++++++++ crates/router/src/types/domain/user.rs | 41 +--- crates/router/src/utils/user.rs | 16 +- crates/router/src/utils/user_role.rs | 25 +-- 15 files changed, 669 insertions(+), 148 deletions(-) create mode 100644 crates/router/src/services/authorization/permission_groups.rs create mode 100644 crates/router/src/services/authorization/roles.rs create mode 100644 crates/router/src/services/authorization/roles/predefined_roles.rs diff --git a/crates/api_models/src/user_role.rs b/crates/api_models/src/user_role.rs index 215227b91a..1c4c28aa99 100644 --- a/crates/api_models/src/user_role.rs +++ b/crates/api_models/src/user_role.rs @@ -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); #[derive(Debug, serde::Serialize)] pub struct RoleInfoResponse { - pub role_id: &'static str, + pub role_id: String, pub permissions: Vec, - pub role_name: &'static str, + pub role_name: String, + pub role_scope: RoleScope, } #[derive(Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/router/src/analytics.rs b/crates/router/src/analytics.rs index d126d23c8a..51fb6ff822 100644 --- a/crates/router/src/analytics.rs +++ b/crates/router/src/analytics.rs @@ -497,7 +497,7 @@ pub mod routes { .await .map(ApplicationResponse::Json) }, - &auth::JWTAuth(Permission::Analytics), + &auth::JWTAuth(Permission::PaymentWrite), api_locking::LockAction::NotApplicable, )) .await diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index dfcfe8dbcd..b490912ca6 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -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::>(); - 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::>(); + 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 { - 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")] diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 16047bc3eb..c2dfd34322 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -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 { +pub async fn list_invitable_roles( + state: AppState, + user_from_token: auth::UserFromToken, +) -> UserResponse { + 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 { - 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> { - 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)); } diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index dacfe0e59a..0568b31338 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -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 diff --git a/crates/router/src/routes/user_role.rs b/crates/router/src/routes/user_role.rs index f84c158332..63e2ce3726 100644 --- a/crates/router/src/routes/user_role.rs +++ b/crates/router/src/routes/user_role.rs @@ -36,7 +36,7 @@ pub async fn list_all_roles(state: web::Data, 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, )) diff --git a/crates/router/src/services/authentication.rs b/crates/router/src/services/authentication.rs index 1004982d29..34153ef6e8 100644 --- a/crates/router/src/services/authentication.rs +++ b/crates/router/src/services/authentication.rs @@ -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() diff --git a/crates/router/src/services/authorization.rs b/crates/router/src/services/authorization.rs index cad9b1ece6..034773a07e 100644 --- a/crates/router/src/services/authorization.rs +++ b/crates/router/src/services/authorization.rs @@ -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> { - predefined_permissions::PREDEFINED_PERMISSIONS - .get(role) - .map(|role_info| role_info.get_permissions()) - .ok_or(ApiErrorResponse::InvalidJwtToken.into()) +pub async fn get_permissions( + state: &A, + token: &AuthToken, +) -> RouterResult> +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 { + groups + .iter() + .flat_map(|group| { + permission_groups::get_permissions_vec(group) + .iter() + .cloned() + }) + .collect() } pub fn check_authorization( diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs new file mode 100644 index 0000000000..246a4ce91d --- /dev/null +++ b/crates/router/src/services/authorization/permission_groups.rs @@ -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, +]; diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index 3e022e8f66..8d436618b3 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -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, diff --git a/crates/router/src/services/authorization/roles.rs b/crates/router/src/services/authorization/roles.rs new file mode 100644 index 0000000000..6372799f98 --- /dev/null +++ b/crates/router/src/services/authorization/roles.rs @@ -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, + 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 { + &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 { + 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 { + 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 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, + } + } +} diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs new file mode 100644 index 0000000000..9accf094ea --- /dev/null +++ b/crates/router/src/services/authorization/roles/predefined_roles.rs @@ -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> = 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 +}); diff --git a/crates/router/src/types/domain/user.rs b/crates/router/src/types/domain/user.rs index ab32febf9b..263b0e52b8 100644 --- a/crates/router/src/types/domain/user.rs +++ b/crates/router/src/types/domain/user.rs @@ -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 for user_role_api::PermissionInfo { } } -pub struct UserAndRoleJoined(pub storage_user::User, pub UserRole); - -impl TryFrom for user_api::UserDetails { - type Error = (); - fn try_from(user_and_role: UserAndRoleJoined) -> Result { - 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 for user_role_api::UserStatus { + fn foreign_from(value: UserStatus) -> Self { + match value { + UserStatus::Active => Self::Active, + UserStatus::InvitationSent => Self::InvitationSent, + } + } +} diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 86b298822a..e9b7143a26 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -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 { + pub async fn get_merchant_account_from_db( + &self, + state: AppState, + ) -> UserResult { 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 { + 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( diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index b677e89269..ef69219b4c 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -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, &'static str)> { - role_info.get_name().map(|name| { - ( - role_info - .get_permissions() - .iter() - .map(|&per| per.into()) - .collect::>(), - name, - ) - }) -} +use crate::services::authorization::permissions::Permission; impl From for user_role_api::Permission { fn from(value: Permission) -> Self {