use crate::authn::ids::{TenantId, UserId};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use uuid::Uuid;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct PiiToken(Arc<str>);
impl PiiToken {
pub fn new() -> Self {
Self::from_uuid(Uuid::new_v4())
}
pub fn from_uuid(uuid: Uuid) -> Self {
Self(Arc::from(format!("pii:{}", uuid.as_hyphenated())))
}
pub fn as_str(&self) -> &str {
&self.0
}
pub fn is_well_formed(s: &str) -> bool {
s.strip_prefix("pii:")
.and_then(|rest| Uuid::parse_str(rest).ok())
.is_some()
}
pub fn parse(s: &str) -> Option<Self> {
if Self::is_well_formed(s) {
Some(Self(Arc::from(s)))
} else {
None
}
}
}
impl Default for PiiToken {
fn default() -> Self {
Self::new()
}
}
impl std::fmt::Display for PiiToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum DevicePiiCategory {
DisplayName,
UserAgentString,
AcceptLanguage,
IpAddress,
ScreenMetrics,
Other,
}
impl DevicePiiCategory {
pub fn as_str(&self) -> &'static str {
match self {
DevicePiiCategory::DisplayName => "display_name",
DevicePiiCategory::UserAgentString => "user_agent_string",
DevicePiiCategory::AcceptLanguage => "accept_language",
DevicePiiCategory::IpAddress => "ip_address",
DevicePiiCategory::ScreenMetrics => "screen_metrics",
DevicePiiCategory::Other => "other",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct DevicePiiMapping {
pub token: PiiToken,
pub subject_id: UserId,
pub tenant_id: TenantId,
pub category: DevicePiiCategory,
pub value: String,
pub created_at: DateTime<Utc>,
}
pub trait DevicePiiStore: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static;
fn record(
&self,
subject_id: &UserId,
tenant_id: &TenantId,
category: DevicePiiCategory,
value: String,
now: DateTime<Utc>,
) -> impl std::future::Future<Output = Result<PiiToken, Self::Error>> + Send;
fn resolve(
&self,
token: &PiiToken,
expected_tenant: &TenantId,
) -> impl std::future::Future<Output = Result<Option<String>, Self::Error>> + Send;
fn erase_subject(
&self,
subject_id: &UserId,
tenant_id: &TenantId,
) -> impl std::future::Future<Output = Result<u64, Self::Error>> + Send;
fn list_for_subject(
&self,
subject_id: &UserId,
tenant_id: &TenantId,
) -> impl std::future::Future<Output = Result<Vec<DevicePiiMapping>, Self::Error>> + Send;
}
pub trait DevicePiiResolver: Send + Sync {
type Error: std::error::Error + Send + Sync + 'static;
fn resolve_or_redacted(
&self,
token: &PiiToken,
expected_tenant: &TenantId,
) -> impl std::future::Future<Output = Result<String, Self::Error>> + Send;
}
pub const REDACTED_PLACEHOLDER: &str = "[redacted]";
#[derive(Debug, Clone, Default)]
pub struct MemoryDevicePiiStore {
mappings: Arc<DashMap<PiiToken, DevicePiiMapping>>,
}
impl MemoryDevicePiiStore {
pub fn new() -> Self {
Self::default()
}
pub fn len(&self) -> usize {
self.mappings.len()
}
pub fn is_empty(&self) -> bool {
self.mappings.is_empty()
}
}
pub type MemoryDevicePiiStoreError = std::convert::Infallible;
impl DevicePiiStore for MemoryDevicePiiStore {
type Error = MemoryDevicePiiStoreError;
async fn record(
&self,
subject_id: &UserId,
tenant_id: &TenantId,
category: DevicePiiCategory,
value: String,
now: DateTime<Utc>,
) -> Result<PiiToken, Self::Error> {
let token = PiiToken::new();
let mapping = DevicePiiMapping {
token: token.clone(),
subject_id: *subject_id,
tenant_id: *tenant_id,
category,
value,
created_at: now,
};
self.mappings.insert(token.clone(), mapping);
Ok(token)
}
async fn resolve(
&self,
token: &PiiToken,
expected_tenant: &TenantId,
) -> Result<Option<String>, Self::Error> {
Ok(self
.mappings
.get(token)
.filter(|m| &m.tenant_id == expected_tenant)
.map(|m| m.value.clone()))
}
async fn erase_subject(
&self,
subject_id: &UserId,
tenant_id: &TenantId,
) -> Result<u64, Self::Error> {
let mut erased = 0u64;
let to_remove: Vec<PiiToken> = self
.mappings
.iter()
.filter(|m| &m.subject_id == subject_id && &m.tenant_id == tenant_id)
.map(|m| m.key().clone())
.collect();
for key in to_remove {
if self.mappings.remove(&key).is_some() {
erased += 1;
}
}
Ok(erased)
}
async fn list_for_subject(
&self,
subject_id: &UserId,
tenant_id: &TenantId,
) -> Result<Vec<DevicePiiMapping>, Self::Error> {
Ok(self
.mappings
.iter()
.filter(|m| &m.subject_id == subject_id && &m.tenant_id == tenant_id)
.map(|m| m.value().clone())
.collect())
}
}
impl<S: DevicePiiStore> DevicePiiResolver for S {
type Error = <S as DevicePiiStore>::Error;
async fn resolve_or_redacted(
&self,
token: &PiiToken,
expected_tenant: &TenantId,
) -> Result<String, Self::Error> {
Ok(self
.resolve(token, expected_tenant)
.await?
.unwrap_or_else(|| REDACTED_PLACEHOLDER.to_string()))
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct RedactedResolver;
impl DevicePiiResolver for RedactedResolver {
type Error = std::convert::Infallible;
async fn resolve_or_redacted(
&self,
token: &PiiToken,
expected_tenant: &TenantId,
) -> Result<String, Self::Error> {
tracing::trace!(
target: "axess::device::pii",
?token,
%expected_tenant,
"RedactedResolver: returning placeholder regardless of token",
);
Ok(REDACTED_PLACEHOLDER.to_string())
}
}
#[cfg(test)]
mod tests;