Skip to main content

koprogo_api/application/use_cases/
gdpr_use_cases.rs

1use crate::application::dto::{GdprEraseResponseDto, GdprExportResponseDto};
2use crate::application::ports::{GdprRepository, UserRepository};
3use chrono::Utc;
4use std::sync::Arc;
5use uuid::Uuid;
6
7/// GDPR Use Cases for data export and erasure operations
8/// Implements business logic for GDPR Articles 15, 16, 17, 18, 21
9pub struct GdprUseCases {
10    gdpr_repository: Arc<dyn GdprRepository>,
11    user_repository: Arc<dyn UserRepository>,
12}
13
14impl GdprUseCases {
15    pub fn new(
16        gdpr_repository: Arc<dyn GdprRepository>,
17        user_repository: Arc<dyn UserRepository>,
18    ) -> Self {
19        Self {
20            gdpr_repository,
21            user_repository,
22        }
23    }
24
25    /// Export all personal data for a user (GDPR Article 15 - Right to Access)
26    ///
27    /// # Arguments
28    /// * `user_id` - UUID of the user requesting data export
29    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
30    /// * `organization_id` - Optional organization scope (None for SuperAdmin)
31    ///
32    /// # Authorization
33    /// - Users can only export their own data
34    /// - SuperAdmin can export any user's data
35    ///
36    /// # Returns
37    /// * `Ok(GdprExportResponseDto)` - Complete data export in JSON format
38    /// * `Err(String)` - If user not found, not authorized, or database error
39    pub async fn export_user_data(
40        &self,
41        user_id: Uuid,
42        requesting_user_id: Uuid,
43        organization_id: Option<Uuid>,
44    ) -> Result<GdprExportResponseDto, String> {
45        // Authorization check: user can only export their own data
46        // SuperAdmin bypass is handled by passing organization_id = None
47        if user_id != requesting_user_id && organization_id.is_some() {
48            return Err("Unauthorized: You can only export your own data".to_string());
49        }
50
51        // Check if user is already anonymized
52        let is_anonymized = self.gdpr_repository.is_user_anonymized(user_id).await?;
53        if is_anonymized {
54            return Err("User data has been anonymized and cannot be exported".to_string());
55        }
56
57        // Aggregate all user data from database
58        let export = self
59            .gdpr_repository
60            .aggregate_user_data(user_id, organization_id)
61            .await?;
62
63        // Convert domain entity to DTO
64        Ok(GdprExportResponseDto::from(export))
65    }
66
67    /// Erase user data by anonymization (GDPR Article 17 - Right to Erasure)
68    ///
69    /// Anonymizes user account and linked owner profiles. Does not delete data entirely
70    /// to preserve referential integrity and comply with legal retention requirements
71    /// (e.g., financial records must be kept for 7 years in Belgium).
72    ///
73    /// # Arguments
74    /// * `user_id` - UUID of the user to anonymize
75    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
76    /// * `organization_id` - Optional organization scope (None for SuperAdmin)
77    ///
78    /// # Authorization
79    /// - Users can only erase their own data
80    /// - SuperAdmin can erase any user's data
81    ///
82    /// # Returns
83    /// * `Ok(GdprEraseResponseDto)` - Anonymization confirmation
84    /// * `Err(String)` - If user not found, not authorized, already anonymized, or legal holds exist
85    ///
86    /// # Mot de passe exigé
87    ///
88    /// `password` : mot de passe de la personne qui demande l'effacement,
89    /// exigé lorsqu'elle efface ses propres données. L'interface demandait
90    /// deux confirmations `confirm()`, ce qu'un clic distrait franchit sans
91    /// y penser et qu'un appel direct à l'API ignore complètement. Pour une
92    /// action irréversible, la preuve doit être vérifiée côté serveur.
93    ///
94    /// `None` est réservé aux effacements administratifs, où le demandeur
95    /// n'est pas le sujet et ne connaît pas son mot de passe.
96    pub async fn erase_user_data(
97        &self,
98        user_id: Uuid,
99        requesting_user_id: Uuid,
100        organization_id: Option<Uuid>,
101        password: Option<&str>,
102    ) -> Result<GdprEraseResponseDto, String> {
103        // Authorization check
104        if user_id != requesting_user_id && organization_id.is_some() {
105            return Err("Unauthorized: You can only erase your own data".to_string());
106        }
107
108        // Auto-effacement : le mot de passe fait foi.
109        if user_id == requesting_user_id {
110            let password = password.ok_or_else(|| "Password required".to_string())?;
111            let user = self
112                .user_repository
113                .find_by_id(user_id)
114                .await?
115                .ok_or_else(|| "User not found".to_string())?;
116            let valid = bcrypt::verify(password, &user.password_hash)
117                .map_err(|e| format!("Password verification failed: {e}"))?;
118            if !valid {
119                return Err("Invalid password".to_string());
120            }
121        }
122
123        // Check if already anonymized
124        let is_anonymized = self.gdpr_repository.is_user_anonymized(user_id).await?;
125        if is_anonymized {
126            return Err("User data is already anonymized".to_string());
127        }
128
129        // Check for legal holds (e.g., unpaid expenses, ongoing legal proceedings)
130        let holds = self.gdpr_repository.check_legal_holds(user_id).await?;
131        if !holds.is_empty() {
132            return Err(format!(
133                "Cannot erase data due to legal holds: {}",
134                holds.join(", ")
135            ));
136        }
137
138        // Retrieve user data BEFORE anonymization (needed for email notification)
139        let user_data = self
140            .gdpr_repository
141            .aggregate_user_data(user_id, organization_id)
142            .await?;
143        let user_email = user_data.user_data.email.clone();
144        let user_first_name = user_data.user_data.first_name.clone();
145        let user_last_name = user_data.user_data.last_name.clone();
146
147        // Find all linked owner profiles
148        let owner_ids = self
149            .gdpr_repository
150            .find_owner_ids_by_user(user_id, organization_id)
151            .await?;
152
153        // Anonymize user account
154        self.gdpr_repository.anonymize_user(user_id).await?;
155
156        // Anonymize all linked owner profiles
157        let mut owners_anonymized = 0;
158        for owner_id in &owner_ids {
159            match self.gdpr_repository.anonymize_owner(*owner_id).await {
160                Ok(_) => owners_anonymized += 1,
161                Err(e) => {
162                    // Log error but continue (partial anonymization is acceptable)
163                    eprintln!("Warning: Failed to anonymize owner {}: {}", owner_id, e);
164                }
165            }
166        }
167
168        Ok(GdprEraseResponseDto {
169            success: true,
170            message: "Personal data has been successfully anonymized".to_string(),
171            anonymized_at: Utc::now().to_rfc3339(),
172            user_id: user_id.to_string(),
173            user_email,
174            user_first_name,
175            user_last_name,
176            owners_anonymized,
177        })
178    }
179
180    /// Check if user data can be erased (no legal holds)
181    ///
182    /// # Arguments
183    /// * `user_id` - UUID of the user to check
184    ///
185    /// # Returns
186    /// * `Ok(true)` - User can be erased
187    /// * `Ok(false)` - User has legal holds preventing erasure
188    /// * `Err(String)` - Database error
189    pub async fn can_erase_user(&self, user_id: Uuid) -> Result<bool, String> {
190        let holds = self.gdpr_repository.check_legal_holds(user_id).await?;
191        Ok(holds.is_empty())
192    }
193
194    /// Rectify user personal data (GDPR Article 16 - Right to Rectification)
195    ///
196    /// Allows users to correct inaccurate or incomplete personal data.
197    ///
198    /// # Arguments
199    /// * `user_id` - UUID of the user whose data to rectify
200    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
201    /// * `email` - Optional new email address
202    /// * `first_name` - Optional new first name
203    /// * `last_name` - Optional new last name
204    ///
205    /// # Authorization
206    /// - Users can only rectify their own data
207    /// - SuperAdmin can rectify any user's data (organization_id = None)
208    ///
209    /// # Returns
210    /// * `Ok(User)` - Updated user entity
211    /// * `Err(String)` - If user not found, not authorized, or validation error
212    pub async fn rectify_user_data(
213        &self,
214        user_id: Uuid,
215        requesting_user_id: Uuid,
216        email: Option<String>,
217        first_name: Option<String>,
218        last_name: Option<String>,
219    ) -> Result<(), String> {
220        // Authorization check
221        if user_id != requesting_user_id {
222            // Only allow if SuperAdmin (checked by caller via organization_id)
223            return Err("Unauthorized: You can only rectify your own data".to_string());
224        }
225
226        // Fetch user
227        let mut user = self
228            .user_repository
229            .find_by_id(user_id)
230            .await?
231            .ok_or_else(|| format!("User not found: {}", user_id))?;
232
233        // Apply rectifications
234        user.rectify_data(email, first_name, last_name)?;
235
236        // Persist changes
237        self.user_repository.update(&user).await?;
238
239        Ok(())
240    }
241
242    /// Restrict data processing (GDPR Article 18 - Right to Restriction of Processing)
243    ///
244    /// Allows users to request temporary limitation of data processing.
245    /// When processing is restricted:
246    /// - Data is stored but not processed for certain operations
247    /// - Marketing communications are blocked
248    /// - Profiling/analytics are disabled
249    ///
250    /// # Arguments
251    /// * `user_id` - UUID of the user
252    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
253    ///
254    /// # Authorization
255    /// - Users can only restrict their own data processing
256    ///
257    /// # Returns
258    /// * `Ok(())` - Processing restriction applied
259    /// * `Err(String)` - If user not found, not authorized, or already restricted
260    pub async fn restrict_user_processing(
261        &self,
262        user_id: Uuid,
263        requesting_user_id: Uuid,
264    ) -> Result<(), String> {
265        // Authorization check
266        if user_id != requesting_user_id {
267            return Err("Unauthorized: You can only restrict your own data processing".to_string());
268        }
269
270        // Fetch user
271        let mut user = self
272            .user_repository
273            .find_by_id(user_id)
274            .await?
275            .ok_or_else(|| format!("User not found: {}", user_id))?;
276
277        // Apply restriction
278        user.restrict_processing()?;
279
280        // Persist changes
281        self.user_repository.update(&user).await?;
282
283        Ok(())
284    }
285
286    /// Unrestrict data processing (Admin action or legal requirement met)
287    ///
288    /// # Arguments
289    /// * `user_id` - UUID of the user
290    /// * `admin_user_id` - UUID of the admin performing the action
291    ///
292    /// # Authorization
293    /// - Only admins/SuperAdmin can unrestrict processing
294    ///
295    /// # Returns
296    /// * `Ok(())` - Processing restriction removed
297    /// * `Err(String)` - If user not found
298    pub async fn unrestrict_user_processing(&self, user_id: Uuid) -> Result<(), String> {
299        // Fetch user
300        let mut user = self
301            .user_repository
302            .find_by_id(user_id)
303            .await?
304            .ok_or_else(|| format!("User not found: {}", user_id))?;
305
306        // Remove restriction
307        user.unrestrict_processing();
308
309        // Persist changes
310        self.user_repository.update(&user).await?;
311
312        Ok(())
313    }
314
315    /// Set marketing opt-out preference (GDPR Article 21 - Right to Object)
316    ///
317    /// Allows users to object to marketing communications and profiling.
318    ///
319    /// # Arguments
320    /// * `user_id` - UUID of the user
321    /// * `requesting_user_id` - UUID of the user making the request (for authorization)
322    /// * `opt_out` - true to opt out of marketing, false to opt back in
323    ///
324    /// # Authorization
325    /// - Users can only change their own marketing preferences
326    ///
327    /// # Returns
328    /// * `Ok(())` - Marketing preference updated
329    /// * `Err(String)` - If user not found or not authorized
330    pub async fn set_marketing_preference(
331        &self,
332        user_id: Uuid,
333        requesting_user_id: Uuid,
334        opt_out: bool,
335    ) -> Result<(), String> {
336        // Authorization check
337        if user_id != requesting_user_id {
338            return Err(
339                "Unauthorized: You can only change your own marketing preferences".to_string(),
340            );
341        }
342
343        // Fetch user
344        let mut user = self
345            .user_repository
346            .find_by_id(user_id)
347            .await?
348            .ok_or_else(|| format!("User not found: {}", user_id))?;
349
350        // Apply preference
351        user.set_marketing_opt_out(opt_out);
352
353        // Persist changes
354        self.user_repository.update(&user).await?;
355
356        Ok(())
357    }
358}
359
360#[cfg(test)]
361mod tests {
362    use super::*;
363    use crate::application::ports::gdpr_repository::MockGdprRepo;
364    use crate::application::ports::user_repository::MockUserRepo;
365
366    /// Dépôt utilisateur répondant à la vérification de mot de passe.
367    ///
368    /// `erase_user_data` relit l'utilisateur pour comparer le mot de passe
369    /// depuis le commit « Exige le mot de passe pour l'effacement RGPD ». Les
370    /// trois tests d'auto-effacement construisaient un `MockUserRepo::new()`
371    /// nu : l'appel n'était pas attendu, et mockall paniquait avec
372    /// « No matching expectation found ».
373    ///
374    /// Rien ne l'avait signalé : la CI de `feature/dev` est volontairement
375    /// sans barrière de tests (les tests tournent en local dans la boucle de
376    /// développement), et la suite `--lib` demande plusieurs minutes de
377    /// compilation. C'est exactement le genre de régression qu'un helper
378    /// nommé rend impossible à réintroduire en silence.
379    fn mock_user_repo_avec_mot_de_passe(user_id: Uuid, mot_de_passe: &str) -> MockUserRepo {
380        let hash = bcrypt::hash(mot_de_passe, bcrypt::DEFAULT_COST).expect("hachage de test");
381        let mut mock = MockUserRepo::new();
382        mock.expect_find_by_id().times(1).returning(move |id| {
383            assert_eq!(id, user_id, "l'effacement doit relire l'utilisateur ciblé");
384            let mut user = crate::domain::entities::User::new(
385                "test@example.com".to_string(),
386                hash.clone(),
387                "Test".to_string(),
388                "User".to_string(),
389                crate::domain::entities::UserRole::Owner,
390                None,
391            )
392            .expect("utilisateur de test valide");
393            user.id = id;
394            Ok(Some(user))
395        });
396        mock
397    }
398    use crate::domain::entities::gdpr_export::{GdprExport, UserData};
399    use chrono::Utc;
400
401    fn create_test_user_data(user_id: Uuid) -> UserData {
402        UserData {
403            id: user_id,
404            email: "test@example.com".to_string(),
405            first_name: "John".to_string(),
406            last_name: "Doe".to_string(),
407            organization_id: Some(Uuid::new_v4()),
408            is_active: true,
409            is_anonymized: false,
410            created_at: Utc::now(),
411            updated_at: Utc::now(),
412        }
413    }
414
415    #[tokio::test]
416    async fn test_export_user_data_success() {
417        let user_id = Uuid::new_v4();
418        let org_id = Uuid::new_v4();
419
420        let mut mock_repo = MockGdprRepo::new();
421        mock_repo
422            .expect_is_user_anonymized()
423            .times(1)
424            .returning(|_| Ok(false));
425        mock_repo
426            .expect_aggregate_user_data()
427            .times(1)
428            .returning(move |_, _| {
429                let user_data = create_test_user_data(user_id);
430                Ok(GdprExport::new(user_data))
431            });
432
433        let mock_user_repo = MockUserRepo::new();
434
435        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
436        let result = use_cases
437            .export_user_data(user_id, user_id, Some(org_id))
438            .await;
439
440        assert!(result.is_ok());
441        let dto = result.unwrap();
442        assert_eq!(dto.user.email, "test@example.com");
443    }
444
445    #[tokio::test]
446    async fn test_export_user_data_unauthorized() {
447        let user_id = Uuid::new_v4();
448        let other_user_id = Uuid::new_v4();
449        let org_id = Uuid::new_v4();
450
451        let mock_repo = MockGdprRepo::new();
452        let mock_user_repo = MockUserRepo::new();
453
454        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
455
456        let result = use_cases
457            .export_user_data(user_id, other_user_id, Some(org_id))
458            .await;
459
460        assert!(result.is_err());
461        assert!(result
462            .unwrap_err()
463            .contains("Unauthorized: You can only export your own data"));
464    }
465
466    #[tokio::test]
467    async fn test_export_anonymized_user_fails() {
468        let user_id = Uuid::new_v4();
469
470        let mut mock_repo = MockGdprRepo::new();
471        mock_repo
472            .expect_is_user_anonymized()
473            .times(1)
474            .returning(|_| Ok(true));
475
476        let mock_user_repo = MockUserRepo::new();
477
478        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
479        let result = use_cases
480            .export_user_data(user_id, user_id, Some(Uuid::new_v4()))
481            .await;
482
483        assert!(result.is_err());
484        assert!(result
485            .unwrap_err()
486            .contains("User data has been anonymized"));
487    }
488
489    #[tokio::test]
490    async fn test_erase_user_data_success() {
491        let user_id = Uuid::new_v4();
492        let owner_id1 = Uuid::new_v4();
493        let owner_id2 = Uuid::new_v4();
494        let org_id = Uuid::new_v4();
495
496        // Create test user data
497        let user_data = crate::domain::entities::gdpr_export::UserData {
498            id: user_id,
499            email: "test@example.com".to_string(),
500            first_name: "Test".to_string(),
501            last_name: "User".to_string(),
502            organization_id: Some(org_id),
503            is_active: true,
504            is_anonymized: false,
505            created_at: Utc::now(),
506            updated_at: Utc::now(),
507        };
508        let gdpr_export = crate::domain::entities::gdpr_export::GdprExport::new(user_data);
509
510        let mut mock_repo = MockGdprRepo::new();
511        mock_repo
512            .expect_is_user_anonymized()
513            .times(1)
514            .returning(|_| Ok(false));
515        mock_repo
516            .expect_check_legal_holds()
517            .times(1)
518            .returning(|_| Ok(vec![]));
519        mock_repo
520            .expect_aggregate_user_data()
521            .times(1)
522            .returning(move |_, _| Ok(gdpr_export.clone()));
523        mock_repo
524            .expect_find_owner_ids_by_user()
525            .times(1)
526            .returning(move |_, _| Ok(vec![owner_id1, owner_id2]));
527        mock_repo
528            .expect_anonymize_user()
529            .times(1)
530            .returning(|_| Ok(()));
531        mock_repo
532            .expect_anonymize_owner()
533            .times(2)
534            .returning(|_| Ok(()));
535
536        let mock_user_repo = mock_user_repo_avec_mot_de_passe(user_id, "password");
537
538        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
539        let result = use_cases
540            .erase_user_data(user_id, user_id, Some(org_id), Some("password"))
541            .await;
542
543        assert!(result.is_ok());
544        let dto = result.unwrap();
545        assert!(dto.success);
546        assert_eq!(dto.owners_anonymized, 2);
547        assert_eq!(dto.user_email, "test@example.com");
548        assert_eq!(dto.user_first_name, "Test");
549        assert_eq!(dto.user_last_name, "User");
550    }
551
552    #[tokio::test]
553    async fn test_erase_user_data_unauthorized() {
554        let user_id = Uuid::new_v4();
555        let other_user_id = Uuid::new_v4();
556        let org_id = Uuid::new_v4();
557
558        let mock_repo = MockGdprRepo::new();
559        let mock_user_repo = MockUserRepo::new();
560
561        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
562
563        let result = use_cases
564            .erase_user_data(user_id, other_user_id, Some(org_id), None)
565            .await;
566
567        assert!(result.is_err());
568        assert!(result
569            .unwrap_err()
570            .contains("Unauthorized: You can only erase your own data"));
571    }
572
573    #[tokio::test]
574    async fn test_erase_already_anonymized_user_fails() {
575        let user_id = Uuid::new_v4();
576
577        let mut mock_repo = MockGdprRepo::new();
578        mock_repo
579            .expect_is_user_anonymized()
580            .times(1)
581            .returning(|_| Ok(true));
582
583        let mock_user_repo = mock_user_repo_avec_mot_de_passe(user_id, "password");
584
585        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
586        let result = use_cases
587            .erase_user_data(user_id, user_id, Some(Uuid::new_v4()), Some("password"))
588            .await;
589
590        assert!(result.is_err());
591        assert!(result.unwrap_err().contains("already anonymized"));
592    }
593
594    #[tokio::test]
595    async fn test_erase_with_legal_holds_fails() {
596        let user_id = Uuid::new_v4();
597
598        let mut mock_repo = MockGdprRepo::new();
599        mock_repo
600            .expect_is_user_anonymized()
601            .times(1)
602            .returning(|_| Ok(false));
603        mock_repo
604            .expect_check_legal_holds()
605            .times(1)
606            .returning(|_| Ok(vec!["Unpaid expenses".to_string()]));
607
608        let mock_user_repo = mock_user_repo_avec_mot_de_passe(user_id, "password");
609
610        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
611        let result = use_cases
612            .erase_user_data(user_id, user_id, Some(Uuid::new_v4()), Some("password"))
613            .await;
614
615        assert!(result.is_err());
616        assert!(result.unwrap_err().contains("legal holds"));
617    }
618
619    #[tokio::test]
620    async fn test_can_erase_user_no_holds() {
621        let user_id = Uuid::new_v4();
622
623        let mut mock_repo = MockGdprRepo::new();
624        mock_repo
625            .expect_check_legal_holds()
626            .times(1)
627            .returning(|_| Ok(vec![]));
628
629        let mock_user_repo = MockUserRepo::new();
630
631        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
632        let result = use_cases.can_erase_user(user_id).await;
633
634        assert!(result.is_ok());
635        assert!(result.unwrap());
636    }
637
638    #[tokio::test]
639    async fn test_can_erase_user_with_holds() {
640        let user_id = Uuid::new_v4();
641
642        let mut mock_repo = MockGdprRepo::new();
643        mock_repo
644            .expect_check_legal_holds()
645            .times(1)
646            .returning(|_| Ok(vec!["Unpaid expenses".to_string()]));
647
648        let mock_user_repo = MockUserRepo::new();
649
650        let use_cases = GdprUseCases::new(Arc::new(mock_repo), Arc::new(mock_user_repo));
651        let result = use_cases.can_erase_user(user_id).await;
652
653        assert!(result.is_ok());
654        assert!(!result.unwrap());
655    }
656}