Skip to main content

mockforge_core/security/
access_review.rs

1//! Automated access review engine for compliance
2//!
3//! This module provides automated access review functionality for:
4//! - Quarterly user access reviews
5//! - Monthly privileged access reviews
6//! - Monthly API token reviews
7//! - Quarterly resource access reviews
8//!
9//! Compliance: SOC 2 CC6 (Logical Access), ISO 27001 A.9.2 (User Access Management)
10
11use chrono::{DateTime, Duration, Utc};
12use serde::{Deserialize, Serialize};
13use std::collections::HashMap;
14use uuid::Uuid;
15
16/// Review frequency types
17#[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 reviews
22    Monthly,
23    /// Quarterly reviews
24    Quarterly,
25    /// Annual reviews
26    Annually,
27}
28
29impl ReviewFrequency {
30    /// Get the duration for this frequency
31    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    /// Calculate the next review date from a given date
40    pub fn next_review_date(&self, from: DateTime<Utc>) -> DateTime<Utc> {
41        from + self.duration()
42    }
43}
44
45/// Review status
46#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(rename_all = "lowercase")]
48pub enum ReviewStatus {
49    /// Review is pending (not yet started)
50    Pending,
51    /// Review is in progress
52    InProgress,
53    /// Review is completed
54    Completed,
55    /// Review was cancelled
56    Cancelled,
57}
58
59/// Review type
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
61#[serde(rename_all = "snake_case")]
62pub enum ReviewType {
63    /// User access review
64    UserAccess,
65    /// Privileged access review
66    PrivilegedAccess,
67    /// API token review
68    ApiToken,
69    /// Resource access review
70    ResourceAccess,
71}
72
73/// User access information for review
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct UserAccessInfo {
76    /// User ID
77    pub user_id: Uuid,
78    /// Username
79    pub username: String,
80    /// Email address
81    pub email: String,
82    /// Current roles
83    pub roles: Vec<String>,
84    /// Permissions
85    pub permissions: Vec<String>,
86    /// Last login date
87    pub last_login: Option<DateTime<Utc>>,
88    /// Access granted date
89    pub access_granted: DateTime<Utc>,
90    /// Days since last activity
91    pub days_inactive: Option<u64>,
92    /// Whether user is active
93    pub is_active: bool,
94}
95
96/// Privileged access information
97#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct PrivilegedAccessInfo {
99    /// User ID
100    pub user_id: Uuid,
101    /// Username
102    pub username: String,
103    /// Privileged roles
104    pub roles: Vec<String>,
105    /// Whether MFA is enabled
106    pub mfa_enabled: bool,
107    /// Access justification
108    pub justification: Option<String>,
109    /// Justification expiration date
110    pub justification_expires: Option<DateTime<Utc>>,
111    /// Recent privileged actions count
112    pub recent_actions_count: u64,
113    /// Last privileged action date
114    pub last_privileged_action: Option<DateTime<Utc>>,
115}
116
117/// API token information for review
118#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct ApiTokenInfo {
120    /// Token ID
121    pub token_id: String,
122    /// Token name/description
123    pub name: Option<String>,
124    /// Token owner user ID
125    pub owner_id: Uuid,
126    /// Token scopes/permissions
127    pub scopes: Vec<String>,
128    /// Creation date
129    pub created_at: DateTime<Utc>,
130    /// Last usage date
131    pub last_used: Option<DateTime<Utc>>,
132    /// Expiration date
133    pub expires_at: Option<DateTime<Utc>>,
134    /// Days since last use
135    pub days_unused: Option<u64>,
136    /// Whether token is active
137    pub is_active: bool,
138}
139
140/// Resource access information
141#[derive(Debug, Clone, Serialize, Deserialize)]
142pub struct ResourceAccessInfo {
143    /// Resource type
144    pub resource_type: String,
145    /// Resource ID
146    pub resource_id: String,
147    /// Users with access
148    pub users_with_access: Vec<Uuid>,
149    /// Access levels
150    pub access_levels: HashMap<Uuid, String>,
151    /// Last access date per user
152    pub last_access: HashMap<Uuid, Option<DateTime<Utc>>>,
153}
154
155/// Review findings
156#[derive(Debug, Clone, Serialize, Deserialize)]
157pub struct ReviewFindings {
158    /// Number of inactive users
159    pub inactive_users: u32,
160    /// Number of users with excessive permissions
161    pub excessive_permissions: u32,
162    /// Number of users with no recent access
163    pub no_recent_access: u32,
164    /// Number of privileged users without MFA
165    pub privileged_without_mfa: u32,
166    /// Number of unused tokens
167    pub unused_tokens: u32,
168    /// Number of tokens with excessive scopes
169    pub excessive_scopes: u32,
170    /// Number of tokens expiring soon
171    pub expiring_soon: u32,
172    /// Additional custom findings
173    pub custom: HashMap<String, u32>,
174}
175
176/// Actions taken during review
177#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ReviewActions {
179    /// Number of users revoked
180    pub users_revoked: u32,
181    /// Number of permissions reduced
182    pub permissions_reduced: u32,
183    /// Number of MFA enforced
184    pub mfa_enforced: u32,
185    /// Number of tokens revoked
186    pub tokens_revoked: u32,
187    /// Number of tokens rotated
188    pub tokens_rotated: u32,
189    /// Number of scopes reduced
190    pub scopes_reduced: u32,
191    /// Additional custom actions
192    pub custom: HashMap<String, u32>,
193}
194
195/// Access review record
196#[derive(Debug, Clone, Serialize, Deserialize)]
197pub struct AccessReview {
198    /// Review ID
199    pub review_id: String,
200    /// Review type
201    pub review_type: ReviewType,
202    /// Review status
203    pub status: ReviewStatus,
204    /// Review date
205    pub review_date: DateTime<Utc>,
206    /// Due date for completion
207    pub due_date: DateTime<Utc>,
208    /// Total items reviewed
209    pub total_items: u32,
210    /// Items reviewed
211    pub items_reviewed: u32,
212    /// Review findings
213    pub findings: ReviewFindings,
214    /// Actions taken
215    pub actions_taken: ReviewActions,
216    /// Pending approvals count
217    pub pending_approvals: u32,
218    /// Next review date
219    pub next_review_date: DateTime<Utc>,
220    /// Review metadata
221    pub metadata: HashMap<String, serde_json::Value>,
222}
223
224/// User access review item (for approval workflow)
225#[derive(Debug, Clone, Serialize, Deserialize)]
226pub struct UserReviewItem {
227    /// Review ID
228    pub review_id: String,
229    /// User ID
230    pub user_id: Uuid,
231    /// User access information
232    pub access_info: UserAccessInfo,
233    /// Review status (pending, approved, rejected)
234    pub status: String,
235    /// Manager user ID (who should review)
236    pub manager_id: Option<Uuid>,
237    /// Approval deadline
238    pub approval_deadline: Option<DateTime<Utc>>,
239    /// Approved by
240    pub approved_by: Option<Uuid>,
241    /// Approved at
242    pub approved_at: Option<DateTime<Utc>>,
243    /// Rejection reason
244    pub rejection_reason: Option<String>,
245}
246
247/// Access review configuration
248#[derive(Debug, Clone, Serialize, Deserialize)]
249#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
250pub struct AccessReviewConfig {
251    /// Whether access review is enabled
252    pub enabled: bool,
253    /// User access review configuration
254    pub user_review: UserReviewConfig,
255    /// Privileged access review configuration
256    pub privileged_review: PrivilegedReviewConfig,
257    /// API token review configuration
258    pub token_review: TokenReviewConfig,
259    /// Resource access review configuration
260    pub resource_review: ResourceReviewConfig,
261    /// Notification configuration
262    pub notifications: NotificationConfig,
263}
264
265/// User access review configuration
266#[derive(Debug, Clone, Serialize, Deserialize)]
267#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
268pub struct UserReviewConfig {
269    /// Whether user review is enabled
270    pub enabled: bool,
271    /// Review frequency
272    pub frequency: ReviewFrequency,
273    /// Inactive threshold in days
274    pub inactive_threshold_days: u64,
275    /// Auto-revoke inactive users
276    pub auto_revoke_inactive: bool,
277    /// Require manager approval
278    pub require_manager_approval: bool,
279    /// Approval timeout in days
280    pub approval_timeout_days: u64,
281}
282
283/// Privileged access review configuration
284#[derive(Debug, Clone, Serialize, Deserialize)]
285#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
286pub struct PrivilegedReviewConfig {
287    /// Whether privileged review is enabled
288    pub enabled: bool,
289    /// Review frequency
290    pub frequency: ReviewFrequency,
291    /// Require MFA
292    pub require_mfa: bool,
293    /// Require justification
294    pub require_justification: bool,
295    /// Alert on privilege escalation
296    pub alert_on_escalation: bool,
297}
298
299/// API token review configuration
300#[derive(Debug, Clone, Serialize, Deserialize)]
301#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
302pub struct TokenReviewConfig {
303    /// Whether token review is enabled
304    pub enabled: bool,
305    /// Review frequency
306    pub frequency: ReviewFrequency,
307    /// Unused threshold in days
308    pub unused_threshold_days: u64,
309    /// Auto-revoke unused tokens
310    pub auto_revoke_unused: bool,
311    /// Rotation threshold in days (before expiration)
312    pub rotation_threshold_days: u64,
313}
314
315/// Resource access review configuration
316#[derive(Debug, Clone, Serialize, Deserialize)]
317#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
318pub struct ResourceReviewConfig {
319    /// Whether resource review is enabled
320    pub enabled: bool,
321    /// Review frequency
322    pub frequency: ReviewFrequency,
323    /// List of sensitive resources
324    pub sensitive_resources: Vec<String>,
325}
326
327/// Notification configuration
328#[derive(Debug, Clone, Serialize, Deserialize)]
329#[cfg_attr(feature = "schema", derive(schemars::JsonSchema))]
330pub struct NotificationConfig {
331    /// Whether notifications are enabled
332    pub enabled: bool,
333    /// Notification channels (email, slack, etc.)
334    pub channels: Vec<String>,
335    /// Recipients (security_team, compliance_team, managers)
336    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
384/// Access review engine
385///
386/// This engine manages automated access reviews according to the configured schedule
387/// and policies. It generates reports, tracks approvals, and can automatically
388/// revoke access when needed.
389pub struct AccessReviewEngine {
390    config: AccessReviewConfig,
391    /// Active reviews (review_id -> review)
392    active_reviews: HashMap<String, AccessReview>,
393    /// User review items (review_id -> user_id -> item)
394    user_review_items: HashMap<String, HashMap<Uuid, UserReviewItem>>,
395}
396
397impl AccessReviewEngine {
398    /// Create a new access review engine
399    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    /// Generate a review ID based on type and date
408    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    /// Start a user access review
421    ///
422    /// This generates a review report and creates review items for each user
423    /// that needs to be reviewed.
424    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        // Analyze users and generate findings
438        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            // Check for inactive users
453            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            // Check for no recent access
460            if user.last_login.is_none() || user.last_login.unwrap() < now - Duration::days(90) {
461                findings.no_recent_access += 1;
462            }
463
464            // Check for excessive permissions (heuristic: more than 10 permissions)
465            if user.permissions.len() > 10 {
466                findings.excessive_permissions += 1;
467            }
468
469            // Create review item
470            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, // Would be populated from user's manager relationship
476                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    /// Start an API token access review.
515    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    /// Start a resource access review.
593    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    /// Approve a user's access in a review
676    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        // Add justification to metadata if provided
704        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    /// Revoke a user's access in a review
714    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        // Add revocation reason to metadata
742        review
743            .metadata
744            .insert(format!("revocation_reason_{}", user_id), serde_json::json!(reason));
745
746        Ok(())
747    }
748
749    /// Update user permissions in a review
750    ///
751    /// This method updates the user's permissions/roles as part of a review action.
752    /// It tracks the permission change in the review and updates the review item.
753    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        // Store old permissions for tracking
776        let old_roles = item.access_info.roles.clone();
777        let old_permissions = item.access_info.permissions.clone();
778
779        // Update the access info
780        item.access_info.roles = new_roles.clone();
781        item.access_info.permissions = new_permissions.clone();
782
783        // Mark as reviewed if permissions were reduced
784        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        // Store permission change metadata
795        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    /// Get review items for a review
812    pub fn get_review_items(&self, review_id: &str) -> Option<&HashMap<Uuid, UserReviewItem>> {
813        self.user_review_items.get(review_id)
814    }
815
816    /// Get a review by ID
817    pub fn get_review(&self, review_id: &str) -> Option<&AccessReview> {
818        self.active_reviews.get(review_id)
819    }
820
821    /// Get all active reviews
822    pub fn get_all_reviews(&self) -> Vec<&AccessReview> {
823        self.active_reviews.values().collect()
824    }
825
826    /// Check for reviews that need auto-revocation
827    ///
828    /// This checks all pending review items and automatically revokes access
829    /// for items that have exceeded their approval deadline.
830    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                            // Auto-revoke
849                            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    /// Complete a review
869    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        // Start review
963        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        // Approve access
969        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        // Start review
998        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        // Revoke access
1004        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}