refactor: extract email validation and PII utils to common_utils crate (#72)

This commit is contained in:
Sanchith Hegde
2022-12-06 15:19:46 +05:30
committed by GitHub
parent ab5988e6ba
commit cbbba37909
12 changed files with 202 additions and 156 deletions

View File

@ -40,7 +40,7 @@ pub struct Card {
}
impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest {
type Error = error_stack::Report<errors::ValidateError>;
type Error = error_stack::Report<errors::ConnectorError>;
fn try_from(item: &types::PaymentsAuthorizeRouterData) -> Result<Self, Self::Error> {
match item.request.payment_method_data {
api::PaymentMethod::Card(ref ccard) => {
@ -62,7 +62,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for BraintreePaymentsRequest {
transaction: braintree_payment_request,
})
}
_ => Err(errors::ValidateError.into()),
_ => Err(errors::ConnectorError::RequestEncodingFailed.into()),
}
}
}

View File

@ -5,7 +5,7 @@ pub(crate) mod utils;
use std::fmt::Display;
use actix_web::{body::BoxBody, http::StatusCode, HttpResponse, ResponseError};
pub use common_utils::errors::{CustomResult, ParsingError};
pub use common_utils::errors::{CustomResult, ParsingError, ValidationError};
use config::ConfigError;
use error_stack;
pub use redis_interface::errors::RedisError;
@ -409,16 +409,6 @@ error_to_process_tracker_error!(
ProcessTrackerError::EValidationError(error_stack::Report<ValidationError>)
);
#[derive(Debug, thiserror::Error)]
pub enum ValidationError {
#[error("Missing required field: {field_name}")]
MissingRequiredField { field_name: String },
#[error("Incorrect value provided for field: {field_name}")]
IncorrectValueProvided { field_name: &'static str },
#[error("{message}")]
InvalidValue { message: String },
}
#[derive(Debug, thiserror::Error)]
pub enum WebhooksFlowError {
#[error("Merchant webhook config not found")]

View File

@ -27,7 +27,6 @@ pub mod core;
pub mod cors;
pub mod db;
pub mod env;
pub mod pii;
pub mod routes;
pub mod scheduler;
@ -61,6 +60,14 @@ pub mod headers {
pub const X_API_VERSION: &str = "X-ApiVersion";
}
pub mod pii {
//! Personal Identifiable Information protection.
pub(crate) use common_utils::pii::{CardNumber, Email, IpAddress};
#[doc(inline)]
pub use masking::*;
}
pub fn mk_app(
state: AppState,
request_body_limit: usize,

View File

@ -1,160 +0,0 @@
//!
//! Personal Identifiable Information protection.
//!
use std::{convert::AsRef, fmt};
#[doc(inline)]
pub use masking::*;
use crate::utils::validate_email;
pub struct CardNumber;
impl<T> Strategy<T> for CardNumber
where
T: AsRef<str>,
{
fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let val_str: &str = val.as_ref();
if val_str.len() < 15 && val_str.len() > 19 {
return WithType::fmt(val, f);
}
write!(f, "{}{}", &val_str[..6], "*".repeat(val_str.len() - 6))
}
}
//pub struct PhoneNumber;
//impl<T> Strategy<T> for PhoneNumber
//where
//T: AsRef<str>,
//{
//fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
//let val_str: &str = val.as_ref();
//if val_str.len() < 10 || val_str.len() > 12 {
//return WithType::fmt(val, f);
//}
//f.write_str(&format!(
//"{}{}{}",
//&val_str[..2],
//"*".repeat(val_str.len() - 5),
//&val_str[(val_str.len() - 3)..]
//))
//}
//}
pub struct Email;
impl<T> Strategy<T> for Email
where
T: AsRef<str>,
{
fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let val_str: &str = val.as_ref();
let is_valid = validate_email(val_str);
if is_valid.is_err() {
return WithType::fmt(val, f);
}
if let Some((a, b)) = val_str.split_once('@') {
write!(f, "{}@{}", "*".repeat(a.len()), b)
} else {
WithType::fmt(val, f)
}
}
}
pub struct IpAddress;
impl<T> Strategy<T> for IpAddress
where
T: AsRef<str>,
{
fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let val_str: &str = val.as_ref();
let segments: Vec<&str> = val_str.split('.').collect();
if segments.len() != 4 {
return WithType::fmt(val, f);
}
for seg in segments.iter() {
if seg.is_empty() || seg.len() > 3 {
return WithType::fmt(val, f);
}
}
write!(f, "{}.**.**.**", segments[0])
}
}
#[cfg(test)]
mod pii_masking_strategy_tests {
use super::{CardNumber, Email, IpAddress, Secret};
#[test]
fn test_valid_card_number_masking() {
let secret: Secret<String, CardNumber> = Secret::new("1234567890987654".to_string());
assert_eq!("123456**********", &format!("{:?}", secret));
}
#[test]
fn test_invalid_card_number_masking() {
let secret: Secret<String, CardNumber> = Secret::new("1234567890".to_string());
assert_eq!("123456****", &format!("{:?}", secret));
}
/* #[test]
fn test_valid_phone_number_masking() {
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299".to_string());
assert_eq!("99*****299", &format!("{}", secret));
}
#[test]
fn test_invalid_phone_number_masking() {
let secret: Secret<String, PhoneNumber> = Secret::new("99229922".to_string());
assert_eq!("*** alloc::string::String ***", &format!("{}", secret));
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299229922".to_string());
assert_eq!("*** alloc::string::String ***", &format!("{}", secret));
} */
#[test]
fn test_valid_email_masking() {
let secret: Secret<String, Email> = Secret::new("myemail@gmail.com".to_string());
assert_eq!("*******@gmail.com", &format!("{:?}", secret));
}
#[test]
fn test_invalid_email_masking() {
let secret: Secret<String, Email> = Secret::new("myemailgmail.com".to_string());
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
let secret: Secret<String, Email> = Secret::new("myemail@gmail@com".to_string());
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
}
#[test]
fn test_valid_ip_addr_masking() {
let secret: Secret<String, IpAddress> = Secret::new("123.23.1.78".to_string());
assert_eq!("123.**.**.**", &format!("{:?}", secret));
}
#[test]
fn test_invalid_ip_addr_masking() {
let secret: Secret<String, IpAddress> = Secret::new("123.4.56".to_string());
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
let secret: Secret<String, IpAddress> = Secret::new("123.4567.12.4".to_string());
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
let secret: Secret<String, IpAddress> = Secret::new("123..4.56".to_string());
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
}
}

View File

@ -5,11 +5,14 @@ mod fp_utils;
#[cfg(feature = "kv_store")]
pub(crate) mod storage_partitioning;
pub(crate) use common_utils::ext_traits::{ByteSliceExt, BytesExt, Encode, StringExt, ValueExt};
pub(crate) use common_utils::{
ext_traits::{ByteSliceExt, BytesExt, Encode, StringExt, ValueExt},
validation::validate_email,
};
use nanoid::nanoid;
pub(crate) use self::{
ext_traits::{validate_address, validate_email, OptionExt, ValidateCall},
ext_traits::{validate_address, OptionExt, ValidateCall},
fp_utils::when,
};
use crate::consts;

View File

@ -1,11 +1,8 @@
use common_utils::ext_traits::ValueExt;
use error_stack::{report, IntoReport, Report, ResultExt};
use once_cell::sync::Lazy;
use regex::Regex;
use crate::{
core::errors::{self, ApiErrorResponse, CustomResult, RouterResult},
logger,
types::api::AddressDetails,
utils::when,
};
@ -132,42 +129,6 @@ where
}
}
pub fn validate_email(email: &str) -> CustomResult<(), errors::ValidationError> {
#[deny(clippy::invalid_regex)]
static EMAIL_REGEX: Lazy<Option<Regex>> = Lazy::new(|| {
match Regex::new(
r"^(?i)[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$",
) {
Ok(regex) => Some(regex),
Err(error) => {
logger::error!(?error);
None
}
}
});
let email_regex = match EMAIL_REGEX.as_ref() {
Some(regex) => Ok(regex),
None => Err(report!(errors::ValidationError::InvalidValue {
message: "Invalid regex expression".into()
})),
}?;
const EMAIL_MAX_LENGTH: usize = 319;
if email.is_empty() || email.chars().count() > EMAIL_MAX_LENGTH {
return Err(report!(errors::ValidationError::InvalidValue {
message: "Email address is either empty or exceeds maximum allowed length".into()
}));
}
if !email_regex.is_match(email) {
return Err(report!(errors::ValidationError::InvalidValue {
message: "Invalid email address format".into()
}));
}
Ok(())
}
pub fn validate_address(address: &serde_json::Value) -> CustomResult<(), errors::ValidationError> {
if let Err(err) = serde_json::from_value::<AddressDetails>(address.clone()) {
return Err(report!(errors::ValidationError::InvalidValue {
@ -176,62 +137,3 @@ pub fn validate_address(address: &serde_json::Value) -> CustomResult<(), errors:
}
Ok(())
}
#[cfg(test)]
mod tests {
use fake::{faker::internet::en::SafeEmail, Fake};
use proptest::{
prop_assert,
strategy::{Just, NewTree, Strategy},
test_runner::TestRunner,
};
use super::*;
#[derive(Debug)]
struct ValidEmail;
impl Strategy for ValidEmail {
type Tree = Just<String>;
type Value = String;
fn new_tree(&self, _runner: &mut TestRunner) -> NewTree<Self> {
Ok(Just(SafeEmail().fake()))
}
}
#[test]
fn test_validate_email() {
let result = validate_email("abc@example.com");
assert!(result.is_ok());
let result = validate_email("abc+123@example.com");
assert!(result.is_ok());
let result = validate_email("");
assert!(result.is_err());
}
proptest::proptest! {
/// Example of unit test
/// Kind of test: output-based testing
#[test]
fn proptest_valid_fake_email(email in ValidEmail) {
prop_assert!(validate_email(&email).is_ok());
}
/// Example of unit test
/// Kind of test: output-based testing
#[test]
fn proptest_invalid_data_email(email in "\\PC*") {
prop_assert!(validate_email(&email).is_err());
}
// TODO: make maybe unit test working
// minimal failing input: email = "+@a"
// #[test]
// fn proptest_invalid_email(email in "[.+]@(.+)") {
// prop_assert!(validate_email(&email).is_err());
// }
}
}