diff --git a/config/config.example.toml b/config/config.example.toml index bd1a1639608..1acb5609ddf 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -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 \ No newline at end of file +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 diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 7c1866b7794..05c1745a6de 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -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 diff --git a/config/development.toml b/config/development.toml index debfebc8110..3a3aeb64402 100644 --- a/config/development.toml +++ b/config/development.toml @@ -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 diff --git a/config/docker_compose.toml b/config/docker_compose.toml index a5d0aeff9e9..048330c8dda 100644 --- a/config/docker_compose.toml +++ b/config/docker_compose.toml @@ -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 diff --git a/crates/api_models/src/events/user.rs b/crates/api_models/src/events/user.rs index 1a20e7b90b0..577d4a3f805 100644 --- a/crates/api_models/src/events/user.rs +++ b/crates/api_models/src/events/user.rs @@ -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 ) ); diff --git a/crates/api_models/src/user.rs b/crates/api_models/src/user.rs index d979a2d5a11..4bc3c262c1b 100644 --- a/crates/api_models/src/user.rs +++ b/crates/api_models/src/user.rs @@ -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, + 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, pub email: pii::Email, pub password: Secret, + pub role_id: String, } #[derive(serde::Deserialize, Debug, serde::Serialize)] diff --git a/crates/common_enums/src/enums.rs b/crates/common_enums/src/enums.rs index 69707bf3cef..0dd7a1b3681 100644 --- a/crates/common_enums/src/enums.rs +++ b/crates/common_enums/src/enums.rs @@ -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)] diff --git a/crates/common_utils/src/consts.rs b/crates/common_utils/src/consts.rs index 1606d731a21..e3eba81b12f 100644 --- a/crates/common_utils/src/consts.rs +++ b/crates/common_utils/src/consts.rs @@ -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; diff --git a/crates/router/src/configs/secrets_transformers.rs b/crates/router/src/configs/secrets_transformers.rs index 308cbe417bb..12cc03eb929 100644 --- a/crates/router/src/configs/secrets_transformers.rs +++ b/crates/router/src/configs/secrets_transformers.rs @@ -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, } } diff --git a/crates/router/src/configs/settings.rs b/crates/router/src/configs/settings.rs index f7120ff4545..c0f2d680d37 100644 --- a/crates/router/src/configs/settings.rs +++ b/crates/router/src/configs/settings.rs @@ -153,6 +153,7 @@ pub struct Settings { pub platform: Platform, pub authentication_providers: AuthenticationProviders, pub open_router: OpenRouter, + pub clone_connector_allowlist: Option, } #[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, + #[serde(deserialize_with = "deserialize_hashset")] + pub connector_names: HashSet, +} + #[derive(Debug, Deserialize, Clone, Default)] pub struct Platform { pub enabled: bool, diff --git a/crates/router/src/core/errors/user.rs b/crates/router/src/core/errors/user.rs index 6af269d916b..4b19cbb0346 100644 --- a/crates/router/src/core/errors/user.rs +++ b/crates/router/src/core/errors/user.rs @@ -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 for UserErrors { @@ -285,6 +289,15 @@ impl common_utils::errors::ErrorSwitch { 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) + } } } } diff --git a/crates/router/src/core/user.rs b/crates/router/src/core/user.rs index 1097f57b198..903ac7c5334 100644 --- a/crates/router/src/core/user.rs +++ b/crates/router/src/core/user.rs @@ -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 { + 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::() + .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") +} diff --git a/crates/router/src/core/user_role.rs b/crates/router/src/core/user_role.rs index 0ece98da135..12f937280fb 100644 --- a/crates/router/src/core/user_role.rs +++ b/crates/router/src/core/user_role.rs @@ -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(), diff --git a/crates/router/src/core/user_role/role.rs b/crates/router/src/core/user_role/role.rs index 9bb8be9cc6e..6a1cc3738e7 100644 --- a/crates/router/src/core/user_role/role.rs +++ b/crates/router/src/core/user_role/role.rs @@ -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(), diff --git a/crates/router/src/routes/app.rs b/crates/router/src/routes/app.rs index 82a91f468c5..bc5d58e4825 100644 --- a/crates/router/src/routes/app.rs +++ b/crates/router/src/routes/app.rs @@ -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( diff --git a/crates/router/src/routes/lock_utils.rs b/crates/router/src/routes/lock_utils.rs index fee72601df7..88f42da350c 100644 --- a/crates/router/src/routes/lock_utils.rs +++ b/crates/router/src/routes/lock_utils.rs @@ -289,7 +289,8 @@ impl From for ApiIdentifier { | Flow::UploadFileToThemeStorage | Flow::CreateTheme | Flow::UpdateTheme - | Flow::DeleteTheme => Self::User, + | Flow::DeleteTheme + | Flow::CloneConnector => Self::User, Flow::ListRolesV2 | Flow::ListInvitableRolesAtEntityLevel diff --git a/crates/router/src/routes/user.rs b/crates/router/src/routes/user.rs index a6d9d75dc43..66b2e7b7a92 100644 --- a/crates/router/src/routes/user.rs +++ b/crates/router/src/routes/user.rs @@ -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, + req: HttpRequest, + json_payload: web::Json, +) -> 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 +} diff --git a/crates/router/src/services/authorization/info.rs b/crates/router/src/services/authorization/info.rs index b4413dfa3b3..4fd57127cf6 100644 --- a/crates/router/src/services/authorization/info.rs +++ b/crates/router/src/services/authorization/info.rs @@ -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 { - PermissionGroup::iter() - .map(get_group_info_from_permission_group) - .collect() +pub fn get_group_authorization_info() -> Option> { + let groups = PermissionGroup::iter() + .filter_map(get_group_info_from_permission_group) + .collect::>(); + + 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 { + 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 } } diff --git a/crates/router/src/services/authorization/permission_groups.rs b/crates/router/src/services/authorization/permission_groups.rs index 2aa58e4d2d8..f49708ceb68 100644 --- a/crates/router/src/services/authorization/permission_groups.rs +++ b/crates/router/src/services/authorization/permission_groups.rs @@ -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, - ) -> HashMap; + ) -> Option>; } 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, - ) -> HashMap { - Self::iter() + ) -> Option> { + 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::>() + .collect::>>()? .join(", "); Some(( @@ -150,7 +155,12 @@ impl ParentGroupExt for ParentGroup { format!("{} {}", permissions::get_scope_name(scopes), resources), )) }) - .collect() + .collect::>(); + + 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, diff --git a/crates/router/src/services/authorization/permissions.rs b/crates/router/src/services/authorization/permissions.rs index 55a562a9f66..9044dd78349 100644 --- a/crates/router/src/services/authorization/permissions.rs +++ b/crates/router/src/services/authorization/permissions.rs @@ -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, } } diff --git a/crates/router/src/services/authorization/roles.rs b/crates/router/src/services/authorization/roles.rs index 995975d12e8..b12df953892 100644 --- a/crates/router/src/services/authorization/roles.rs +++ b/crates/router/src/services/authorization/roles.rs @@ -116,6 +116,10 @@ impl RoleInfo { acl } + pub fn from_predefined_roles(role_id: &str) -> Option { + predefined_roles::PREDEFINED_ROLES.get(role_id).cloned() + } + pub async fn from_role_id_in_lineage( state: &SessionState, role_id: &str, diff --git a/crates/router/src/services/authorization/roles/predefined_roles.rs b/crates/router/src/services/authorization/roles/predefined_roles.rs index fe7498de9eb..0fce440ca62 100644 --- a/crates/router/src/services/authorization/roles/predefined_roles.rs +++ b/crates/router/src/services/authorization/roles/predefined_roles.rs @@ -67,6 +67,31 @@ pub static PREDEFINED_ROLES: Lazy> = 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( diff --git a/crates/router/src/utils/user.rs b/crates/router/src/utils/user.rs index 18e227912ef..172f8bddccf 100644 --- a/crates/router/src/utils/user.rs +++ b/crates/router/src/utils/user.rs @@ -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, + destination_connector_label: Option, +) -> UserResult { + let source_mca_name = source_mca + .connector_name + .parse::() + .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::, _>>() + }) + .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::("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") + .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, + }) +} diff --git a/crates/router/src/utils/user_role.rs b/crates/router/src/utils/user_role.rs index b3e51136193..64cdf2ad4c3 100644 --- a/crates/router/src/utils/user_role.rs +++ b/crates/router/src/utils/user_role.rs @@ -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() { diff --git a/crates/router_env/src/logger/types.rs b/crates/router_env/src/logger/types.rs index bea51e808bf..7b9e4e9a347 100644 --- a/crates/router_env/src/logger/types.rs +++ b/crates/router_env/src/logger/types.rs @@ -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