gestura_core_security/
gdpr.rs

1//! GDPR compliance features for Gestura.app
2//! Provides data export, deletion, consent management, and audit trails
3
4use gestura_core_config::AppConfig;
5use gestura_core_foundation::AppError;
6use std::collections::HashMap;
7use std::path::PathBuf;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10
11/// GDPR data categories
12#[derive(Debug, Clone, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
13pub enum DataCategory {
14    PersonalIdentifiers,
15    VoiceRecordings,
16    BiometricData,
17    DeviceData,
18    UsageAnalytics,
19    ConfigurationData,
20    LogData,
21}
22
23/// Consent status for data processing
24#[derive(Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
25pub enum ConsentStatus {
26    Granted,
27    Denied,
28    Withdrawn,
29    Pending,
30}
31
32/// Consent record
33#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
34pub struct ConsentRecord {
35    pub user_id: String,
36    pub category: DataCategory,
37    pub status: ConsentStatus,
38    pub granted_at: Option<chrono::DateTime<chrono::Utc>>,
39    pub withdrawn_at: Option<chrono::DateTime<chrono::Utc>>,
40    pub purpose: String,
41    pub legal_basis: String,
42}
43
44/// Data audit entry
45#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
46pub struct DataAuditEntry {
47    pub timestamp: chrono::DateTime<chrono::Utc>,
48    pub user_id: String,
49    pub operation: DataOperation,
50    pub category: DataCategory,
51    pub details: String,
52    pub legal_basis: String,
53}
54
55/// Data operations for audit trail
56#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
57pub enum DataOperation {
58    Collect,
59    Process,
60    Store,
61    Access,
62    Modify,
63    Delete,
64    Export,
65    Share,
66}
67
68/// GDPR compliance manager
69pub struct GdprManager {
70    consent_records: Arc<RwLock<HashMap<String, Vec<ConsentRecord>>>>,
71    audit_trail: Arc<RwLock<Vec<DataAuditEntry>>>,
72    data_locations: Arc<RwLock<HashMap<DataCategory, Vec<PathBuf>>>>,
73    max_audit_entries: usize,
74}
75
76impl GdprManager {
77    /// Create a new GDPR manager
78    pub fn new(max_audit_entries: usize) -> Self {
79        Self {
80            consent_records: Arc::new(RwLock::new(HashMap::new())),
81            audit_trail: Arc::new(RwLock::new(Vec::new())),
82            data_locations: Arc::new(RwLock::new(HashMap::new())),
83            max_audit_entries,
84        }
85    }
86
87    /// Register consent for data processing
88    pub async fn register_consent(
89        &self,
90        user_id: String,
91        category: DataCategory,
92        purpose: String,
93        legal_basis: String,
94    ) -> Result<(), AppError> {
95        let consent = ConsentRecord {
96            user_id: user_id.clone(),
97            category: category.clone(),
98            status: ConsentStatus::Granted,
99            granted_at: Some(chrono::Utc::now()),
100            withdrawn_at: None,
101            purpose,
102            legal_basis: legal_basis.clone(),
103        };
104
105        let mut consents = self.consent_records.write().await;
106        consents
107            .entry(user_id.clone())
108            .or_insert_with(Vec::new)
109            .push(consent);
110
111        // Audit the consent
112        self.audit_data_operation(
113            user_id.clone(),
114            DataOperation::Collect,
115            category,
116            "Consent granted".to_string(),
117            legal_basis,
118        )
119        .await;
120
121        tracing::info!("Consent registered for user: {}", user_id);
122        Ok(())
123    }
124
125    /// Withdraw consent for data processing
126    pub async fn withdraw_consent(
127        &self,
128        user_id: &str,
129        category: &DataCategory,
130    ) -> Result<(), AppError> {
131        let mut consents = self.consent_records.write().await;
132
133        if let Some(user_consents) = consents.get_mut(user_id) {
134            for consent in user_consents.iter_mut() {
135                if consent.category == *category && consent.status == ConsentStatus::Granted {
136                    consent.status = ConsentStatus::Withdrawn;
137                    consent.withdrawn_at = Some(chrono::Utc::now());
138
139                    // Audit the withdrawal
140                    self.audit_data_operation(
141                        user_id.to_string(),
142                        DataOperation::Modify,
143                        category.clone(),
144                        "Consent withdrawn".to_string(),
145                        consent.legal_basis.clone(),
146                    )
147                    .await;
148
149                    tracing::info!(
150                        "Consent withdrawn for user: {} category: {:?}",
151                        user_id,
152                        category
153                    );
154                    return Ok(());
155                }
156            }
157        }
158
159        Err(AppError::Io(std::io::Error::new(
160            std::io::ErrorKind::NotFound,
161            "Consent record not found",
162        )))
163    }
164
165    /// Check if user has given consent for a data category
166    pub async fn has_consent(&self, user_id: &str, category: &DataCategory) -> bool {
167        let consents = self.consent_records.read().await;
168
169        if let Some(user_consents) = consents.get(user_id) {
170            user_consents
171                .iter()
172                .any(|c| c.category == *category && c.status == ConsentStatus::Granted)
173        } else {
174            false
175        }
176    }
177
178    /// Export all user data (GDPR Article 20)
179    pub async fn export_user_data(&self, user_id: &str) -> Result<serde_json::Value, AppError> {
180        // Audit the export request
181        self.audit_data_operation(
182            user_id.to_string(),
183            DataOperation::Export,
184            DataCategory::PersonalIdentifiers,
185            "Data export requested".to_string(),
186            "GDPR Article 20".to_string(),
187        )
188        .await;
189
190        let mut export_data = serde_json::Map::new();
191
192        // Export consent records
193        let user_consents: Vec<ConsentRecord> = {
194            let consents = self.consent_records.read().await;
195            consents.get(user_id).cloned().unwrap_or_default()
196        };
197        if !user_consents.is_empty() {
198            export_data.insert(
199                "consents".to_string(),
200                serde_json::to_value(&user_consents)?,
201            );
202        }
203
204        // Export configuration data
205        let config = AppConfig::load_from_path(AppConfig::default_path());
206        export_data.insert("configuration".to_string(), serde_json::to_value(&config)?);
207
208        // Export audit trail for this user
209        let audit_entries = self.get_user_audit_trail(user_id).await;
210        export_data.insert(
211            "audit_trail".to_string(),
212            serde_json::to_value(&audit_entries)?,
213        );
214
215        // Export voice data locations (metadata only)
216        let voice_locations: Vec<PathBuf> = {
217            let data_locations = self.data_locations.read().await;
218            data_locations
219                .get(&DataCategory::VoiceRecordings)
220                .cloned()
221                .unwrap_or_default()
222        };
223        if !voice_locations.is_empty() {
224            let voice_metadata: Vec<_> = voice_locations
225                .iter()
226                .map(|path| {
227                    serde_json::json!({
228                        "path": path.to_string_lossy(),
229                        "category": "voice_recording"
230                    })
231                })
232                .collect();
233            export_data.insert(
234                "voice_data_locations".to_string(),
235                serde_json::Value::Array(voice_metadata),
236            );
237        }
238
239        tracing::info!("Data export completed for user: {}", user_id);
240        Ok(serde_json::Value::Object(export_data))
241    }
242
243    /// Register data location for tracking
244    pub async fn register_data_location(&self, category: DataCategory, path: PathBuf) {
245        let mut locations = self.data_locations.write().await;
246        locations
247            .entry(category)
248            .or_insert_with(Vec::new)
249            .push(path);
250    }
251
252    /// Audit data operation
253    pub async fn audit_data_operation(
254        &self,
255        user_id: String,
256        operation: DataOperation,
257        category: DataCategory,
258        details: String,
259        legal_basis: String,
260    ) {
261        let entry = DataAuditEntry {
262            timestamp: chrono::Utc::now(),
263            user_id,
264            operation,
265            category,
266            details,
267            legal_basis,
268        };
269
270        let mut audit_trail = self.audit_trail.write().await;
271        audit_trail.push(entry);
272
273        // Trim audit trail if needed
274        if audit_trail.len() > self.max_audit_entries {
275            audit_trail.remove(0);
276        }
277    }
278
279    /// Get audit trail for a specific user
280    pub async fn get_user_audit_trail(&self, user_id: &str) -> Vec<DataAuditEntry> {
281        let audit_trail = self.audit_trail.read().await;
282        audit_trail
283            .iter()
284            .filter(|entry| entry.user_id == user_id)
285            .cloned()
286            .collect()
287    }
288
289    /// Get full audit trail
290    pub async fn get_audit_trail(&self, limit: Option<usize>) -> Vec<DataAuditEntry> {
291        let audit_trail = self.audit_trail.read().await;
292        if let Some(limit) = limit {
293            audit_trail.iter().rev().take(limit).cloned().collect()
294        } else {
295            audit_trail.clone()
296        }
297    }
298
299    /// Get consent status for user
300    pub async fn get_user_consents(&self, user_id: &str) -> Vec<ConsentRecord> {
301        let consents = self.consent_records.read().await;
302        consents.get(user_id).cloned().unwrap_or_default()
303    }
304}
305
306impl GdprManager {
307    /// Delete all user data (GDPR Article 17 - Right to be forgotten).
308    ///
309    /// If `verify` is `true`, this **does not** delete anything; it returns a list of
310    /// items that *would* be deleted.
311    ///
312    /// Note: this function intentionally avoids holding async lock guards across
313    /// `.await` points (e.g., while deleting files) to prevent deadlocks and satisfy
314    /// `clippy::await_holding_lock`.
315    pub async fn delete_user_data(
316        &self,
317        user_id: &str,
318        verify: bool,
319    ) -> Result<Vec<String>, AppError> {
320        let mut deleted_items = Vec::new();
321
322        // Audit the deletion request
323        self.audit_data_operation(
324            user_id.to_string(),
325            DataOperation::Delete,
326            DataCategory::PersonalIdentifiers,
327            "Data deletion requested".to_string(),
328            "GDPR Article 17".to_string(),
329        )
330        .await;
331
332        // Delete consent records
333        {
334            let mut consents = self.consent_records.write().await;
335            if consents.remove(user_id).is_some() {
336                deleted_items.push("Consent records".to_string());
337            }
338        }
339
340        // Gather candidate file paths without holding the lock across await.
341        let candidates: Vec<(DataCategory, PathBuf)> = {
342            let data_locations = self.data_locations.read().await;
343            data_locations
344                .iter()
345                .flat_map(|(category, locations)| {
346                    locations
347                        .iter()
348                        .filter(|p| p.to_string_lossy().contains(user_id))
349                        .cloned()
350                        .map(|p| (category.clone(), p))
351                        .collect::<Vec<_>>()
352                })
353                .collect()
354        };
355
356        for (category, path) in candidates {
357            if verify {
358                deleted_items.push(format!("{category:?}: {}", path.display()));
359                continue;
360            }
361
362            match tokio::fs::remove_file(&path).await {
363                Ok(()) => {
364                    deleted_items.push(format!("{category:?}: {}", path.display()));
365                    tracing::info!(category = ?category, path = %path.display(), "Deleted user data file");
366                }
367                Err(e) => {
368                    // Match legacy GUI behavior: log and continue.
369                    tracing::error!(
370                        category = ?category,
371                        path = %path.display(),
372                        error = %e,
373                        "Failed to delete user data file"
374                    );
375                }
376            }
377        }
378
379        // Remove user from audit trail (anonymize)
380        if !verify {
381            let mut audit_trail = self.audit_trail.write().await;
382            for entry in audit_trail.iter_mut() {
383                if entry.user_id == user_id {
384                    entry.user_id = "[DELETED]".to_string();
385                }
386            }
387        }
388
389        if verify {
390            tracing::info!(user_id = %user_id, "Data deletion verification completed");
391        } else {
392            tracing::info!(user_id = %user_id, "Data deletion completed");
393        }
394
395        Ok(deleted_items)
396    }
397}
398
399impl GdprManager {
400    /// Generate a privacy report summarizing stored GDPR-related metadata.
401    ///
402    /// This report is intended for administrative visibility and does not contain raw
403    /// user data. It includes counts for consents, audit entries, and tracked data
404    /// locations.
405    pub async fn generate_privacy_report(&self) -> serde_json::Value {
406        let (total_users, total_consents, granted, withdrawn, denied, pending) = {
407            let consents = self.consent_records.read().await;
408            let total_users = consents.len();
409            let total_consents: usize = consents.values().map(|v| v.len()).sum();
410
411            let mut granted = 0usize;
412            let mut withdrawn = 0usize;
413            let mut denied = 0usize;
414            let mut pending = 0usize;
415
416            for c in consents.values().flatten() {
417                match c.status {
418                    ConsentStatus::Granted => granted += 1,
419                    ConsentStatus::Withdrawn => withdrawn += 1,
420                    ConsentStatus::Denied => denied += 1,
421                    ConsentStatus::Pending => pending += 1,
422                }
423            }
424
425            (
426                total_users,
427                total_consents,
428                granted,
429                withdrawn,
430                denied,
431                pending,
432            )
433        };
434
435        let total_audit_entries = {
436            let audit_trail = self.audit_trail.read().await;
437            audit_trail.len()
438        };
439
440        let (total_data_locations, categories) = {
441            let data_locations = self.data_locations.read().await;
442            let total_data_locations: usize = data_locations.values().map(|v| v.len()).sum();
443            let categories = data_locations.keys().cloned().collect::<Vec<_>>();
444            (total_data_locations, categories)
445        };
446
447        serde_json::json!({
448            "generated_at": chrono::Utc::now(),
449            "summary": {
450                "total_users": total_users,
451                "total_consents": total_consents,
452                "total_audit_entries": total_audit_entries,
453                "total_data_locations": total_data_locations
454            },
455            "consent_breakdown": {
456                "granted": granted,
457                "withdrawn": withdrawn,
458                "denied": denied,
459                "pending": pending
460            },
461            "data_categories": categories
462        })
463    }
464}
465
466/// Global GDPR manager instance
467static GDPR_MANAGER: tokio::sync::OnceCell<GdprManager> = tokio::sync::OnceCell::const_new();
468
469/// Get the global GDPR manager
470pub async fn get_gdpr_manager() -> &'static GdprManager {
471    GDPR_MANAGER
472        .get_or_init(|| async { GdprManager::new(50000) })
473        .await
474}
475
476#[cfg(test)]
477mod tests {
478    use super::*;
479    use chrono::TimeZone;
480
481    /// Assert that a value can round-trip through JSON (via `serde_json::Value`) without loss.
482    fn assert_json_roundtrip<T>(value: &T)
483    where
484        T: serde::Serialize + for<'de> serde::Deserialize<'de>,
485    {
486        let v1 = serde_json::to_value(value).expect("serialize");
487        let decoded: T = serde_json::from_value(v1.clone()).expect("deserialize");
488        let v2 = serde_json::to_value(&decoded).expect("serialize (round-tripped)");
489        assert_eq!(v1, v2);
490    }
491
492    #[tokio::test]
493    async fn test_gdpr_manager_basic_flow() {
494        let manager = GdprManager::new(1000);
495
496        // Consent registration
497        manager
498            .register_consent(
499                "test-user".to_string(),
500                DataCategory::VoiceRecordings,
501                "Voice processing".to_string(),
502                "User consent".to_string(),
503            )
504            .await
505            .unwrap();
506
507        assert!(
508            manager
509                .has_consent("test-user", &DataCategory::VoiceRecordings)
510                .await
511        );
512
513        // Withdrawal
514        manager
515            .withdraw_consent("test-user", &DataCategory::VoiceRecordings)
516            .await
517            .unwrap();
518        assert!(
519            !manager
520                .has_consent("test-user", &DataCategory::VoiceRecordings)
521                .await
522        );
523
524        // Export
525        let export = manager.export_user_data("test-user").await.unwrap();
526        assert!(export.get("configuration").is_some());
527
528        // Privacy report (shape)
529        let report = manager.generate_privacy_report().await;
530        assert!(report.get("summary").is_some());
531    }
532
533    #[tokio::test]
534    async fn test_delete_user_data_verify_and_delete() {
535        let manager = GdprManager::new(1000);
536        let user_id = "user-to-delete";
537
538        manager
539            .register_consent(
540                user_id.to_string(),
541                DataCategory::PersonalIdentifiers,
542                "Testing".to_string(),
543                "Consent".to_string(),
544            )
545            .await
546            .unwrap();
547
548        // Create a temp file path containing the user_id
549        let mut tmp_path = std::env::temp_dir();
550        tmp_path.push(format!(
551            "gestura_gdpr_test_{user_id}_{}",
552            uuid::Uuid::new_v4()
553        ));
554        std::fs::write(&tmp_path, b"test").unwrap();
555
556        manager
557            .register_data_location(DataCategory::VoiceRecordings, tmp_path.clone())
558            .await;
559
560        // Verify mode should not delete
561        let preview = manager.delete_user_data(user_id, true).await.unwrap();
562        assert!(preview.iter().any(|s| s.contains(user_id)));
563        assert!(tmp_path.exists(), "file should still exist in verify mode");
564
565        // Non-verify should delete
566        let deleted = manager.delete_user_data(user_id, false).await.unwrap();
567        assert!(deleted.iter().any(|s| s.contains(user_id)));
568        assert!(!tmp_path.exists(), "file should be deleted");
569    }
570
571    #[test]
572    fn test_gdpr_models_serde_roundtrip() {
573        assert_json_roundtrip(&DataCategory::VoiceRecordings);
574        assert_json_roundtrip(&ConsentStatus::Granted);
575        assert_json_roundtrip(&DataOperation::Collect);
576
577        let fixed_ts = chrono::Utc
578            .with_ymd_and_hms(2025, 1, 1, 0, 0, 0)
579            .single()
580            .expect("valid timestamp");
581
582        let consent = ConsentRecord {
583            user_id: "user-1".to_string(),
584            category: DataCategory::VoiceRecordings,
585            status: ConsentStatus::Granted,
586            granted_at: Some(fixed_ts),
587            withdrawn_at: None,
588            purpose: "Voice processing".to_string(),
589            legal_basis: "Consent".to_string(),
590        };
591        assert_json_roundtrip(&consent);
592
593        let audit = DataAuditEntry {
594            timestamp: fixed_ts,
595            user_id: "user-1".to_string(),
596            operation: DataOperation::Access,
597            category: DataCategory::VoiceRecordings,
598            details: "Accessed voice data".to_string(),
599            legal_basis: "Consent".to_string(),
600        };
601        assert_json_roundtrip(&audit);
602    }
603
604    #[tokio::test]
605    async fn test_consent_timestamps_invariants() {
606        let manager = GdprManager::new(1000);
607        let user_id = "test-user-invariants";
608
609        manager
610            .register_consent(
611                user_id.to_string(),
612                DataCategory::VoiceRecordings,
613                "Voice processing".to_string(),
614                "Consent".to_string(),
615            )
616            .await
617            .unwrap();
618
619        let consents = manager.get_user_consents(user_id).await;
620        let c = consents
621            .iter()
622            .find(|c| c.category == DataCategory::VoiceRecordings)
623            .unwrap();
624        assert_eq!(c.status, ConsentStatus::Granted);
625        assert!(c.granted_at.is_some());
626        assert!(c.withdrawn_at.is_none());
627
628        manager
629            .withdraw_consent(user_id, &DataCategory::VoiceRecordings)
630            .await
631            .unwrap();
632
633        let consents = manager.get_user_consents(user_id).await;
634        let c = consents
635            .iter()
636            .find(|c| c.category == DataCategory::VoiceRecordings)
637            .unwrap();
638        assert_eq!(c.status, ConsentStatus::Withdrawn);
639        assert!(c.granted_at.is_some());
640        assert!(c.withdrawn_at.is_some());
641    }
642}