feat(router): add api to migrate card from basilisk to rust (#2853)

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
Shankar Singh C
2023-11-16 19:02:54 +05:30
committed by GitHub
parent 62c9ccae6a
commit b8b20c412d
24 changed files with 274 additions and 20 deletions

View File

@ -112,6 +112,7 @@ kms_encrypted_recon_admin_api_key = "" # Base64-encoded (KMS encrypted) ciph
# like card details
[locker]
host = "" # Locker host
host_rs = "" # Rust Locker host
mock_locker = true # Emulate a locker locally using Postgres
basilisk_host = "" # Basilisk host
locker_signing_key_id = "1" # Key_id to sign basilisk hs locker
@ -130,6 +131,7 @@ locker_encryption_key2 = "" # public key 2 in pem format, corresponding private
locker_decryption_key1 = "" # private key 1 in pem format, corresponding public key in basilisk
locker_decryption_key2 = "" # private key 2 in pem format, corresponding public key in basilisk
vault_encryption_key = "" # public key in pem format, corresponding private key in basilisk-hs
rust_locker_encryption_key = "" # public key in pem format, corresponding private key in rust locker
vault_private_key = "" # private key in pem format, corresponding public key in basilisk-hs

View File

@ -48,6 +48,7 @@ applepay_endpoint = "DOMAIN SPECIFIC ENDPOINT"
[locker]
host = ""
host_rs = ""
mock_locker = true
basilisk_host = ""
@ -59,6 +60,7 @@ locker_encryption_key2 = ""
locker_decryption_key1 = ""
locker_decryption_key2 = ""
vault_encryption_key = ""
rust_locker_encryption_key = ""
vault_private_key = ""
tunnel_private_key = ""

View File

@ -44,6 +44,7 @@ recon_admin_api_key = "recon_test_admin"
[locker]
host = ""
host_rs = ""
mock_locker = true
basilisk_host = ""
@ -55,6 +56,7 @@ locker_encryption_key2 = ""
locker_decryption_key1 = ""
locker_decryption_key2 = ""
vault_encryption_key = ""
rust_locker_encryption_key = ""
vault_private_key = ""
[redis]

View File

@ -562,3 +562,9 @@ pub enum RetryAction {
/// Denotes that the payment is requeued
Requeue,
}
#[derive(Clone, Copy)]
pub enum LockerChoice {
Basilisk,
Tartarus,
}

View File

@ -1,5 +1,6 @@
pub mod customer;
pub mod gsm;
mod locker_migration;
pub mod payment;
#[cfg(feature = "payouts")]
pub mod payouts;

View File

@ -0,0 +1,9 @@
use common_utils::events::ApiEventMetric;
use crate::locker_migration::MigrateCardResponse;
impl ApiEventMetric for MigrateCardResponse {
fn get_api_event_type(&self) -> Option<common_utils::events::ApiEventsType> {
Some(common_utils::events::ApiEventsType::RustLocker)
}
}

View File

@ -13,6 +13,7 @@ pub mod errors;
pub mod events;
pub mod files;
pub mod gsm;
pub mod locker_migration;
pub mod mandates;
pub mod organization;
pub mod payment_methods;

View File

@ -0,0 +1,8 @@
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MigrateCardResponse {
pub status_message: String,
pub status_code: String,
pub customers_moved: usize,
pub cards_moved: usize,
}

View File

@ -44,6 +44,7 @@ pub enum ApiEventsType {
Gsm,
// TODO: This has to be removed once the corresponding apiEventTypes are created
Miscellaneous,
RustLocker,
}
impl ApiEventMetric for serde_json::Value {}

View File

@ -48,6 +48,7 @@ impl Default for super::settings::Locker {
fn default() -> Self {
Self {
host: "localhost".into(),
host_rs: "localhost".into(),
mock_locker: true,
basilisk_host: "localhost".into(),
locker_signing_key_id: "1".into(),

View File

@ -18,6 +18,7 @@ impl KmsDecrypt for settings::Jwekey {
self.locker_decryption_key1,
self.locker_decryption_key2,
self.vault_encryption_key,
self.rust_locker_encryption_key,
self.vault_private_key,
self.tunnel_private_key,
) = tokio::try_join!(
@ -26,6 +27,7 @@ impl KmsDecrypt for settings::Jwekey {
kms_client.decrypt(self.locker_decryption_key1),
kms_client.decrypt(self.locker_decryption_key2),
kms_client.decrypt(self.vault_encryption_key),
kms_client.decrypt(self.rust_locker_encryption_key),
kms_client.decrypt(self.vault_private_key),
kms_client.decrypt(self.tunnel_private_key),
)?;

View File

@ -420,6 +420,7 @@ pub struct Secrets {
#[serde(default)]
pub struct Locker {
pub host: String,
pub host_rs: String,
pub mock_locker: bool,
pub basilisk_host: String,
pub locker_signing_key_id: String,
@ -448,6 +449,7 @@ pub struct Jwekey {
pub locker_decryption_key1: String,
pub locker_decryption_key2: String,
pub vault_encryption_key: String,
pub rust_locker_encryption_key: String,
pub vault_private_key: String,
pub tunnel_private_key: String,
}

View File

@ -9,6 +9,7 @@ pub mod disputes;
pub mod errors;
pub mod files;
pub mod gsm;
pub mod locker_migration;
pub mod mandate;
pub mod metrics;
pub mod payment_link;

View File

@ -0,0 +1,131 @@
use api_models::{enums as api_enums, locker_migration::MigrateCardResponse};
use common_utils::errors::CustomResult;
use diesel_models::PaymentMethod;
use error_stack::{FutureExt, ResultExt};
use futures::TryFutureExt;
use super::{errors::StorageErrorExt, payment_methods::cards};
use crate::{
errors,
routes::AppState,
services::{self, logger},
types::{api, domain},
};
pub async fn rust_locker_migration(
state: AppState,
merchant_id: &str,
) -> CustomResult<services::ApplicationResponse<MigrateCardResponse>, errors::ApiErrorResponse> {
let db = state.store.as_ref();
let key_store = state
.store
.get_merchant_key_store_by_merchant_id(
merchant_id,
&state.store.get_master_key().to_vec().into(),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let merchant_account = db
.find_merchant_account_by_merchant_id(merchant_id, &key_store)
.await
.to_not_found_response(errors::ApiErrorResponse::MerchantAccountNotFound)
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let domain_customers = db
.list_customers_by_merchant_id(merchant_id, &key_store)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;
let mut customers_moved = 0;
let mut cards_moved = 0;
for customer in domain_customers {
let result = db
.find_payment_method_by_customer_id_merchant_id_list(&customer.customer_id, merchant_id)
.change_context(errors::ApiErrorResponse::InternalServerError)
.and_then(|pm| {
call_to_locker(
&state,
pm,
&customer.customer_id,
merchant_id,
&merchant_account,
)
})
.await?;
customers_moved += 1;
cards_moved += result;
}
Ok(services::api::ApplicationResponse::Json(
MigrateCardResponse {
status_code: "200".to_string(),
status_message: "Card migration completed".to_string(),
customers_moved,
cards_moved,
},
))
}
pub async fn call_to_locker(
state: &AppState,
payment_methods: Vec<PaymentMethod>,
customer_id: &String,
merchant_id: &str,
merchant_account: &domain::MerchantAccount,
) -> CustomResult<usize, errors::ApiErrorResponse> {
let mut cards_moved = 0;
for pm in payment_methods {
let card =
cards::get_card_from_locker(state, customer_id, merchant_id, &pm.payment_method_id)
.await?;
let card_details = api::CardDetail {
card_number: card.card_number,
card_exp_month: card.card_exp_month,
card_exp_year: card.card_exp_year,
card_holder_name: card.name_on_card,
nick_name: card.nick_name.map(masking::Secret::new),
};
let pm_create = api::PaymentMethodCreate {
payment_method: pm.payment_method,
payment_method_type: pm.payment_method_type,
payment_method_issuer: pm.payment_method_issuer,
payment_method_issuer_code: pm.payment_method_issuer_code,
card: Some(card_details.clone()),
metadata: pm.metadata,
customer_id: Some(pm.customer_id),
card_network: card.card_brand,
};
let (_add_card_rs_resp, _is_duplicate) = cards::add_card_hs(
state,
pm_create,
card_details,
customer_id.to_string(),
merchant_account,
api_enums::LockerChoice::Tartarus,
Some(&pm.payment_method_id),
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)
.attach_printable(format!(
"Card migration failed for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ",
pm.payment_method_id
))?;
cards_moved += 1;
logger::info!(
"Card migrated for merchant_id: {merchant_id}, customer_id: {customer_id}, payment_method_id: {} ",
pm.payment_method_id
);
}
Ok(cards_moved)
}

View File

@ -214,7 +214,15 @@ pub async fn add_card_to_locker(
metrics::STORED_TO_LOCKER.add(&metrics::CONTEXT, 1, &[]);
request::record_operation_time(
async {
add_card_hs(state, req, card, customer_id, merchant_account)
add_card_hs(
state,
req,
card,
customer_id,
merchant_account,
api_enums::LockerChoice::Basilisk,
None,
)
.await
.map_err(|error| {
metrics::CARD_LOCKER_FAILURES.add(&metrics::CONTEXT, 1, &[]);
@ -282,10 +290,13 @@ pub async fn add_card_hs(
card: api::CardDetail,
customer_id: String,
merchant_account: &domain::MerchantAccount,
locker_choice: api_enums::LockerChoice,
card_reference: Option<&str>,
) -> errors::CustomResult<(api::PaymentMethodResponse, bool), errors::VaultError> {
let payload = payment_methods::StoreLockerReq::LockerCard(payment_methods::StoreCardReq {
merchant_id: &merchant_account.merchant_id,
merchant_customer_id: customer_id.to_owned(),
card_reference: card_reference.map(str::to_string),
card: payment_methods::Card {
card_number: card.card_number.to_owned(),
name_on_card: card.card_holder_name.to_owned(),
@ -296,7 +307,8 @@ pub async fn add_card_hs(
nick_name: card.nick_name.as_ref().map(masking::Secret::peek).cloned(),
},
});
let store_card_payload = call_to_locker_hs(state, &payload, &customer_id).await?;
let store_card_payload =
call_to_locker_hs(state, &payload, &customer_id, locker_choice).await?;
let payment_method_resp = payment_methods::mk_add_card_response_hs(
card,
@ -394,6 +406,7 @@ pub async fn call_to_locker_hs<'a>(
state: &routes::AppState,
payload: &payment_methods::StoreLockerReq<'a>,
customer_id: &str,
locker_choice: api_enums::LockerChoice,
) -> errors::CustomResult<payment_methods::StoreCardRespPayload, errors::VaultError> {
let locker = &state.conf.locker;
#[cfg(not(feature = "kms"))]
@ -402,7 +415,9 @@ pub async fn call_to_locker_hs<'a>(
let jwekey = &state.kms_secrets;
let db = &*state.store;
let stored_card_response = if !locker.mock_locker {
let request = payment_methods::mk_add_locker_request_hs(jwekey, locker, payload).await?;
let request =
payment_methods::mk_add_locker_request_hs(jwekey, locker, payload, locker_choice)
.await?;
let response = services::call_connector_api(state, request)
.await
.change_context(errors::VaultError::SaveCardFailed);

View File

@ -1,5 +1,6 @@
use std::str::FromStr;
use api_models::enums as api_enums;
use common_utils::{ext_traits::StringExt, pii::Email};
use error_stack::ResultExt;
use josekit::jwe;
@ -26,6 +27,8 @@ pub enum StoreLockerReq<'a> {
pub struct StoreCardReq<'a> {
pub merchant_id: &'a str,
pub merchant_customer_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub card_reference: Option<String>,
pub card: Card,
}
@ -224,6 +227,7 @@ pub async fn mk_basilisk_req(
#[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets,
#[cfg(not(feature = "kms"))] jwekey: &settings::Jwekey,
jws: &str,
locker_choice: api_enums::LockerChoice,
) -> CustomResult<encryption::JweBody, errors::VaultError> {
let jws_payload: Vec<&str> = jws.split('.').collect();
@ -241,10 +245,18 @@ pub async fn mk_basilisk_req(
.change_context(errors::VaultError::SaveCardFailed)?;
#[cfg(feature = "kms")]
let public_key = jwekey.jwekey.peek().vault_encryption_key.as_bytes();
let public_key = match locker_choice {
api_enums::LockerChoice::Basilisk => jwekey.jwekey.peek().vault_encryption_key.as_bytes(),
api_enums::LockerChoice::Tartarus => {
jwekey.jwekey.peek().rust_locker_encryption_key.as_bytes()
}
};
#[cfg(not(feature = "kms"))]
let public_key = jwekey.vault_encryption_key.as_bytes();
let public_key = match locker_choice {
api_enums::LockerChoice::Basilisk => jwekey.vault_encryption_key.as_bytes(),
api_enums::LockerChoice::Tartarus => jwekey.rust_locker_encryption_key.as_bytes(),
};
let jwe_encrypted = encryption::encrypt_jwe(&payload, public_key)
.await
@ -272,6 +284,7 @@ pub async fn mk_add_locker_request_hs<'a>(
#[cfg(feature = "kms")] jwekey: &settings::ActiveKmsSecrets,
locker: &settings::Locker,
payload: &StoreLockerReq<'a>,
locker_choice: api_enums::LockerChoice,
) -> CustomResult<services::Request, errors::VaultError> {
let payload = utils::Encode::<StoreCardReq<'_>>::encode_to_vec(&payload)
.change_context(errors::VaultError::RequestEncodingFailed)?;
@ -286,11 +299,14 @@ pub async fn mk_add_locker_request_hs<'a>(
.await
.change_context(errors::VaultError::RequestEncodingFailed)?;
let jwe_payload = mk_basilisk_req(jwekey, &jws).await?;
let jwe_payload = mk_basilisk_req(jwekey, &jws, locker_choice).await?;
let body = utils::Encode::<encryption::JweBody>::encode_to_value(&jwe_payload)
.change_context(errors::VaultError::RequestEncodingFailed)?;
let mut url = locker.host.to_owned();
let mut url = match locker_choice {
api_enums::LockerChoice::Basilisk => locker.host.to_owned(),
api_enums::LockerChoice::Tartarus => locker.host_rs.to_owned(),
};
url.push_str("/cards/add");
let mut request = services::Request::new(services::Method::Post, &url);
request.add_header(headers::CONTENT_TYPE, "application/json".into());
@ -432,7 +448,7 @@ pub async fn mk_get_card_request_hs(
.await
.change_context(errors::VaultError::RequestEncodingFailed)?;
let jwe_payload = mk_basilisk_req(jwekey, &jws).await?;
let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?;
let body = utils::Encode::<encryption::JweBody>::encode_to_value(&jwe_payload)
.change_context(errors::VaultError::RequestEncodingFailed)?;
@ -512,7 +528,7 @@ pub async fn mk_delete_card_request_hs(
.await
.change_context(errors::VaultError::RequestEncodingFailed)?;
let jwe_payload = mk_basilisk_req(jwekey, &jws).await?;
let jwe_payload = mk_basilisk_req(jwekey, &jws, api_enums::LockerChoice::Basilisk).await?;
let body = utils::Encode::<encryption::JweBody>::encode_to_value(&jwe_payload)
.change_context(errors::VaultError::RequestEncodingFailed)?;

View File

@ -152,6 +152,7 @@ pub async fn save_payout_data_to_locker(
card_isin: None,
nick_name: None,
},
card_reference: None,
});
(
payload,
@ -195,7 +196,12 @@ pub async fn save_payout_data_to_locker(
}
};
// Store payout method in locker
let stored_resp = cards::call_to_locker_hs(state, &locker_req, &payout_attempt.customer_id)
let stored_resp = cards::call_to_locker_hs(
state,
&locker_req,
&payout_attempt.customer_id,
api_enums::LockerChoice::Basilisk,
)
.await
.change_context(errors::ApiErrorResponse::InternalServerError)?;

View File

@ -145,6 +145,7 @@ pub fn mk_app(
.service(routes::Disputes::server(state.clone()))
.service(routes::Analytics::server(state.clone()))
.service(routes::Routing::server(state.clone()))
.service(routes::LockerMigrate::server(state.clone()))
.service(routes::Gsm::server(state.clone()))
.service(routes::User::server(state.clone()))
}

View File

@ -29,6 +29,7 @@ pub mod user;
pub mod verification;
pub mod webhooks;
pub mod locker_migration;
#[cfg(feature = "dummy_connector")]
pub use self::app::DummyConnector;
#[cfg(feature = "payouts")]
@ -39,8 +40,8 @@ pub use self::app::Routing;
pub use self::app::Verify;
pub use self::app::{
ApiKeys, AppState, BusinessProfile, Cache, Cards, Configs, Customers, Disputes, EphemeralKey,
Files, Gsm, Health, Mandates, MerchantAccount, MerchantConnectorAccount, PaymentLink,
PaymentMethods, Payments, Refunds, User, Webhooks,
Files, Gsm, Health, LockerMigrate, Mandates, MerchantAccount, MerchantConnectorAccount,
PaymentLink, PaymentMethods, Payments, Refunds, User, Webhooks,
};
#[cfg(feature = "stripe")]
pub use super::compatibility::stripe::StripeApis;

View File

@ -19,7 +19,7 @@ use super::routing as cloud_routing;
#[cfg(all(feature = "olap", feature = "kms"))]
use super::verification::{apple_pay_merchant_registration, retrieve_apple_pay_verified_domains};
#[cfg(feature = "olap")]
use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, user::*};
use super::{admin::*, api_keys::*, disputes::*, files::*, gsm::*, locker_migration, user::*};
use super::{cache::*, health::*, payment_link::*};
#[cfg(any(feature = "olap", feature = "oltp"))]
use super::{configs::*, customers::*, mandates::*, payments::*, refunds::*};
@ -743,3 +743,16 @@ impl User {
.service(web::resource("/v2/signup").route(web::post().to(user_connect_account)))
}
}
pub struct LockerMigrate;
#[cfg(feature = "olap")]
impl LockerMigrate {
pub fn server(state: AppState) -> Scope {
web::scope("locker_migration/{merchant_id}")
.app_data(web::Data::new(state))
.service(
web::resource("").route(web::post().to(locker_migration::rust_locker_migration)),
)
}
}

View File

@ -23,6 +23,7 @@ pub enum ApiIdentifier {
ApiKeys,
PaymentLink,
Routing,
RustLockerMigration,
Gsm,
User,
}
@ -131,6 +132,7 @@ impl From<Flow> for ApiIdentifier {
Flow::Verification => Self::Verification,
Flow::PaymentLinkInitiate | Flow::PaymentLinkRetrieve => Self::PaymentLink,
Flow::RustLockerMigration => Self::RustLockerMigration,
Flow::GsmRuleCreate
| Flow::GsmRuleRetrieve
| Flow::GsmRuleUpdate

View File

@ -0,0 +1,27 @@
use actix_web::{web, HttpRequest, HttpResponse};
use router_env::Flow;
use super::AppState;
use crate::{
core::{api_locking, locker_migration},
services::{api, authentication as auth},
};
pub async fn rust_locker_migration(
state: web::Data<AppState>,
req: HttpRequest,
path: web::Path<String>,
) -> HttpResponse {
let flow = Flow::RustLockerMigration;
let merchant_id = path.into_inner();
api::server_wrap(
flow,
state,
&req,
&merchant_id,
|state, _, _| locker_migration::rust_locker_migration(state, &merchant_id),
&auth::AdminApiAuth,
api_locking::LockAction::NotApplicable,
)
.await
}

View File

@ -235,6 +235,8 @@ pub enum Flow {
BusinessProfileList,
/// Different verification flows
Verification,
/// Rust locker migration
RustLockerMigration,
/// Gsm Rule Creation flow
GsmRuleCreate,
/// Gsm Rule Retrieve flow

View File

@ -30,6 +30,7 @@ jwt_secret = "secret"
[locker]
host = ""
host_rs = ""
mock_locker = true
basilisk_host = ""
@ -48,6 +49,7 @@ locker_encryption_key2 = ""
locker_decryption_key1 = ""
locker_decryption_key2 = ""
vault_encryption_key = ""
rust_locker_encryption_key = ""
vault_private_key = ""
[webhooks]