use chrono::Utc;
use serde::{Deserialize, Serialize};
use super::event::AuditEvent;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataSubjectReport {
pub data_subject_id: String,
pub events: Vec<AuditEvent>,
pub generated_at: chrono::DateTime<Utc>,
pub event_count: usize,
}
impl DataSubjectReport {
pub fn to_json(&self) -> Result<String, serde_json::Error> {
serde_json::to_string(self)
}
}
pub struct GdprService;
impl GdprService {
pub fn data_subject_report(events: &[AuditEvent], data_subject_id: &str) -> DataSubjectReport {
let mut matching: Vec<AuditEvent> = events
.iter()
.filter(|e| e.data_subject_id.as_deref() == Some(data_subject_id))
.cloned()
.collect();
matching.sort_by_key(|e| e.timestamp);
let event_count = matching.len();
DataSubjectReport {
data_subject_id: data_subject_id.to_string(),
events: matching,
generated_at: Utc::now(),
event_count,
}
}
pub fn pseudonymise(events: &mut [AuditEvent], data_subject_id: &str) -> usize {
Self::pseudonymise_with(events, data_subject_id, |_key, _value| {
"[redacted]".to_string()
})
}
pub fn pseudonymise_with<F>(
events: &mut [AuditEvent],
data_subject_id: &str,
mut scrub_metadata: F,
) -> usize
where
F: FnMut(&str, &str) -> String,
{
let mut count = 0usize;
for event in events.iter_mut() {
if event.data_subject_id.as_deref() == Some(data_subject_id) {
event.actor.actor_id = "[redacted]".to_string();
event.actor.ip_address = Some("[redacted]".to_string());
event.actor.session_id = Some("[redacted]".to_string());
event.data_subject_id = Some("[redacted]".to_string());
for (key, value) in event.metadata.iter_mut() {
*value = scrub_metadata(key, value);
}
count += 1;
}
}
count
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::audit::event::{ActorType, AuditActor, AuditEventKind, AuditOutcome, AuditResource};
fn sample_event(subject: &str, query_text: &str) -> AuditEvent {
AuditEvent::new(
AuditEventKind::DataAccess,
"sparql.select",
AuditActor {
actor_id: "admin-1".to_string(),
actor_type: ActorType::User,
ip_address: Some("203.0.113.7".to_string()),
session_id: Some("sess-abc".to_string()),
},
AuditResource {
resource_type: "query".to_string(),
resource_id: "q-1".to_string(),
tenant_id: None,
},
AuditOutcome::Success,
)
.with_metadata("query_text", query_text)
.with_metadata("bytes_transferred", "4096")
.with_data_subject(subject)
}
#[test]
fn regression_pseudonymise_scrubs_metadata_pii() {
let mut events = vec![sample_event(
"alice@example.org",
"SELECT ?x WHERE { ?x <mbox> \"alice@example.org\" }",
)];
let modified = GdprService::pseudonymise(&mut events, "alice@example.org");
assert_eq!(modified, 1);
let ev = &events[0];
assert_eq!(ev.actor.actor_id, "[redacted]");
assert_eq!(ev.actor.ip_address.as_deref(), Some("[redacted]"));
assert_eq!(ev.data_subject_id.as_deref(), Some("[redacted]"));
for (key, value) in &ev.metadata {
assert_eq!(value, "[redacted]", "metadata[{key}] was not scrubbed");
}
assert!(ev.metadata.contains_key("query_text"));
assert!(ev.metadata.contains_key("bytes_transferred"));
}
#[test]
fn regression_pseudonymise_with_selective_scrubber() {
let mut events = vec![sample_event(
"bob@example.org",
"SELECT * WHERE { ?s ?p ?o }",
)];
let modified =
GdprService::pseudonymise_with(&mut events, "bob@example.org", |key, value| {
if key == "bytes_transferred" {
value.to_string()
} else {
"[redacted]".to_string()
}
});
assert_eq!(modified, 1);
let ev = &events[0];
assert_eq!(
ev.metadata.get("query_text").map(String::as_str),
Some("[redacted]")
);
assert_eq!(
ev.metadata.get("bytes_transferred").map(String::as_str),
Some("4096")
);
}
}