diff --git a/crates/router/src/core/admin.rs b/crates/router/src/core/admin.rs index 736360edf0..821057764e 100644 --- a/crates/router/src/core/admin.rs +++ b/crates/router/src/core/admin.rs @@ -2817,7 +2817,7 @@ async fn validate_pm_auth( pub async fn retrieve_connector( state: SessionState, merchant_id: id_type::MerchantId, - _profile_id: Option, + profile_id: Option, merchant_connector_id: String, ) -> RouterResponse { let store = state.store.as_ref(); @@ -2847,6 +2847,7 @@ pub async fn retrieve_connector( .to_not_found_response(errors::ApiErrorResponse::MerchantConnectorAccountNotFound { id: merchant_connector_id.clone(), })?; + core_utils::validate_profile_id_from_auth_layer(profile_id, &mca)?; Ok(service_api::ApplicationResponse::Json( mca.foreign_try_into()?, @@ -2897,7 +2898,7 @@ pub async fn retrieve_connector( pub async fn list_payment_connectors( state: SessionState, merchant_id: id_type::MerchantId, - _profile_id_list: Option>, + profile_id_list: Option>, ) -> RouterResponse> { let store = state.store.as_ref(); let key_manager_state = &(&state).into(); @@ -2925,6 +2926,10 @@ pub async fn list_payment_connectors( ) .await .to_not_found_response(errors::ApiErrorResponse::InternalServerError)?; + let merchant_connector_accounts = core_utils::filter_objects_based_on_profile_id_list( + profile_id_list, + merchant_connector_accounts, + ); let mut response = vec![]; // The can be eliminated once [#79711](https://github.com/rust-lang/rust/issues/79711) is stabilized @@ -2938,7 +2943,7 @@ pub async fn list_payment_connectors( pub async fn update_connector( state: SessionState, merchant_id: &id_type::MerchantId, - _profile_id: Option, + profile_id: Option, merchant_connector_id: &str, req: api_models::admin::MerchantConnectorUpdate, ) -> RouterResponse { @@ -2968,6 +2973,7 @@ pub async fn update_connector( key_manager_state, ) .await?; + core_utils::validate_profile_id_from_auth_layer(profile_id, &mca)?; let payment_connector = req .clone() diff --git a/crates/router/src/core/disputes.rs b/crates/router/src/core/disputes.rs index 37f6d111de..7321595930 100644 --- a/crates/router/src/core/disputes.rs +++ b/crates/router/src/core/disputes.rs @@ -26,7 +26,7 @@ use crate::{ pub async fn retrieve_dispute( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id: Option, + profile_id: Option, req: disputes::DisputeId, ) -> RouterResponse { let dispute = state @@ -36,6 +36,7 @@ pub async fn retrieve_dispute( .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { dispute_id: req.dispute_id, })?; + core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?; let dispute_response = api_models::disputes::DisputeResponse::foreign_from(dispute); Ok(services::ApplicationResponse::Json(dispute_response)) } @@ -64,7 +65,7 @@ pub async fn retrieve_disputes_list( pub async fn accept_dispute( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id: Option, + profile_id: Option, key_store: domain::MerchantKeyStore, req: disputes::DisputeId, ) -> RouterResponse { @@ -76,6 +77,7 @@ pub async fn accept_dispute( .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { dispute_id: req.dispute_id, })?; + core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?; let dispute_id = dispute.dispute_id.clone(); common_utils::fp_utils::when( !(dispute.dispute_stage == storage_enums::DisputeStage::Dispute @@ -167,7 +169,7 @@ pub async fn accept_dispute( pub async fn submit_evidence( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id: Option, + profile_id: Option, key_store: domain::MerchantKeyStore, req: dispute_models::SubmitEvidenceRequest, ) -> RouterResponse { @@ -179,6 +181,7 @@ pub async fn submit_evidence( .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { dispute_id: req.dispute_id.clone(), })?; + core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?; let dispute_id = dispute.dispute_id.clone(); common_utils::fp_utils::when( !(dispute.dispute_stage == storage_enums::DisputeStage::Dispute @@ -333,7 +336,7 @@ pub async fn submit_evidence( pub async fn attach_evidence( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id: Option, + profile_id: Option, key_store: domain::MerchantKeyStore, attach_evidence_request: api::AttachEvidenceRequest, ) -> RouterResponse { @@ -349,6 +352,7 @@ pub async fn attach_evidence( .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { dispute_id: dispute_id.clone(), })?; + core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?; common_utils::fp_utils::when( !(dispute.dispute_stage == storage_enums::DisputeStage::Dispute && dispute.dispute_status == storage_enums::DisputeStatus::DisputeOpened), @@ -411,7 +415,7 @@ pub async fn attach_evidence( pub async fn retrieve_dispute_evidence( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id: Option, + profile_id: Option, req: disputes::DisputeId, ) -> RouterResponse> { let dispute = state @@ -421,6 +425,7 @@ pub async fn retrieve_dispute_evidence( .to_not_found_response(errors::ApiErrorResponse::DisputeNotFound { dispute_id: req.dispute_id, })?; + core_utils::validate_profile_id_from_auth_layer(profile_id, &dispute)?; let dispute_evidence: api::DisputeEvidence = dispute .evidence .clone() diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index b4f369f71b..cc324fa529 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -103,6 +103,7 @@ pub async fn payments_operation_core( state: &SessionState, req_state: ReqState, merchant_account: domain::MerchantAccount, + profile_id_from_auth_layer: Option, key_store: domain::MerchantKeyStore, operation: Op, req: Req, @@ -162,6 +163,11 @@ where ) .await?; + utils::validate_profile_id_from_auth_layer( + profile_id_from_auth_layer, + &payment_data.payment_intent, + )?; + let (operation, customer) = operation .to_domain()? .get_or_create_customer_details( @@ -788,7 +794,7 @@ pub async fn payments_core( state: SessionState, req_state: ReqState, merchant_account: domain::MerchantAccount, - _profile_id: Option, + profile_id: Option, key_store: domain::MerchantKeyStore, operation: Op, req: Req, @@ -825,6 +831,7 @@ where &state, req_state, merchant_account, + profile_id, key_store, operation.clone(), req, @@ -2877,7 +2884,7 @@ pub fn is_operation_complete_authorize(operation: &Op) -> bool { pub async fn list_payments( state: SessionState, merchant: domain::MerchantAccount, - _profile_id_list: Option>, + profile_id_list: Option>, key_store: domain::MerchantKeyStore, constraints: api::PaymentListConstraints, ) -> RouterResponse { @@ -2894,6 +2901,8 @@ pub async fn list_payments( ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; + let payment_intents = + utils::filter_objects_based_on_profile_id_list(profile_id_list, payment_intents); let collected_futures = payment_intents.into_iter().map(|pi| { async { @@ -2950,7 +2959,7 @@ pub async fn list_payments( pub async fn apply_filters_on_payments( state: SessionState, merchant: domain::MerchantAccount, - _profile_id_list: Option>, + profile_id_list: Option>, merchant_key_store: domain::MerchantKeyStore, constraints: api::PaymentListFilterConstraints, ) -> RouterResponse { @@ -2967,7 +2976,7 @@ pub async fn apply_filters_on_payments( ) .await .to_not_found_response(errors::ApiErrorResponse::PaymentNotFound)?; - + let list = utils::filter_objects_based_on_profile_id_list(profile_id_list, list); let data: Vec = list.into_iter().map(ForeignFrom::foreign_from).collect(); @@ -3048,10 +3057,11 @@ pub async fn get_filters_for_payments( pub async fn get_payment_filters( state: SessionState, merchant: domain::MerchantAccount, - _profile_id_list: Option>, + profile_id_list: Option>, ) -> RouterResponse { let merchant_connector_accounts = if let services::ApplicationResponse::Json(data) = - super::admin::list_payment_connectors(state, merchant.get_id().to_owned(), None).await? + super::admin::list_payment_connectors(state, merchant.get_id().to_owned(), profile_id_list) + .await? { data } else { diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 92763e43d2..b0819f4a5f 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -358,6 +358,7 @@ pub async fn payouts_confirm_core( let mut payout_data = make_payout_data( &state, &merchant_account, + None, &key_store, &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), ) @@ -422,6 +423,7 @@ pub async fn payouts_update_core( let mut payout_data = make_payout_data( &state, &merchant_account, + None, &key_store, &payouts::PayoutRequest::PayoutCreateRequest(req.to_owned()), ) @@ -482,18 +484,18 @@ pub async fn payouts_update_core( pub async fn payouts_retrieve_core( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id: Option, + profile_id: Option, key_store: domain::MerchantKeyStore, req: payouts::PayoutRetrieveRequest, ) -> RouterResponse { let mut payout_data = make_payout_data( &state, &merchant_account, + profile_id, &key_store, &payouts::PayoutRequest::PayoutRetrieveRequest(req.to_owned()), ) .await?; - let payout_attempt = payout_data.payout_attempt.to_owned(); let status = payout_attempt.status; @@ -533,6 +535,7 @@ pub async fn payouts_cancel_core( let mut payout_data = make_payout_data( &state, &merchant_account, + None, &key_store, &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), ) @@ -628,6 +631,7 @@ pub async fn payouts_fulfill_core( let mut payout_data = make_payout_data( &state, &merchant_account, + None, &key_store, &payouts::PayoutRequest::PayoutActionRequest(req.to_owned()), ) @@ -708,7 +712,7 @@ pub async fn payouts_fulfill_core( pub async fn payouts_list_core( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id_list: Option>, + profile_id_list: Option>, key_store: domain::MerchantKeyStore, constraints: payouts::PayoutListConstraints, ) -> RouterResponse { @@ -723,6 +727,7 @@ pub async fn payouts_list_core( ) .await .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; + let payouts = core_utils::filter_objects_based_on_profile_id_list(profile_id_list, payouts); let collected_futures = payouts.into_iter().map(|payouts| async { match db @@ -807,7 +812,7 @@ pub async fn payouts_list_core( pub async fn payouts_filtered_list_core( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id_list: Option>, + profile_id_list: Option>, key_store: domain::MerchantKeyStore, filters: payouts::PayoutListFilterConstraints, ) -> RouterResponse { @@ -826,7 +831,7 @@ pub async fn payouts_filtered_list_core( ) .await .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; - + let list = core_utils::filter_objects_based_on_profile_id_list(profile_id_list, list); let data: Vec = join_all(list.into_iter().map(|(p, pa, c)| async { match domain::Customer::convert_back( &(&state).into(), @@ -2327,6 +2332,7 @@ pub async fn payout_create_db_entries( pub async fn make_payout_data( state: &SessionState, merchant_account: &domain::MerchantAccount, + auth_profile_id: Option, key_store: &domain::MerchantKeyStore, req: &payouts::PayoutRequest, ) -> RouterResult { @@ -2346,6 +2352,7 @@ pub async fn make_payout_data( ) .await .to_not_found_response(errors::ApiErrorResponse::PayoutNotFound)?; + core_utils::validate_profile_id_from_auth_layer(auth_profile_id, &payouts)?; let payout_attempt_id = utils::get_payment_attempt_id(payout_id, payouts.attempt_count); diff --git a/crates/router/src/core/refunds.rs b/crates/router/src/core/refunds.rs index 9f8fc2e30a..878ec24185 100644 --- a/crates/router/src/core/refunds.rs +++ b/crates/router/src/core/refunds.rs @@ -401,7 +401,7 @@ where pub async fn refund_retrieve_core( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id: Option, + profile_id: Option, key_store: domain::MerchantKeyStore, request: refunds::RefundsRetrieveRequest, ) -> RouterResult { @@ -419,7 +419,7 @@ pub async fn refund_retrieve_core( ) .await .to_not_found_response(errors::ApiErrorResponse::RefundNotFound)?; - + core_utils::validate_profile_id_from_auth_layer(profile_id, &refund)?; let payment_id = refund.payment_id.as_str(); payment_intent = db .find_payment_intent_by_payment_id_merchant_id( @@ -987,11 +987,15 @@ pub async fn refund_manual_update( pub async fn get_filters_for_refunds( state: SessionState, merchant_account: domain::MerchantAccount, - _profile_id_list: Option>, + profile_id_list: Option>, ) -> RouterResponse { let merchant_connector_accounts = if let services::ApplicationResponse::Json(data) = - super::admin::list_payment_connectors(state, merchant_account.get_id().to_owned(), None) - .await? + super::admin::list_payment_connectors( + state, + merchant_account.get_id().to_owned(), + profile_id_list, + ) + .await? { data } else { diff --git a/crates/router/src/core/utils.rs b/crates/router/src/core/utils.rs index acedad05b6..7bc4505b97 100644 --- a/crates/router/src/core/utils.rs +++ b/crates/router/src/core/utils.rs @@ -1,4 +1,4 @@ -use std::{marker::PhantomData, str::FromStr}; +use std::{collections::HashSet, marker::PhantomData, str::FromStr}; use api_models::enums::{DisputeStage, DisputeStatus}; #[cfg(feature = "payouts")] @@ -8,7 +8,10 @@ use common_enums::{IntentStatus, RequestIncrementalAuthorization}; use common_utils::{crypto::Encryptable, pii::Email}; use common_utils::{errors::CustomResult, ext_traits::AsyncExt, types::MinorUnit}; use error_stack::{report, ResultExt}; -use hyperswitch_domain_models::{payment_address::PaymentAddress, router_data::ErrorResponse}; +use hyperswitch_domain_models::{ + merchant_connector_account::MerchantConnectorAccount, payment_address::PaymentAddress, + router_data::ErrorResponse, +}; #[cfg(feature = "payouts")] use masking::{ExposeInterface, PeekInterface}; use maud::{html, PreEscaped}; @@ -450,6 +453,64 @@ mod tests { let generated_id = generate_id(consts::ID_LENGTH, "ref"); assert_eq!(generated_id.len(), consts::ID_LENGTH + 4) } + + #[test] + fn test_filter_objects_based_on_profile_id_list() { + #[derive(PartialEq, Debug, Clone)] + struct Object { + profile_id: Option, + } + + impl Object { + pub fn new(profile_id: &str) -> Self { + Self { + profile_id: Some(profile_id.to_string()), + } + } + } + + impl GetProfileId for Object { + fn get_profile_id(&self) -> Option<&String> { + self.profile_id.as_ref() + } + } + + // non empty object_list and profile_id_list + let object_list = vec![ + Object::new("p1"), + Object::new("p2"), + Object::new("p2"), + Object::new("p4"), + Object::new("p5"), + ]; + let profile_id_list = vec!["p1".to_string(), "p2".to_string(), "p3".to_string()]; + let filtered_list = + filter_objects_based_on_profile_id_list(Some(profile_id_list), object_list.clone()); + let expected_result = vec![Object::new("p1"), Object::new("p2"), Object::new("p2")]; + assert_eq!(filtered_list, expected_result); + + // non empty object_list and empty profile_id_list + let empty_profile_id_list = vec![]; + let filtered_list = filter_objects_based_on_profile_id_list( + Some(empty_profile_id_list), + object_list.clone(), + ); + let expected_result = vec![]; + assert_eq!(filtered_list, expected_result); + + // non empty object_list and None profile_id_list + let profile_id_list_as_none = None; + let filtered_list = + filter_objects_based_on_profile_id_list(profile_id_list_as_none, object_list); + let expected_result = vec![ + Object::new("p1"), + Object::new("p2"), + Object::new("p2"), + Object::new("p4"), + Object::new("p5"), + ]; + assert_eq!(filtered_list, expected_result); + } } // Dispute Stage can move linearly from PreDispute -> Dispute -> PreArbitration @@ -1267,3 +1328,100 @@ pub fn get_incremental_authorization_allowed_value( incremental_authorization_allowed } } + +pub(super) trait GetProfileId { + fn get_profile_id(&self) -> Option<&String>; +} + +impl GetProfileId for MerchantConnectorAccount { + fn get_profile_id(&self) -> Option<&String> { + self.profile_id.as_ref() + } +} + +impl GetProfileId for storage::PaymentIntent { + fn get_profile_id(&self) -> Option<&String> { + self.profile_id.as_ref() + } +} + +impl GetProfileId for (storage::PaymentIntent, A) { + fn get_profile_id(&self) -> Option<&String> { + self.0.get_profile_id() + } +} + +impl GetProfileId for diesel_models::Dispute { + fn get_profile_id(&self) -> Option<&String> { + self.profile_id.as_ref() + } +} + +impl GetProfileId for diesel_models::Refund { + fn get_profile_id(&self) -> Option<&String> { + self.profile_id.as_ref() + } +} + +#[cfg(feature = "payouts")] +impl GetProfileId for storage::Payouts { + fn get_profile_id(&self) -> Option<&String> { + Some(&self.profile_id) + } +} +#[cfg(feature = "payouts")] +impl GetProfileId for (storage::Payouts, T, F) { + fn get_profile_id(&self) -> Option<&String> { + self.0.get_profile_id() + } +} + +/// Filter Objects based on profile ids +pub(super) fn filter_objects_based_on_profile_id_list( + profile_id_list_auth_layer: Option>, + object_list: Vec, +) -> Vec { + if let Some(profile_id_list) = profile_id_list_auth_layer { + let profile_ids_to_filter: HashSet<_> = profile_id_list.iter().collect(); + object_list + .into_iter() + .filter_map(|item| { + if item + .get_profile_id() + .is_some_and(|profile_id| profile_ids_to_filter.contains(profile_id)) + { + Some(item) + } else { + None + } + }) + .collect() + } else { + object_list + } +} + +pub(super) fn validate_profile_id_from_auth_layer( + profile_id_auth_layer: Option, + object: &T, +) -> RouterResult<()> { + match (profile_id_auth_layer, object.get_profile_id()) { + (Some(auth_profile_id), Some(object_profile_id)) => { + auth_profile_id.eq(object_profile_id).then_some(()).ok_or( + errors::ApiErrorResponse::PreconditionFailed { + message: "Profile id authentication failed. Please use the correct JWT token" + .to_string(), + } + .into(), + ) + } + (Some(_auth_profile_id), None) => RouterResult::Err( + errors::ApiErrorResponse::PreconditionFailed { + message: "Couldn't find profile_id in record for authentication".to_string(), + } + .into(), + ) + .attach_printable(format!("Couldn't find profile_id in entity {:?}", object)), + (None, None) | (None, Some(_)) => Ok(()), + } +} diff --git a/crates/router/src/core/verification.rs b/crates/router/src/core/verification.rs index 9294b8f83e..795e188c8a 100644 --- a/crates/router/src/core/verification.rs +++ b/crates/router/src/core/verification.rs @@ -12,7 +12,7 @@ pub async fn verify_merchant_creds_for_applepay( state: SessionState, body: verifications::ApplepayMerchantVerificationRequest, merchant_id: common_utils::id_type::MerchantId, - _profile_id: Option, + profile_id: Option, ) -> CustomResult, errors::ApiErrorResponse> { let applepay_merchant_configs = state.conf.applepay_merchant_configs.get_inner(); @@ -62,6 +62,7 @@ pub async fn verify_merchant_creds_for_applepay( utils::check_existence_and_add_domain_to_db( &state, merchant_id, + profile_id, body.merchant_connector_account_id.clone(), body.domain_names.clone(), ) diff --git a/crates/router/src/core/verification/utils.rs b/crates/router/src/core/verification/utils.rs index 43a4bfdc73..dd0aa3edef 100644 --- a/crates/router/src/core/verification/utils.rs +++ b/crates/router/src/core/verification/utils.rs @@ -2,7 +2,10 @@ use common_utils::errors::CustomResult; use error_stack::{Report, ResultExt}; use crate::{ - core::errors::{self, utils::StorageErrorExt}, + core::{ + errors::{self, utils::StorageErrorExt}, + utils, + }, logger, routes::SessionState, types, @@ -12,6 +15,7 @@ use crate::{ pub async fn check_existence_and_add_domain_to_db( state: &SessionState, merchant_id: common_utils::id_type::MerchantId, + profile_id_from_auth_layer: Option, merchant_connector_id: String, domain_from_req: Vec, ) -> CustomResult, errors::ApiErrorResponse> { @@ -48,7 +52,10 @@ pub async fn check_existence_and_add_domain_to_db( let _ = domain_from_req; todo!() }; - + utils::validate_profile_id_from_auth_layer( + profile_id_from_auth_layer, + &merchant_connector_account, + )?; let mut already_verified_domains = merchant_connector_account .applepay_verified_domains .clone() diff --git a/crates/router/src/core/webhooks/incoming.rs b/crates/router/src/core/webhooks/incoming.rs index 223acf48b5..7c292d158c 100644 --- a/crates/router/src/core/webhooks/incoming.rs +++ b/crates/router/src/core/webhooks/incoming.rs @@ -706,7 +706,8 @@ async fn payouts_incoming_webhook_flow( }); let payout_data = - payouts::make_payout_data(&state, &merchant_account, &key_store, &action_req).await?; + payouts::make_payout_data(&state, &merchant_account, None, &key_store, &action_req) + .await?; let updated_payout_attempt = db .update_payout_attempt( diff --git a/crates/router/src/workflows/attach_payout_account_workflow.rs b/crates/router/src/workflows/attach_payout_account_workflow.rs index 37de784d5e..fa481fdb9d 100644 --- a/crates/router/src/workflows/attach_payout_account_workflow.rs +++ b/crates/router/src/workflows/attach_payout_account_workflow.rs @@ -47,7 +47,7 @@ impl ProcessTrackerWorkflow for AttachPayoutAccountWorkflow { let request = api::payouts::PayoutRequest::PayoutRetrieveRequest(tracking_data); let mut payout_data = - payouts::make_payout_data(state, &merchant_account, &key_store, &request).await?; + payouts::make_payout_data(state, &merchant_account, None, &key_store, &request).await?; payouts::payouts_core( state, diff --git a/crates/router/src/workflows/outgoing_webhook_retry.rs b/crates/router/src/workflows/outgoing_webhook_retry.rs index c6cf82d784..40dfcfd005 100644 --- a/crates/router/src/workflows/outgoing_webhook_retry.rs +++ b/crates/router/src/workflows/outgoing_webhook_retry.rs @@ -505,7 +505,8 @@ async fn get_outgoing_webhook_content_and_event_type( ); let payout_data = - payouts::make_payout_data(&state, &merchant_account, &key_store, &request).await?; + payouts::make_payout_data(&state, &merchant_account, None, &key_store, &request) + .await?; let router_response = payouts::response_handler(&merchant_account, &payout_data).await?; diff --git a/crates/router/src/workflows/payment_sync.rs b/crates/router/src/workflows/payment_sync.rs index 5d93849b20..c536ec7e4e 100644 --- a/crates/router/src/workflows/payment_sync.rs +++ b/crates/router/src/workflows/payment_sync.rs @@ -67,6 +67,7 @@ impl ProcessTrackerWorkflow for PaymentsSyncWorkflow { state, state.get_req_state(), merchant_account.clone(), + None, key_store.clone(), operations::PaymentStatus, tracking_data.clone(),