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#[serde(rename_all = "lowercase")]
19pub enum ReviewFrequency {
20 Monthly,
22 Quarterly,
24 Annually,
26}
27
28impl ReviewFrequency {
29 pub fn duration(&self) -> Duration {
31 match self {
32 ReviewFrequency::Monthly => Duration::days(30),
33 ReviewFrequency::Quarterly => Duration::days(90),
34 ReviewFrequency::Annually => Duration::days(365),
35 }
36 }
37
38 pub fn next_review_date(&self, from: DateTime<Utc>) -> DateTime<Utc> {
40 from + self.duration()
41 }
42}
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum ReviewStatus {
48 Pending,
50 InProgress,
52 Completed,
54 Cancelled,
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum ReviewType {
62 UserAccess,
64 PrivilegedAccess,
66 ApiToken,
68 ResourceAccess,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
74pub struct UserAccessInfo {
75 pub user_id: Uuid,
77 pub username: String,
79 pub email: String,
81 pub roles: Vec<String>,
83 pub permissions: Vec<String>,
85 pub last_login: Option<DateTime<Utc>>,
87 pub access_granted: DateTime<Utc>,
89 pub days_inactive: Option<u64>,
91 pub is_active: bool,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct PrivilegedAccessInfo {
98 pub user_id: Uuid,
100 pub username: String,
102 pub roles: Vec<String>,
104 pub mfa_enabled: bool,
106 pub justification: Option<String>,
108 pub justification_expires: Option<DateTime<Utc>>,
110 pub recent_actions_count: u64,
112 pub last_privileged_action: Option<DateTime<Utc>>,
114}
115
116#[derive(Debug, Clone, Serialize, Deserialize)]
118pub struct ApiTokenInfo {
119 pub token_id: String,
121 pub name: Option<String>,
123 pub owner_id: Uuid,
125 pub scopes: Vec<String>,
127 pub created_at: DateTime<Utc>,
129 pub last_used: Option<DateTime<Utc>>,
131 pub expires_at: Option<DateTime<Utc>>,
133 pub days_unused: Option<u64>,
135 pub is_active: bool,
137}
138
139#[derive(Debug, Clone, Serialize, Deserialize)]
141pub struct ResourceAccessInfo {
142 pub resource_type: String,
144 pub resource_id: String,
146 pub users_with_access: Vec<Uuid>,
148 pub access_levels: HashMap<Uuid, String>,
150 pub last_access: HashMap<Uuid, Option<DateTime<Utc>>>,
152}
153
154#[derive(Debug, Clone, Serialize, Deserialize)]
156pub struct ReviewFindings {
157 pub inactive_users: u32,
159 pub excessive_permissions: u32,
161 pub no_recent_access: u32,
163 pub privileged_without_mfa: u32,
165 pub unused_tokens: u32,
167 pub excessive_scopes: u32,
169 pub expiring_soon: u32,
171 pub custom: HashMap<String, u32>,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize)]
177pub struct ReviewActions {
178 pub users_revoked: u32,
180 pub permissions_reduced: u32,
182 pub mfa_enforced: u32,
184 pub tokens_revoked: u32,
186 pub tokens_rotated: u32,
188 pub scopes_reduced: u32,
190 pub custom: HashMap<String, u32>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct AccessReview {
197 pub review_id: String,
199 pub review_type: ReviewType,
201 pub status: ReviewStatus,
203 pub review_date: DateTime<Utc>,
205 pub due_date: DateTime<Utc>,
207 pub total_items: u32,
209 pub items_reviewed: u32,
211 pub findings: ReviewFindings,
213 pub actions_taken: ReviewActions,
215 pub pending_approvals: u32,
217 pub next_review_date: DateTime<Utc>,
219 pub metadata: HashMap<String, serde_json::Value>,
221}
222
223#[derive(Debug, Clone, Serialize, Deserialize)]
225pub struct UserReviewItem {
226 pub review_id: String,
228 pub user_id: Uuid,
230 pub access_info: UserAccessInfo,
232 pub status: String,
234 pub manager_id: Option<Uuid>,
236 pub approval_deadline: Option<DateTime<Utc>>,
238 pub approved_by: Option<Uuid>,
240 pub approved_at: Option<DateTime<Utc>>,
242 pub rejection_reason: Option<String>,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct AccessReviewConfig {
249 pub enabled: bool,
251 pub user_review: UserReviewConfig,
253 pub privileged_review: PrivilegedReviewConfig,
255 pub token_review: TokenReviewConfig,
257 pub resource_review: ResourceReviewConfig,
259 pub notifications: NotificationConfig,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize)]
265pub struct UserReviewConfig {
266 pub enabled: bool,
268 pub frequency: ReviewFrequency,
270 pub inactive_threshold_days: u64,
272 pub auto_revoke_inactive: bool,
274 pub require_manager_approval: bool,
276 pub approval_timeout_days: u64,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize)]
282pub struct PrivilegedReviewConfig {
283 pub enabled: bool,
285 pub frequency: ReviewFrequency,
287 pub require_mfa: bool,
289 pub require_justification: bool,
291 pub alert_on_escalation: bool,
293}
294
295#[derive(Debug, Clone, Serialize, Deserialize)]
297pub struct TokenReviewConfig {
298 pub enabled: bool,
300 pub frequency: ReviewFrequency,
302 pub unused_threshold_days: u64,
304 pub auto_revoke_unused: bool,
306 pub rotation_threshold_days: u64,
308}
309
310#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct ResourceReviewConfig {
313 pub enabled: bool,
315 pub frequency: ReviewFrequency,
317 pub sensitive_resources: Vec<String>,
319}
320
321#[derive(Debug, Clone, Serialize, Deserialize)]
323pub struct NotificationConfig {
324 pub enabled: bool,
326 pub channels: Vec<String>,
328 pub recipients: Vec<String>,
330}
331
332impl Default for AccessReviewConfig {
333 fn default() -> Self {
334 Self {
335 enabled: false,
336 user_review: UserReviewConfig {
337 enabled: true,
338 frequency: ReviewFrequency::Quarterly,
339 inactive_threshold_days: 90,
340 auto_revoke_inactive: true,
341 require_manager_approval: true,
342 approval_timeout_days: 30,
343 },
344 privileged_review: PrivilegedReviewConfig {
345 enabled: true,
346 frequency: ReviewFrequency::Monthly,
347 require_mfa: true,
348 require_justification: true,
349 alert_on_escalation: true,
350 },
351 token_review: TokenReviewConfig {
352 enabled: true,
353 frequency: ReviewFrequency::Monthly,
354 unused_threshold_days: 90,
355 auto_revoke_unused: true,
356 rotation_threshold_days: 30,
357 },
358 resource_review: ResourceReviewConfig {
359 enabled: true,
360 frequency: ReviewFrequency::Quarterly,
361 sensitive_resources: vec![
362 "billing".to_string(),
363 "user_data".to_string(),
364 "audit_logs".to_string(),
365 "security_settings".to_string(),
366 ],
367 },
368 notifications: NotificationConfig {
369 enabled: true,
370 channels: vec!["email".to_string()],
371 recipients: vec!["security_team".to_string(), "compliance_team".to_string()],
372 },
373 }
374 }
375}
376
377pub struct AccessReviewEngine {
383 config: AccessReviewConfig,
384 active_reviews: HashMap<String, AccessReview>,
386 user_review_items: HashMap<String, HashMap<Uuid, UserReviewItem>>,
388}
389
390impl AccessReviewEngine {
391 pub fn new(config: AccessReviewConfig) -> Self {
393 Self {
394 config,
395 active_reviews: HashMap::new(),
396 user_review_items: HashMap::new(),
397 }
398 }
399
400 pub fn generate_review_id(&self, review_type: ReviewType, date: DateTime<Utc>) -> String {
402 let type_str = match review_type {
403 ReviewType::UserAccess => "user",
404 ReviewType::PrivilegedAccess => "privileged",
405 ReviewType::ApiToken => "token",
406 ReviewType::ResourceAccess => "resource",
407 };
408
409 let date_str = date.format("%Y-%m-%d");
410 format!("review-{}-{}", date_str, type_str)
411 }
412
413 pub async fn start_user_access_review(
418 &mut self,
419 users: Vec<UserAccessInfo>,
420 ) -> Result<AccessReview, crate::Error> {
421 if !self.config.enabled || !self.config.user_review.enabled {
422 return Err(crate::Error::Generic(
423 "User access review is not enabled".to_string(),
424 ));
425 }
426
427 let now = Utc::now();
428 let review_id = self.generate_review_id(ReviewType::UserAccess, now);
429 let due_date = now + chrono::Duration::days(self.config.user_review.approval_timeout_days as i64);
430 let next_review = self.config.user_review.frequency.next_review_date(now);
431
432 let mut findings = ReviewFindings {
434 inactive_users: 0,
435 excessive_permissions: 0,
436 no_recent_access: 0,
437 privileged_without_mfa: 0,
438 unused_tokens: 0,
439 excessive_scopes: 0,
440 expiring_soon: 0,
441 custom: HashMap::new(),
442 };
443
444 let mut review_items = HashMap::new();
445
446 for user in &users {
447 if let Some(days) = user.days_inactive {
449 if days > self.config.user_review.inactive_threshold_days {
450 findings.inactive_users += 1;
451 }
452 }
453
454 if user.last_login.is_none() || user.last_login.unwrap() < now - chrono::Duration::days(90) {
456 findings.no_recent_access += 1;
457 }
458
459 if user.permissions.len() > 10 {
461 findings.excessive_permissions += 1;
462 }
463
464 let review_item = UserReviewItem {
466 review_id: review_id.clone(),
467 user_id: user.user_id,
468 access_info: user.clone(),
469 status: "pending".to_string(),
470 manager_id: None, approval_deadline: Some(due_date),
472 approved_by: None,
473 approved_at: None,
474 rejection_reason: None,
475 };
476
477 review_items.insert(user.user_id, review_item);
478 }
479
480 let review = AccessReview {
481 review_id: review_id.clone(),
482 review_type: ReviewType::UserAccess,
483 status: ReviewStatus::InProgress,
484 review_date: now,
485 due_date,
486 total_items: users.len() as u32,
487 items_reviewed: 0,
488 findings: findings.clone(),
489 actions_taken: ReviewActions {
490 users_revoked: 0,
491 permissions_reduced: 0,
492 mfa_enforced: 0,
493 tokens_revoked: 0,
494 tokens_rotated: 0,
495 scopes_reduced: 0,
496 custom: HashMap::new(),
497 },
498 pending_approvals: review_items.len() as u32,
499 next_review_date: next_review,
500 metadata: HashMap::new(),
501 };
502
503 self.active_reviews.insert(review_id.clone(), review.clone());
504 self.user_review_items.insert(review_id, review_items);
505
506 Ok(review)
507 }
508
509 pub fn approve_user_access(
511 &mut self,
512 review_id: &str,
513 user_id: Uuid,
514 approved_by: Uuid,
515 justification: Option<String>,
516 ) -> Result<(), crate::Error> {
517 let review = self
518 .active_reviews
519 .get_mut(review_id)
520 .ok_or_else(|| crate::Error::Generic(format!("Review {} not found", review_id)))?;
521
522 let items = self
523 .user_review_items
524 .get_mut(review_id)
525 .ok_or_else(|| crate::Error::Generic(format!("Review items for {} not found", review_id)))?;
526
527 let item = items
528 .get_mut(&user_id)
529 .ok_or_else(|| crate::Error::Generic(format!("User {} not found in review", user_id)))?;
530
531 item.status = "approved".to_string();
532 item.approved_by = Some(approved_by);
533 item.approved_at = Some(Utc::now());
534
535 review.items_reviewed += 1;
536 review.pending_approvals = review.pending_approvals.saturating_sub(1);
537
538 if let Some(just) = justification {
540 review
541 .metadata
542 .insert(format!("justification_{}", user_id), serde_json::json!(just));
543 }
544
545 Ok(())
546 }
547
548 pub fn revoke_user_access(
550 &mut self,
551 review_id: &str,
552 user_id: Uuid,
553 revoked_by: Uuid,
554 reason: String,
555 ) -> Result<(), crate::Error> {
556 let review = self
557 .active_reviews
558 .get_mut(review_id)
559 .ok_or_else(|| crate::Error::Generic(format!("Review {} not found", review_id)))?;
560
561 let items = self
562 .user_review_items
563 .get_mut(review_id)
564 .ok_or_else(|| crate::Error::Generic(format!("Review items for {} not found", review_id)))?;
565
566 let item = items
567 .get_mut(&user_id)
568 .ok_or_else(|| crate::Error::Generic(format!("User {} not found in review", user_id)))?;
569
570 item.status = "revoked".to_string();
571 item.rejection_reason = Some(reason.clone());
572
573 review.items_reviewed += 1;
574 review.pending_approvals = review.pending_approvals.saturating_sub(1);
575 review.actions_taken.users_revoked += 1;
576
577 review
579 .metadata
580 .insert(format!("revocation_reason_{}", user_id), serde_json::json!(reason));
581
582 Ok(())
583 }
584
585 pub fn get_review(&self, review_id: &str) -> Option<&AccessReview> {
587 self.active_reviews.get(review_id)
588 }
589
590 pub fn get_all_reviews(&self) -> Vec<&AccessReview> {
592 self.active_reviews.values().collect()
593 }
594
595 pub fn get_review_items(&self, review_id: &str) -> Option<&HashMap<Uuid, UserReviewItem>> {
597 self.user_review_items.get(review_id)
598 }
599
600 pub fn check_auto_revocation(&mut self) -> Vec<(String, Uuid)> {
605 let now = Utc::now();
606 let mut revoked = Vec::new();
607
608 for (review_id, items) in &mut self.user_review_items {
609 let review = match self.active_reviews.get_mut(review_id) {
610 Some(r) => r,
611 None => continue,
612 };
613
614 if !self.config.user_review.auto_revoke_inactive {
615 continue;
616 }
617
618 for (user_id, item) in items.iter_mut() {
619 if item.status == "pending" {
620 if let Some(deadline) = item.approval_deadline {
621 if now > deadline {
622 item.status = "auto_revoked".to_string();
624 item.rejection_reason = Some(
625 "Access automatically revoked due to missing approval within deadline".to_string(),
626 );
627
628 review.items_reviewed += 1;
629 review.pending_approvals = review.pending_approvals.saturating_sub(1);
630 review.actions_taken.users_revoked += 1;
631
632 revoked.push((review_id.clone(), *user_id));
633 }
634 }
635 }
636 }
637 }
638
639 revoked
640 }
641
642 pub fn complete_review(&mut self, review_id: &str) -> Result<(), crate::Error> {
644 let review = self
645 .active_reviews
646 .get_mut(review_id)
647 .ok_or_else(|| crate::Error::Generic(format!("Review {} not found", review_id)))?;
648
649 review.status = ReviewStatus::Completed;
650
651 Ok(())
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::*;
658
659 #[test]
660 fn test_review_frequency_duration() {
661 assert_eq!(ReviewFrequency::Monthly.duration(), Duration::days(30));
662 assert_eq!(ReviewFrequency::Quarterly.duration(), Duration::days(90));
663 assert_eq!(ReviewFrequency::Annually.duration(), Duration::days(365));
664 }
665
666 #[test]
667 fn test_generate_review_id() {
668 let config = AccessReviewConfig::default();
669 let engine = AccessReviewEngine::new(config);
670 let date = Utc::now();
671 let id = engine.generate_review_id(ReviewType::UserAccess, date);
672 assert!(id.starts_with("review-"));
673 assert!(id.contains("user"));
674 }
675
676 #[tokio::test]
677 async fn test_start_user_access_review() {
678 let mut config = AccessReviewConfig::default();
679 config.enabled = true;
680 config.user_review.enabled = true;
681
682 let mut engine = AccessReviewEngine::new(config);
683
684 let users = vec![
685 UserAccessInfo {
686 user_id: Uuid::new_v4(),
687 username: "user1".to_string(),
688 email: "user1@example.com".to_string(),
689 roles: vec!["editor".to_string()],
690 permissions: vec!["read".to_string(), "write".to_string()],
691 last_login: Some(Utc::now() - Duration::days(10)),
692 access_granted: Utc::now() - Duration::days(100),
693 days_inactive: Some(10),
694 is_active: true,
695 },
696 UserAccessInfo {
697 user_id: Uuid::new_v4(),
698 username: "user2".to_string(),
699 email: "user2@example.com".to_string(),
700 roles: vec!["admin".to_string()],
701 permissions: (0..15).map(|i| format!("perm{}", i)).collect(),
702 last_login: Some(Utc::now() - Duration::days(120)),
703 access_granted: Utc::now() - Duration::days(200),
704 days_inactive: Some(120),
705 is_active: true,
706 },
707 ];
708
709 let review = engine.start_user_access_review(users).await.unwrap();
710 assert_eq!(review.review_type, ReviewType::UserAccess);
711 assert_eq!(review.total_items, 2);
712 assert!(review.findings.inactive_users > 0);
713 assert!(review.findings.excessive_permissions > 0);
714 }
715
716 #[test]
717 fn test_approve_user_access() {
718 let mut config = AccessReviewConfig::default();
719 config.enabled = true;
720 config.user_review.enabled = true;
721
722 let mut engine = AccessReviewEngine::new(config);
723
724 let user = UserAccessInfo {
725 user_id: Uuid::new_v4(),
726 username: "user1".to_string(),
727 email: "user1@example.com".to_string(),
728 roles: vec!["editor".to_string()],
729 permissions: vec!["read".to_string()],
730 last_login: Some(Utc::now()),
731 access_granted: Utc::now() - Duration::days(10),
732 days_inactive: Some(0),
733 is_active: true,
734 };
735
736 let review = futures::executor::block_on(engine.start_user_access_review(vec![user.clone()])).unwrap();
738 let review_id = review.review_id.clone();
739
740 let approver_id = Uuid::new_v4();
742 engine
743 .approve_user_access(&review_id, user.user_id, approver_id, None)
744 .unwrap();
745
746 let review = engine.get_review(&review_id).unwrap();
747 assert_eq!(review.items_reviewed, 1);
748 assert_eq!(review.pending_approvals, 0);
749 }
750
751 #[test]
752 fn test_revoke_user_access() {
753 let mut config = AccessReviewConfig::default();
754 config.enabled = true;
755 config.user_review.enabled = true;
756
757 let mut engine = AccessReviewEngine::new(config);
758
759 let user = UserAccessInfo {
760 user_id: Uuid::new_v4(),
761 username: "user1".to_string(),
762 email: "user1@example.com".to_string(),
763 roles: vec!["editor".to_string()],
764 permissions: vec!["read".to_string()],
765 last_login: Some(Utc::now()),
766 access_granted: Utc::now() - Duration::days(10),
767 days_inactive: Some(0),
768 is_active: true,
769 };
770
771 let review = futures::executor::block_on(engine.start_user_access_review(vec![user.clone()])).unwrap();
773 let review_id = review.review_id.clone();
774
775 let revoker_id = Uuid::new_v4();
777 engine
778 .revoke_user_access(&review_id, user.user_id, revoker_id, "No longer needed".to_string())
779 .unwrap();
780
781 let review = engine.get_review(&review_id).unwrap();
782 assert_eq!(review.actions_taken.users_revoked, 1);
783 }
784}