Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
ef87aff
feat: create clone connector api (untested)
ThisIsMani Apr 26, 2025
90c559f
feat: add role_type in JwtAuth
ThisIsMani Apr 26, 2025
16fc2b1
refactor: convert boolean to result
ThisIsMani Apr 26, 2025
d23940b
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani Apr 28, 2025
ca0bd16
fix: decrypt creds manually (payments working)
ThisIsMani Apr 29, 2025
5b5e69b
feat: add internal permission types (untested)
ThisIsMani Apr 30, 2025
e666ba6
revert: role_type JWTAuth
ThisIsMani Apr 30, 2025
9733d68
refactor: update configuration and user models
ThisIsMani May 2, 2025
4b8b220
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani May 2, 2025
23fd38a
refactor: add merchant_id and profile_id in destination
ThisIsMani May 2, 2025
215d465
refactor: remove comments and add extra checks
ThisIsMani May 2, 2025
6f62436
chore: run formatter
hyperswitch-bot[bot] May 2, 2025
b85513c
revert: payment permission changes
ThisIsMani May 3, 2025
dbd9445
fix: clippy
ThisIsMani May 3, 2025
a11bd23
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani May 6, 2025
6f6f30a
fix: add connector names in development.toml
ThisIsMani May 6, 2025
469ad00
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani May 6, 2025
4090824
feat: add connector names in docker compose config
ThisIsMani May 7, 2025
59c581e
fix: add internal_admin check and remove profile_id from request
ThisIsMani May 7, 2025
a2af6fd
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani May 12, 2025
0260320
refactor: change whitelist to allowlist
ThisIsMani May 12, 2025
39555d8
refactor: add context messages
ThisIsMani May 12, 2025
f7206ac
refactor: change whitelist to allowlist
ThisIsMani May 12, 2025
455a018
feat: make env optional
ThisIsMani May 12, 2025
36b07be
Merge branch 'main' into clone-connector
ThisIsMani May 12, 2025
715368f
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani May 13, 2025
3023974
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani May 13, 2025
4d1ca18
chore: run formatter
hyperswitch-bot[bot] May 13, 2025
52a55f8
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani May 13, 2025
5387142
Merge branch 'main' of https://github.com/juspay/hyperswitch into clo…
ThisIsMani May 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions config/config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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
4 changes: 4 additions & 0 deletions config/deployments/env_specific.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions config/development.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 4 additions & 0 deletions config/docker_compose.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
5 changes: 3 additions & 2 deletions crates/api_models/src/events/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -71,7 +71,8 @@ common_utils::impl_api_event_type!(
UpdateUserAuthenticationMethodRequest,
GetSsoAuthUrlRequest,
SsoSignInRequest,
AuthSelectRequest
AuthSelectRequest,
CloneConnectorRequest
)
);

Expand Down
20 changes: 20 additions & 0 deletions crates/api_models/src/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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)]
Expand Down
3 changes: 3 additions & 0 deletions crates/common_enums/src/enums.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7214,6 +7214,7 @@ pub enum PermissionGroup {
ReconReportsManage,
ReconOpsView,
ReconOpsManage,
InternalManage,
}

#[derive(Clone, Debug, serde::Serialize, PartialEq, Eq, Hash, strum::EnumIter)]
Expand All @@ -7226,6 +7227,7 @@ pub enum ParentGroup {
ReconOps,
ReconReports,
Account,
Internal,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize)]
Expand Down Expand Up @@ -7255,6 +7257,7 @@ pub enum Resource {
RunRecon,
ReconConfig,
RevenueRecovery,
InternalConnector,
}

#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, serde::Serialize, Hash)]
Expand Down
2 changes: 2 additions & 0 deletions crates/common_utils/src/consts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions crates/router/src/configs/secrets_transformers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}
11 changes: 11 additions & 0 deletions crates/router/src/configs/settings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,13 +153,24 @@ 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)]
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,
Expand Down
19 changes: 19 additions & 0 deletions crates/router/src/core/errors/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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,
)),
}
}
}
Expand Down Expand Up @@ -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)
}
}
}
}
137 changes: 130 additions & 7 deletions crates/router/src/core/user.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand Down Expand Up @@ -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();

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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")
}
13 changes: 9 additions & 4 deletions crates/router/src/core/user_role.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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()
});
Expand Down Expand Up @@ -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(),
Expand Down
Loading
Loading