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:
Mani Chandra
2025-05-14 20:33:40 +05:30
committed by GitHub
parent 46e830a87f
commit 82f15e950f
25 changed files with 493 additions and 87 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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
)
);

View File

@ -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)]

View File

@ -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)]

View File

@ -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;

View File

@ -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,
}
}

View File

@ -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,

View File

@ -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)
}
}
}
}

View File

@ -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")
}

View File

@ -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(),

View File

@ -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(),

View File

@ -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(

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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,

View File

@ -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,
}
}

View File

@ -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,

View File

@ -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(

View File

@ -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,
})
}

View File

@ -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() {

View File

@ -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