From 43e28812a70394a80d8d175a07dba8a2525a73a5 Mon Sep 17 00:00:00 2001 From: Kashif Date: Thu, 22 Aug 2024 16:21:58 +0530 Subject: [PATCH 1/6] feat: add test_mode for quickly testing payout links --- crates/api_models/src/admin.rs | 4 + crates/api_models/src/payouts.rs | 8 + crates/common_utils/src/link_utils.rs | 2 + crates/diesel_models/src/business_profile.rs | 1 + .../payout_link/initiate/script.js | 6 +- crates/router/src/core/payout_link.rs | 13 +- crates/router/src/core/payouts.rs | 38 ++-- crates/router/src/core/payouts/validator.rs | 170 ++++++++++-------- crates/router/src/types/transformers.rs | 2 + 9 files changed, 154 insertions(+), 90 deletions(-) diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index 081b0a1884a0..74b8b6e6dcd0 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2413,6 +2413,10 @@ pub struct BusinessGenericLinkConfig { #[serde(flatten)] #[schema(value_type = GenericLinkUiConfig)] pub ui_config: link_utils::GenericLinkUiConfig, + + /// `test_mode` allows for removing any validations which are necessary in a production environment + #[schema(value_type = Option, default = false)] + pub test_mode: Option, } impl BusinessGenericLinkConfig { diff --git a/crates/api_models/src/payouts.rs b/crates/api_models/src/payouts.rs index 0010c2988e10..fc3bd5fcda7e 100644 --- a/crates/api_models/src/payouts.rs +++ b/crates/api_models/src/payouts.rs @@ -202,6 +202,12 @@ pub struct PayoutCreatePayoutLinkConfig { /// List of payout methods shown on collect UI #[schema(value_type = Option>, example = r#"[{"payment_method": "bank_transfer", "payment_method_types": ["ach", "bacs"]}]"#)] pub enabled_payment_methods: Option>, + + /// `test_mode` allows for opening payout links without any restrictions. This removes + /// - domain name validations + /// - check for making sure link is accessed within an iframe + #[schema(value_type = Option, example = false)] + pub test_mode: Option, } /// The payout method information required for carrying out a payout @@ -772,6 +778,7 @@ pub struct PayoutLinkDetails { pub amount: common_utils::types::StringMajorUnit, pub currency: common_enums::Currency, pub locale: String, + pub test_mode: bool, } #[derive(Clone, Debug, serde::Serialize)] @@ -787,4 +794,5 @@ pub struct PayoutLinkStatusDetails { pub error_message: Option, #[serde(flatten)] pub ui_config: link_utils::GenericLinkUiConfigFormData, + pub test_mode: bool, } diff --git a/crates/common_utils/src/link_utils.rs b/crates/common_utils/src/link_utils.rs index e95832eeba9b..dc7153f2c5b6 100644 --- a/crates/common_utils/src/link_utils.rs +++ b/crates/common_utils/src/link_utils.rs @@ -167,6 +167,8 @@ pub struct PayoutLinkData { pub currency: enums::Currency, /// A list of allowed domains (glob patterns) where this link can be embedded / opened from pub allowed_domains: HashSet, + /// `test_mode` can be used for testing payout links without any restrictions + pub test_mode: Option, } crate::impl_to_sql_from_sql_json!(PayoutLinkData); diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 087785973759..b36768b8557d 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -506,6 +506,7 @@ pub struct BusinessGenericLinkConfig { pub allowed_domains: HashSet, #[serde(flatten)] pub ui_config: common_utils::link_utils::GenericLinkUiConfig, + pub test_mode: Option, } common_utils::impl_to_sql_from_sql_json!(BusinessPayoutLinkConfig); diff --git a/crates/router/src/core/generic_link/payout_link/initiate/script.js b/crates/router/src/core/generic_link/payout_link/initiate/script.js index 2327bfba13c6..276ef6449487 100644 --- a/crates/router/src/core/generic_link/payout_link/initiate/script.js +++ b/crates/router/src/core/generic_link/payout_link/initiate/script.js @@ -1,6 +1,10 @@ // @ts-check // Top level checks +// @ts-ignore +var payoutDetails = window.__PAYOUT_DETAILS; +var isTestMode = payoutDetails.test_mode; + var isFramed = false; try { isFramed = window.parent.location !== window.location; @@ -12,7 +16,7 @@ try { } // Remove the script from DOM incase it's not iframed -if (!isFramed) { +if (!isTestMode && !isFramed) { function initializePayoutSDK() { var errMsg = "{{i18n_not_allowed}}"; var contentElement = document.getElementById("payout-link"); diff --git a/crates/router/src/core/payout_link.rs b/crates/router/src/core/payout_link.rs index 0e4806051125..39ab1ab2560a 100644 --- a/crates/router/src/core/payout_link.rs +++ b/crates/router/src/core/payout_link.rs @@ -79,7 +79,10 @@ pub async fn initiate_payout_link( message: "payout link not found".to_string(), })?; - validator::validate_payout_link_render_request(request_headers, &payout_link)?; + let allowed_domains = validator::validate_payout_link_render_request_and_get_allowed_domains( + request_headers, + &payout_link, + )?; // Check status and return form data accordingly let has_expired = common_utils::date_time::now() > payout_link.expiry; @@ -120,7 +123,7 @@ pub async fn initiate_payout_link( Ok(services::ApplicationResponse::GenericLinkForm(Box::new( GenericLinks { - allowed_domains: (link_data.allowed_domains), + allowed_domains, data: GenericLinksData::ExpiredLink(expired_link_data), locale, }, @@ -204,6 +207,7 @@ pub async fn initiate_payout_link( amount, currency: payout.destination_currency, locale: locale.clone(), + test_mode: link_data.test_mode.unwrap_or(false), }; let serialized_css_content = String::new(); @@ -224,7 +228,7 @@ pub async fn initiate_payout_link( }; Ok(services::ApplicationResponse::GenericLinkForm(Box::new( GenericLinks { - allowed_domains: (link_data.allowed_domains), + allowed_domains, data: GenericLinksData::PayoutLink(generic_form_data), locale, }, @@ -249,6 +253,7 @@ pub async fn initiate_payout_link( error_code: payout_attempt.error_code, error_message: payout_attempt.error_message, ui_config: ui_config_data, + test_mode: link_data.test_mode.unwrap_or(false), }; let serialized_css_content = String::new(); @@ -267,7 +272,7 @@ pub async fn initiate_payout_link( }; Ok(services::ApplicationResponse::GenericLinkForm(Box::new( GenericLinks { - allowed_domains: (link_data.allowed_domains), + allowed_domains, data: GenericLinksData::PayoutLinkStatus(generic_status_data), locale, }, diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index a54f00d6aa04..11e5fb8bc684 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -4,7 +4,7 @@ pub mod helpers; pub mod retry; pub mod transformers; pub mod validator; -use std::vec::IntoIter; +use std::{collections::HashSet, vec::IntoIter}; use api_models::{self, enums as api_enums, payouts::PayoutLinkResponse}; #[cfg(feature = "payout_retry")] @@ -28,7 +28,7 @@ use futures::future::join_all; use masking::{PeekInterface, Secret}; #[cfg(feature = "payout_retry")] use retry::GsmValidation; -use router_env::{instrument, logger, tracing}; +use router_env::{instrument, logger, tracing, Env}; use scheduler::utils as pt_utils; use serde_json; use time::Duration; @@ -2614,15 +2614,32 @@ pub async fn create_payout_link( .and_then(|config| config.ui_config.clone()) .or(profile_ui_config); - // Validate allowed_domains presence - let allowed_domains = profile_config + let test_mode_in_config = payout_link_config_req .as_ref() - .map(|config| config.config.allowed_domains.to_owned()) - .get_required_value("allowed_domains") - .change_context(errors::ApiErrorResponse::LinkConfigurationError { - message: "Payout links cannot be used without setting allowed_domains in profile" - .to_string(), - })?; + .and_then(|config| config.test_mode) + .or_else(|| profile_config.as_ref().and_then(|c| c.config.test_mode)); + let is_test_mode_enabled = test_mode_in_config.unwrap_or(false); + + let allowed_domains = match router_env::which() { + Env::Production => Err(report!(errors::ApiErrorResponse::LinkConfigurationError { + message: "test_mode cannot be true for creating payout_links in production".to_string() + })), + _ => { + if is_test_mode_enabled { + Ok(HashSet::new()) + } else { + profile_config + .as_ref() + .map(|config| config.config.allowed_domains.to_owned()) + .get_required_value("allowed_domains") + .change_context(errors::ApiErrorResponse::LinkConfigurationError { + message: + "Payout links cannot be used without setting allowed_domains in profile. If you're using a non-production environment, you can set test_mode to true while in payout_link_config" + .to_string(), + }) + } + } + }?; // Form data to be injected in the link let (logo, merchant_name, theme) = match ui_config { @@ -2685,6 +2702,7 @@ pub async fn create_payout_link( amount: MinorUnit::from(*amount), currency: *currency, allowed_domains, + test_mode: test_mode_in_config, }; create_payout_link_db_entry(state, merchant_id, &data, req.return_url.clone()).await diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index d425725645b8..6b9ede96a761 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -1,3 +1,5 @@ +use std::collections::HashSet; + use actix_web::http::header; #[cfg(feature = "olap")] use common_utils::errors::CustomResult; @@ -5,7 +7,7 @@ use common_utils::validation::validate_domain_against_allowed_domains; use diesel_models::generic_link::PayoutLink; use error_stack::{report, ResultExt}; pub use hyperswitch_domain_models::errors::StorageError; -use router_env::{instrument, tracing}; +use router_env::{instrument, tracing, which as router_env_which, Env}; use url::Url; use super::helpers; @@ -225,88 +227,106 @@ pub(super) fn validate_payout_list_request_for_joins( Ok(()) } -pub fn validate_payout_link_render_request( +pub fn validate_payout_link_render_request_and_get_allowed_domains( request_headers: &header::HeaderMap, payout_link: &PayoutLink, -) -> RouterResult<()> { +) -> RouterResult> { let link_id = payout_link.link_id.to_owned(); let link_data = payout_link.link_data.to_owned(); - // Fetch destination is "iframe" - match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) { - Some("iframe") => Ok(()), - Some(requestor) => Err(report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - })) - .attach_printable_lazy(|| { - format!( - "Access to payout_link [{}] is forbidden when requested through {}", - link_id, requestor - ) - }), - None => Err(report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - })) - .attach_printable_lazy(|| { - format!( - "Access to payout_link [{}] is forbidden when sec-fetch-dest is not present in request headers", - link_id - ) - }), - }?; + let is_test_mode_enabled = link_data.test_mode.unwrap_or(false); - // Validate origin / referer - let domain_in_req = { - let origin_or_referer = request_headers - .get("origin") - .or_else(|| request_headers.get("referer")) - .and_then(|v| v.to_str().ok()) - .ok_or_else(|| { - report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - }) - }) - .attach_printable_lazy(|| { - format!( - "Access to payout_link [{}] is forbidden when origin or referer is not present in request headers", - link_id - ) - })?; + match router_env_which() { + Env::Production => Err(report!(errors::ApiErrorResponse::LinkConfigurationError { + message: "test_mode cannot be true for rendering payout_links in production" + .to_string() + })), + _ => { + // Skip all validations when test mode is enabled in non prod env + if is_test_mode_enabled { + Ok(HashSet::new()) + } else { + // Fetch destination is "iframe" + match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) { + Some("iframe") => Ok(()), + Some(requestor) => Err(report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + })) + .attach_printable_lazy(|| { + format!( + "Access to payout_link [{}] is forbidden when requested through {}", + link_id, requestor + ) + }), + None => Err(report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + })) + .attach_printable_lazy(|| { + format!( + "Access to payout_link [{}] is forbidden when sec-fetch-dest is not present in request headers", + link_id + ) + }), + }?; - let url = Url::parse(origin_or_referer) - .map_err(|_| { - report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - }) - }) - .attach_printable_lazy(|| { - format!("Invalid URL found in request headers {}", origin_or_referer) - })?; + // Validate origin / referer + let domain_in_req = { + let origin_or_referer = request_headers + .get("origin") + .or_else(|| request_headers.get("referer")) + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + }) + }) + .attach_printable_lazy(|| { + format!( + "Access to payout_link [{}] is forbidden when origin or referer is not present in request headers", + link_id + ) + })?; - url.host_str() - .and_then(|host| url.port().map(|port| format!("{}:{}", host, port))) - .or_else(|| url.host_str().map(String::from)) - .ok_or_else(|| { - report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - }) - }) - .attach_printable_lazy(|| { - format!("host or port not found in request headers {:?}", url) - })? - }; + let url = Url::parse(origin_or_referer) + .map_err(|_| { + report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + }) + }) + .attach_printable_lazy(|| { + format!("Invalid URL found in request headers {}", origin_or_referer) + })?; - if validate_domain_against_allowed_domains(&domain_in_req, link_data.allowed_domains) { - Ok(()) - } else { - Err(report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - })) - .attach_printable_lazy(|| { - format!( - "Access to payout_link [{}] is forbidden from requestor - {}", - link_id, domain_in_req - ) - }) + url.host_str() + .and_then(|host| url.port().map(|port| format!("{}:{}", host, port))) + .or_else(|| url.host_str().map(String::from)) + .ok_or_else(|| { + report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + }) + }) + .attach_printable_lazy(|| { + format!("host or port not found in request headers {:?}", url) + })? + }; + + if validate_domain_against_allowed_domains( + &domain_in_req, + link_data.allowed_domains.clone(), + ) { + Ok(link_data.allowed_domains) + } else { + Err(report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + })) + .attach_printable_lazy(|| { + format!( + "Access to payout_link [{}] is forbidden from requestor - {}", + link_id, domain_in_req + ) + }) + } + } + } } } diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 67af63b154e0..4f68f963720e 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1776,6 +1776,7 @@ impl ForeignFrom domain_name: item.domain_name, allowed_domains: item.allowed_domains, ui_config: item.ui_config, + test_mode: item.test_mode, } } } @@ -1788,6 +1789,7 @@ impl ForeignFrom domain_name: item.domain_name, allowed_domains: item.allowed_domains, ui_config: item.ui_config, + test_mode: item.test_mode, } } } From 7d2362bb24c4dbb65ea4772cd47d2f80f2e05940 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Thu, 22 Aug 2024 11:10:08 +0000 Subject: [PATCH 2/6] docs(openapi): re-generate OpenAPI specification --- api-reference-v2/openapi_spec.json | 12 ++++++++++++ api-reference/openapi_spec.json | 12 ++++++++++++ 2 files changed, 24 insertions(+) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index e7a502be5225..7e296bb4e4ba 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -2908,6 +2908,12 @@ }, "description": "A list of allowed domains (glob patterns) where this link can be embedded / opened from", "uniqueItems": true + }, + "test_mode": { + "type": "boolean", + "description": "`test_mode` allows for removing any validations which are necessary in a production environment", + "default": false, + "nullable": true } } } @@ -13886,6 +13892,12 @@ "description": "List of payout methods shown on collect UI", "example": "[{\"payment_method\": \"bank_transfer\", \"payment_method_types\": [\"ach\", \"bacs\"]}]", "nullable": true + }, + "test_mode": { + "type": "boolean", + "description": "`test_mode` allows for opening payout links without any restrictions. This removes\n- domain name validations\n- check for making sure link is accessed within an iframe", + "example": false, + "nullable": true } } } diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index d7f03d40c43a..a4b9c4c92d44 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -7129,6 +7129,12 @@ }, "description": "A list of allowed domains (glob patterns) where this link can be embedded / opened from", "uniqueItems": true + }, + "test_mode": { + "type": "boolean", + "description": "`test_mode` allows for removing any validations which are necessary in a production environment", + "default": false, + "nullable": true } } } @@ -18698,6 +18704,12 @@ "description": "List of payout methods shown on collect UI", "example": "[{\"payment_method\": \"bank_transfer\", \"payment_method_types\": [\"ach\", \"bacs\"]}]", "nullable": true + }, + "test_mode": { + "type": "boolean", + "description": "`test_mode` allows for opening payout links without any restrictions. This removes\n- domain name validations\n- check for making sure link is accessed within an iframe", + "example": false, + "nullable": true } } } From 093e1aa0711f7327a38fc66297eaa3460714c2bc Mon Sep 17 00:00:00 2001 From: Kashif Date: Mon, 26 Aug 2024 12:17:38 +0530 Subject: [PATCH 3/6] refactor: add missing check for test_mode in production --- crates/router/src/core/payouts.rs | 34 +++-- crates/router/src/core/payouts/validator.rs | 153 ++++++++++---------- 2 files changed, 93 insertions(+), 94 deletions(-) diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 11e5fb8bc684..73e4b6033971 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -2620,24 +2620,26 @@ pub async fn create_payout_link( .or_else(|| profile_config.as_ref().and_then(|c| c.config.test_mode)); let is_test_mode_enabled = test_mode_in_config.unwrap_or(false); - let allowed_domains = match router_env::which() { - Env::Production => Err(report!(errors::ApiErrorResponse::LinkConfigurationError { + let allowed_domains = match (router_env::which(), is_test_mode_enabled) { + // Throw error in case test_mode was enabled in production + (Env::Production, true) => Err(report!(errors::ApiErrorResponse::LinkConfigurationError { message: "test_mode cannot be true for creating payout_links in production".to_string() })), - _ => { - if is_test_mode_enabled { - Ok(HashSet::new()) - } else { - profile_config - .as_ref() - .map(|config| config.config.allowed_domains.to_owned()) - .get_required_value("allowed_domains") - .change_context(errors::ApiErrorResponse::LinkConfigurationError { - message: - "Payout links cannot be used without setting allowed_domains in profile. If you're using a non-production environment, you can set test_mode to true while in payout_link_config" - .to_string(), - }) - } + // Send empty set of whitelisted domains + (_, true) => { + Ok(HashSet::new()) + }, + // Otherwise, fetch and use allowed domains from profile config + (_, false) => { + profile_config + .as_ref() + .map(|config| config.config.allowed_domains.to_owned()) + .get_required_value("allowed_domains") + .change_context(errors::ApiErrorResponse::LinkConfigurationError { + message: + "Payout links cannot be used without setting allowed_domains in profile. If you're using a non-production environment, you can set test_mode to true while in payout_link_config" + .to_string(), + }) } }?; diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index 6b9ede96a761..51084678655c 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -236,96 +236,93 @@ pub fn validate_payout_link_render_request_and_get_allowed_domains( let is_test_mode_enabled = link_data.test_mode.unwrap_or(false); - match router_env_which() { - Env::Production => Err(report!(errors::ApiErrorResponse::LinkConfigurationError { + match (router_env_which(), is_test_mode_enabled) { + (Env::Production, true) => Err(report!(errors::ApiErrorResponse::LinkConfigurationError { message: "test_mode cannot be true for rendering payout_links in production" .to_string() })), - _ => { - // Skip all validations when test mode is enabled in non prod env - if is_test_mode_enabled { - Ok(HashSet::new()) - } else { - // Fetch destination is "iframe" - match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) { - Some("iframe") => Ok(()), - Some(requestor) => Err(report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - })) - .attach_printable_lazy(|| { - format!( - "Access to payout_link [{}] is forbidden when requested through {}", - link_id, requestor - ) - }), - None => Err(report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - })) + // Skip all validations when test mode is enabled in non prod env + (_, true) => Ok(HashSet::new()), + (_, false) => { + // Fetch destination is "iframe" + match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) { + Some("iframe") => Ok(()), + Some(requestor) => Err(report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + })) + .attach_printable_lazy(|| { + format!( + "Access to payout_link [{}] is forbidden when requested through {}", + link_id, requestor + ) + }), + None => Err(report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + })) + .attach_printable_lazy(|| { + format!( + "Access to payout_link [{}] is forbidden when sec-fetch-dest is not present in request headers", + link_id + ) + }), + }?; + + // Validate origin / referer + let domain_in_req = { + let origin_or_referer = request_headers + .get("origin") + .or_else(|| request_headers.get("referer")) + .and_then(|v| v.to_str().ok()) + .ok_or_else(|| { + report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + }) + }) .attach_printable_lazy(|| { format!( - "Access to payout_link [{}] is forbidden when sec-fetch-dest is not present in request headers", + "Access to payout_link [{}] is forbidden when origin or referer is not present in request headers", link_id ) - }), - }?; - - // Validate origin / referer - let domain_in_req = { - let origin_or_referer = request_headers - .get("origin") - .or_else(|| request_headers.get("referer")) - .and_then(|v| v.to_str().ok()) - .ok_or_else(|| { - report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - }) - }) - .attach_printable_lazy(|| { - format!( - "Access to payout_link [{}] is forbidden when origin or referer is not present in request headers", - link_id - ) - })?; + })?; - let url = Url::parse(origin_or_referer) - .map_err(|_| { - report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - }) + let url = Url::parse(origin_or_referer) + .map_err(|_| { + report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), }) - .attach_printable_lazy(|| { - format!("Invalid URL found in request headers {}", origin_or_referer) - })?; + }) + .attach_printable_lazy(|| { + format!("Invalid URL found in request headers {}", origin_or_referer) + })?; - url.host_str() - .and_then(|host| url.port().map(|port| format!("{}:{}", host, port))) - .or_else(|| url.host_str().map(String::from)) - .ok_or_else(|| { - report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - }) + url.host_str() + .and_then(|host| url.port().map(|port| format!("{}:{}", host, port))) + .or_else(|| url.host_str().map(String::from)) + .ok_or_else(|| { + report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), }) - .attach_printable_lazy(|| { - format!("host or port not found in request headers {:?}", url) - })? - }; - - if validate_domain_against_allowed_domains( - &domain_in_req, - link_data.allowed_domains.clone(), - ) { - Ok(link_data.allowed_domains) - } else { - Err(report!(errors::ApiErrorResponse::AccessForbidden { - resource: "payout_link".to_string(), - })) - .attach_printable_lazy(|| { - format!( - "Access to payout_link [{}] is forbidden from requestor - {}", - link_id, domain_in_req - ) }) - } + .attach_printable_lazy(|| { + format!("host or port not found in request headers {:?}", url) + })? + }; + + if validate_domain_against_allowed_domains( + &domain_in_req, + link_data.allowed_domains.clone(), + ) { + Ok(link_data.allowed_domains) + } else { + Err(report!(errors::ApiErrorResponse::AccessForbidden { + resource: "payout_link".to_string(), + })) + .attach_printable_lazy(|| { + format!( + "Access to payout_link [{}] is forbidden from requestor - {}", + link_id, domain_in_req + ) + }) } } } From 82ced54319ffe67c08a21d734dd8a9ab9823b2c3 Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 06:50:26 +0000 Subject: [PATCH 4/6] docs(openapi): re-generate OpenAPI specification --- api-reference-v2/openapi_spec.json | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index 401c2777d200..b677ec53cbb8 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -10289,17 +10289,6 @@ } } }, - { - "type": "object", - "required": [ - "paypal" - ], - "properties": { - "paypal": { - "type": "object" - } - } - }, { "type": "object", "required": [ From ec942fef56bfe7d97e83e92ec798c1c8d09d26c3 Mon Sep 17 00:00:00 2001 From: Kashif Date: Mon, 26 Aug 2024 14:59:43 +0530 Subject: [PATCH 5/6] refactor: rename test_mode to payout_test_mode --- crates/api_models/src/admin.rs | 8 ++++---- crates/diesel_models/src/business_profile.rs | 2 +- crates/router/src/core/payouts.rs | 2 +- crates/router/src/core/payouts/validator.rs | 2 ++ crates/router/src/types/transformers.rs | 4 ++-- 5 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/api_models/src/admin.rs b/crates/api_models/src/admin.rs index a0c9c2d74207..c18825ae3780 100644 --- a/crates/api_models/src/admin.rs +++ b/crates/api_models/src/admin.rs @@ -2472,6 +2472,10 @@ pub struct BusinessCollectLinkConfig { pub struct BusinessPayoutLinkConfig { #[serde(flatten)] pub config: BusinessGenericLinkConfig, + + /// Allows for removing any validations / pre-requisites which are necessary in a production environment + #[schema(value_type = Option, default = false)] + pub payout_test_mode: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize, ToSchema)] @@ -2485,10 +2489,6 @@ pub struct BusinessGenericLinkConfig { #[serde(flatten)] #[schema(value_type = GenericLinkUiConfig)] pub ui_config: link_utils::GenericLinkUiConfig, - - /// `test_mode` allows for removing any validations which are necessary in a production environment - #[schema(value_type = Option, default = false)] - pub test_mode: Option, } impl BusinessGenericLinkConfig { diff --git a/crates/diesel_models/src/business_profile.rs b/crates/diesel_models/src/business_profile.rs index 4b343637a042..a06f81a2a826 100644 --- a/crates/diesel_models/src/business_profile.rs +++ b/crates/diesel_models/src/business_profile.rs @@ -530,6 +530,7 @@ common_utils::impl_to_sql_from_sql_json!(BusinessPaymentLinkConfig); pub struct BusinessPayoutLinkConfig { #[serde(flatten)] pub config: BusinessGenericLinkConfig, + pub payout_test_mode: Option, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] @@ -538,7 +539,6 @@ pub struct BusinessGenericLinkConfig { pub allowed_domains: HashSet, #[serde(flatten)] pub ui_config: common_utils::link_utils::GenericLinkUiConfig, - pub test_mode: Option, } common_utils::impl_to_sql_from_sql_json!(BusinessPayoutLinkConfig); diff --git a/crates/router/src/core/payouts.rs b/crates/router/src/core/payouts.rs index 73e4b6033971..e0bd37aef0de 100644 --- a/crates/router/src/core/payouts.rs +++ b/crates/router/src/core/payouts.rs @@ -2617,7 +2617,7 @@ pub async fn create_payout_link( let test_mode_in_config = payout_link_config_req .as_ref() .and_then(|config| config.test_mode) - .or_else(|| profile_config.as_ref().and_then(|c| c.config.test_mode)); + .or_else(|| profile_config.as_ref().and_then(|c| c.payout_test_mode)); let is_test_mode_enabled = test_mode_in_config.unwrap_or(false); let allowed_domains = match (router_env::which(), is_test_mode_enabled) { diff --git a/crates/router/src/core/payouts/validator.rs b/crates/router/src/core/payouts/validator.rs index 51084678655c..2d47a60f23d4 100644 --- a/crates/router/src/core/payouts/validator.rs +++ b/crates/router/src/core/payouts/validator.rs @@ -237,12 +237,14 @@ pub fn validate_payout_link_render_request_and_get_allowed_domains( let is_test_mode_enabled = link_data.test_mode.unwrap_or(false); match (router_env_which(), is_test_mode_enabled) { + // Throw error in case test_mode was enabled in production (Env::Production, true) => Err(report!(errors::ApiErrorResponse::LinkConfigurationError { message: "test_mode cannot be true for rendering payout_links in production" .to_string() })), // Skip all validations when test mode is enabled in non prod env (_, true) => Ok(HashSet::new()), + // Otherwise, perform validations (_, false) => { // Fetch destination is "iframe" match request_headers.get("sec-fetch-dest").and_then(|v| v.to_str().ok()) { diff --git a/crates/router/src/types/transformers.rs b/crates/router/src/types/transformers.rs index 4f68f963720e..da483674fe0c 100644 --- a/crates/router/src/types/transformers.rs +++ b/crates/router/src/types/transformers.rs @@ -1754,6 +1754,7 @@ impl ForeignFrom fn foreign_from(item: api_models::admin::BusinessPayoutLinkConfig) -> Self { Self { config: item.config.foreign_into(), + payout_test_mode: item.payout_test_mode, } } } @@ -1764,6 +1765,7 @@ impl ForeignFrom fn foreign_from(item: diesel_models::business_profile::BusinessPayoutLinkConfig) -> Self { Self { config: item.config.foreign_into(), + payout_test_mode: item.payout_test_mode, } } } @@ -1776,7 +1778,6 @@ impl ForeignFrom domain_name: item.domain_name, allowed_domains: item.allowed_domains, ui_config: item.ui_config, - test_mode: item.test_mode, } } } @@ -1789,7 +1790,6 @@ impl ForeignFrom domain_name: item.domain_name, allowed_domains: item.allowed_domains, ui_config: item.ui_config, - test_mode: item.test_mode, } } } From 09389ce45136963453b70d9a9932a02d5127765d Mon Sep 17 00:00:00 2001 From: "hyperswitch-bot[bot]" <148525504+hyperswitch-bot[bot]@users.noreply.github.com> Date: Mon, 26 Aug 2024 09:35:16 +0000 Subject: [PATCH 6/6] docs(openapi): re-generate OpenAPI specification --- api-reference-v2/openapi_spec.json | 16 +++++++++------- api-reference/openapi_spec.json | 16 +++++++++------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/api-reference-v2/openapi_spec.json b/api-reference-v2/openapi_spec.json index e74f7a81c6a7..cc1b2f3137ef 100644 --- a/api-reference-v2/openapi_spec.json +++ b/api-reference-v2/openapi_spec.json @@ -2908,12 +2908,6 @@ }, "description": "A list of allowed domains (glob patterns) where this link can be embedded / opened from", "uniqueItems": true - }, - "test_mode": { - "type": "boolean", - "description": "`test_mode` allows for removing any validations which are necessary in a production environment", - "default": false, - "nullable": true } } } @@ -2959,7 +2953,15 @@ "$ref": "#/components/schemas/BusinessGenericLinkConfig" }, { - "type": "object" + "type": "object", + "properties": { + "payout_test_mode": { + "type": "boolean", + "description": "Allows for removing any validations / pre-requisites which are necessary in a production environment", + "default": false, + "nullable": true + } + } } ] }, diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index c3873565e7d9..c88c4c8a90c9 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -7129,12 +7129,6 @@ }, "description": "A list of allowed domains (glob patterns) where this link can be embedded / opened from", "uniqueItems": true - }, - "test_mode": { - "type": "boolean", - "description": "`test_mode` allows for removing any validations which are necessary in a production environment", - "default": false, - "nullable": true } } } @@ -7180,7 +7174,15 @@ "$ref": "#/components/schemas/BusinessGenericLinkConfig" }, { - "type": "object" + "type": "object", + "properties": { + "payout_test_mode": { + "type": "boolean", + "description": "Allows for removing any validations / pre-requisites which are necessary in a production environment", + "default": false, + "nullable": true + } + } } ] },