mirror of
https://github.com/juspay/hyperswitch.git
synced 2025-10-30 09:38:33 +08:00
refactor(masking): PII improvements (#77)
This commit is contained in:
7
Cargo.lock
generated
7
Cargo.lock
generated
@ -1829,6 +1829,7 @@ dependencies = [
|
|||||||
"diesel",
|
"diesel",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"subtle",
|
||||||
"zeroize",
|
"zeroize",
|
||||||
]
|
]
|
||||||
|
|
||||||
@ -3066,6 +3067,12 @@ dependencies = [
|
|||||||
"syn",
|
"syn",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "subtle"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "supports-color"
|
name = "supports-color"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
|
|||||||
@ -21,11 +21,7 @@ where
|
|||||||
return WithType::fmt(val, f);
|
return WithType::fmt(val, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
f.write_str(&format!(
|
write!(f, "{}{}", &val_str[..6], "*".repeat(val_str.len() - 6))
|
||||||
"{}{}",
|
|
||||||
&val_str[..6],
|
|
||||||
"*".repeat(val_str.len() - 6)
|
|
||||||
))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -45,12 +41,13 @@ where
|
|||||||
return WithType::fmt(val, f);
|
return WithType::fmt(val, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
f.write_str(&format!(
|
write!(
|
||||||
|
f,
|
||||||
"{}{}{}",
|
"{}{}{}",
|
||||||
&val_str[..2],
|
&val_str[..2],
|
||||||
"*".repeat(val_str.len() - 5),
|
"*".repeat(val_str.len() - 5),
|
||||||
&val_str[(val_str.len() - 3)..]
|
&val_str[(val_str.len() - 3)..]
|
||||||
))
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
@ -71,12 +68,11 @@ where
|
|||||||
return WithType::fmt(val, f);
|
return WithType::fmt(val, f);
|
||||||
}
|
}
|
||||||
|
|
||||||
let parts: Vec<&str> = val_str.split('@').collect();
|
if let Some((a, b)) = val_str.split_once('@') {
|
||||||
if parts.len() != 2 {
|
write!(f, "{}@{}", "*".repeat(a.len()), b)
|
||||||
return WithType::fmt(val, f);
|
} else {
|
||||||
|
WithType::fmt(val, f)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.write_str(&format!("{}@{}", "*".repeat(parts[0].len()), parts[1]))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,7 +98,7 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
f.write_str(&format!("{}.**.**.**", segments[0]))
|
write!(f, "{}.**.**.**", segments[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -115,62 +111,62 @@ mod pii_masking_strategy_tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_valid_card_number_masking() {
|
fn test_valid_card_number_masking() {
|
||||||
let secret: Secret<String, CardNumber> = Secret::new("1234567890987654".to_string());
|
let secret: Secret<String, CardNumber> = Secret::new("1234567890987654".to_string());
|
||||||
assert_eq!("123456**********", &format!("{:?}", secret));
|
assert_eq!("123456**********", format!("{:?}", secret));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_card_number_masking() {
|
fn test_invalid_card_number_masking() {
|
||||||
let secret: Secret<String, CardNumber> = Secret::new("1234567890".to_string());
|
let secret: Secret<String, CardNumber> = Secret::new("1234567890".to_string());
|
||||||
assert_eq!("123456****", &format!("{:?}", secret));
|
assert_eq!("123456****", format!("{:?}", secret));
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
/*
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_phone_number_masking() {
|
fn test_valid_phone_number_masking() {
|
||||||
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299".to_string());
|
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299".to_string());
|
||||||
assert_eq!("99*****299", &format!("{}", secret));
|
assert_eq!("99*****299", format!("{}", secret));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_phone_number_masking() {
|
fn test_invalid_phone_number_masking() {
|
||||||
let secret: Secret<String, PhoneNumber> = Secret::new("99229922".to_string());
|
let secret: Secret<String, PhoneNumber> = Secret::new("99229922".to_string());
|
||||||
assert_eq!("*** alloc::string::String ***", &format!("{}", secret));
|
assert_eq!("*** alloc::string::String ***", format!("{}", secret));
|
||||||
|
|
||||||
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299229922".to_string());
|
let secret: Secret<String, PhoneNumber> = Secret::new("9922992299229922".to_string());
|
||||||
assert_eq!("*** alloc::string::String ***", &format!("{}", secret));
|
assert_eq!("*** alloc::string::String ***", format!("{}", secret));
|
||||||
}
|
}
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_email_masking() {
|
fn test_valid_email_masking() {
|
||||||
let secret: Secret<String, Email> = Secret::new("myemail@gmail.com".to_string());
|
let secret: Secret<String, Email> = Secret::new("myemail@gmail.com".to_string());
|
||||||
assert_eq!("*******@gmail.com", &format!("{:?}", secret));
|
assert_eq!("*******@gmail.com", format!("{:?}", secret));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_email_masking() {
|
fn test_invalid_email_masking() {
|
||||||
let secret: Secret<String, Email> = Secret::new("myemailgmail.com".to_string());
|
let secret: Secret<String, Email> = Secret::new("myemailgmail.com".to_string());
|
||||||
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
|
assert_eq!("*** alloc::string::String ***", format!("{:?}", secret));
|
||||||
|
|
||||||
let secret: Secret<String, Email> = Secret::new("myemail@gmail@com".to_string());
|
let secret: Secret<String, Email> = Secret::new("myemail@gmail@com".to_string());
|
||||||
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
|
assert_eq!("*** alloc::string::String ***", format!("{:?}", secret));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_ip_addr_masking() {
|
fn test_valid_ip_addr_masking() {
|
||||||
let secret: Secret<String, IpAddress> = Secret::new("123.23.1.78".to_string());
|
let secret: Secret<String, IpAddress> = Secret::new("123.23.1.78".to_string());
|
||||||
assert_eq!("123.**.**.**", &format!("{:?}", secret));
|
assert_eq!("123.**.**.**", format!("{:?}", secret));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_ip_addr_masking() {
|
fn test_invalid_ip_addr_masking() {
|
||||||
let secret: Secret<String, IpAddress> = Secret::new("123.4.56".to_string());
|
let secret: Secret<String, IpAddress> = Secret::new("123.4.56".to_string());
|
||||||
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
|
assert_eq!("*** alloc::string::String ***", format!("{:?}", secret));
|
||||||
|
|
||||||
let secret: Secret<String, IpAddress> = Secret::new("123.4567.12.4".to_string());
|
let secret: Secret<String, IpAddress> = Secret::new("123.4567.12.4".to_string());
|
||||||
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
|
assert_eq!("*** alloc::string::String ***", format!("{:?}", secret));
|
||||||
|
|
||||||
let secret: Secret<String, IpAddress> = Secret::new("123..4.56".to_string());
|
let secret: Secret<String, IpAddress> = Secret::new("123..4.56".to_string());
|
||||||
assert_eq!("*** alloc::string::String ***", &format!("{:?}", secret));
|
assert_eq!("*** alloc::string::String ***", format!("{:?}", secret));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -16,6 +16,7 @@ all-features = true
|
|||||||
rustdoc-args = ["--cfg", "docsrs"]
|
rustdoc-args = ["--cfg", "docsrs"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
subtle = "2.4.1"
|
||||||
bytes = { version = "1", optional = true }
|
bytes = { version = "1", optional = true }
|
||||||
diesel = { git = "https://github.com/juspay/diesel", features = ["postgres", "serde_json", "time"], optional = true, rev = "22f3f59f1db8a3f61623e4d6b375d64cd7bd3d02" }
|
diesel = { git = "https://github.com/juspay/diesel", features = ["postgres", "serde_json", "time"], optional = true, rev = "22f3f59f1db8a3f61623e4d6b375d64cd7bd3d02" }
|
||||||
serde = { version = "1", features = ["derive"], optional = true }
|
serde = { version = "1", features = ["derive"], optional = true }
|
||||||
|
|||||||
@ -4,6 +4,12 @@ Personal Identifiable Information protection.
|
|||||||
Wrapper types and traits for secret management which help ensure they aren't accidentally copied, logged, or otherwise exposed (as much as possible), and also ensure secrets are securely wiped from memory when dropped.
|
Wrapper types and traits for secret management which help ensure they aren't accidentally copied, logged, or otherwise exposed (as much as possible), and also ensure secrets are securely wiped from memory when dropped.
|
||||||
Secret-keeping library inspired by `secrecy`.
|
Secret-keeping library inspired by `secrecy`.
|
||||||
|
|
||||||
|
This solution has such advantages over alternatives:
|
||||||
|
- alternatives have not implemented several traits from the box which are needed
|
||||||
|
- alternatives do not have WeakSecret and Secret differentiation
|
||||||
|
- alternatives do not support masking strategies
|
||||||
|
- alternatives had several minor problems
|
||||||
|
|
||||||
## How to use
|
## How to use
|
||||||
|
|
||||||
To convert non-secret variable into secret use `new()`. Sample:
|
To convert non-secret variable into secret use `new()`. Sample:
|
||||||
@ -19,13 +25,13 @@ To get value from secret use `expose()`. Sample:
|
|||||||
last4_digits: Some(card_number.expose())
|
last4_digits: Some(card_number.expose())
|
||||||
```
|
```
|
||||||
|
|
||||||
Most fields are under `Option`. To simplify dealing with `Option`, use `expose_cloning()`. Sample:
|
Most fields are under `Option`. To simplify dealing with `Option`, use `expose_option()`. Sample:
|
||||||
|
|
||||||
```rust,ignore
|
```rust,ignore
|
||||||
card_info.push_str(
|
card_info.push_str(
|
||||||
&card_detail
|
&card_detail
|
||||||
.card_holder_name
|
.card_holder_name
|
||||||
.expose_cloning()
|
.expose_option()
|
||||||
.unwrap_or_default(),
|
.unwrap_or_default(),
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
@ -38,7 +44,6 @@ Most fields are under `Option`. To simplify dealing with `Option`, use `expose_c
|
|||||||
<!-- ```text
|
<!-- ```text
|
||||||
|
|
||||||
├── src : source code
|
├── src : source code
|
||||||
│ ├── bachstd : utilities
|
|
||||||
└── tests : unit and integration tests
|
└── tests : unit and integration tests
|
||||||
|
|
||||||
``` -->
|
``` -->
|
||||||
|
|||||||
@ -10,10 +10,10 @@ pub trait PeekInterface<S> {
|
|||||||
fn peek(&self) -> &S;
|
fn peek(&self) -> &S;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interface to expose a clone of secret
|
/// Interface that consumes a option secret and returns the value.
|
||||||
pub trait PeekOptionInterface<S> {
|
pub trait ExposeOptionInterface<S> {
|
||||||
/// Expose option.
|
/// Expose option.
|
||||||
fn peek_cloning(&self) -> S;
|
fn expose_option(self) -> S;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Interface that consumes a secret and returns the inner value.
|
/// Interface that consumes a secret and returns the inner value.
|
||||||
@ -22,23 +22,13 @@ pub trait ExposeInterface<S> {
|
|||||||
fn expose(self) -> S;
|
fn expose(self) -> S;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> PeekOptionInterface<Option<S>> for Option<Secret<S, I>>
|
impl<S, I> ExposeOptionInterface<Option<S>> for Option<Secret<S, I>>
|
||||||
where
|
where
|
||||||
S: Clone,
|
S: Clone,
|
||||||
I: crate::Strategy<S>,
|
I: crate::Strategy<S>,
|
||||||
{
|
{
|
||||||
fn peek_cloning(&self) -> Option<S> {
|
fn expose_option(self) -> Option<S> {
|
||||||
self.as_ref().map(|val| val.peek().clone())
|
self.map(ExposeInterface::expose)
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<S, I> PeekOptionInterface<S> for Secret<S, I>
|
|
||||||
where
|
|
||||||
S: Clone,
|
|
||||||
I: crate::Strategy<S>,
|
|
||||||
{
|
|
||||||
fn peek_cloning(&self) -> S {
|
|
||||||
self.peek().clone()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,13 +14,13 @@ use super::{PeekInterface, ZeroizableSecret};
|
|||||||
/// Because of the nature of how the `BytesMut` type works, it needs some special
|
/// Because of the nature of how the `BytesMut` type works, it needs some special
|
||||||
/// care in order to have a proper zeroizing drop handler.
|
/// care in order to have a proper zeroizing drop handler.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "bytes")))]
|
#[cfg_attr(docsrs, cfg(feature = "bytes"))]
|
||||||
pub struct SecretBytesMut(BytesMut);
|
pub struct SecretBytesMut(BytesMut);
|
||||||
|
|
||||||
impl SecretBytesMut {
|
impl SecretBytesMut {
|
||||||
/// Wrap bytes in `SecretBytesMut`
|
/// Wrap bytes in `SecretBytesMut`
|
||||||
pub fn new(bytes: impl Into<BytesMut>) -> SecretBytesMut {
|
pub fn new(bytes: impl Into<BytesMut>) -> Self {
|
||||||
SecretBytesMut(bytes.into())
|
Self(bytes.into())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -37,8 +37,8 @@ impl fmt::Debug for SecretBytesMut {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl From<BytesMut> for SecretBytesMut {
|
impl From<BytesMut> for SecretBytesMut {
|
||||||
fn from(bytes: BytesMut) -> SecretBytesMut {
|
fn from(bytes: BytesMut) -> Self {
|
||||||
SecretBytesMut::new(bytes)
|
Self::new(bytes)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg_hide))]
|
||||||
|
#![cfg_attr(docsrs, doc(cfg_hide(doc)))]
|
||||||
#![forbid(unsafe_code)]
|
#![forbid(unsafe_code)]
|
||||||
#![warn(
|
#![warn(
|
||||||
missing_docs,
|
missing_docs,
|
||||||
@ -11,7 +12,8 @@
|
|||||||
clippy::panicking_unwrap,
|
clippy::panicking_unwrap,
|
||||||
clippy::unreachable,
|
clippy::unreachable,
|
||||||
clippy::unwrap_in_result,
|
clippy::unwrap_in_result,
|
||||||
clippy::unwrap_used
|
clippy::unwrap_used,
|
||||||
|
clippy::use_self
|
||||||
)]
|
)]
|
||||||
|
|
||||||
//!
|
//!
|
||||||
@ -27,7 +29,7 @@ mod strategy;
|
|||||||
|
|
||||||
pub use strategy::{Strategy, WithType, WithoutType};
|
pub use strategy::{Strategy, WithType, WithoutType};
|
||||||
mod abs;
|
mod abs;
|
||||||
pub use abs::{ExposeInterface, PeekInterface, PeekOptionInterface};
|
pub use abs::{ExposeInterface, ExposeOptionInterface, PeekInterface};
|
||||||
|
|
||||||
mod secret;
|
mod secret;
|
||||||
mod strong_secret;
|
mod strong_secret;
|
||||||
@ -60,7 +62,7 @@ pub use crate::serde::{Deserialize, SerializableSecret, Serialize};
|
|||||||
/// `use masking::prelude::*;`
|
/// `use masking::prelude::*;`
|
||||||
///
|
///
|
||||||
pub mod prelude {
|
pub mod prelude {
|
||||||
pub use super::{ExposeInterface, PeekInterface, PeekOptionInterface};
|
pub use super::{ExposeInterface, ExposeOptionInterface, PeekInterface};
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "diesel")]
|
#[cfg(feature = "diesel")]
|
||||||
|
|||||||
@ -31,9 +31,7 @@ use crate::{strategy::Strategy, PeekInterface};
|
|||||||
/// T: fmt::Display
|
/// T: fmt::Display
|
||||||
/// {
|
/// {
|
||||||
/// fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
/// fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
/// f.write_str(
|
/// write!(f, "{}", val.to_string().to_ascii_lowercase())
|
||||||
/// &format!("{}", val).to_ascii_lowercase()
|
|
||||||
/// )
|
|
||||||
/// }
|
/// }
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
@ -42,93 +40,93 @@ use crate::{strategy::Strategy, PeekInterface};
|
|||||||
/// assert_eq!("hello", &format!("{:?}", my_secret));
|
/// assert_eq!("hello", &format!("{:?}", my_secret));
|
||||||
/// ```
|
/// ```
|
||||||
///
|
///
|
||||||
pub struct Secret<S, I = crate::WithType>
|
pub struct Secret<Secret, MaskingStrategy = crate::WithType>
|
||||||
where
|
where
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<Secret>,
|
||||||
{
|
{
|
||||||
/// Inner secret value
|
pub(crate) inner_secret: Secret,
|
||||||
pub(crate) inner_secret: S,
|
pub(crate) masking_strategy: PhantomData<MaskingStrategy>,
|
||||||
pub(crate) marker: PhantomData<I>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> Secret<S, I>
|
impl<SecretValue, MaskingStrategy> Secret<SecretValue, MaskingStrategy>
|
||||||
where
|
where
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<SecretValue>,
|
||||||
{
|
{
|
||||||
/// Take ownership of a secret value
|
/// Take ownership of a secret value
|
||||||
pub fn new(secret: S) -> Self {
|
pub fn new(secret: SecretValue) -> Self {
|
||||||
Secret {
|
Self {
|
||||||
inner_secret: secret,
|
inner_secret: secret,
|
||||||
marker: PhantomData,
|
masking_strategy: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> PeekInterface<S> for Secret<S, I>
|
impl<SecretValue, MaskingStrategy> PeekInterface<SecretValue>
|
||||||
|
for Secret<SecretValue, MaskingStrategy>
|
||||||
where
|
where
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<SecretValue>,
|
||||||
{
|
{
|
||||||
fn peek(&self) -> &S {
|
fn peek(&self) -> &SecretValue {
|
||||||
&self.inner_secret
|
&self.inner_secret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> From<S> for Secret<S, I>
|
impl<SecretValue, MaskingStrategy> From<SecretValue> for Secret<SecretValue, MaskingStrategy>
|
||||||
where
|
where
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<SecretValue>,
|
||||||
{
|
{
|
||||||
fn from(secret: S) -> Secret<S, I> {
|
fn from(secret: SecretValue) -> Self {
|
||||||
Self::new(secret)
|
Self::new(secret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> Clone for Secret<S, I>
|
impl<SecretValue, MaskingStrategy> Clone for Secret<SecretValue, MaskingStrategy>
|
||||||
where
|
where
|
||||||
S: Clone,
|
SecretValue: Clone,
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<SecretValue>,
|
||||||
{
|
{
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Secret {
|
Self {
|
||||||
inner_secret: self.inner_secret.clone(),
|
inner_secret: self.inner_secret.clone(),
|
||||||
marker: PhantomData,
|
masking_strategy: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> PartialEq for Secret<S, I>
|
impl<SecretValue, MaskingStrategy> PartialEq for Secret<SecretValue, MaskingStrategy>
|
||||||
where
|
where
|
||||||
Self: PeekInterface<S>,
|
Self: PeekInterface<SecretValue>,
|
||||||
S: PartialEq,
|
SecretValue: PartialEq,
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<SecretValue>,
|
||||||
{
|
{
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.peek().eq(other.peek())
|
self.peek().eq(other.peek())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> Eq for Secret<S, I>
|
impl<SecretValue, MaskingStrategy> Eq for Secret<SecretValue, MaskingStrategy>
|
||||||
where
|
where
|
||||||
Self: PeekInterface<S>,
|
Self: PeekInterface<SecretValue>,
|
||||||
S: Eq,
|
SecretValue: Eq,
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<SecretValue>,
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> fmt::Debug for Secret<S, I>
|
impl<SecretValue, MaskingStrategy> fmt::Debug for Secret<SecretValue, MaskingStrategy>
|
||||||
where
|
where
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<SecretValue>,
|
||||||
{
|
{
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
I::fmt(&self.inner_secret, f)
|
MaskingStrategy::fmt(&self.inner_secret, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S, I> Default for Secret<S, I>
|
impl<SecretValue, MaskingStrategy> Default for Secret<SecretValue, MaskingStrategy>
|
||||||
where
|
where
|
||||||
S: Default,
|
SecretValue: Default,
|
||||||
I: Strategy<S>,
|
MaskingStrategy: Strategy<SecretValue>,
|
||||||
{
|
{
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
S::default().into()
|
SecretValue::default().into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ use crate::{PeekInterface, Secret, Strategy, StrongSecret, ZeroizableSecret};
|
|||||||
/// via `serde` serialization.
|
/// via `serde` serialization.
|
||||||
///
|
///
|
||||||
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
|
#[cfg_attr(docsrs, cfg(feature = "serde"))]
|
||||||
pub trait SerializableSecret: Serialize {}
|
pub trait SerializableSecret: Serialize {}
|
||||||
// #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
|
// #[cfg_attr(docsrs, doc(cfg(feature = "serde")))]
|
||||||
// pub trait NonSerializableSecret: Serialize {}
|
// pub trait NonSerializableSecret: Serialize {}
|
||||||
@ -33,7 +33,7 @@ where
|
|||||||
where
|
where
|
||||||
D: de::Deserializer<'de>,
|
D: de::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
T::deserialize(deserializer).map(Secret::new)
|
T::deserialize(deserializer).map(Self::new)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +59,7 @@ where
|
|||||||
where
|
where
|
||||||
D: serde::Deserializer<'de>,
|
D: serde::Deserializer<'de>,
|
||||||
{
|
{
|
||||||
T::deserialize(deserializer).map(StrongSecret::new)
|
T::deserialize(deserializer).map(Self::new)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,14 +3,14 @@ use core::fmt;
|
|||||||
/// Debugging trait which is specialized for handling secret values
|
/// Debugging trait which is specialized for handling secret values
|
||||||
pub trait Strategy<T> {
|
pub trait Strategy<T> {
|
||||||
/// Format information about the secret's type.
|
/// Format information about the secret's type.
|
||||||
fn fmt(value: &T, fmt: &mut fmt::Formatter<'_>) -> std::fmt::Result;
|
fn fmt(value: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Debug with type
|
/// Debug with type
|
||||||
pub struct WithType;
|
pub struct WithType;
|
||||||
|
|
||||||
impl<T> Strategy<T> for WithType {
|
impl<T> Strategy<T> for WithType {
|
||||||
fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fmt.write_str("*** ")?;
|
fmt.write_str("*** ")?;
|
||||||
fmt.write_str(std::any::type_name::<T>())?;
|
fmt.write_str(std::any::type_name::<T>())?;
|
||||||
fmt.write_str(" ***")
|
fmt.write_str(" ***")
|
||||||
@ -21,18 +21,7 @@ impl<T> Strategy<T> for WithType {
|
|||||||
pub struct WithoutType;
|
pub struct WithoutType;
|
||||||
|
|
||||||
impl<T> Strategy<T> for WithoutType {
|
impl<T> Strategy<T> for WithoutType {
|
||||||
fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(_: &T, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
fmt.write_str("*** ***")
|
fmt.write_str("*** ***")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct NoMasking;
|
|
||||||
|
|
||||||
impl<T> Strategy<T> for NoMasking
|
|
||||||
where
|
|
||||||
T: fmt::Display,
|
|
||||||
{
|
|
||||||
fn fmt(val: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt::Display::fmt(val, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ where
|
|||||||
type Err = core::convert::Infallible;
|
type Err = core::convert::Infallible;
|
||||||
|
|
||||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||||
Ok(Secret::<String, I>::new(src.to_string()))
|
Ok(Self::new(src.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -34,6 +34,6 @@ where
|
|||||||
type Err = core::convert::Infallible;
|
type Err = core::convert::Infallible;
|
||||||
|
|
||||||
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
fn from_str(src: &str) -> Result<Self, Self::Err> {
|
||||||
Ok(StrongSecret::<String, I>::new(src.to_string()))
|
Ok(Self::new(src.to_string()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
use std::{fmt, marker::PhantomData};
|
use std::{fmt, marker::PhantomData};
|
||||||
|
|
||||||
|
use subtle::ConstantTimeEq;
|
||||||
use zeroize::{self, Zeroize as ZeroizableSecret};
|
use zeroize::{self, Zeroize as ZeroizableSecret};
|
||||||
|
|
||||||
use crate::{strategy::Strategy, PeekInterface};
|
use crate::{strategy::Strategy, PeekInterface};
|
||||||
@ -13,84 +14,106 @@ use crate::{strategy::Strategy, PeekInterface};
|
|||||||
///
|
///
|
||||||
/// To get access to value use method `expose()` of trait [`crate::ExposeInterface`].
|
/// To get access to value use method `expose()` of trait [`crate::ExposeInterface`].
|
||||||
///
|
///
|
||||||
|
pub struct StrongSecret<Secret: ZeroizableSecret, MaskingStrategy = crate::WithType> {
|
||||||
pub struct StrongSecret<S: ZeroizableSecret, I = crate::WithType> {
|
|
||||||
/// Inner secret value
|
/// Inner secret value
|
||||||
pub(crate) inner_secret: S,
|
pub(crate) inner_secret: Secret,
|
||||||
pub(crate) marker: PhantomData<I>,
|
pub(crate) masking_strategy: PhantomData<MaskingStrategy>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: ZeroizableSecret, I> StrongSecret<S, I> {
|
impl<Secret: ZeroizableSecret, MaskingStrategy> StrongSecret<Secret, MaskingStrategy> {
|
||||||
/// Take ownership of a secret value
|
/// Take ownership of a secret value
|
||||||
pub fn new(secret: S) -> Self {
|
pub fn new(secret: Secret) -> Self {
|
||||||
StrongSecret {
|
Self {
|
||||||
inner_secret: secret,
|
inner_secret: secret,
|
||||||
marker: PhantomData,
|
masking_strategy: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: ZeroizableSecret, I> PeekInterface<S> for StrongSecret<S, I> {
|
impl<Secret: ZeroizableSecret, MaskingStrategy> PeekInterface<Secret>
|
||||||
fn peek(&self) -> &S {
|
for StrongSecret<Secret, MaskingStrategy>
|
||||||
|
{
|
||||||
|
fn peek(&self) -> &Secret {
|
||||||
&self.inner_secret
|
&self.inner_secret
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: ZeroizableSecret, I> From<S> for StrongSecret<S, I> {
|
impl<Secret: ZeroizableSecret, MaskingStrategy> From<Secret>
|
||||||
fn from(secret: S) -> StrongSecret<S, I> {
|
for StrongSecret<Secret, MaskingStrategy>
|
||||||
|
{
|
||||||
|
fn from(secret: Secret) -> Self {
|
||||||
Self::new(secret)
|
Self::new(secret)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Clone + ZeroizableSecret, I> Clone for StrongSecret<S, I> {
|
impl<Secret: Clone + ZeroizableSecret, MaskingStrategy> Clone
|
||||||
|
for StrongSecret<Secret, MaskingStrategy>
|
||||||
|
{
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
StrongSecret {
|
Self {
|
||||||
inner_secret: self.inner_secret.clone(),
|
inner_secret: self.inner_secret.clone(),
|
||||||
marker: PhantomData,
|
masking_strategy: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: ZeroizableSecret, I> PartialEq for StrongSecret<S, I>
|
impl<Secret, MaskingStrategy> PartialEq for StrongSecret<Secret, MaskingStrategy>
|
||||||
where
|
where
|
||||||
Self: PeekInterface<S>,
|
Self: PeekInterface<Secret>,
|
||||||
S: PartialEq,
|
Secret: ZeroizableSecret + StrongEq,
|
||||||
{
|
{
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
self.peek().eq(other.peek())
|
StrongEq::strong_eq(self.peek(), other.peek())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: ZeroizableSecret, I> Eq for StrongSecret<S, I>
|
impl<Secret, MaskingStrategy> Eq for StrongSecret<Secret, MaskingStrategy>
|
||||||
where
|
where
|
||||||
Self: PeekInterface<S>,
|
Self: PeekInterface<Secret>,
|
||||||
S: Eq,
|
Secret: ZeroizableSecret + StrongEq,
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: ZeroizableSecret, I: Strategy<S>> fmt::Debug for StrongSecret<S, I> {
|
impl<Secret: ZeroizableSecret, MaskingStrategy: Strategy<Secret>> fmt::Debug
|
||||||
|
for StrongSecret<Secret, MaskingStrategy>
|
||||||
|
{
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
I::fmt(&self.inner_secret, f)
|
MaskingStrategy::fmt(&self.inner_secret, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: ZeroizableSecret, I: Strategy<S>> fmt::Display for StrongSecret<S, I> {
|
impl<Secret: ZeroizableSecret, MaskingStrategy: Strategy<Secret>> fmt::Display
|
||||||
|
for StrongSecret<Secret, MaskingStrategy>
|
||||||
|
{
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
I::fmt(&self.inner_secret, f)
|
MaskingStrategy::fmt(&self.inner_secret, f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: ZeroizableSecret, I> Default for StrongSecret<S, I>
|
impl<Secret: ZeroizableSecret, MaskingStrategy> Default for StrongSecret<Secret, MaskingStrategy>
|
||||||
where
|
where
|
||||||
S: ZeroizableSecret + Default,
|
Secret: ZeroizableSecret + Default,
|
||||||
{
|
{
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
S::default().into()
|
Secret::default().into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: ZeroizableSecret, S> Drop for StrongSecret<T, S> {
|
impl<Secret: ZeroizableSecret, MaskingStrategy> Drop for StrongSecret<Secret, MaskingStrategy> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.inner_secret.zeroize();
|
self.inner_secret.zeroize();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
trait StrongEq {
|
||||||
|
fn strong_eq(&self, other: &Self) -> bool;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StrongEq for String {
|
||||||
|
fn strong_eq(&self, other: &Self) -> bool {
|
||||||
|
let lhs = self.as_bytes();
|
||||||
|
let rhs = other.as_bytes();
|
||||||
|
|
||||||
|
bool::from(lhs.ct_eq(rhs))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@ -8,7 +8,7 @@ use uuid::Uuid;
|
|||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
core::errors,
|
core::errors,
|
||||||
pii::{self, PeekOptionInterface, Secret},
|
pii::{self, ExposeOptionInterface, Secret},
|
||||||
services,
|
services,
|
||||||
types::{self, api, storage::enums},
|
types::{self, api, storage::enums},
|
||||||
};
|
};
|
||||||
@ -200,8 +200,8 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest {
|
|||||||
name: shipping.address.as_mut().map(|a| {
|
name: shipping.address.as_mut().map(|a| {
|
||||||
format!(
|
format!(
|
||||||
"{} {}",
|
"{} {}",
|
||||||
a.first_name.peek_cloning().unwrap_or_default(),
|
a.first_name.clone().expose_option().unwrap_or_default(),
|
||||||
a.last_name.peek_cloning().unwrap_or_default()
|
a.last_name.clone().expose_option().unwrap_or_default()
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
@ -209,7 +209,7 @@ impl TryFrom<&types::PaymentsAuthorizeRouterData> for PaymentIntentRequest {
|
|||||||
format!(
|
format!(
|
||||||
"{}{}",
|
"{}{}",
|
||||||
p.country_code.unwrap_or_default(),
|
p.country_code.unwrap_or_default(),
|
||||||
p.number.peek_cloning().unwrap_or_default()
|
p.number.expose_option().unwrap_or_default()
|
||||||
)
|
)
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
|
|||||||
@ -493,17 +493,24 @@ impl BasiliskCardSupport {
|
|||||||
) -> RouterResult<api::CardDetailFromLocker> {
|
) -> RouterResult<api::CardDetailFromLocker> {
|
||||||
let card_number = card
|
let card_number = card
|
||||||
.card_number
|
.card_number
|
||||||
.peek_cloning()
|
.clone()
|
||||||
|
.expose_option()
|
||||||
.get_required_value("card_number")?;
|
.get_required_value("card_number")?;
|
||||||
let card_exp_month = card
|
let card_exp_month = card
|
||||||
.expiry_month
|
.expiry_month
|
||||||
.peek_cloning()
|
.clone()
|
||||||
|
.expose_option()
|
||||||
.get_required_value("expiry_month")?;
|
.get_required_value("expiry_month")?;
|
||||||
let card_exp_year = card
|
let card_exp_year = card
|
||||||
.expiry_year
|
.expiry_year
|
||||||
.peek_cloning()
|
.clone()
|
||||||
|
.expose_option()
|
||||||
.get_required_value("expiry_year")?;
|
.get_required_value("expiry_year")?;
|
||||||
let card_holder_name = card.card_holder_name.peek_cloning().unwrap_or_default();
|
let card_holder_name = card
|
||||||
|
.card_holder_name
|
||||||
|
.clone()
|
||||||
|
.expose_option()
|
||||||
|
.unwrap_or_default();
|
||||||
let card_detail = api::CardDetail {
|
let card_detail = api::CardDetail {
|
||||||
card_number: card_number.into(),
|
card_number: card_number.into(),
|
||||||
card_exp_month: card_exp_month.into(),
|
card_exp_month: card_exp_month.into(),
|
||||||
@ -529,20 +536,28 @@ impl BasiliskCardSupport {
|
|||||||
) -> RouterResult<api::CardDetailFromLocker> {
|
) -> RouterResult<api::CardDetailFromLocker> {
|
||||||
let card_number = card
|
let card_number = card
|
||||||
.card_number
|
.card_number
|
||||||
.peek_cloning()
|
.clone()
|
||||||
|
.expose_option()
|
||||||
.get_required_value("card_number")?;
|
.get_required_value("card_number")?;
|
||||||
let card_exp_month = card
|
let card_exp_month = card
|
||||||
.expiry_month
|
.expiry_month
|
||||||
.peek_cloning()
|
.clone()
|
||||||
|
.expose_option()
|
||||||
.get_required_value("expiry_month")?;
|
.get_required_value("expiry_month")?;
|
||||||
let card_exp_year = card
|
let card_exp_year = card
|
||||||
.expiry_year
|
.expiry_year
|
||||||
.peek_cloning()
|
.clone()
|
||||||
|
.expose_option()
|
||||||
.get_required_value("expiry_year")?;
|
.get_required_value("expiry_year")?;
|
||||||
let card_holder_name = card.card_holder_name.peek_cloning().unwrap_or_default();
|
let card_holder_name = card
|
||||||
|
.card_holder_name
|
||||||
|
.clone()
|
||||||
|
.expose_option()
|
||||||
|
.unwrap_or_default();
|
||||||
let card_fingerprint = card
|
let card_fingerprint = card
|
||||||
.card_fingerprint
|
.card_fingerprint
|
||||||
.peek_cloning()
|
.clone()
|
||||||
|
.expose_option()
|
||||||
.get_required_value("card_fingerprint")?;
|
.get_required_value("card_fingerprint")?;
|
||||||
let value1 = payment_methods::mk_card_value1(
|
let value1 = payment_methods::mk_card_value1(
|
||||||
card_number,
|
card_number,
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use std::borrow::Cow;
|
|||||||
|
|
||||||
// TODO : Evaluate all the helper functions ()
|
// TODO : Evaluate all the helper functions ()
|
||||||
use error_stack::{report, IntoReport, ResultExt};
|
use error_stack::{report, IntoReport, ResultExt};
|
||||||
use masking::{PeekInterface, PeekOptionInterface};
|
use masking::{ExposeOptionInterface, PeekInterface};
|
||||||
use router_env::{instrument, tracing};
|
use router_env::{instrument, tracing};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
@ -633,7 +633,7 @@ pub async fn create_customer_if_not_exist<'a, F: Clone, R>(
|
|||||||
let new_customer = storage::CustomerNew {
|
let new_customer = storage::CustomerNew {
|
||||||
customer_id: customer_id.to_string(),
|
customer_id: customer_id.to_string(),
|
||||||
merchant_id: merchant_id.to_string(),
|
merchant_id: merchant_id.to_string(),
|
||||||
name: req.name.peek_cloning(),
|
name: req.name.expose_option(),
|
||||||
email: req.email.clone(),
|
email: req.email.clone(),
|
||||||
phone: req.phone.clone(),
|
phone: req.phone.clone(),
|
||||||
phone_country_code: req.phone_country_code.clone(),
|
phone_country_code: req.phone_country_code.clone(),
|
||||||
@ -725,17 +725,17 @@ impl Vault {
|
|||||||
let card = resp.card;
|
let card = resp.card;
|
||||||
let card_number = card
|
let card_number = card
|
||||||
.card_number
|
.card_number
|
||||||
.peek_cloning()
|
.expose_option()
|
||||||
.get_required_value("card_number")?;
|
.get_required_value("card_number")?;
|
||||||
let card_exp_month = card
|
let card_exp_month = card
|
||||||
.card_exp_month
|
.card_exp_month
|
||||||
.peek_cloning()
|
.expose_option()
|
||||||
.get_required_value("expiry_month")?;
|
.get_required_value("expiry_month")?;
|
||||||
let card_exp_year = card
|
let card_exp_year = card
|
||||||
.card_exp_year
|
.card_exp_year
|
||||||
.peek_cloning()
|
.expose_option()
|
||||||
.get_required_value("expiry_year")?;
|
.get_required_value("expiry_year")?;
|
||||||
let card_holder_name = card.name_on_card.peek_cloning().unwrap_or_default();
|
let card_holder_name = card.name_on_card.expose_option().unwrap_or_default();
|
||||||
let card = api::PaymentMethod::Card(api::CCard {
|
let card = api::PaymentMethod::Card(api::CCard {
|
||||||
card_number: card_number.into(),
|
card_number: card_number.into(),
|
||||||
card_exp_month: card_exp_month.into(),
|
card_exp_month: card_exp_month.into(),
|
||||||
|
|||||||
@ -6,7 +6,7 @@ use std::{borrow::Cow, collections::HashMap, fmt::Debug, future::Future, str, ti
|
|||||||
use actix_web::{body, HttpRequest, HttpResponse, Responder};
|
use actix_web::{body, HttpRequest, HttpResponse, Responder};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use error_stack::{report, IntoReport, Report, ResultExt};
|
use error_stack::{report, IntoReport, Report, ResultExt};
|
||||||
use masking::PeekOptionInterface;
|
use masking::ExposeOptionInterface;
|
||||||
use router_env::{
|
use router_env::{
|
||||||
tracing::{self, instrument},
|
tracing::{self, instrument},
|
||||||
Tag,
|
Tag,
|
||||||
@ -223,7 +223,7 @@ async fn send_request(
|
|||||||
client.body(url_encoded_payload).send()
|
client.body(url_encoded_payload).send()
|
||||||
}
|
}
|
||||||
None => client
|
None => client
|
||||||
.body(request.payload.peek_cloning().unwrap_or_default())
|
.body(request.payload.expose_option().unwrap_or_default())
|
||||||
.send(),
|
.send(),
|
||||||
}
|
}
|
||||||
.await
|
.await
|
||||||
|
|||||||
Reference in New Issue
Block a user