mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-28 12:15:40 +08:00
feat(connector_cloning): Create API for cloning connectors between merchants and profiles. (#7949)
Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
@ -1068,6 +1068,9 @@ billing_connectors_which_require_payment_sync = "stripebilling, recurly" # List
|
||||
enabled = true # Enable or disable Open Router
|
||||
url = "http://localhost:8080" # Open Router URL
|
||||
|
||||
|
||||
[billing_connectors_invoice_sync]
|
||||
billing_connectors_which_requires_invoice_sync_call = "recurly" # List of billing connectors which has invoice sync api call
|
||||
|
||||
[clone_connector_allowlist]
|
||||
merchant_ids = "merchant_ids" # Comma-separated list of allowed merchant IDs
|
||||
connector_names = "connector_names" # Comma-separated list of allowed connector names
|
||||
|
||||
@ -362,3 +362,7 @@ background_color = "#FFFFFF" # Background color of email bod
|
||||
|
||||
[connectors.unified_authentication_service] #Unified Authentication Service Configuration
|
||||
base_url = "http://localhost:8000" #base url to call unified authentication service
|
||||
|
||||
[clone_connector_allowlist]
|
||||
merchant_ids = "merchant_ids" # Comma-separated list of allowed merchant IDs
|
||||
connector_names = "connector_names" # Comma-separated list of allowed connector names
|
||||
|
||||
@ -1156,3 +1156,7 @@ click_to_pay = {connector_list = "adyen, cybersource"}
|
||||
[open_router]
|
||||
enabled = false
|
||||
url = "http://localhost:8080"
|
||||
|
||||
[clone_connector_allowlist]
|
||||
merchant_ids = "merchant_123, merchant_234" # Comma-separated list of allowed merchant IDs
|
||||
connector_names = "stripe, adyen" # Comma-separated list of allowed connector names
|
||||
|
||||
@ -1051,3 +1051,7 @@ enabled = true
|
||||
|
||||
[authentication_providers]
|
||||
click_to_pay = {connector_list = "adyen, cybersource"}
|
||||
|
||||
[clone_connector_allowlist]
|
||||
merchant_ids = "merchant_123, merchant_234" # Comma-separated list of allowed merchant IDs
|
||||
connector_names = "stripe, adyen" # Comma-separated list of allowed connector names
|
||||
|
||||
@ -11,7 +11,7 @@ use crate::user::{
|
||||
GetMetaDataRequest, GetMetaDataResponse, GetMultipleMetaDataPayload, SetMetaDataRequest,
|
||||
},
|
||||
AcceptInviteFromEmailRequest, AuthSelectRequest, AuthorizeResponse, BeginTotpResponse,
|
||||
ChangePasswordRequest, ConnectAccountRequest, CreateInternalUserRequest,
|
||||
ChangePasswordRequest, CloneConnectorRequest, ConnectAccountRequest, CreateInternalUserRequest,
|
||||
CreateTenantUserRequest, CreateUserAuthenticationMethodRequest, ForgotPasswordRequest,
|
||||
GetSsoAuthUrlRequest, GetUserAuthenticationMethodsRequest, GetUserDetailsResponse,
|
||||
GetUserRoleDetailsRequest, GetUserRoleDetailsResponseV2, InviteUserRequest,
|
||||
@ -71,7 +71,8 @@ common_utils::impl_api_event_type!(
|
||||
UpdateUserAuthenticationMethodRequest,
|
||||
GetSsoAuthUrlRequest,
|
||||
SsoSignInRequest,
|
||||
AuthSelectRequest
|
||||
AuthSelectRequest,
|
||||
CloneConnectorRequest
|
||||
)
|
||||
);
|
||||
|
||||
|
||||
@ -108,11 +108,31 @@ pub struct SwitchProfileRequest {
|
||||
pub profile_id: id_type::ProfileId,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct CloneConnectorSource {
|
||||
pub mca_id: id_type::MerchantConnectorAccountId,
|
||||
pub merchant_id: id_type::MerchantId,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct CloneConnectorDestination {
|
||||
pub connector_label: Option<String>,
|
||||
pub profile_id: id_type::ProfileId,
|
||||
pub merchant_id: id_type::MerchantId,
|
||||
}
|
||||
|
||||
#[derive(Debug, serde::Deserialize, serde::Serialize)]
|
||||
pub struct CloneConnectorRequest {
|
||||
pub source: CloneConnectorSource,
|
||||
pub destination: CloneConnectorDestination,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, serde::Serialize)]
|
||||
pub struct CreateInternalUserRequest {
|
||||
pub name: Secret<String>,
|
||||
pub email: pii::Email,
|
||||
pub password: Secret<String>,
|
||||
pub role_id: String,
|
||||
}
|
||||
|
||||
#[derive(serde::Deserialize, Debug, serde::Serialize)]
|
||||
|
||||
@ -7214,6 +7214,7 @@ pub enum PermissionGroup {
|
||||
ReconReportsManage,
|
||||
ReconOpsView,
|
||||
ReconOpsManage,
|
||||
InternalManage,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash, strum::EnumIter)]
|
||||
@ -7226,6 +7227,7 @@ pub enum ParentGroup {
|
||||
ReconOps,
|
||||
ReconReports,
|
||||
Account,
|
||||
Internal,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize)]
|
||||
@ -7255,6 +7257,7 @@ pub enum Resource {
|
||||
RunRecon,
|
||||
ReconConfig,
|
||||
RevenueRecovery,
|
||||
InternalConnector,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)]
|
||||
|
||||
@ -127,6 +127,8 @@ pub const ROLE_ID_ORGANIZATION_ADMIN: &str = "org_admin";
|
||||
pub const ROLE_ID_INTERNAL_VIEW_ONLY_USER: &str = "internal_view_only";
|
||||
/// Role ID for Internal Admin
|
||||
pub const ROLE_ID_INTERNAL_ADMIN: &str = "internal_admin";
|
||||
/// Role ID for Internal Demo
|
||||
pub const ROLE_ID_INTERNAL_DEMO: &str = "internal_demo";
|
||||
|
||||
/// Max length allowed for Description
|
||||
pub const MAX_DESCRIPTION_LENGTH: u16 = 255;
|
||||
|
||||
@ -536,5 +536,6 @@ pub(crate) async fn fetch_raw_secrets(
|
||||
platform: conf.platform,
|
||||
authentication_providers: conf.authentication_providers,
|
||||
open_router: conf.open_router,
|
||||
clone_connector_allowlist: conf.clone_connector_allowlist,
|
||||
}
|
||||
}
|
||||
|
||||
@ -153,6 +153,7 @@ pub struct Settings<S: SecretState> {
|
||||
pub platform: Platform,
|
||||
pub authentication_providers: AuthenticationProviders,
|
||||
pub open_router: OpenRouter,
|
||||
pub clone_connector_allowlist: Option<CloneConnectorAllowlistConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
@ -160,6 +161,16 @@ pub struct OpenRouter {
|
||||
pub enabled: bool,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
#[serde(default)]
|
||||
pub struct CloneConnectorAllowlistConfig {
|
||||
#[serde(deserialize_with = "deserialize_merchant_ids")]
|
||||
pub merchant_ids: HashSet<id_type::MerchantId>,
|
||||
#[serde(deserialize_with = "deserialize_hashset")]
|
||||
pub connector_names: HashSet<enums::Connector>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, Clone, Default)]
|
||||
pub struct Platform {
|
||||
pub enabled: bool,
|
||||
|
||||
@ -110,6 +110,10 @@ pub enum UserErrors {
|
||||
MissingEmailConfig,
|
||||
#[error("Invalid Auth Method Operation: {0}")]
|
||||
InvalidAuthMethodOperationWithMessage(String),
|
||||
#[error("Invalid Clone Connector Operation: {0}")]
|
||||
InvalidCloneConnectorOperation(String),
|
||||
#[error("Error cloning connector: {0}")]
|
||||
ErrorCloningConnector(String),
|
||||
}
|
||||
|
||||
impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorResponse> for UserErrors {
|
||||
@ -285,6 +289,15 @@ impl common_utils::errors::ErrorSwitch<api_models::errors::types::ApiErrorRespon
|
||||
Self::InvalidAuthMethodOperationWithMessage(_) => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 57, self.get_error_message(), None))
|
||||
}
|
||||
Self::InvalidCloneConnectorOperation(_) => {
|
||||
AER::BadRequest(ApiError::new(sub_code, 58, self.get_error_message(), None))
|
||||
}
|
||||
Self::ErrorCloningConnector(_) => AER::InternalServerError(ApiError::new(
|
||||
sub_code,
|
||||
59,
|
||||
self.get_error_message(),
|
||||
None,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -355,6 +368,12 @@ impl UserErrors {
|
||||
Self::InvalidAuthMethodOperationWithMessage(operation) => {
|
||||
format!("Invalid Auth Method Operation: {}", operation)
|
||||
}
|
||||
Self::InvalidCloneConnectorOperation(operation) => {
|
||||
format!("Invalid Clone Connector Operation: {}", operation)
|
||||
}
|
||||
Self::ErrorCloningConnector(error_message) => {
|
||||
format!("Error cloning connector: {}", error_message)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -7,9 +7,9 @@ use api_models::{
|
||||
payments::RedirectionResponse,
|
||||
user::{self as user_api, InviteMultipleUserResponse, NameIdUnit},
|
||||
};
|
||||
use common_enums::{EntityType, UserAuthType};
|
||||
use common_enums::{connector_enums, EntityType, UserAuthType};
|
||||
use common_utils::{
|
||||
type_name,
|
||||
fp_utils, type_name,
|
||||
types::{keymanager::Identifier, user::LineageContext},
|
||||
};
|
||||
#[cfg(feature = "email")]
|
||||
@ -128,7 +128,8 @@ pub async fn get_user_details(
|
||||
.unwrap_or(&state.tenant.tenant_id),
|
||||
)
|
||||
.await
|
||||
.change_context(UserErrors::InternalServerError)?;
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Failed to retrieve role information")?;
|
||||
|
||||
let key_manager_state = &(&state).into();
|
||||
|
||||
@ -1365,6 +1366,15 @@ pub async fn create_internal_user(
|
||||
state: SessionState,
|
||||
request: user_api::CreateInternalUserRequest,
|
||||
) -> UserResponse<()> {
|
||||
let role_info = roles::RoleInfo::from_predefined_roles(request.role_id.as_str())
|
||||
.ok_or(UserErrors::InvalidRoleId)?;
|
||||
|
||||
fp_utils::when(
|
||||
role_info.is_internal().not()
|
||||
|| request.role_id == common_utils::consts::ROLE_ID_INTERNAL_ADMIN,
|
||||
|| Err(UserErrors::InvalidRoleId),
|
||||
)?;
|
||||
|
||||
let key_manager_state = &(&state).into();
|
||||
let key_store = state
|
||||
.store
|
||||
@ -1430,10 +1440,7 @@ pub async fn create_internal_user(
|
||||
.map(domain::user::UserFromStorage::from)?;
|
||||
|
||||
new_user
|
||||
.get_no_level_user_role(
|
||||
common_utils::consts::ROLE_ID_INTERNAL_VIEW_ONLY_USER.to_string(),
|
||||
UserStatus::Active,
|
||||
)
|
||||
.get_no_level_user_role(role_info.get_role_id().to_string(), UserStatus::Active)
|
||||
.add_entity(domain::MerchantLevel {
|
||||
tenant_id: default_tenant_id,
|
||||
org_id: internal_merchant.organization_id,
|
||||
@ -3637,3 +3644,119 @@ pub async fn switch_profile_for_user_in_org_and_merchant(
|
||||
|
||||
auth::cookies::set_cookie_response(response, token)
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
pub async fn clone_connector(
|
||||
state: SessionState,
|
||||
request: user_api::CloneConnectorRequest,
|
||||
) -> UserResponse<api_models::admin::MerchantConnectorResponse> {
|
||||
let Some(allowlist) = &state.conf.clone_connector_allowlist else {
|
||||
return Err(UserErrors::InvalidCloneConnectorOperation(
|
||||
"Cloning is not allowed".to_string(),
|
||||
)
|
||||
.into());
|
||||
};
|
||||
|
||||
fp_utils::when(
|
||||
allowlist
|
||||
.merchant_ids
|
||||
.contains(&request.source.merchant_id)
|
||||
.not(),
|
||||
|| {
|
||||
Err(UserErrors::InvalidCloneConnectorOperation(
|
||||
"Cloning is not allowed from this merchant".to_string(),
|
||||
))
|
||||
},
|
||||
)?;
|
||||
|
||||
let key_manager_state = &(&state).into();
|
||||
|
||||
let source_key_store = state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
key_manager_state,
|
||||
&request.source.merchant_id,
|
||||
&state.store.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidCloneConnectorOperation(
|
||||
"Source merchant account not found".to_string(),
|
||||
))?;
|
||||
|
||||
let source_mca = state
|
||||
.store
|
||||
.find_by_merchant_connector_account_merchant_id_merchant_connector_id(
|
||||
key_manager_state,
|
||||
&request.source.merchant_id,
|
||||
&request.source.mca_id,
|
||||
&source_key_store,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidCloneConnectorOperation(
|
||||
"Source merchant connector account not found".to_string(),
|
||||
))?;
|
||||
|
||||
let source_mca_name = source_mca
|
||||
.connector_name
|
||||
.parse::<connector_enums::Connector>()
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Invalid connector name received")?;
|
||||
|
||||
fp_utils::when(
|
||||
allowlist.connector_names.contains(&source_mca_name).not(),
|
||||
|| {
|
||||
Err(UserErrors::InvalidCloneConnectorOperation(
|
||||
"Cloning is not allowed for this connector".to_string(),
|
||||
))
|
||||
},
|
||||
)?;
|
||||
|
||||
let merchant_connector_create = utils::user::build_cloned_connector_create_request(
|
||||
source_mca,
|
||||
Some(request.destination.profile_id.clone()),
|
||||
request.destination.connector_label,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let destination_key_store = state
|
||||
.store
|
||||
.get_merchant_key_store_by_merchant_id(
|
||||
key_manager_state,
|
||||
&request.destination.merchant_id,
|
||||
&state.store.get_master_key().to_vec().into(),
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidCloneConnectorOperation(
|
||||
"Destination merchant account not found".to_string(),
|
||||
))?;
|
||||
|
||||
let destination_merchant_account = state
|
||||
.store
|
||||
.find_merchant_account_by_merchant_id(
|
||||
key_manager_state,
|
||||
&request.destination.merchant_id,
|
||||
&destination_key_store,
|
||||
)
|
||||
.await
|
||||
.to_not_found_response(UserErrors::InvalidCloneConnectorOperation(
|
||||
"Destination merchant account not found".to_string(),
|
||||
))?;
|
||||
|
||||
let destination_context = domain::MerchantContext::NormalMerchant(Box::new(domain::Context(
|
||||
destination_merchant_account,
|
||||
destination_key_store,
|
||||
)));
|
||||
|
||||
admin::create_connector(
|
||||
state,
|
||||
merchant_connector_create,
|
||||
destination_context,
|
||||
Some(request.destination.profile_id),
|
||||
)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
let message = e.current_context().error_message();
|
||||
e.change_context(UserErrors::ErrorCloningConnector(message))
|
||||
})
|
||||
.attach_printable("Failed to create cloned connector")
|
||||
}
|
||||
|
||||
@ -40,6 +40,8 @@ pub async fn get_authorization_info_with_groups(
|
||||
Ok(ApplicationResponse::Json(
|
||||
user_role_api::AuthorizationInfoResponse(
|
||||
info::get_group_authorization_info()
|
||||
.ok_or(UserErrors::InternalServerError)
|
||||
.attach_printable("No visible groups found")?
|
||||
.into_iter()
|
||||
.map(user_role_api::AuthorizationInfo::Group)
|
||||
.collect(),
|
||||
@ -60,10 +62,12 @@ pub async fn get_authorization_info_with_group_tag(
|
||||
},
|
||||
)
|
||||
.into_iter()
|
||||
.map(|(name, value)| user_role_api::ParentInfo {
|
||||
name: name.clone(),
|
||||
description: info::get_parent_group_description(name),
|
||||
groups: value,
|
||||
.filter_map(|(name, value)| {
|
||||
Some(user_role_api::ParentInfo {
|
||||
name: name.clone(),
|
||||
description: info::get_parent_group_description(name)?,
|
||||
groups: value,
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
});
|
||||
@ -99,6 +103,7 @@ pub async fn get_parent_group_info(
|
||||
role_info.get_entity_type(),
|
||||
PermissionGroup::iter().collect(),
|
||||
)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|(parent_group, description)| role_api::ParentGroupInfo {
|
||||
name: parent_group.clone(),
|
||||
|
||||
@ -214,6 +214,11 @@ pub async fn get_parent_info_for_role(
|
||||
role_info.get_entity_type(),
|
||||
role_info.get_permission_groups().to_vec(),
|
||||
)
|
||||
.ok_or(UserErrors::InternalServerError)
|
||||
.attach_printable(format!(
|
||||
"No group descriptions found for role_id: {}",
|
||||
role.role_id
|
||||
))?
|
||||
.into_iter()
|
||||
.map(|(parent_group, description)| role_api::ParentGroupInfo {
|
||||
name: parent_group.clone(),
|
||||
|
||||
@ -2206,7 +2206,7 @@ impl User {
|
||||
#[cfg(all(feature = "olap", feature = "v1"))]
|
||||
impl User {
|
||||
pub fn server(state: AppState) -> Scope {
|
||||
let mut route = web::scope("/user").app_data(web::Data::new(state));
|
||||
let mut route = web::scope("/user").app_data(web::Data::new(state.clone()));
|
||||
|
||||
route = route
|
||||
.service(web::resource("").route(web::get().to(user::get_user_details)))
|
||||
@ -2428,6 +2428,12 @@ impl User {
|
||||
),
|
||||
);
|
||||
|
||||
if state.conf().clone_connector_allowlist.is_some() {
|
||||
route = route.service(
|
||||
web::resource("/clone_connector").route(web::post().to(user::clone_connector)),
|
||||
);
|
||||
}
|
||||
|
||||
// Role information
|
||||
route =
|
||||
route.service(
|
||||
|
||||
@ -289,7 +289,8 @@ impl From<Flow> for ApiIdentifier {
|
||||
| Flow::UploadFileToThemeStorage
|
||||
| Flow::CreateTheme
|
||||
| Flow::UpdateTheme
|
||||
| Flow::DeleteTheme => Self::User,
|
||||
| Flow::DeleteTheme
|
||||
| Flow::CloneConnector => Self::User,
|
||||
|
||||
Flow::ListRolesV2
|
||||
| Flow::ListInvitableRolesAtEntityLevel
|
||||
|
||||
@ -1014,3 +1014,25 @@ pub async fn switch_profile_for_user_in_org_and_merchant(
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
pub async fn clone_connector(
|
||||
state: web::Data<AppState>,
|
||||
req: HttpRequest,
|
||||
json_payload: web::Json<user_api::CloneConnectorRequest>,
|
||||
) -> HttpResponse {
|
||||
let flow = Flow::CloneConnector;
|
||||
|
||||
Box::pin(api::server_wrap(
|
||||
flow,
|
||||
state.clone(),
|
||||
&req,
|
||||
json_payload.into_inner(),
|
||||
|state, _: auth::UserFromToken, req, _| user_core::clone_connector(state, req),
|
||||
&auth::JWTAuth {
|
||||
permission: Permission::MerchantInternalConnectorWrite,
|
||||
},
|
||||
api_locking::LockAction::NotApplicable,
|
||||
))
|
||||
.await
|
||||
}
|
||||
|
||||
@ -1,61 +1,67 @@
|
||||
use std::ops::Not;
|
||||
|
||||
use api_models::user_role::GroupInfo;
|
||||
use common_enums::{ParentGroup, PermissionGroup};
|
||||
use strum::IntoEnumIterator;
|
||||
|
||||
// TODO: To be deprecated
|
||||
pub fn get_group_authorization_info() -> Vec<GroupInfo> {
|
||||
PermissionGroup::iter()
|
||||
.map(get_group_info_from_permission_group)
|
||||
.collect()
|
||||
pub fn get_group_authorization_info() -> Option<Vec<GroupInfo>> {
|
||||
let groups = PermissionGroup::iter()
|
||||
.filter_map(get_group_info_from_permission_group)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
groups.is_empty().not().then_some(groups)
|
||||
}
|
||||
|
||||
// TODO: To be deprecated
|
||||
fn get_group_info_from_permission_group(group: PermissionGroup) -> GroupInfo {
|
||||
let description = get_group_description(group);
|
||||
GroupInfo { group, description }
|
||||
fn get_group_info_from_permission_group(group: PermissionGroup) -> Option<GroupInfo> {
|
||||
let description = get_group_description(group)?;
|
||||
Some(GroupInfo { group, description })
|
||||
}
|
||||
|
||||
// TODO: To be deprecated
|
||||
fn get_group_description(group: PermissionGroup) -> &'static str {
|
||||
fn get_group_description(group: PermissionGroup) -> Option<&'static str> {
|
||||
match group {
|
||||
PermissionGroup::OperationsView => {
|
||||
"View Payments, Refunds, Payouts, Mandates, Disputes and Customers"
|
||||
Some("View Payments, Refunds, Payouts, Mandates, Disputes and Customers")
|
||||
}
|
||||
PermissionGroup::OperationsManage => {
|
||||
"Create, modify and delete Payments, Refunds, Payouts, Mandates, Disputes and Customers"
|
||||
Some("Create, modify and delete Payments, Refunds, Payouts, Mandates, Disputes and Customers")
|
||||
}
|
||||
PermissionGroup::ConnectorsView => {
|
||||
"View connected Payment Processors, Payout Processors and Fraud & Risk Manager details"
|
||||
Some("View connected Payment Processors, Payout Processors and Fraud & Risk Manager details")
|
||||
}
|
||||
PermissionGroup::ConnectorsManage => "Create, modify and delete connectors like Payment Processors, Payout Processors and Fraud & Risk Manager",
|
||||
PermissionGroup::ConnectorsManage => Some("Create, modify and delete connectors like Payment Processors, Payout Processors and Fraud & Risk Manager"),
|
||||
PermissionGroup::WorkflowsView => {
|
||||
"View Routing, 3DS Decision Manager, Surcharge Decision Manager"
|
||||
Some("View Routing, 3DS Decision Manager, Surcharge Decision Manager")
|
||||
}
|
||||
PermissionGroup::WorkflowsManage => {
|
||||
"Create, modify and delete Routing, 3DS Decision Manager, Surcharge Decision Manager"
|
||||
Some("Create, modify and delete Routing, 3DS Decision Manager, Surcharge Decision Manager")
|
||||
}
|
||||
PermissionGroup::AnalyticsView => "View Analytics",
|
||||
PermissionGroup::UsersView => "View Users",
|
||||
PermissionGroup::UsersManage => "Manage and invite Users to the Team",
|
||||
PermissionGroup::MerchantDetailsView | PermissionGroup::AccountView => "View Merchant Details",
|
||||
PermissionGroup::MerchantDetailsManage | PermissionGroup::AccountManage => "Create, modify and delete Merchant Details like api keys, webhooks, etc",
|
||||
PermissionGroup::OrganizationManage => "Manage organization level tasks like create new Merchant accounts, Organization level roles, etc",
|
||||
PermissionGroup::ReconReportsView => "View reconciliation reports and analytics",
|
||||
PermissionGroup::ReconReportsManage => "Manage reconciliation reports",
|
||||
PermissionGroup::ReconOpsView => "View and access all reconciliation operations including reports and analytics",
|
||||
PermissionGroup::ReconOpsManage => "Manage all reconciliation operations including reports and analytics",
|
||||
PermissionGroup::AnalyticsView => Some("View Analytics"),
|
||||
PermissionGroup::UsersView => Some("View Users"),
|
||||
PermissionGroup::UsersManage => Some("Manage and invite Users to the Team"),
|
||||
PermissionGroup::MerchantDetailsView | PermissionGroup::AccountView => Some("View Merchant Details"),
|
||||
PermissionGroup::MerchantDetailsManage | PermissionGroup::AccountManage => Some("Create, modify and delete Merchant Details like api keys, webhooks, etc"),
|
||||
PermissionGroup::OrganizationManage => Some("Manage organization level tasks like create new Merchant accounts, Organization level roles, etc"),
|
||||
PermissionGroup::ReconReportsView => Some("View reconciliation reports and analytics"),
|
||||
PermissionGroup::ReconReportsManage => Some("Manage reconciliation reports"),
|
||||
PermissionGroup::ReconOpsView => Some("View and access all reconciliation operations including reports and analytics"),
|
||||
PermissionGroup::ReconOpsManage => Some("Manage all reconciliation operations including reports and analytics"),
|
||||
PermissionGroup::InternalManage => None, // Internal group, no user-facing description
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_parent_group_description(group: ParentGroup) -> &'static str {
|
||||
pub fn get_parent_group_description(group: ParentGroup) -> Option<&'static str> {
|
||||
match group {
|
||||
ParentGroup::Operations => "Payments, Refunds, Payouts, Mandates, Disputes and Customers",
|
||||
ParentGroup::Connectors => "Create, modify and delete connectors like Payment Processors, Payout Processors and Fraud & Risk Manager",
|
||||
ParentGroup::Workflows => "Create, modify and delete Routing, 3DS Decision Manager, Surcharge Decision Manager",
|
||||
ParentGroup::Analytics => "View Analytics",
|
||||
ParentGroup::Users => "Manage and invite Users to the Team",
|
||||
ParentGroup::Account => "Create, modify and delete Merchant Details like api keys, webhooks, etc",
|
||||
ParentGroup::ReconOps => "View, manage reconciliation operations like upload and process files, run reconciliation etc",
|
||||
ParentGroup::ReconReports => "View, manage reconciliation reports and analytics",
|
||||
ParentGroup::Operations => Some("Payments, Refunds, Payouts, Mandates, Disputes and Customers"),
|
||||
ParentGroup::Connectors => Some("Create, modify and delete connectors like Payment Processors, Payout Processors and Fraud & Risk Manager"),
|
||||
ParentGroup::Workflows => Some("Create, modify and delete Routing, 3DS Decision Manager, Surcharge Decision Manager"),
|
||||
ParentGroup::Analytics => Some("View Analytics"),
|
||||
ParentGroup::Users => Some("Manage and invite Users to the Team"),
|
||||
ParentGroup::Account => Some("Create, modify and delete Merchant Details like api keys, webhooks, etc"),
|
||||
ParentGroup::ReconOps => Some("View, manage reconciliation operations like upload and process files, run reconciliation etc"),
|
||||
ParentGroup::ReconReports => Some("View, manage reconciliation reports and analytics"),
|
||||
ParentGroup::Internal => None, // Internal group, no user-facing description
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
use std::{collections::HashMap, ops::Not};
|
||||
|
||||
use common_enums::{EntityType, ParentGroup, PermissionGroup, PermissionScope, Resource};
|
||||
use strum::IntoEnumIterator;
|
||||
@ -33,7 +33,8 @@ impl PermissionGroupExt for PermissionGroup {
|
||||
| Self::OrganizationManage
|
||||
| Self::AccountManage
|
||||
| Self::ReconOpsManage
|
||||
| Self::ReconReportsManage => PermissionScope::Write,
|
||||
| Self::ReconReportsManage
|
||||
| Self::InternalManage => PermissionScope::Write,
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,6 +52,7 @@ impl PermissionGroupExt for PermissionGroup {
|
||||
| Self::AccountManage => ParentGroup::Account,
|
||||
Self::ReconOpsView | Self::ReconOpsManage => ParentGroup::ReconOps,
|
||||
Self::ReconReportsView | Self::ReconReportsManage => ParentGroup::ReconReports,
|
||||
Self::InternalManage => ParentGroup::Internal,
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,6 +101,8 @@ impl PermissionGroupExt for PermissionGroup {
|
||||
|
||||
Self::AccountView => vec![Self::AccountView],
|
||||
Self::AccountManage => vec![Self::AccountView, Self::AccountManage],
|
||||
|
||||
Self::InternalManage => vec![Self::InternalManage],
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -108,7 +112,7 @@ pub trait ParentGroupExt {
|
||||
fn get_descriptions_for_groups(
|
||||
entity_type: EntityType,
|
||||
groups: Vec<PermissionGroup>,
|
||||
) -> HashMap<ParentGroup, String>;
|
||||
) -> Option<HashMap<ParentGroup, String>>;
|
||||
}
|
||||
|
||||
impl ParentGroupExt for ParentGroup {
|
||||
@ -122,14 +126,15 @@ impl ParentGroupExt for ParentGroup {
|
||||
Self::Account => ACCOUNT.to_vec(),
|
||||
Self::ReconOps => RECON_OPS.to_vec(),
|
||||
Self::ReconReports => RECON_REPORTS.to_vec(),
|
||||
Self::Internal => INTERNAL.to_vec(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_descriptions_for_groups(
|
||||
entity_type: EntityType,
|
||||
groups: Vec<PermissionGroup>,
|
||||
) -> HashMap<Self, String> {
|
||||
Self::iter()
|
||||
) -> Option<HashMap<Self, String>> {
|
||||
let descriptions_map = Self::iter()
|
||||
.filter_map(|parent| {
|
||||
let scopes = groups
|
||||
.iter()
|
||||
@ -142,7 +147,7 @@ impl ParentGroupExt for ParentGroup {
|
||||
.iter()
|
||||
.filter(|res| res.entities().iter().any(|entity| entity <= &entity_type))
|
||||
.map(|res| permissions::get_resource_name(*res, entity_type))
|
||||
.collect::<Vec<_>>()
|
||||
.collect::<Option<Vec<_>>>()?
|
||||
.join(", ");
|
||||
|
||||
Some((
|
||||
@ -150,7 +155,12 @@ impl ParentGroupExt for ParentGroup {
|
||||
format!("{} {}", permissions::get_scope_name(scopes), resources),
|
||||
))
|
||||
})
|
||||
.collect()
|
||||
.collect::<HashMap<_, _>>();
|
||||
|
||||
descriptions_map
|
||||
.is_empty()
|
||||
.not()
|
||||
.then_some(descriptions_map)
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,6 +202,8 @@ pub static RECON_OPS: [Resource; 8] = [
|
||||
Resource::Account,
|
||||
];
|
||||
|
||||
pub static INTERNAL: [Resource; 1] = [Resource::InternalConnector];
|
||||
|
||||
pub static RECON_REPORTS: [Resource; 4] = [
|
||||
Resource::ReconToken,
|
||||
Resource::ReconAndSettlementAnalytics,
|
||||
|
||||
@ -98,39 +98,46 @@ generate_permissions! {
|
||||
RevenueRecovery: {
|
||||
scopes: [Read],
|
||||
entities: [Profile]
|
||||
},
|
||||
InternalConnector: {
|
||||
scopes: [Write],
|
||||
entities: [Merchant]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
pub fn get_resource_name(resource: Resource, entity_type: EntityType) -> &'static str {
|
||||
pub fn get_resource_name(resource: Resource, entity_type: EntityType) -> Option<&'static str> {
|
||||
match (resource, entity_type) {
|
||||
(Resource::Payment, _) => "Payments",
|
||||
(Resource::Refund, _) => "Refunds",
|
||||
(Resource::Dispute, _) => "Disputes",
|
||||
(Resource::Mandate, _) => "Mandates",
|
||||
(Resource::Customer, _) => "Customers",
|
||||
(Resource::Payout, _) => "Payouts",
|
||||
(Resource::ApiKey, _) => "Api Keys",
|
||||
(Resource::Connector, _) => "Payment Processors, Payout Processors, Fraud & Risk Managers",
|
||||
(Resource::Routing, _) => "Routing",
|
||||
(Resource::RevenueRecovery, _) => "Revenue Recovery",
|
||||
(Resource::ThreeDsDecisionManager, _) => "3DS Decision Manager",
|
||||
(Resource::SurchargeDecisionManager, _) => "Surcharge Decision Manager",
|
||||
(Resource::Analytics, _) => "Analytics",
|
||||
(Resource::Report, _) => "Operation Reports",
|
||||
(Resource::User, _) => "Users",
|
||||
(Resource::WebhookEvent, _) => "Webhook Events",
|
||||
(Resource::ReconUpload, _) => "Reconciliation File Upload",
|
||||
(Resource::RunRecon, _) => "Run Reconciliation Process",
|
||||
(Resource::ReconConfig, _) => "Reconciliation Configurations",
|
||||
(Resource::ReconToken, _) => "Generate & Verify Reconciliation Token",
|
||||
(Resource::ReconFiles, _) => "Reconciliation Process Manager",
|
||||
(Resource::ReconReports, _) => "Reconciliation Reports",
|
||||
(Resource::ReconAndSettlementAnalytics, _) => "Reconciliation Analytics",
|
||||
(Resource::Account, EntityType::Profile) => "Business Profile Account",
|
||||
(Resource::Account, EntityType::Merchant) => "Merchant Account",
|
||||
(Resource::Account, EntityType::Organization) => "Organization Account",
|
||||
(Resource::Account, EntityType::Tenant) => "Tenant Account",
|
||||
(Resource::Payment, _) => Some("Payments"),
|
||||
(Resource::Refund, _) => Some("Refunds"),
|
||||
(Resource::Dispute, _) => Some("Disputes"),
|
||||
(Resource::Mandate, _) => Some("Mandates"),
|
||||
(Resource::Customer, _) => Some("Customers"),
|
||||
(Resource::Payout, _) => Some("Payouts"),
|
||||
(Resource::ApiKey, _) => Some("Api Keys"),
|
||||
(Resource::Connector, _) => {
|
||||
Some("Payment Processors, Payout Processors, Fraud & Risk Managers")
|
||||
}
|
||||
(Resource::Routing, _) => Some("Routing"),
|
||||
(Resource::RevenueRecovery, _) => Some("Revenue Recovery"),
|
||||
(Resource::ThreeDsDecisionManager, _) => Some("3DS Decision Manager"),
|
||||
(Resource::SurchargeDecisionManager, _) => Some("Surcharge Decision Manager"),
|
||||
(Resource::Analytics, _) => Some("Analytics"),
|
||||
(Resource::Report, _) => Some("Operation Reports"),
|
||||
(Resource::User, _) => Some("Users"),
|
||||
(Resource::WebhookEvent, _) => Some("Webhook Events"),
|
||||
(Resource::ReconUpload, _) => Some("Reconciliation File Upload"),
|
||||
(Resource::RunRecon, _) => Some("Run Reconciliation Process"),
|
||||
(Resource::ReconConfig, _) => Some("Reconciliation Configurations"),
|
||||
(Resource::ReconToken, _) => Some("Generate & Verify Reconciliation Token"),
|
||||
(Resource::ReconFiles, _) => Some("Reconciliation Process Manager"),
|
||||
(Resource::ReconReports, _) => Some("Reconciliation Reports"),
|
||||
(Resource::ReconAndSettlementAnalytics, _) => Some("Reconciliation Analytics"),
|
||||
(Resource::Account, EntityType::Profile) => Some("Business Profile Account"),
|
||||
(Resource::Account, EntityType::Merchant) => Some("Merchant Account"),
|
||||
(Resource::Account, EntityType::Organization) => Some("Organization Account"),
|
||||
(Resource::Account, EntityType::Tenant) => Some("Tenant Account"),
|
||||
(Resource::InternalConnector, _) => None,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -116,6 +116,10 @@ impl RoleInfo {
|
||||
acl
|
||||
}
|
||||
|
||||
pub fn from_predefined_roles(role_id: &str) -> Option<Self> {
|
||||
predefined_roles::PREDEFINED_ROLES.get(role_id).cloned()
|
||||
}
|
||||
|
||||
pub async fn from_role_id_in_lineage(
|
||||
state: &SessionState,
|
||||
role_id: &str,
|
||||
|
||||
@ -67,6 +67,31 @@ pub static PREDEFINED_ROLES: Lazy<HashMap<&'static str, RoleInfo>> = Lazy::new(|
|
||||
is_internal: true,
|
||||
},
|
||||
);
|
||||
roles.insert(
|
||||
common_utils::consts::ROLE_ID_INTERNAL_DEMO,
|
||||
RoleInfo {
|
||||
groups: vec![
|
||||
PermissionGroup::OperationsView,
|
||||
PermissionGroup::ConnectorsView,
|
||||
PermissionGroup::WorkflowsView,
|
||||
PermissionGroup::AnalyticsView,
|
||||
PermissionGroup::UsersView,
|
||||
PermissionGroup::MerchantDetailsView,
|
||||
PermissionGroup::AccountView,
|
||||
PermissionGroup::ReconOpsView,
|
||||
PermissionGroup::ReconReportsView,
|
||||
PermissionGroup::InternalManage,
|
||||
],
|
||||
role_id: common_utils::consts::ROLE_ID_INTERNAL_DEMO.to_string(),
|
||||
role_name: "internal_demo".to_string(),
|
||||
scope: RoleScope::Organization,
|
||||
entity_type: EntityType::Merchant,
|
||||
is_invitable: false,
|
||||
is_deletable: false,
|
||||
is_updatable: false,
|
||||
is_internal: true,
|
||||
},
|
||||
);
|
||||
|
||||
// Tenant Roles
|
||||
roles.insert(
|
||||
|
||||
@ -1,7 +1,13 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
use api_models::admin as admin_api;
|
||||
use api_models::user as user_api;
|
||||
#[cfg(feature = "v1")]
|
||||
use common_enums::connector_enums;
|
||||
use common_enums::UserAuthType;
|
||||
#[cfg(feature = "v1")]
|
||||
use common_utils::ext_traits::ValueExt;
|
||||
use common_utils::{
|
||||
encryption::Encryption,
|
||||
errors::CustomResult,
|
||||
@ -10,10 +16,16 @@ use common_utils::{
|
||||
};
|
||||
use diesel_models::organization::{self, OrganizationBridge};
|
||||
use error_stack::ResultExt;
|
||||
#[cfg(feature = "v1")]
|
||||
use hyperswitch_domain_models::merchant_connector_account::MerchantConnectorAccount as DomainMerchantConnectorAccount;
|
||||
#[cfg(feature = "v1")]
|
||||
use masking::PeekInterface;
|
||||
use masking::{ExposeInterface, Secret};
|
||||
use redis_interface::RedisConnectionPool;
|
||||
use router_env::{env, logger};
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
use crate::types::AdditionalMerchantData;
|
||||
use crate::{
|
||||
consts::user::{REDIS_SSO_PREFIX, REDIS_SSO_TTL},
|
||||
core::errors::{StorageError, UserErrors, UserResult},
|
||||
@ -388,3 +400,105 @@ pub fn get_base_url(state: &SessionState) -> &str {
|
||||
&state.tenant.user.control_center_url
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "v1")]
|
||||
pub async fn build_cloned_connector_create_request(
|
||||
source_mca: DomainMerchantConnectorAccount,
|
||||
destination_profile_id: Option<id_type::ProfileId>,
|
||||
destination_connector_label: Option<String>,
|
||||
) -> UserResult<admin_api::MerchantConnectorCreate> {
|
||||
let source_mca_name = source_mca
|
||||
.connector_name
|
||||
.parse::<connector_enums::Connector>()
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Invalid connector name received")?;
|
||||
|
||||
let payment_methods_enabled = source_mca
|
||||
.payment_methods_enabled
|
||||
.clone()
|
||||
.map(|data| {
|
||||
let val = data.into_iter().map(|secret| secret.expose()).collect();
|
||||
serde_json::Value::Array(val)
|
||||
.parse_value("PaymentMethods")
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Unable to deserialize PaymentMethods")
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let frm_configs = source_mca
|
||||
.frm_configs
|
||||
.as_ref()
|
||||
.map(|configs_vec| {
|
||||
configs_vec
|
||||
.iter()
|
||||
.map(|config_secret| {
|
||||
config_secret
|
||||
.peek()
|
||||
.clone()
|
||||
.parse_value("FrmConfigs")
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Unable to deserialize FrmConfigs")
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let connector_webhook_details = source_mca
|
||||
.connector_webhook_details
|
||||
.map(|webhook_details| {
|
||||
serde_json::Value::parse_value(
|
||||
webhook_details.expose(),
|
||||
"MerchantConnectorWebhookDetails",
|
||||
)
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Unable to deserialize connector_webhook_details")
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let connector_wallets_details = source_mca
|
||||
.connector_wallets_details
|
||||
.map(|secret_value| {
|
||||
secret_value
|
||||
.into_inner()
|
||||
.expose()
|
||||
.parse_value::<admin_api::ConnectorWalletDetails>("ConnectorWalletDetails")
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Unable to parse ConnectorWalletDetails from Value")
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let additional_merchant_data = source_mca
|
||||
.additional_merchant_data
|
||||
.map(|secret_value| {
|
||||
secret_value
|
||||
.into_inner()
|
||||
.expose()
|
||||
.parse_value::<AdditionalMerchantData>("AdditionalMerchantData")
|
||||
.change_context(UserErrors::InternalServerError)
|
||||
.attach_printable("Unable to parse AdditionalMerchantData from Value")
|
||||
})
|
||||
.transpose()?
|
||||
.map(admin_api::AdditionalMerchantData::foreign_from);
|
||||
|
||||
Ok(admin_api::MerchantConnectorCreate {
|
||||
connector_type: source_mca.connector_type,
|
||||
connector_name: source_mca_name,
|
||||
connector_label: destination_connector_label.or(source_mca.connector_label.clone()),
|
||||
merchant_connector_id: None,
|
||||
connector_account_details: Some(source_mca.connector_account_details.clone().into_inner()),
|
||||
test_mode: source_mca.test_mode,
|
||||
disabled: source_mca.disabled,
|
||||
payment_methods_enabled,
|
||||
metadata: source_mca.metadata,
|
||||
business_country: source_mca.business_country,
|
||||
business_label: source_mca.business_label.clone(),
|
||||
business_sub_label: source_mca.business_sub_label.clone(),
|
||||
frm_configs,
|
||||
connector_webhook_details,
|
||||
profile_id: destination_profile_id,
|
||||
pm_auth_config: source_mca.pm_auth_config.clone(),
|
||||
connector_wallets_details,
|
||||
status: Some(source_mca.status),
|
||||
additional_merchant_data,
|
||||
})
|
||||
}
|
||||
|
||||
@ -31,9 +31,11 @@ pub fn validate_role_groups(groups: &[PermissionGroup]) -> UserResult<()> {
|
||||
|
||||
let unique_groups: HashSet<_> = groups.iter().copied().collect();
|
||||
|
||||
if unique_groups.contains(&PermissionGroup::OrganizationManage) {
|
||||
if unique_groups.contains(&PermissionGroup::OrganizationManage)
|
||||
|| unique_groups.contains(&PermissionGroup::InternalManage)
|
||||
{
|
||||
return Err(report!(UserErrors::InvalidRoleOperation))
|
||||
.attach_printable("Organization manage group cannot be added to role");
|
||||
.attach_printable("Invalid groups present in the custom role");
|
||||
}
|
||||
|
||||
if unique_groups.len() != groups.len() {
|
||||
|
||||
@ -586,6 +586,8 @@ pub enum Flow {
|
||||
TotalPaymentMethodCount,
|
||||
/// Process Tracker Revenue Recovery Workflow Retrieve
|
||||
RevenueRecoveryRetrieve,
|
||||
/// Clone Connector flow
|
||||
CloneConnector,
|
||||
}
|
||||
|
||||
/// Trait for providing generic behaviour to flow metric
|
||||
|
||||
Reference in New Issue
Block a user