1use chrono::{DateTime, Duration, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use uuid::Uuid;
15
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
18#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
19#[serde(rename_all = "lowercase")]
20pub enum ReviewFrequency {
21 Monthly,
23 Quarterly,
25 Annually,
27}
28
29impl ReviewFrequency {
30 pub fn duration(&self) -> Duration {
32 match self {
33 ReviewFrequency::Monthly => Duration::days(30),
34 ReviewFrequency::Quarterly => Duration::days(90),
35 ReviewFrequency::Annually => Duration::days(365),
36 }
37 }
38
39 pub fn next_review_date(&self, from: DateTime<Utc>) -> DateTime<Utc> {
41 from + self.duration()
42 }
43}
44
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "lowercase")]
48pub enum ReviewStatus {
49 Pending,
51 InProgress,
53 Completed,
55 Cancelled,
57}
58
59#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum ReviewType {
63 UserAccess,
65 PrivilegedAccess,
67 ApiToken,
69 ResourceAccess,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct UserAccessInfo {
76 pub user_id: Uuid,
78 pub username: String,
80 pub email: String,
82 pub roles: Vec<String>,
84 pub permissions: Vec<String>,
86 pub last_login: Option<DateTime<Utc>>,
88 pub access_granted: DateTime<Utc>,
90 pub days_inactive: Option<u64>,
92 pub is_active: bool,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct PrivilegedAccessInfo {
99 pub user_id: Uuid,
101 pub username: String,
103 pub roles: Vec<String>,
105 pub mfa_enabled: bool,
107 pub justification: Option<String>,
109 pub justification_expires: Option<DateTime<Utc>>,
111 pub recent_actions_count: u64,
113 pub last_privileged_action: Option<DateTime<Utc>>,
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ApiTokenInfo {
120 pub token_id: String,
122 pub name: Option<String>,
124 pub owner_id: Uuid,
126 pub scopes: Vec<String>,
128 pub created_at: DateTime<Utc>,
130 pub last_used: Option<DateTime<Utc>>,
132 pub expires_at: Option<DateTime<Utc>>,
134 pub days_unused: Option<u64>,
136 pub is_active: bool,
138}
139
140#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ResourceAccessInfo {
143 pub resource_type: String,
145 pub resource_id: String,
147 pub users_with_access: Vec<Uuid>,
149 pub access_levels: HashMap<Uuid, String>,
151 pub last_access: HashMap<Uuid, Option<DateTime<Utc>>>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ReviewFindings {
158 pub inactive_users: u32,
160 pub excessive_permissions: u32,
162 pub no_recent_access: u32,
164 pub privileged_without_mfa: u32,
166 pub unused_tokens: u32,
168 pub excessive_scopes: u32,
170 pub expiring_soon: u32,
172 pub custom: HashMap<String, u32>,
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ReviewActions {
179 pub users_revoked: u32,
181 pub permissions_reduced: u32,
183 pub mfa_enforced: u32,
185 pub tokens_revoked: u32,
187 pub tokens_rotated: u32,
189 pub scopes_reduced: u32,
191 pub custom: HashMap<String, u32>,
193}
194
195#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct AccessReview {
198 pub review_id: String,
200 pub review_type: ReviewType,
202 pub status: ReviewStatus,
204 pub review_date: DateTime<Utc>,
206 pub due_date: DateTime<Utc>,
208 pub total_items: u32,
210 pub items_reviewed: u32,
212 pub findings: ReviewFindings,
214 pub actions_taken: ReviewActions,
216 pub pending_approvals: u32,
218 pub next_review_date: DateTime<Utc>,
220 pub metadata: HashMap<String, serde_json::Value>,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct UserReviewItem {
227 pub review_id: String,
229 pub user_id: Uuid,
231 pub access_info: UserAccessInfo,
233 pub status: String,
235 pub manager_id: Option<Uuid>,
237 pub approval_deadline: Option<DateTime<Utc>>,
239 pub approved_by: Option<Uuid>,
241 pub approved_at: Option<DateTime<Utc>>,
243 pub rejection_reason: Option<String>,
245}
246
247#[derive(Debug, Clone, Serialize, Deserialize)]
249#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
250pub struct AccessReviewConfig {
251 pub enabled: bool,
253 pub user_review: UserReviewConfig,
255 pub privileged_review: PrivilegedReviewConfig,
257 pub token_review: TokenReviewConfig,
259 pub resource_review: ResourceReviewConfig,
261 pub notifications: NotificationConfig,
263}
264
265#[derive(Debug, Clone, Serialize, Deserialize)]
267#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
268pub struct UserReviewConfig {
269 pub enabled: bool,
271 pub frequency: ReviewFrequency,
273 pub inactive_threshold_days: u64,
275 pub auto_revoke_inactive: bool,
277 pub require_manager_approval: bool,
279 pub approval_timeout_days: u64,
281}
282
283#[derive(Debug, Clone, Serialize, Deserialize)]
285#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
286pub struct PrivilegedReviewConfig {
287 pub enabled: bool,
289 pub frequency: ReviewFrequency,
291 pub require_mfa: bool,
293 pub require_justification: bool,
295 pub alert_on_escalation: bool,
297}
298
299#[derive(Debug, Clone, Serialize, Deserialize)]
301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
302pub struct TokenReviewConfig {
303 pub enabled: bool,
305 pub frequency: ReviewFrequency,
307 pub unused_threshold_days: u64,
309 pub auto_revoke_unused: bool,
311 pub rotation_threshold_days: u64,
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
318pub struct ResourceReviewConfig {
319 pub enabled: bool,
321 pub frequency: ReviewFrequency,
323 pub sensitive_resources: Vec<String>,
325}
326
327#[derive(Debug, Clone, Serialize, Deserialize)]
329#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
330pub struct NotificationConfig {
331 pub enabled: bool,
333 pub channels: Vec<String>,
335 pub recipients: Vec<String>,
337}
338
339impl Default for AccessReviewConfig {
340 fn default() -> Self {
341 Self {
342 enabled: false,
343 user_review: UserReviewConfig {
344 enabled: true,
345 frequency: ReviewFrequency::Quarterly,
346 inactive_threshold_days: 90,
347 auto_revoke_inactive: true,
348 require_manager_approval: true,
349 approval_timeout_days: 30,
350 },
351 privileged_review: PrivilegedReviewConfig {
352 enabled: true,
353 frequency: ReviewFrequency::Monthly,
354 require_mfa: true,
355 require_justification: true,
356 alert_on_escalation: true,
357 },
358 token_review: TokenReviewConfig {
359 enabled: true,
360 frequency: ReviewFrequency::Monthly,
361 unused_threshold_days: 90,
362 auto_revoke_unused: true,
363 rotation_threshold_days: 30,
364 },
365 resource_review: ResourceReviewConfig {
366 enabled: true,
367 frequency: ReviewFrequency::Quarterly,
368 sensitive_resources: vec![
369 "billing".to_string(),
370 "user_data".to_string(),
371 "audit_logs".to_string(),
372 "security_settings".to_string(),
373 ],
374 },
375 notifications: NotificationConfig {
376 enabled: true,
377 channels: vec!["email".to_string()],
378 recipients: vec!["security_team".to_string(), "compliance_team".to_string()],
379 },
380 }
381 }
382}
383
384pub struct AccessReviewEngine {
390 config: AccessReviewConfig,
391 active_reviews: HashMap<String, AccessReview>,
393 user_review_items: HashMap<String, HashMap<Uuid, UserReviewItem>>,
395}
396
397impl AccessReviewEngine {
398 pub fn new(config: AccessReviewConfig) -> Self {
400 Self {
401 config,
402 active_reviews: HashMap::new(),
403 user_review_items: HashMap::new(),
404 }
405 }
406
407 pub fn generate_review_id(&self, review_type: ReviewType, date: DateTime<Utc>) -> String {
409 let type_str = match review_type {
410 ReviewType::UserAccess => "user",
411 ReviewType::PrivilegedAccess => "privileged",
412 ReviewType::ApiToken => "token",
413 ReviewType::ResourceAccess => "resource",
414 };
415
416 let date_str = date.format("%Y-%m-%d");
417 format!("review-{}-{}", date_str, type_str)
418 }
419
420 pub async fn start_user_access_review(
425 &mut self,
426 users: Vec<UserAccessInfo>,
427 ) -> Result<AccessReview, crate::Error> {
428 if !self.config.enabled || !self.config.user_review.enabled {
429 return Err(crate::Error::Generic("User access review is not enabled".to_string()));
430 }
431
432 let now = Utc::now();
433 let review_id = self.generate_review_id(ReviewType::UserAccess, now);
434 let due_date = now + Duration::days(self.config.user_review.approval_timeout_days as i64);
435 let next_review = self.config.user_review.frequency.next_review_date(now);
436
437 let mut findings = ReviewFindings {
439 inactive_users: 0,
440 excessive_permissions: 0,
441 no_recent_access: 0,
442 privileged_without_mfa: 0,
443 unused_tokens: 0,
444 excessive_scopes: 0,
445 expiring_soon: 0,
446 custom: HashMap::new(),
447 };
448
449 let mut review_items = HashMap::new();
450
451 for user in &users {
452 if let Some(days) = user.days_inactive {
454 if days > self.config.user_review.inactive_threshold_days {
455 findings.inactive_users += 1;
456 }
457 }
458
459 if user.last_login.is_none() || user.last_login.unwrap() < now - Duration::days(90) {
461 findings.no_recent_access += 1;
462 }
463
464 if user.permissions.len() > 10 {
466 findings.excessive_permissions += 1;
467 }
468
469 let review_item = UserReviewItem {
471 review_id: review_id.clone(),
472 user_id: user.user_id,
473 access_info: user.clone(),
474 status: "pending".to_string(),
475 manager_id: None, approval_deadline: Some(due_date),
477 approved_by: None,
478 approved_at: None,
479 rejection_reason: None,
480 };
481
482 review_items.insert(user.user_id, review_item);
483 }
484
485 let review = AccessReview {
486 review_id: review_id.clone(),
487 review_type: ReviewType::UserAccess,
488 status: ReviewStatus::InProgress,
489 review_date: now,
490 due_date,
491 total_items: users.len() as u32,
492 items_reviewed: 0,
493 findings: findings.clone(),
494 actions_taken: ReviewActions {
495 users_revoked: 0,
496 permissions_reduced: 0,
497 mfa_enforced: 0,
498 tokens_revoked: 0,
499 tokens_rotated: 0,
500 scopes_reduced: 0,
501 custom: HashMap::new(),
502 },
503 pending_approvals: review_items.len() as u32,
504 next_review_date: next_review,
505 metadata: HashMap::new(),
506 };
507
508 self.active_reviews.insert(review_id.clone(), review.clone());
509 self.user_review_items.insert(review_id, review_items);
510
511 Ok(review)
512 }
513
514 pub async fn start_api_token_review(
516 &mut self,
517 tokens: Vec<ApiTokenInfo>,
518 ) -> Result<AccessReview, crate::Error> {
519 if !self.config.enabled || !self.config.token_review.enabled {
520 return Err(crate::Error::Generic("API token review is not enabled".to_string()));
521 }
522
523 let now = Utc::now();
524 let review_id = self.generate_review_id(ReviewType::ApiToken, now);
525 let due_date = now + Duration::days(14);
526 let next_review = self.config.token_review.frequency.next_review_date(now);
527
528 let mut findings = ReviewFindings {
529 inactive_users: 0,
530 excessive_permissions: 0,
531 no_recent_access: 0,
532 privileged_without_mfa: 0,
533 unused_tokens: 0,
534 excessive_scopes: 0,
535 expiring_soon: 0,
536 custom: HashMap::new(),
537 };
538
539 for token in &tokens {
540 if token
541 .days_unused
542 .is_some_and(|days| days > self.config.token_review.unused_threshold_days)
543 {
544 findings.unused_tokens += 1;
545 }
546
547 if token.scopes.len() > 5 {
548 findings.excessive_scopes += 1;
549 }
550
551 if token.expires_at.is_some_and(|expires| {
552 expires
553 <= now + Duration::days(self.config.token_review.rotation_threshold_days as i64)
554 }) {
555 findings.expiring_soon += 1;
556 }
557 }
558
559 let mut metadata = HashMap::new();
560 metadata.insert(
561 "token_ids".to_string(),
562 serde_json::json!(tokens.iter().map(|t| t.token_id.clone()).collect::<Vec<_>>()),
563 );
564
565 let review = AccessReview {
566 review_id: review_id.clone(),
567 review_type: ReviewType::ApiToken,
568 status: ReviewStatus::InProgress,
569 review_date: now,
570 due_date,
571 total_items: tokens.len() as u32,
572 items_reviewed: 0,
573 findings,
574 actions_taken: ReviewActions {
575 users_revoked: 0,
576 permissions_reduced: 0,
577 mfa_enforced: 0,
578 tokens_revoked: 0,
579 tokens_rotated: 0,
580 scopes_reduced: 0,
581 custom: HashMap::new(),
582 },
583 pending_approvals: tokens.len() as u32,
584 next_review_date: next_review,
585 metadata,
586 };
587
588 self.active_reviews.insert(review_id, review.clone());
589 Ok(review)
590 }
591
592 pub async fn start_resource_access_review(
594 &mut self,
595 resources: Vec<ResourceAccessInfo>,
596 ) -> Result<AccessReview, crate::Error> {
597 if !self.config.enabled || !self.config.resource_review.enabled {
598 return Err(crate::Error::Generic("Resource access review is not enabled".to_string()));
599 }
600
601 let now = Utc::now();
602 let review_id = self.generate_review_id(ReviewType::ResourceAccess, now);
603 let due_date = now + Duration::days(30);
604 let next_review = self.config.resource_review.frequency.next_review_date(now);
605 let stale_threshold =
606 now - Duration::days(self.config.user_review.inactive_threshold_days as i64);
607
608 let mut findings = ReviewFindings {
609 inactive_users: 0,
610 excessive_permissions: 0,
611 no_recent_access: 0,
612 privileged_without_mfa: 0,
613 unused_tokens: 0,
614 excessive_scopes: 0,
615 expiring_soon: 0,
616 custom: HashMap::new(),
617 };
618
619 let mut sensitive_resource_count = 0u32;
620 for resource in &resources {
621 if self
622 .config
623 .resource_review
624 .sensitive_resources
625 .iter()
626 .any(|r| r == &resource.resource_type)
627 {
628 sensitive_resource_count += 1;
629 }
630
631 let stale_accesses = resource
632 .last_access
633 .values()
634 .filter_map(|d| *d)
635 .filter(|d| *d < stale_threshold)
636 .count() as u32;
637 findings.no_recent_access += stale_accesses;
638
639 if resource.users_with_access.len() > 20 {
640 findings.excessive_permissions += 1;
641 }
642 }
643
644 findings
645 .custom
646 .insert("sensitive_resources_reviewed".to_string(), sensitive_resource_count);
647
648 let review = AccessReview {
649 review_id: review_id.clone(),
650 review_type: ReviewType::ResourceAccess,
651 status: ReviewStatus::InProgress,
652 review_date: now,
653 due_date,
654 total_items: resources.len() as u32,
655 items_reviewed: 0,
656 findings,
657 actions_taken: ReviewActions {
658 users_revoked: 0,
659 permissions_reduced: 0,
660 mfa_enforced: 0,
661 tokens_revoked: 0,
662 tokens_rotated: 0,
663 scopes_reduced: 0,
664 custom: HashMap::new(),
665 },
666 pending_approvals: resources.len() as u32,
667 next_review_date: next_review,
668 metadata: HashMap::new(),
669 };
670
671 self.active_reviews.insert(review_id, review.clone());
672 Ok(review)
673 }
674
675 pub fn approve_user_access(
677 &mut self,
678 review_id: &str,
679 user_id: Uuid,
680 approved_by: Uuid,
681 justification: Option<String>,
682 ) -> Result<(), crate::Error> {
683 let review = self
684 .active_reviews
685 .get_mut(review_id)
686 .ok_or_else(|| crate::Error::Generic(format!("Review {} not found", review_id)))?;
687
688 let items = self.user_review_items.get_mut(review_id).ok_or_else(|| {
689 crate::Error::Generic(format!("Review items for {} not found", review_id))
690 })?;
691
692 let item = items.get_mut(&user_id).ok_or_else(|| {
693 crate::Error::Generic(format!("User {} not found in review", user_id))
694 })?;
695
696 item.status = "approved".to_string();
697 item.approved_by = Some(approved_by);
698 item.approved_at = Some(Utc::now());
699
700 review.items_reviewed += 1;
701 review.pending_approvals = review.pending_approvals.saturating_sub(1);
702
703 if let Some(just) = justification {
705 review
706 .metadata
707 .insert(format!("justification_{}", user_id), serde_json::json!(just));
708 }
709
710 Ok(())
711 }
712
713 pub fn revoke_user_access(
715 &mut self,
716 review_id: &str,
717 user_id: Uuid,
718 _revoked_by: Uuid,
719 reason: String,
720 ) -> Result<(), crate::Error> {
721 let review = self
722 .active_reviews
723 .get_mut(review_id)
724 .ok_or_else(|| crate::Error::Generic(format!("Review {} not found", review_id)))?;
725
726 let items = self.user_review_items.get_mut(review_id).ok_or_else(|| {
727 crate::Error::Generic(format!("Review items for {} not found", review_id))
728 })?;
729
730 let item = items.get_mut(&user_id).ok_or_else(|| {
731 crate::Error::Generic(format!("User {} not found in review", user_id))
732 })?;
733
734 item.status = "revoked".to_string();
735 item.rejection_reason = Some(reason.clone());
736
737 review.items_reviewed += 1;
738 review.pending_approvals = review.pending_approvals.saturating_sub(1);
739 review.actions_taken.users_revoked += 1;
740
741 review
743 .metadata
744 .insert(format!("revocation_reason_{}", user_id), serde_json::json!(reason));
745
746 Ok(())
747 }
748
749 pub fn update_user_permissions(
754 &mut self,
755 review_id: &str,
756 user_id: Uuid,
757 updated_by: Uuid,
758 new_roles: Vec<String>,
759 new_permissions: Vec<String>,
760 reason: Option<String>,
761 ) -> Result<(), crate::Error> {
762 let review = self
763 .active_reviews
764 .get_mut(review_id)
765 .ok_or_else(|| crate::Error::Generic(format!("Review {} not found", review_id)))?;
766
767 let items = self.user_review_items.get_mut(review_id).ok_or_else(|| {
768 crate::Error::Generic(format!("Review items for {} not found", review_id))
769 })?;
770
771 let item = items.get_mut(&user_id).ok_or_else(|| {
772 crate::Error::Generic(format!("User {} not found in review", user_id))
773 })?;
774
775 let old_roles = item.access_info.roles.clone();
777 let old_permissions = item.access_info.permissions.clone();
778
779 item.access_info.roles = new_roles.clone();
781 item.access_info.permissions = new_permissions.clone();
782
783 let roles_reduced = new_roles.len() < old_roles.len();
785 let permissions_reduced = new_permissions.len() < old_permissions.len();
786
787 if roles_reduced || permissions_reduced {
788 item.status = "permissions_updated".to_string();
789 review.items_reviewed += 1;
790 review.pending_approvals = review.pending_approvals.saturating_sub(1);
791 review.actions_taken.permissions_reduced += 1;
792 }
793
794 let change_metadata = serde_json::json!({
796 "updated_by": updated_by.to_string(),
797 "old_roles": old_roles,
798 "new_roles": new_roles,
799 "old_permissions": old_permissions,
800 "new_permissions": new_permissions,
801 "reason": reason,
802 "updated_at": Utc::now(),
803 });
804 review
805 .metadata
806 .insert(format!("permission_update_{}", user_id), change_metadata);
807
808 Ok(())
809 }
810
811 pub fn get_review_items(&self, review_id: &str) -> Option<&HashMap<Uuid, UserReviewItem>> {
813 self.user_review_items.get(review_id)
814 }
815
816 pub fn get_review(&self, review_id: &str) -> Option<&AccessReview> {
818 self.active_reviews.get(review_id)
819 }
820
821 pub fn get_all_reviews(&self) -> Vec<&AccessReview> {
823 self.active_reviews.values().collect()
824 }
825
826 pub fn check_auto_revocation(&mut self) -> Vec<(String, Uuid)> {
831 let now = Utc::now();
832 let mut revoked = Vec::new();
833
834 for (review_id, items) in &mut self.user_review_items {
835 let review = match self.active_reviews.get_mut(review_id) {
836 Some(r) => r,
837 None => continue,
838 };
839
840 if !self.config.user_review.auto_revoke_inactive {
841 continue;
842 }
843
844 for (user_id, item) in items.iter_mut() {
845 if item.status == "pending" {
846 if let Some(deadline) = item.approval_deadline {
847 if now > deadline {
848 item.status = "auto_revoked".to_string();
850 item.rejection_reason = Some(
851 "Access automatically revoked due to missing approval within deadline".to_string(),
852 );
853
854 review.items_reviewed += 1;
855 review.pending_approvals = review.pending_approvals.saturating_sub(1);
856 review.actions_taken.users_revoked += 1;
857
858 revoked.push((review_id.clone(), *user_id));
859 }
860 }
861 }
862 }
863 }
864
865 revoked
866 }
867
868 pub fn complete_review(&mut self, review_id: &str) -> Result<(), crate::Error> {
870 let review = self
871 .active_reviews
872 .get_mut(review_id)
873 .ok_or_else(|| crate::Error::Generic(format!("Review {} not found", review_id)))?;
874
875 review.status = ReviewStatus::Completed;
876
877 Ok(())
878 }
879}
880
881#[cfg(test)]
882mod tests {
883 use super::*;
884
885 #[test]
886 fn test_review_frequency_duration() {
887 assert_eq!(ReviewFrequency::Monthly.duration(), Duration::days(30));
888 assert_eq!(ReviewFrequency::Quarterly.duration(), Duration::days(90));
889 assert_eq!(ReviewFrequency::Annually.duration(), Duration::days(365));
890 }
891
892 #[test]
893 fn test_generate_review_id() {
894 let config = AccessReviewConfig::default();
895 let engine = AccessReviewEngine::new(config);
896 let date = Utc::now();
897 let id = engine.generate_review_id(ReviewType::UserAccess, date);
898 assert!(id.starts_with("review-"));
899 assert!(id.contains("user"));
900 }
901
902 #[tokio::test]
903 async fn test_start_user_access_review() {
904 let mut config = AccessReviewConfig::default();
905 config.enabled = true;
906 config.user_review.enabled = true;
907
908 let mut engine = AccessReviewEngine::new(config);
909
910 let users = vec![
911 UserAccessInfo {
912 user_id: Uuid::new_v4(),
913 username: "user1".to_string(),
914 email: "user1@example.com".to_string(),
915 roles: vec!["editor".to_string()],
916 permissions: vec!["read".to_string(), "write".to_string()],
917 last_login: Some(Utc::now() - Duration::days(10)),
918 access_granted: Utc::now() - Duration::days(100),
919 days_inactive: Some(10),
920 is_active: true,
921 },
922 UserAccessInfo {
923 user_id: Uuid::new_v4(),
924 username: "user2".to_string(),
925 email: "user2@example.com".to_string(),
926 roles: vec!["admin".to_string()],
927 permissions: (0..15).map(|i| format!("perm{}", i)).collect(),
928 last_login: Some(Utc::now() - Duration::days(120)),
929 access_granted: Utc::now() - Duration::days(200),
930 days_inactive: Some(120),
931 is_active: true,
932 },
933 ];
934
935 let review = engine.start_user_access_review(users).await.unwrap();
936 assert_eq!(review.review_type, ReviewType::UserAccess);
937 assert_eq!(review.total_items, 2);
938 assert!(review.findings.inactive_users > 0);
939 assert!(review.findings.excessive_permissions > 0);
940 }
941
942 #[test]
943 fn test_approve_user_access() {
944 let mut config = AccessReviewConfig::default();
945 config.enabled = true;
946 config.user_review.enabled = true;
947
948 let mut engine = AccessReviewEngine::new(config);
949
950 let user = UserAccessInfo {
951 user_id: Uuid::new_v4(),
952 username: "user1".to_string(),
953 email: "user1@example.com".to_string(),
954 roles: vec!["editor".to_string()],
955 permissions: vec!["read".to_string()],
956 last_login: Some(Utc::now()),
957 access_granted: Utc::now() - Duration::days(10),
958 days_inactive: Some(0),
959 is_active: true,
960 };
961
962 let review =
964 futures::executor::block_on(engine.start_user_access_review(vec![user.clone()]))
965 .unwrap();
966 let review_id = review.review_id.clone();
967
968 let approver_id = Uuid::new_v4();
970 engine.approve_user_access(&review_id, user.user_id, approver_id, None).unwrap();
971
972 let review = engine.get_review(&review_id).unwrap();
973 assert_eq!(review.items_reviewed, 1);
974 assert_eq!(review.pending_approvals, 0);
975 }
976
977 #[test]
978 fn test_revoke_user_access() {
979 let mut config = AccessReviewConfig::default();
980 config.enabled = true;
981 config.user_review.enabled = true;
982
983 let mut engine = AccessReviewEngine::new(config);
984
985 let user = UserAccessInfo {
986 user_id: Uuid::new_v4(),
987 username: "user1".to_string(),
988 email: "user1@example.com".to_string(),
989 roles: vec!["editor".to_string()],
990 permissions: vec!["read".to_string()],
991 last_login: Some(Utc::now()),
992 access_granted: Utc::now() - Duration::days(10),
993 days_inactive: Some(0),
994 is_active: true,
995 };
996
997 let review =
999 futures::executor::block_on(engine.start_user_access_review(vec![user.clone()]))
1000 .unwrap();
1001 let review_id = review.review_id.clone();
1002
1003 let revoker_id = Uuid::new_v4();
1005 engine
1006 .revoke_user_access(
1007 &review_id,
1008 user.user_id,
1009 revoker_id,
1010 "No longer needed".to_string(),
1011 )
1012 .unwrap();
1013
1014 let review = engine.get_review(&review_id).unwrap();
1015 assert_eq!(review.actions_taken.users_revoked, 1);
1016 }
1017
1018 #[tokio::test]
1019 async fn test_start_resource_access_review() {
1020 let mut config = AccessReviewConfig::default();
1021 config.enabled = true;
1022 config.resource_review.enabled = true;
1023
1024 let mut engine = AccessReviewEngine::new(config);
1025 let user_id = Uuid::new_v4();
1026 let mut access_levels = HashMap::new();
1027 access_levels.insert(user_id, "admin".to_string());
1028 let mut last_access = HashMap::new();
1029 last_access.insert(user_id, Some(Utc::now() - Duration::days(120)));
1030
1031 let resources = vec![ResourceAccessInfo {
1032 resource_type: "billing".to_string(),
1033 resource_id: "res-1".to_string(),
1034 users_with_access: vec![user_id],
1035 access_levels,
1036 last_access,
1037 }];
1038
1039 let review = engine.start_resource_access_review(resources).await.unwrap();
1040 assert_eq!(review.review_type, ReviewType::ResourceAccess);
1041 assert_eq!(review.total_items, 1);
1042 assert_eq!(review.findings.custom.get("sensitive_resources_reviewed"), Some(&1));
1043 assert!(review.findings.no_recent_access >= 1);
1044 }
1045}