feat: add test_mode for quickly testing payout links (#5669)

Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com>
This commit is contained in:
Kashif
2024-08-27 13:33:03 +05:30
committed by GitHub
parent b63d723b8b
commit 406256c067
11 changed files with 183 additions and 90 deletions

View File

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

View File

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

View File

@ -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;
@ -2628,15 +2628,34 @@ 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.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) {
// 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()
})),
// 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(),
})
}
}?;
// Form data to be injected in the link
let (logo, merchant_name, theme) = match ui_config {
@ -2699,6 +2718,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

View File

@ -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,105 @@ 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<HashSet<String>> {
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 {
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()) {
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 origin or referer is not present in request headers",
link_id
)
})?;
let url = Url::parse(origin_or_referer)
.map_err(|_| {
report!(errors::ApiErrorResponse::AccessForbidden {
}))
.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!("Invalid URL found in request headers {}", origin_or_referer)
})?;
}))
.attach_printable_lazy(|| {
format!(
"Access to payout_link [{}] is forbidden when sec-fetch-dest 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 {
// 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(),
})
})
.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(),
})
})
.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) {
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
)
})
}
}
}
}

View File

@ -1789,6 +1789,7 @@ impl ForeignFrom<api_models::admin::BusinessPayoutLinkConfig>
fn foreign_from(item: api_models::admin::BusinessPayoutLinkConfig) -> Self {
Self {
config: item.config.foreign_into(),
payout_test_mode: item.payout_test_mode,
}
}
}
@ -1799,6 +1800,7 @@ impl ForeignFrom<diesel_models::business_profile::BusinessPayoutLinkConfig>
fn foreign_from(item: diesel_models::business_profile::BusinessPayoutLinkConfig) -> Self {
Self {
config: item.config.foreign_into(),
payout_test_mode: item.payout_test_mode,
}
}
}