From ae2a34e02cd8bc0ab2e213c18953583046b17241 Mon Sep 17 00:00:00 2001 From: Sandeep Kumar <83278309+tsdk02@users.noreply.github.com> Date: Fri, 5 Jul 2024 20:36:59 +0530 Subject: [PATCH] feat(events): add hashed customer_email and feature_metadata (#5220) Co-authored-by: hyperswitch-bot[bot] <148525504+hyperswitch-bot[bot]@users.noreply.github.com> --- Cargo.lock | 2 ++ api-reference/openapi_spec.json | 8 ++++++ config/deployments/env_specific.toml | 1 + crates/analytics/src/search.rs | 26 +++++++++++++++++-- crates/api_models/src/analytics/search.rs | 3 ++- crates/api_models/src/payments.rs | 5 +++- crates/common_utils/Cargo.toml | 2 ++ crates/common_utils/src/hashing.rs | 22 ++++++++++++++++ crates/common_utils/src/lib.rs | 2 ++ crates/common_utils/src/pii.rs | 3 ++- crates/router/src/core/payments.rs | 2 ++ .../src/services/kafka/payment_intent.rs | 21 ++++++++++++--- .../services/kafka/payment_intent_event.rs | 21 ++++++++++++--- 13 files changed, 105 insertions(+), 13 deletions(-) create mode 100644 crates/common_utils/src/hashing.rs diff --git a/Cargo.lock b/Cargo.lock index d0b9894970..d40d7e2ee0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1577,6 +1577,7 @@ dependencies = [ "cc", "cfg-if 1.0.0", "constant_time_eq 0.3.0", + "serde", ] [[package]] @@ -1958,6 +1959,7 @@ name = "common_utils" version = "0.1.0" dependencies = [ "async-trait", + "blake3", "bytes 1.6.0", "common_enums", "diesel", diff --git a/api-reference/openapi_spec.json b/api-reference/openapi_spec.json index c7831f179b..929952e8c1 100644 --- a/api-reference/openapi_spec.json +++ b/api-reference/openapi_spec.json @@ -9485,6 +9485,14 @@ } ], "nullable": true + }, + "search_tags": { + "allOf": [ + { + "$ref": "#/components/schemas/RedirectResponse" + } + ], + "nullable": true } } }, diff --git a/config/deployments/env_specific.toml b/config/deployments/env_specific.toml index 7d32e76928..67f32396b7 100644 --- a/config/deployments/env_specific.toml +++ b/config/deployments/env_specific.toml @@ -82,6 +82,7 @@ audit_events_topic = "topic" # Kafka topic to be used for Payment payout_analytics_topic = "topic" # Kafka topic to be used for Payouts and PayoutAttempt events consolidated_events_topic = "topic" # Kafka topic to be used for Consolidated events authentication_analytics_topic = "topic" # Kafka topic to be used for Authentication events +fraud_check_analytics_topic = "topic" # Kafka topic to be used for Fraud Check events # File storage configuration [file_storage] diff --git a/crates/analytics/src/search.rs b/crates/analytics/src/search.rs index 1237db061b..269864bf44 100644 --- a/crates/analytics/src/search.rs +++ b/crates/analytics/src/search.rs @@ -51,7 +51,18 @@ pub async fn msearch_results( if let Some(customer_email) = filters.customer_email { if !customer_email.is_empty() { query_builder - .add_filter_clause("customer_email.keyword".to_string(), customer_email.clone()) + .add_filter_clause( + "customer_email.keyword".to_string(), + customer_email + .iter() + .filter_map(|email| { + // TODO: Add trait based inputs instead of converting this to strings + serde_json::to_value(email) + .ok() + .and_then(|a| a.as_str().map(|a| a.to_string())) + }) + .collect(), + ) .switch()?; } }; @@ -147,7 +158,18 @@ pub async fn search_results( if let Some(customer_email) = filters.customer_email { if !customer_email.is_empty() { query_builder - .add_filter_clause("customer_email.keyword".to_string(), customer_email.clone()) + .add_filter_clause( + "customer_email.keyword".to_string(), + customer_email + .iter() + .filter_map(|email| { + // TODO: Add trait based inputs instead of converting this to strings + serde_json::to_value(email) + .ok() + .and_then(|a| a.as_str().map(|a| a.to_string())) + }) + .collect(), + ) .switch()?; } }; diff --git a/crates/api_models/src/analytics/search.rs b/crates/api_models/src/analytics/search.rs index b2af4f6759..034a2a9435 100644 --- a/crates/api_models/src/analytics/search.rs +++ b/crates/api_models/src/analytics/search.rs @@ -1,3 +1,4 @@ +use common_utils::hashing::HashedString; use serde_json::Value; #[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] @@ -5,7 +6,7 @@ pub struct SearchFilters { pub payment_method: Option>, pub currency: Option>, pub status: Option>, - pub customer_email: Option>, + pub customer_email: Option>>, } #[derive(Clone, Debug, serde::Deserialize, serde::Serialize)] diff --git a/crates/api_models/src/payments.rs b/crates/api_models/src/payments.rs index 31f75c05c3..97fc20d944 100644 --- a/crates/api_models/src/payments.rs +++ b/crates/api_models/src/payments.rs @@ -4821,8 +4821,11 @@ pub struct PaymentsStartRequest { #[derive(Debug, Clone, serde::Deserialize, serde::Serialize, ToSchema)] pub struct FeatureMetadata { /// Redirection response coming in request as metadata field only for redirection scenarios - #[schema(value_type = Option)] pub redirect_response: Option, + // TODO: Convert this to hashedstrings to avoid PII sensitive data + /// Additional tags to be used for global search + #[schema(value_type = Option)] + pub search_tags: Option>>, } ///frm message is an object sent inside the payments response...when frm is invoked, its value is Some(...), else its None diff --git a/crates/common_utils/Cargo.toml b/crates/common_utils/Cargo.toml index 58cdf447fd..568dc00a97 100644 --- a/crates/common_utils/Cargo.toml +++ b/crates/common_utils/Cargo.toml @@ -44,6 +44,8 @@ tokio = { version = "1.37.0", features = ["macros", "rt-multi-thread"], optional url = { version = "2.5.0", features = ["serde"] } utoipa = { version = "4.2.0", features = ["preserve_order", "preserve_path_order"] } uuid = { version = "1.8.0", features = ["v7"] } +blake3 = { version = "1.5.1", features = ["serde"] } + # First party crates rusty-money = { git = "https://github.com/varunsrin/rusty_money", rev = "bbc0150742a0fff905225ff11ee09388e9babdcc", features = ["iso", "crypto"] } diff --git a/crates/common_utils/src/hashing.rs b/crates/common_utils/src/hashing.rs new file mode 100644 index 0000000000..d08cd9f086 --- /dev/null +++ b/crates/common_utils/src/hashing.rs @@ -0,0 +1,22 @@ +use masking::{PeekInterface, Secret, Strategy}; +use serde::{Deserialize, Serialize, Serializer}; + +#[derive(Clone, Debug, Deserialize)] +/// Represents a hashed string using blake3's hashing strategy. +pub struct HashedString>(Secret); + +impl> Serialize for HashedString { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + let hashed_value = blake3::hash(self.0.peek().as_bytes()).to_hex(); + hashed_value.serialize(serializer) + } +} + +impl> From> for HashedString { + fn from(value: Secret) -> Self { + Self(value) + } +} diff --git a/crates/common_utils/src/lib.rs b/crates/common_utils/src/lib.rs index 8e305dd3d6..d1449f2a7d 100644 --- a/crates/common_utils/src/lib.rs +++ b/crates/common_utils/src/lib.rs @@ -31,6 +31,8 @@ pub mod static_cache; pub mod types; pub mod validation; +/// Used for hashing +pub mod hashing; #[cfg(feature = "metrics")] pub mod metrics; diff --git a/crates/common_utils/src/pii.rs b/crates/common_utils/src/pii.rs index 2921b23717..9d4b96200b 100644 --- a/crates/common_utils/src/pii.rs +++ b/crates/common_utils/src/pii.rs @@ -14,6 +14,7 @@ use error_stack::ResultExt; use masking::{ExposeInterface, Secret, Strategy, WithType}; #[cfg(feature = "logs")] use router_env::logger; +use serde::Deserialize; use crate::{ crypto::Encryptable, @@ -205,7 +206,7 @@ where } /// Strategy for masking Email -#[derive(Debug)] +#[derive(Debug, Copy, Clone, Deserialize)] pub enum EmailStrategy {} impl Strategy for EmailStrategy diff --git a/crates/router/src/core/payments.rs b/crates/router/src/core/payments.rs index 4af5a2ed30..18e62b9c1c 100644 --- a/crates/router/src/core/payments.rs +++ b/crates/router/src/core/payments.rs @@ -944,6 +944,7 @@ impl PaymentRedirectFlow for PaymentRedirectCompleteAuthorize { param: req.param.map(Secret::new), json_payload: Some(req.json_payload.unwrap_or(serde_json::json!({})).into()), }), + search_tags: None, }), ..Default::default() }; @@ -1230,6 +1231,7 @@ impl PaymentRedirectFlow for PaymentAuthenticateCompleteAuthorize { req.json_payload.unwrap_or(serde_json::json!({})).into(), ), }), + search_tags: None, }), ..Default::default() }; diff --git a/crates/router/src/services/kafka/payment_intent.rs b/crates/router/src/services/kafka/payment_intent.rs index 07af1cf23d..9711db75cf 100644 --- a/crates/router/src/services/kafka/payment_intent.rs +++ b/crates/router/src/services/kafka/payment_intent.rs @@ -1,7 +1,8 @@ -use common_utils::{crypto::Encryptable, id_type, types::MinorUnit}; +use common_utils::{crypto::Encryptable, hashing::HashedString, id_type, pii, types::MinorUnit}; use diesel_models::enums as storage_enums; use hyperswitch_domain_models::payments::PaymentIntent; -use masking::Secret; +use masking::{PeekInterface, Secret}; +use serde_json::Value; use time::OffsetDateTime; #[derive(serde::Serialize, Debug)] @@ -33,7 +34,10 @@ pub struct KafkaPaymentIntent<'a> { pub business_label: Option<&'a String>, pub attempt_count: i16, pub payment_confirm_source: Option, - pub billing_details: Option>>, + pub customer_email: Option>, + pub feature_metadata: Option<&'a Value>, + pub merchant_order_reference_id: Option<&'a String>, + pub billing_details: Option>>, } impl<'a> KafkaPaymentIntent<'a> { @@ -63,7 +67,16 @@ impl<'a> KafkaPaymentIntent<'a> { business_label: intent.business_label.as_ref(), attempt_count: intent.attempt_count, payment_confirm_source: intent.payment_confirm_source, - billing_details: intent.billing_details.clone(), + customer_email: intent + .customer_details + .as_ref() + .and_then(|value| value.get_inner().peek().as_object()) + .and_then(|obj| obj.get("email")) + .and_then(|email| email.as_str()) + .map(|email| HashedString::from(Secret::new(email.to_string()))), + feature_metadata: intent.feature_metadata.as_ref(), + merchant_order_reference_id: intent.merchant_order_reference_id.as_ref(), + billing_details: None, } } } diff --git a/crates/router/src/services/kafka/payment_intent_event.rs b/crates/router/src/services/kafka/payment_intent_event.rs index 9e8c2dbff7..a8ffc22e33 100644 --- a/crates/router/src/services/kafka/payment_intent_event.rs +++ b/crates/router/src/services/kafka/payment_intent_event.rs @@ -1,7 +1,8 @@ -use common_utils::{crypto::Encryptable, id_type, types::MinorUnit}; +use common_utils::{crypto::Encryptable, hashing::HashedString, id_type, pii, types::MinorUnit}; use diesel_models::enums as storage_enums; use hyperswitch_domain_models::payments::PaymentIntent; -use masking::Secret; +use masking::{PeekInterface, Secret}; +use serde_json::Value; use time::OffsetDateTime; #[serde_with::skip_serializing_none] @@ -34,7 +35,10 @@ pub struct KafkaPaymentIntentEvent<'a> { pub business_label: Option<&'a String>, pub attempt_count: i16, pub payment_confirm_source: Option, - pub billing_details: Option>>, + pub customer_email: Option>, + pub feature_metadata: Option<&'a Value>, + pub merchant_order_reference_id: Option<&'a String>, + pub billing_details: Option>>, } impl<'a> KafkaPaymentIntentEvent<'a> { @@ -64,7 +68,16 @@ impl<'a> KafkaPaymentIntentEvent<'a> { business_label: intent.business_label.as_ref(), attempt_count: intent.attempt_count, payment_confirm_source: intent.payment_confirm_source, - billing_details: intent.billing_details.clone(), + customer_email: intent + .customer_details + .as_ref() + .and_then(|value| value.get_inner().peek().as_object()) + .and_then(|obj| obj.get("email")) + .and_then(|email| email.as_str()) + .map(|email| HashedString::from(Secret::new(email.to_string()))), + feature_metadata: intent.feature_metadata.as_ref(), + merchant_order_reference_id: intent.merchant_order_reference_id.as_ref(), + billing_details: None, } } }