Skip to main content

aegis_server/
consent.rs

1//! Aegis Consent Management Module
2//!
3//! GDPR/CCPA compliance with consent tracking, audit trails, and enforcement.
4//! Provides APIs for recording, querying, and withdrawing user consent.
5//!
6//! @version 0.1.0
7//! @author AutomataNexus Development Team
8
9use crate::state::AppState;
10use axum::{
11    extract::{Path, State},
12    http::StatusCode,
13    response::IntoResponse,
14    Json,
15};
16use parking_lot::RwLock;
17use serde::{Deserialize, Serialize};
18use std::collections::HashMap;
19use std::path::PathBuf;
20use std::time::{SystemTime, UNIX_EPOCH};
21
22// =============================================================================
23// Consent Types
24// =============================================================================
25
26/// Purpose categories for consent under GDPR/CCPA.
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
28#[serde(rename_all = "snake_case")]
29pub enum Purpose {
30    /// Marketing communications and promotional materials
31    Marketing,
32    /// Analytics and usage tracking
33    Analytics,
34    /// Sharing data with third parties
35    ThirdPartySharing,
36    /// General data processing for service operation
37    DataProcessing,
38    /// Personalization and recommendations
39    Personalization,
40    /// Location tracking and geolocation services
41    LocationTracking,
42    /// Profiling for automated decision-making
43    Profiling,
44    /// Cross-device tracking
45    CrossDeviceTracking,
46    /// Advertising and targeted ads
47    Advertising,
48    /// Research and statistical purposes
49    Research,
50    /// CCPA-specific: Do Not Sell My Personal Information
51    DoNotSell,
52    /// Custom purpose with arbitrary identifier
53    Custom(u32),
54}
55
56impl Purpose {
57    /// Get a human-readable description of the purpose.
58    pub fn description(&self) -> &'static str {
59        match self {
60            Purpose::Marketing => "Marketing communications and promotional materials",
61            Purpose::Analytics => "Analytics and usage tracking",
62            Purpose::ThirdPartySharing => "Sharing data with third parties",
63            Purpose::DataProcessing => "General data processing for service operation",
64            Purpose::Personalization => "Personalization and recommendations",
65            Purpose::LocationTracking => "Location tracking and geolocation services",
66            Purpose::Profiling => "Profiling for automated decision-making",
67            Purpose::CrossDeviceTracking => "Cross-device tracking",
68            Purpose::Advertising => "Advertising and targeted ads",
69            Purpose::Research => "Research and statistical purposes",
70            Purpose::DoNotSell => "CCPA: Do Not Sell My Personal Information",
71            Purpose::Custom(_) => "Custom purpose",
72        }
73    }
74
75    /// Parse a purpose from a string identifier.
76    pub fn from_str(s: &str) -> Option<Self> {
77        match s.to_lowercase().as_str() {
78            "marketing" => Some(Purpose::Marketing),
79            "analytics" => Some(Purpose::Analytics),
80            "third_party_sharing" | "thirdpartysharing" => Some(Purpose::ThirdPartySharing),
81            "data_processing" | "dataprocessing" => Some(Purpose::DataProcessing),
82            "personalization" => Some(Purpose::Personalization),
83            "location_tracking" | "locationtracking" => Some(Purpose::LocationTracking),
84            "profiling" => Some(Purpose::Profiling),
85            "cross_device_tracking" | "crossdevicetracking" => Some(Purpose::CrossDeviceTracking),
86            "advertising" => Some(Purpose::Advertising),
87            "research" => Some(Purpose::Research),
88            "do_not_sell" | "donotsell" => Some(Purpose::DoNotSell),
89            s if s.starts_with("custom:") => s
90                .strip_prefix("custom:")
91                .and_then(|id| id.parse().ok())
92                .map(Purpose::Custom),
93            _ => None,
94        }
95    }
96
97    /// Convert purpose to a string identifier.
98    pub fn to_str(&self) -> String {
99        match self {
100            Purpose::Marketing => "marketing".to_string(),
101            Purpose::Analytics => "analytics".to_string(),
102            Purpose::ThirdPartySharing => "third_party_sharing".to_string(),
103            Purpose::DataProcessing => "data_processing".to_string(),
104            Purpose::Personalization => "personalization".to_string(),
105            Purpose::LocationTracking => "location_tracking".to_string(),
106            Purpose::Profiling => "profiling".to_string(),
107            Purpose::CrossDeviceTracking => "cross_device_tracking".to_string(),
108            Purpose::Advertising => "advertising".to_string(),
109            Purpose::Research => "research".to_string(),
110            Purpose::DoNotSell => "do_not_sell".to_string(),
111            Purpose::Custom(id) => format!("custom:{}", id),
112        }
113    }
114}
115
116impl std::fmt::Display for Purpose {
117    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
118        write!(f, "{}", self.to_str())
119    }
120}
121
122/// Source of consent (how it was obtained).
123#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
124#[serde(rename_all = "snake_case")]
125pub enum ConsentSource {
126    /// User explicitly provided consent via web form
127    WebForm,
128    /// Consent obtained through mobile app
129    MobileApp,
130    /// Consent obtained through API call
131    Api,
132    /// Consent imported from external system
133    Import,
134    /// Consent obtained through paper form (digitized)
135    PaperForm,
136    /// Consent obtained via email confirmation
137    Email,
138    /// Consent obtained via verbal agreement (logged)
139    Verbal,
140    /// System-generated default (e.g., legitimate interest)
141    SystemDefault,
142    /// Custom source
143    Custom(String),
144}
145
146impl ConsentSource {
147    pub fn from_str(s: &str) -> Self {
148        match s.to_lowercase().as_str() {
149            "web_form" | "webform" | "web" => ConsentSource::WebForm,
150            "mobile_app" | "mobileapp" | "mobile" => ConsentSource::MobileApp,
151            "api" => ConsentSource::Api,
152            "import" => ConsentSource::Import,
153            "paper_form" | "paperform" | "paper" => ConsentSource::PaperForm,
154            "email" => ConsentSource::Email,
155            "verbal" => ConsentSource::Verbal,
156            "system_default" | "systemdefault" | "system" => ConsentSource::SystemDefault,
157            other => ConsentSource::Custom(other.to_string()),
158        }
159    }
160}
161
162impl std::fmt::Display for ConsentSource {
163    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
164        match self {
165            ConsentSource::WebForm => write!(f, "web_form"),
166            ConsentSource::MobileApp => write!(f, "mobile_app"),
167            ConsentSource::Api => write!(f, "api"),
168            ConsentSource::Import => write!(f, "import"),
169            ConsentSource::PaperForm => write!(f, "paper_form"),
170            ConsentSource::Email => write!(f, "email"),
171            ConsentSource::Verbal => write!(f, "verbal"),
172            ConsentSource::SystemDefault => write!(f, "system_default"),
173            ConsentSource::Custom(s) => write!(f, "{}", s),
174        }
175    }
176}
177
178/// A consent record tracking a subject's consent for a specific purpose.
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ConsentRecord {
181    /// Unique identifier for this consent record
182    pub id: String,
183    /// Identifier of the data subject (user/customer)
184    pub subject_id: String,
185    /// The purpose for which consent is given/denied
186    pub purpose: Purpose,
187    /// Whether consent is granted (true) or denied/withdrawn (false)
188    pub granted: bool,
189    /// Timestamp when this consent was recorded (milliseconds since epoch)
190    pub timestamp: u64,
191    /// How the consent was obtained
192    pub source: ConsentSource,
193    /// Optional expiration timestamp (milliseconds since epoch)
194    pub expires_at: Option<u64>,
195    /// Privacy policy version this consent applies to
196    pub version: String,
197    /// Additional metadata (e.g., IP address, user agent, consent text shown)
198    #[serde(default)]
199    pub metadata: HashMap<String, String>,
200}
201
202impl ConsentRecord {
203    /// Create a new consent record.
204    pub fn new(
205        subject_id: &str,
206        purpose: Purpose,
207        granted: bool,
208        source: ConsentSource,
209        version: &str,
210    ) -> Self {
211        Self {
212            id: generate_consent_id(),
213            subject_id: subject_id.to_string(),
214            purpose,
215            granted,
216            timestamp: now_timestamp_ms(),
217            source,
218            expires_at: None,
219            version: version.to_string(),
220            metadata: HashMap::new(),
221        }
222    }
223
224    /// Check if this consent has expired.
225    pub fn is_expired(&self) -> bool {
226        if let Some(expires_at) = self.expires_at {
227            now_timestamp_ms() > expires_at
228        } else {
229            false
230        }
231    }
232
233    /// Check if consent is currently valid (granted and not expired).
234    pub fn is_valid(&self) -> bool {
235        self.granted && !self.is_expired()
236    }
237}
238
239/// Consent history entry for audit trail.
240#[derive(Debug, Clone, Serialize, Deserialize)]
241pub struct ConsentHistoryEntry {
242    /// The consent record at this point in history
243    pub record: ConsentRecord,
244    /// Action that created this entry
245    pub action: ConsentAction,
246    /// Who/what initiated this action (user_id, system, etc.)
247    pub actor: String,
248    /// Reason for the action (if applicable)
249    pub reason: Option<String>,
250}
251
252/// Actions that can be performed on consent.
253#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
254#[serde(rename_all = "snake_case")]
255pub enum ConsentAction {
256    /// Initial consent grant
257    Grant,
258    /// Consent denial
259    Deny,
260    /// Consent withdrawal
261    Withdraw,
262    /// Consent renewal/refresh
263    Renew,
264    /// Consent update (e.g., policy version change)
265    Update,
266    /// Consent expiration
267    Expire,
268}
269
270// =============================================================================
271// Consent Manager
272// =============================================================================
273
274/// Manager for consent records with in-memory storage.
275/// In production, this would be backed by persistent storage.
276pub struct ConsentManager {
277    /// Current consent records indexed by subject_id -> purpose -> record
278    records: RwLock<HashMap<String, HashMap<Purpose, ConsentRecord>>>,
279    /// Full audit history indexed by subject_id
280    history: RwLock<HashMap<String, Vec<ConsentHistoryEntry>>>,
281    /// CCPA Do Not Sell list (subject_ids)
282    do_not_sell_list: RwLock<std::collections::HashSet<String>>,
283    /// Record counter for ID generation
284    record_counter: RwLock<u64>,
285    /// Optional data directory for disk persistence
286    data_dir: Option<PathBuf>,
287}
288
289/// Serializable snapshot of all consent data for disk persistence.
290#[derive(Serialize, Deserialize)]
291struct ConsentSnapshot {
292    records: HashMap<String, HashMap<Purpose, ConsentRecord>>,
293    history: HashMap<String, Vec<ConsentHistoryEntry>>,
294    do_not_sell_list: std::collections::HashSet<String>,
295    record_counter: u64,
296}
297
298impl ConsentManager {
299    /// Create a new consent manager with no disk persistence.
300    pub fn new() -> Self {
301        Self::with_data_dir(None)
302    }
303
304    /// Create a new consent manager with optional disk persistence.
305    /// If `data_dir` is provided, attempts to load existing data from `{data_dir}/consent.json`.
306    pub fn with_data_dir(data_dir: Option<PathBuf>) -> Self {
307        if let Some(ref dir) = data_dir {
308            let path = dir.join("consent.json");
309            if path.exists() {
310                match std::fs::read_to_string(&path) {
311                    Ok(contents) => match serde_json::from_str::<ConsentSnapshot>(&contents) {
312                        Ok(snapshot) => {
313                            tracing::info!("Loaded consent data from {}", path.display());
314                            return Self {
315                                records: RwLock::new(snapshot.records),
316                                history: RwLock::new(snapshot.history),
317                                do_not_sell_list: RwLock::new(snapshot.do_not_sell_list),
318                                record_counter: RwLock::new(snapshot.record_counter),
319                                data_dir,
320                            };
321                        }
322                        Err(e) => {
323                            tracing::error!(
324                                "Failed to deserialize consent data from {}: {}",
325                                path.display(),
326                                e
327                            );
328                        }
329                    },
330                    Err(e) => {
331                        tracing::error!(
332                            "Failed to read consent data from {}: {}",
333                            path.display(),
334                            e
335                        );
336                    }
337                }
338            }
339        }
340
341        Self {
342            records: RwLock::new(HashMap::new()),
343            history: RwLock::new(HashMap::new()),
344            do_not_sell_list: RwLock::new(std::collections::HashSet::new()),
345            record_counter: RwLock::new(0),
346            data_dir,
347        }
348    }
349
350    /// Flush all consent data to disk if a data directory is configured.
351    fn flush_to_disk(&self) {
352        let Some(ref dir) = self.data_dir else {
353            return;
354        };
355
356        let snapshot = ConsentSnapshot {
357            records: self.records.read().clone(),
358            history: self.history.read().clone(),
359            do_not_sell_list: self.do_not_sell_list.read().clone(),
360            record_counter: *self.record_counter.read(),
361        };
362
363        if let Err(e) = std::fs::create_dir_all(dir) {
364            tracing::error!(
365                "Failed to create consent data directory {}: {}",
366                dir.display(),
367                e
368            );
369            return;
370        }
371
372        let path = dir.join("consent.json");
373        match serde_json::to_string_pretty(&snapshot) {
374            Ok(json) => {
375                if let Err(e) = std::fs::write(&path, json) {
376                    tracing::error!("Failed to write consent data to {}: {}", path.display(), e);
377                }
378            }
379            Err(e) => {
380                tracing::error!("Failed to serialize consent data: {}", e);
381            }
382        }
383    }
384
385    /// Record new consent or update existing consent.
386    pub fn record_consent(
387        &self,
388        subject_id: &str,
389        purpose: Purpose,
390        granted: bool,
391        source: ConsentSource,
392        version: &str,
393        expires_at: Option<u64>,
394        metadata: Option<HashMap<String, String>>,
395        actor: &str,
396    ) -> ConsentRecord {
397        let mut record = ConsentRecord::new(subject_id, purpose, granted, source, version);
398        record.expires_at = expires_at;
399        if let Some(meta) = metadata {
400            record.metadata = meta;
401        }
402
403        // Determine the action
404        let action = {
405            let records = self.records.read();
406            if let Some(subject_records) = records.get(subject_id) {
407                if let Some(existing) = subject_records.get(&purpose) {
408                    if granted && !existing.granted {
409                        ConsentAction::Grant
410                    } else if !granted && existing.granted {
411                        ConsentAction::Withdraw
412                    } else if granted {
413                        ConsentAction::Renew
414                    } else {
415                        ConsentAction::Deny
416                    }
417                } else if granted {
418                    ConsentAction::Grant
419                } else {
420                    ConsentAction::Deny
421                }
422            } else if granted {
423                ConsentAction::Grant
424            } else {
425                ConsentAction::Deny
426            }
427        };
428
429        // Store the record
430        {
431            let mut records = self.records.write();
432            let subject_records = records.entry(subject_id.to_string()).or_default();
433            subject_records.insert(purpose, record.clone());
434        }
435
436        // Update Do Not Sell list if applicable
437        if purpose == Purpose::DoNotSell {
438            let mut dns_list = self.do_not_sell_list.write();
439            if granted {
440                // If they grant "do not sell", add to the list
441                dns_list.insert(subject_id.to_string());
442            } else {
443                // If they opt back in (deny do-not-sell), remove from list
444                dns_list.remove(subject_id);
445            }
446        }
447
448        // Add to history (capped at 1000 entries per subject)
449        {
450            let mut history = self.history.write();
451            let subject_history = history.entry(subject_id.to_string()).or_default();
452            subject_history.push(ConsentHistoryEntry {
453                record: record.clone(),
454                action,
455                actor: actor.to_string(),
456                reason: None,
457            });
458            // Evict oldest entries if over limit
459            if subject_history.len() > 1000 {
460                let excess = subject_history.len() - 1000;
461                subject_history.drain(..excess);
462            }
463        }
464
465        tracing::info!(
466            "Consent recorded: subject={}, purpose={}, granted={}, action={:?}",
467            subject_id,
468            purpose,
469            granted,
470            action
471        );
472
473        self.flush_to_disk();
474
475        record
476    }
477
478    /// Get current consent status for a subject.
479    pub fn get_consent(&self, subject_id: &str) -> Vec<ConsentRecord> {
480        let records = self.records.read();
481        records
482            .get(subject_id)
483            .map(|r| r.values().cloned().collect())
484            .unwrap_or_default()
485    }
486
487    /// Get consent for a specific purpose.
488    pub fn get_consent_for_purpose(
489        &self,
490        subject_id: &str,
491        purpose: Purpose,
492    ) -> Option<ConsentRecord> {
493        let records = self.records.read();
494        records
495            .get(subject_id)
496            .and_then(|r| r.get(&purpose))
497            .cloned()
498    }
499
500    /// Check if consent is granted for a specific purpose.
501    /// This is the main enforcement function to call before data operations.
502    pub fn check_consent(&self, subject_id: &str, purpose: Purpose) -> bool {
503        let records = self.records.read();
504        if let Some(subject_records) = records.get(subject_id) {
505            if let Some(record) = subject_records.get(&purpose) {
506                return record.is_valid();
507            }
508        }
509        false
510    }
511
512    /// Withdraw consent for a specific purpose.
513    pub fn withdraw_consent(
514        &self,
515        subject_id: &str,
516        purpose: Purpose,
517        actor: &str,
518        reason: Option<&str>,
519    ) -> Result<ConsentRecord, String> {
520        // Check if consent exists
521        let existing = {
522            let records = self.records.read();
523            records
524                .get(subject_id)
525                .and_then(|r| r.get(&purpose))
526                .cloned()
527        };
528
529        let existing = existing.ok_or_else(|| {
530            format!(
531                "No consent record found for subject {} and purpose {}",
532                subject_id, purpose
533            )
534        })?;
535
536        // Create withdrawal record
537        let mut record = ConsentRecord::new(
538            subject_id,
539            purpose,
540            false,
541            existing.source.clone(),
542            &existing.version,
543        );
544        record.metadata = existing.metadata.clone();
545        record
546            .metadata
547            .insert("withdrawn_from".to_string(), existing.id.clone());
548
549        // Update records
550        {
551            let mut records = self.records.write();
552            if let Some(subject_records) = records.get_mut(subject_id) {
553                subject_records.insert(purpose, record.clone());
554            }
555        }
556
557        // Update Do Not Sell list if applicable
558        if purpose == Purpose::DoNotSell {
559            let mut dns_list = self.do_not_sell_list.write();
560            // Withdrawing "do not sell" means they're opting back in
561            dns_list.remove(subject_id);
562        }
563
564        // Add to history
565        {
566            let mut history = self.history.write();
567            let subject_history = history.entry(subject_id.to_string()).or_default();
568            subject_history.push(ConsentHistoryEntry {
569                record: record.clone(),
570                action: ConsentAction::Withdraw,
571                actor: actor.to_string(),
572                reason: reason.map(String::from),
573            });
574        }
575
576        tracing::info!(
577            "Consent withdrawn: subject={}, purpose={}, actor={}",
578            subject_id,
579            purpose,
580            actor
581        );
582
583        self.flush_to_disk();
584
585        Ok(record)
586    }
587
588    /// Get consent audit history for a subject.
589    pub fn get_history(&self, subject_id: &str) -> Vec<ConsentHistoryEntry> {
590        let history = self.history.read();
591        history.get(subject_id).cloned().unwrap_or_default()
592    }
593
594    /// Get history for a specific purpose.
595    pub fn get_history_for_purpose(
596        &self,
597        subject_id: &str,
598        purpose: Purpose,
599    ) -> Vec<ConsentHistoryEntry> {
600        let history = self.history.read();
601        history
602            .get(subject_id)
603            .map(|h| {
604                h.iter()
605                    .filter(|e| e.record.purpose == purpose)
606                    .cloned()
607                    .collect()
608            })
609            .unwrap_or_default()
610    }
611
612    /// Check if a subject is on the CCPA Do Not Sell list.
613    pub fn is_on_do_not_sell_list(&self, subject_id: &str) -> bool {
614        self.do_not_sell_list.read().contains(subject_id)
615    }
616
617    /// Get all subjects on the Do Not Sell list.
618    pub fn get_do_not_sell_list(&self) -> Vec<String> {
619        self.do_not_sell_list.read().iter().cloned().collect()
620    }
621
622    /// Export all consent records for a subject (for data portability/GDPR).
623    pub fn export_subject_data(&self, subject_id: &str) -> SubjectConsentExport {
624        let records = self.get_consent(subject_id);
625        let history = self.get_history(subject_id);
626        let on_dns_list = self.is_on_do_not_sell_list(subject_id);
627
628        SubjectConsentExport {
629            subject_id: subject_id.to_string(),
630            export_timestamp: now_timestamp_ms(),
631            current_consents: records,
632            consent_history: history,
633            on_do_not_sell_list: on_dns_list,
634        }
635    }
636
637    /// Delete all consent records for a subject (for GDPR right to erasure).
638    pub fn delete_subject_data(&self, subject_id: &str, actor: &str) -> bool {
639        let had_records = {
640            let mut records = self.records.write();
641            records.remove(subject_id).is_some()
642        };
643
644        let had_history = {
645            let mut history = self.history.write();
646            history.remove(subject_id).is_some()
647        };
648
649        {
650            let mut dns_list = self.do_not_sell_list.write();
651            dns_list.remove(subject_id);
652        }
653
654        if had_records || had_history {
655            tracing::info!(
656                "Subject consent data deleted: subject={}, actor={}",
657                subject_id,
658                actor
659            );
660            self.flush_to_disk();
661        }
662
663        had_records || had_history
664    }
665
666    /// Get statistics about consent records.
667    pub fn get_stats(&self) -> ConsentStats {
668        let records = self.records.read();
669        let history = self.history.read();
670        let dns_list = self.do_not_sell_list.read();
671
672        let total_subjects = records.len();
673        let total_records: usize = records.values().map(|r| r.len()).sum();
674        let total_history_entries: usize = history.values().map(|h| h.len()).sum();
675
676        let mut consents_by_purpose: HashMap<String, (usize, usize)> = HashMap::new();
677        for subject_records in records.values() {
678            for (purpose, record) in subject_records {
679                let entry = consents_by_purpose
680                    .entry(purpose.to_str())
681                    .or_insert((0, 0));
682                if record.is_valid() {
683                    entry.0 += 1;
684                } else {
685                    entry.1 += 1;
686                }
687            }
688        }
689
690        ConsentStats {
691            total_subjects,
692            total_records,
693            total_history_entries,
694            do_not_sell_count: dns_list.len(),
695            consents_by_purpose,
696        }
697    }
698}
699
700impl Default for ConsentManager {
701    fn default() -> Self {
702        Self::new()
703    }
704}
705
706/// Export format for subject consent data.
707#[derive(Debug, Clone, Serialize, Deserialize)]
708pub struct SubjectConsentExport {
709    pub subject_id: String,
710    pub export_timestamp: u64,
711    pub current_consents: Vec<ConsentRecord>,
712    pub consent_history: Vec<ConsentHistoryEntry>,
713    pub on_do_not_sell_list: bool,
714}
715
716/// Statistics about consent records.
717#[derive(Debug, Clone, Serialize, Deserialize)]
718pub struct ConsentStats {
719    pub total_subjects: usize,
720    pub total_records: usize,
721    pub total_history_entries: usize,
722    pub do_not_sell_count: usize,
723    /// Map of purpose -> (granted_count, denied_count)
724    pub consents_by_purpose: HashMap<String, (usize, usize)>,
725}
726
727// =============================================================================
728// API Request/Response Types
729// =============================================================================
730
731/// Request to record new consent.
732#[derive(Debug, Clone, Deserialize)]
733pub struct RecordConsentRequest {
734    pub subject_id: String,
735    pub purpose: String,
736    pub granted: bool,
737    #[serde(default = "default_source")]
738    pub source: String,
739    #[serde(default = "default_version")]
740    pub version: String,
741    pub expires_at: Option<u64>,
742    #[serde(default)]
743    pub metadata: Option<HashMap<String, String>>,
744}
745
746fn default_source() -> String {
747    "api".to_string()
748}
749
750fn default_version() -> String {
751    "1.0".to_string()
752}
753
754/// Response for consent operations.
755#[derive(Debug, Clone, Serialize)]
756pub struct ConsentResponse {
757    pub success: bool,
758    pub record: Option<ConsentRecord>,
759    pub error: Option<String>,
760}
761
762/// Response for consent status query.
763#[derive(Debug, Clone, Serialize)]
764pub struct ConsentStatusResponse {
765    pub subject_id: String,
766    pub consents: Vec<ConsentRecord>,
767    pub on_do_not_sell_list: bool,
768}
769
770/// Request to withdraw consent.
771#[derive(Debug, Clone, Deserialize)]
772pub struct WithdrawConsentRequest {
773    pub reason: Option<String>,
774}
775
776// =============================================================================
777// API Handlers
778// =============================================================================
779
780/// Record new consent.
781/// POST /api/v1/compliance/consent
782pub async fn record_consent(
783    State(state): State<AppState>,
784    Json(request): Json<RecordConsentRequest>,
785) -> impl IntoResponse {
786    let purpose = match Purpose::from_str(&request.purpose) {
787        Some(p) => p,
788        None => {
789            return (
790                StatusCode::BAD_REQUEST,
791                Json(ConsentResponse {
792                    success: false,
793                    record: None,
794                    error: Some(format!("Invalid purpose: {}", request.purpose)),
795                }),
796            );
797        }
798    };
799
800    let source = ConsentSource::from_str(&request.source);
801
802    let record = state.consent_manager.record_consent(
803        &request.subject_id,
804        purpose,
805        request.granted,
806        source,
807        &request.version,
808        request.expires_at,
809        request.metadata,
810        "api", // In production, get from authenticated user
811    );
812
813    state.activity.log_write(
814        &format!(
815            "Consent recorded: subject={}, purpose={}, granted={}",
816            request.subject_id, purpose, request.granted
817        ),
818        None,
819    );
820
821    (
822        StatusCode::CREATED,
823        Json(ConsentResponse {
824            success: true,
825            record: Some(record),
826            error: None,
827        }),
828    )
829}
830
831/// Get consent status for a subject.
832/// GET /api/v1/compliance/consent/{subject_id}
833pub async fn get_consent_status(
834    State(state): State<AppState>,
835    Path(subject_id): Path<String>,
836) -> Json<ConsentStatusResponse> {
837    let consents = state.consent_manager.get_consent(&subject_id);
838    let on_dns_list = state.consent_manager.is_on_do_not_sell_list(&subject_id);
839
840    Json(ConsentStatusResponse {
841        subject_id,
842        consents,
843        on_do_not_sell_list: on_dns_list,
844    })
845}
846
847/// Withdraw consent for a specific purpose.
848/// DELETE /api/v1/compliance/consent/{subject_id}/{purpose}
849pub async fn withdraw_consent(
850    State(state): State<AppState>,
851    Path((subject_id, purpose_str)): Path<(String, String)>,
852    Json(request): Json<WithdrawConsentRequest>,
853) -> impl IntoResponse {
854    let purpose = match Purpose::from_str(&purpose_str) {
855        Some(p) => p,
856        None => {
857            return (
858                StatusCode::BAD_REQUEST,
859                Json(ConsentResponse {
860                    success: false,
861                    record: None,
862                    error: Some(format!("Invalid purpose: {}", purpose_str)),
863                }),
864            );
865        }
866    };
867
868    match state.consent_manager.withdraw_consent(
869        &subject_id,
870        purpose,
871        "api", // In production, get from authenticated user
872        request.reason.as_deref(),
873    ) {
874        Ok(record) => {
875            state.activity.log_write(
876                &format!(
877                    "Consent withdrawn: subject={}, purpose={}",
878                    subject_id, purpose
879                ),
880                None,
881            );
882
883            (
884                StatusCode::OK,
885                Json(ConsentResponse {
886                    success: true,
887                    record: Some(record),
888                    error: None,
889                }),
890            )
891        }
892        Err(e) => (
893            StatusCode::NOT_FOUND,
894            Json(ConsentResponse {
895                success: false,
896                record: None,
897                error: Some(e),
898            }),
899        ),
900    }
901}
902
903/// Get consent audit history for a subject.
904/// GET /api/v1/compliance/consent/{subject_id}/history
905pub async fn get_consent_history(
906    State(state): State<AppState>,
907    Path(subject_id): Path<String>,
908    axum::extract::Query(params): axum::extract::Query<HashMap<String, String>>,
909) -> Json<Vec<ConsentHistoryEntry>> {
910    let history = if let Some(purpose_str) = params.get("purpose") {
911        if let Some(purpose) = Purpose::from_str(purpose_str) {
912            state
913                .consent_manager
914                .get_history_for_purpose(&subject_id, purpose)
915        } else {
916            state.consent_manager.get_history(&subject_id)
917        }
918    } else {
919        state.consent_manager.get_history(&subject_id)
920    };
921
922    Json(history)
923}
924
925/// Check consent for a specific purpose.
926/// GET /api/v1/compliance/consent/{subject_id}/check/{purpose}
927pub async fn check_consent_status(
928    State(state): State<AppState>,
929    Path((subject_id, purpose_str)): Path<(String, String)>,
930) -> impl IntoResponse {
931    let purpose = match Purpose::from_str(&purpose_str) {
932        Some(p) => p,
933        None => {
934            return (
935                StatusCode::BAD_REQUEST,
936                Json(serde_json::json!({
937                    "error": format!("Invalid purpose: {}", purpose_str)
938                })),
939            );
940        }
941    };
942
943    let granted = state.consent_manager.check_consent(&subject_id, purpose);
944
945    (
946        StatusCode::OK,
947        Json(serde_json::json!({
948            "subject_id": subject_id,
949            "purpose": purpose.to_str(),
950            "granted": granted
951        })),
952    )
953}
954
955/// Export all consent data for a subject (GDPR data portability).
956/// GET /api/v1/compliance/consent/{subject_id}/export
957pub async fn export_consent_data(
958    State(state): State<AppState>,
959    Path(subject_id): Path<String>,
960) -> Json<SubjectConsentExport> {
961    let export = state.consent_manager.export_subject_data(&subject_id);
962    Json(export)
963}
964
965/// Delete all consent data for a subject (GDPR right to erasure).
966/// DELETE /api/v1/compliance/consent/{subject_id}
967pub async fn delete_consent_data(
968    State(state): State<AppState>,
969    Path(subject_id): Path<String>,
970) -> impl IntoResponse {
971    let deleted = state
972        .consent_manager
973        .delete_subject_data(&subject_id, "api");
974
975    state.activity.log(
976        crate::activity::ActivityType::Delete,
977        &format!("Consent data deleted for subject: {}", subject_id),
978    );
979
980    if deleted {
981        (
982            StatusCode::OK,
983            Json(serde_json::json!({
984                "success": true,
985                "message": format!("Consent data deleted for subject: {}", subject_id)
986            })),
987        )
988    } else {
989        (
990            StatusCode::NOT_FOUND,
991            Json(serde_json::json!({
992                "success": false,
993                "error": format!("No consent data found for subject: {}", subject_id)
994            })),
995        )
996    }
997}
998
999/// Get consent statistics.
1000/// GET /api/v1/compliance/consent/stats
1001pub async fn get_consent_stats(State(state): State<AppState>) -> Json<ConsentStats> {
1002    Json(state.consent_manager.get_stats())
1003}
1004
1005/// Get the CCPA Do Not Sell list.
1006/// GET /api/v1/compliance/do-not-sell
1007pub async fn get_do_not_sell_list(State(state): State<AppState>) -> Json<Vec<String>> {
1008    Json(state.consent_manager.get_do_not_sell_list())
1009}
1010
1011// =============================================================================
1012// Consent Enforcement Helper
1013// =============================================================================
1014
1015/// Check consent before performing a data operation.
1016/// Returns true if the operation is allowed, false otherwise.
1017///
1018/// Usage:
1019/// ```ignore
1020/// if !check_consent(&state, "user123", Purpose::Analytics) {
1021///     return Err("Consent not granted for analytics");
1022/// }
1023/// // Proceed with analytics operation
1024/// ```
1025pub fn check_consent(state: &AppState, subject_id: &str, purpose: Purpose) -> bool {
1026    state.consent_manager.check_consent(subject_id, purpose)
1027}
1028
1029/// Check multiple purposes at once.
1030/// Returns true only if all purposes have valid consent.
1031pub fn check_all_consents(state: &AppState, subject_id: &str, purposes: &[Purpose]) -> bool {
1032    purposes
1033        .iter()
1034        .all(|p| state.consent_manager.check_consent(subject_id, *p))
1035}
1036
1037/// Check if any of the purposes have valid consent.
1038pub fn check_any_consent(state: &AppState, subject_id: &str, purposes: &[Purpose]) -> bool {
1039    purposes
1040        .iter()
1041        .any(|p| state.consent_manager.check_consent(subject_id, *p))
1042}
1043
1044// =============================================================================
1045// Helper Functions
1046// =============================================================================
1047
1048/// Get current timestamp in milliseconds.
1049fn now_timestamp_ms() -> u64 {
1050    SystemTime::now()
1051        .duration_since(UNIX_EPOCH)
1052        .unwrap_or_default()
1053        .as_millis() as u64
1054}
1055
1056/// Generate a unique consent record ID.
1057fn generate_consent_id() -> String {
1058    use std::sync::atomic::{AtomicU64, Ordering};
1059    static COUNTER: AtomicU64 = AtomicU64::new(0);
1060
1061    let count = COUNTER.fetch_add(1, Ordering::SeqCst);
1062    let timestamp = now_timestamp_ms();
1063    format!("consent-{}-{:06}", timestamp, count)
1064}
1065
1066// =============================================================================
1067// Tests
1068// =============================================================================
1069
1070#[cfg(test)]
1071mod tests {
1072    use super::*;
1073
1074    #[test]
1075    fn test_consent_record_creation() {
1076        let record = ConsentRecord::new(
1077            "user123",
1078            Purpose::Marketing,
1079            true,
1080            ConsentSource::WebForm,
1081            "1.0",
1082        );
1083
1084        assert_eq!(record.subject_id, "user123");
1085        assert_eq!(record.purpose, Purpose::Marketing);
1086        assert!(record.granted);
1087        assert!(record.is_valid());
1088    }
1089
1090    #[test]
1091    fn test_consent_expiration() {
1092        let mut record = ConsentRecord::new(
1093            "user123",
1094            Purpose::Analytics,
1095            true,
1096            ConsentSource::Api,
1097            "1.0",
1098        );
1099
1100        // Set expiration to the past
1101        record.expires_at = Some(1000);
1102        assert!(record.is_expired());
1103        assert!(!record.is_valid());
1104    }
1105
1106    #[test]
1107    fn test_consent_manager_basic_operations() {
1108        let manager = ConsentManager::new();
1109
1110        // Record consent
1111        let record = manager.record_consent(
1112            "user123",
1113            Purpose::Marketing,
1114            true,
1115            ConsentSource::WebForm,
1116            "1.0",
1117            None,
1118            None,
1119            "test",
1120        );
1121
1122        assert!(record.granted);
1123
1124        // Check consent
1125        assert!(manager.check_consent("user123", Purpose::Marketing));
1126        assert!(!manager.check_consent("user123", Purpose::Analytics));
1127        assert!(!manager.check_consent("user456", Purpose::Marketing));
1128    }
1129
1130    #[test]
1131    fn test_consent_withdrawal() {
1132        let manager = ConsentManager::new();
1133
1134        // Grant consent
1135        manager.record_consent(
1136            "user123",
1137            Purpose::Marketing,
1138            true,
1139            ConsentSource::WebForm,
1140            "1.0",
1141            None,
1142            None,
1143            "test",
1144        );
1145
1146        assert!(manager.check_consent("user123", Purpose::Marketing));
1147
1148        // Withdraw consent
1149        let result =
1150            manager.withdraw_consent("user123", Purpose::Marketing, "test", Some("User request"));
1151        assert!(result.is_ok());
1152        assert!(!manager.check_consent("user123", Purpose::Marketing));
1153    }
1154
1155    #[test]
1156    fn test_consent_history() {
1157        let manager = ConsentManager::new();
1158
1159        // Record multiple consents
1160        manager.record_consent(
1161            "user123",
1162            Purpose::Marketing,
1163            true,
1164            ConsentSource::WebForm,
1165            "1.0",
1166            None,
1167            None,
1168            "test",
1169        );
1170
1171        manager.record_consent(
1172            "user123",
1173            Purpose::Marketing,
1174            false,
1175            ConsentSource::WebForm,
1176            "1.0",
1177            None,
1178            None,
1179            "test",
1180        );
1181
1182        let history = manager.get_history("user123");
1183        assert_eq!(history.len(), 2);
1184        assert_eq!(history[0].action, ConsentAction::Grant);
1185        assert_eq!(history[1].action, ConsentAction::Withdraw);
1186    }
1187
1188    #[test]
1189    fn test_do_not_sell_list() {
1190        let manager = ConsentManager::new();
1191
1192        // Opt into Do Not Sell
1193        manager.record_consent(
1194            "user123",
1195            Purpose::DoNotSell,
1196            true,
1197            ConsentSource::WebForm,
1198            "1.0",
1199            None,
1200            None,
1201            "test",
1202        );
1203
1204        assert!(manager.is_on_do_not_sell_list("user123"));
1205        assert!(!manager.is_on_do_not_sell_list("user456"));
1206
1207        // Opt out of Do Not Sell
1208        manager.record_consent(
1209            "user123",
1210            Purpose::DoNotSell,
1211            false,
1212            ConsentSource::WebForm,
1213            "1.0",
1214            None,
1215            None,
1216            "test",
1217        );
1218
1219        assert!(!manager.is_on_do_not_sell_list("user123"));
1220    }
1221
1222    #[test]
1223    fn test_purpose_parsing() {
1224        assert_eq!(Purpose::from_str("marketing"), Some(Purpose::Marketing));
1225        assert_eq!(Purpose::from_str("ANALYTICS"), Some(Purpose::Analytics));
1226        assert_eq!(
1227            Purpose::from_str("third_party_sharing"),
1228            Some(Purpose::ThirdPartySharing)
1229        );
1230        assert_eq!(Purpose::from_str("do_not_sell"), Some(Purpose::DoNotSell));
1231        assert_eq!(Purpose::from_str("custom:42"), Some(Purpose::Custom(42)));
1232        assert_eq!(Purpose::from_str("unknown"), None);
1233    }
1234
1235    #[test]
1236    fn test_consent_stats() {
1237        let manager = ConsentManager::new();
1238
1239        manager.record_consent(
1240            "user1",
1241            Purpose::Marketing,
1242            true,
1243            ConsentSource::Api,
1244            "1.0",
1245            None,
1246            None,
1247            "test",
1248        );
1249        manager.record_consent(
1250            "user1",
1251            Purpose::Analytics,
1252            false,
1253            ConsentSource::Api,
1254            "1.0",
1255            None,
1256            None,
1257            "test",
1258        );
1259        manager.record_consent(
1260            "user2",
1261            Purpose::Marketing,
1262            true,
1263            ConsentSource::Api,
1264            "1.0",
1265            None,
1266            None,
1267            "test",
1268        );
1269
1270        let stats = manager.get_stats();
1271        assert_eq!(stats.total_subjects, 2);
1272        assert_eq!(stats.total_records, 3);
1273
1274        let marketing_stats = stats.consents_by_purpose.get("marketing").unwrap();
1275        assert_eq!(marketing_stats.0, 2); // 2 granted
1276        assert_eq!(marketing_stats.1, 0); // 0 denied
1277    }
1278
1279    #[test]
1280    fn test_subject_data_export() {
1281        let manager = ConsentManager::new();
1282
1283        manager.record_consent(
1284            "user123",
1285            Purpose::Marketing,
1286            true,
1287            ConsentSource::Api,
1288            "1.0",
1289            None,
1290            None,
1291            "test",
1292        );
1293        manager.record_consent(
1294            "user123",
1295            Purpose::Analytics,
1296            true,
1297            ConsentSource::Api,
1298            "1.0",
1299            None,
1300            None,
1301            "test",
1302        );
1303
1304        let export = manager.export_subject_data("user123");
1305        assert_eq!(export.subject_id, "user123");
1306        assert_eq!(export.current_consents.len(), 2);
1307        assert_eq!(export.consent_history.len(), 2);
1308    }
1309
1310    #[test]
1311    fn test_subject_data_deletion() {
1312        let manager = ConsentManager::new();
1313
1314        manager.record_consent(
1315            "user123",
1316            Purpose::Marketing,
1317            true,
1318            ConsentSource::Api,
1319            "1.0",
1320            None,
1321            None,
1322            "test",
1323        );
1324
1325        assert!(manager.delete_subject_data("user123", "test"));
1326        assert!(manager.get_consent("user123").is_empty());
1327        assert!(manager.get_history("user123").is_empty());
1328
1329        // Deleting non-existent data should return false
1330        assert!(!manager.delete_subject_data("user123", "test"));
1331    }
1332}