mockforge_core/security/
mfa_tracking.rs1use chrono::{DateTime, Utc};
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9use tokio::sync::RwLock;
10use uuid::Uuid;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum MfaMethod {
16 Totp,
18 Sms,
20 Email,
22 HardwareKey,
24 Push,
26}
27
28#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct MfaStatus {
31 pub user_id: Uuid,
33 pub enabled: bool,
35 pub methods: Vec<MfaMethod>,
37 pub enabled_at: Option<DateTime<Utc>>,
39 pub last_verification: Option<DateTime<Utc>>,
41 pub backup_codes_remaining: u32,
43}
44
45#[async_trait::async_trait]
47pub trait MfaStorage: Send + Sync {
48 async fn get_mfa_status(&self, user_id: Uuid) -> Result<Option<MfaStatus>, crate::Error>;
50
51 async fn set_mfa_status(&self, status: MfaStatus) -> Result<(), crate::Error>;
53
54 async fn get_users_with_mfa(&self) -> Result<Vec<Uuid>, crate::Error>;
56
57 async fn get_privileged_users_without_mfa(
59 &self,
60 privileged_user_ids: &[Uuid],
61 ) -> Result<Vec<Uuid>, crate::Error>;
62}
63
64pub struct InMemoryMfaStorage {
66 mfa_statuses: Arc<RwLock<HashMap<Uuid, MfaStatus>>>,
67}
68
69impl InMemoryMfaStorage {
70 pub fn new() -> Self {
72 Self {
73 mfa_statuses: Arc::new(RwLock::new(HashMap::new())),
74 }
75 }
76}
77
78impl Default for InMemoryMfaStorage {
79 fn default() -> Self {
80 Self::new()
81 }
82}
83
84#[async_trait::async_trait]
85impl MfaStorage for InMemoryMfaStorage {
86 async fn get_mfa_status(&self, user_id: Uuid) -> Result<Option<MfaStatus>, crate::Error> {
87 let statuses = self.mfa_statuses.read().await;
88 Ok(statuses.get(&user_id).cloned())
89 }
90
91 async fn set_mfa_status(&self, status: MfaStatus) -> Result<(), crate::Error> {
92 let mut statuses = self.mfa_statuses.write().await;
93 statuses.insert(status.user_id, status);
94 Ok(())
95 }
96
97 async fn get_users_with_mfa(&self) -> Result<Vec<Uuid>, crate::Error> {
98 let statuses = self.mfa_statuses.read().await;
99 Ok(statuses
100 .iter()
101 .filter(|(_, status)| status.enabled)
102 .map(|(user_id, _)| *user_id)
103 .collect())
104 }
105
106 async fn get_privileged_users_without_mfa(
107 &self,
108 privileged_user_ids: &[Uuid],
109 ) -> Result<Vec<Uuid>, crate::Error> {
110 let statuses = self.mfa_statuses.read().await;
111 Ok(privileged_user_ids
112 .iter()
113 .filter(|user_id| {
114 statuses.get(user_id).map(|s| !s.enabled).unwrap_or(true) })
116 .copied()
117 .collect())
118 }
119}
120
121#[cfg(test)]
122mod tests {
123 use super::*;
124
125 #[tokio::test]
126 async fn test_mfa_storage() {
127 let storage = InMemoryMfaStorage::new();
128 let user_id = Uuid::new_v4();
129 let status = MfaStatus {
130 user_id,
131 enabled: true,
132 methods: vec![MfaMethod::Totp],
133 enabled_at: Some(Utc::now()),
134 last_verification: Some(Utc::now()),
135 backup_codes_remaining: 5,
136 };
137
138 storage.set_mfa_status(status).await.unwrap();
139 let retrieved = storage.get_mfa_status(user_id).await.unwrap();
140 assert!(retrieved.is_some());
141 assert!(retrieved.unwrap().enabled);
142 }
143}