oxirs-core 0.3.1

Core RDF and SPARQL functionality for OxiRS - native Rust implementation with zero dependencies
Documentation
//! GDPR compliance services for the audit trail.
//!
//! Implements the operational primitives required under:
//! - **Article 15** – right of access (data subject report).
//! - **Article 17** – right to erasure (pseudonymisation of PII fields).

use chrono::Utc;
use serde::{Deserialize, Serialize};

use super::event::AuditEvent;

// ─────────────────────────────────────────────
// DataSubjectReport
// ─────────────────────────────────────────────

/// A complete record of all audit events that concern a specific data subject.
///
/// Generated by [`GdprService::data_subject_report`] to satisfy GDPR Article 15
/// (subject access request). The report is serialisable to JSON for handover to
/// the data subject or a data protection authority.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DataSubjectReport {
    /// The data subject whose events this report covers.
    pub data_subject_id: String,
    /// All matching audit events, in chronological order.
    pub events: Vec<AuditEvent>,
    /// UTC timestamp at which this report was generated.
    pub generated_at: chrono::DateTime<Utc>,
    /// Convenience count; equals `events.len()`.
    pub event_count: usize,
}

impl DataSubjectReport {
    /// Serialise this report to a compact JSON string.
    ///
    /// Returns a [`serde_json::Error`] if serialisation fails (should not
    /// happen for well-formed reports, but callers must handle the error).
    pub fn to_json(&self) -> Result<String, serde_json::Error> {
        serde_json::to_string(self)
    }
}

// ─────────────────────────────────────────────
// GdprService
// ─────────────────────────────────────────────

/// GDPR compliance operations over an in-memory audit log.
///
/// All methods operate on borrowed slices or mutable `Vec` values so that
/// they compose with any storage backend.
pub struct GdprService;

impl GdprService {
    /// **GDPR Article 15** — Generate a data subject access report.
    ///
    /// Collects every event where `data_subject_id` equals `subject_id`,
    /// sorts them chronologically, and packages them in a [`DataSubjectReport`].
    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();

        // Chronological order for the subject's convenience.
        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,
        }
    }

    /// **GDPR Article 17** — Pseudonymise PII fields for a data subject.
    ///
    /// Replaces the following fields with `"[redacted]"` in every event where
    /// `data_subject_id` matches the target:
    ///
    /// | Field | Location |
    /// |---|---|
    /// | `actor.actor_id` | [`AuditActor`](super::event::AuditActor) |
    /// | `actor.ip_address` | [`AuditActor`](super::event::AuditActor) |
    /// | `actor.session_id` | [`AuditActor`](super::event::AuditActor) |
    /// | `data_subject_id` | [`AuditEvent`] |
    ///
    /// The event itself is **not deleted** — deleting records would break the
    /// audit chain required by SOC2. Instead, PII is replaced in-place while
    /// the event skeleton (timestamp, kind, action, outcome, resource) is
    /// preserved for compliance reporting.
    ///
    /// Returns the count of events that were modified.
    pub fn pseudonymise(events: &mut [AuditEvent], data_subject_id: &str) -> usize {
        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());
                count += 1;
            }
        }
        count
    }
}