1use chrono::{DateTime, Utc};
7use mockforge_core::security::access_review::{ApiTokenInfo, PrivilegedAccessInfo, UserAccessInfo};
8use mockforge_core::security::{
9 ApiTokenStorage, JustificationStorage, MfaStorage, UserDataProvider,
10};
11use mockforge_core::Error;
12use sqlx::{Pool, Sqlite};
13use std::sync::Arc;
14use uuid::Uuid;
15
16pub struct CollabUserDataProvider {
18 db: Pool<Sqlite>,
19 user_service: Arc<crate::user::UserService>,
20 workspace_service: Arc<crate::workspace::WorkspaceService>,
21 token_storage: Option<Arc<dyn ApiTokenStorage>>,
22 mfa_storage: Option<Arc<dyn MfaStorage>>,
23 justification_storage: Option<Arc<dyn JustificationStorage>>,
24}
25
26impl CollabUserDataProvider {
27 pub fn new(
29 db: Pool<Sqlite>,
30 user_service: Arc<crate::user::UserService>,
31 workspace_service: Arc<crate::workspace::WorkspaceService>,
32 ) -> Self {
33 Self {
34 db,
35 user_service,
36 workspace_service,
37 token_storage: None,
38 mfa_storage: None,
39 justification_storage: None,
40 }
41 }
42
43 pub fn with_storage(
45 db: Pool<Sqlite>,
46 user_service: Arc<crate::user::UserService>,
47 workspace_service: Arc<crate::workspace::WorkspaceService>,
48 token_storage: Option<Arc<dyn ApiTokenStorage>>,
49 mfa_storage: Option<Arc<dyn MfaStorage>>,
50 justification_storage: Option<Arc<dyn JustificationStorage>>,
51 ) -> Self {
52 Self {
53 db,
54 user_service,
55 workspace_service,
56 token_storage,
57 mfa_storage,
58 justification_storage,
59 }
60 }
61}
62
63#[async_trait::async_trait]
64impl UserDataProvider for CollabUserDataProvider {
65 async fn get_all_users(&self) -> Result<Vec<UserAccessInfo>, Error> {
66 let users = sqlx::query_as!(
68 crate::models::User,
69 r#"
70 SELECT id as "id: Uuid", username, email, password_hash, display_name, avatar_url,
71 created_at as "created_at: chrono::DateTime<chrono::Utc>",
72 updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
73 is_active as "is_active: bool"
74 FROM users
75 WHERE is_active = TRUE
76 ORDER BY created_at
77 "#,
78 )
79 .fetch_all(&self.db)
80 .await
81 .map_err(|e| Error::Generic(format!("Failed to fetch users: {}", e)))?;
82
83 let mut user_access_list = Vec::new();
85
86 for user in users {
87 let memberships = sqlx::query_as!(
89 crate::models::WorkspaceMember,
90 r#"
91 SELECT
92 id as "id: Uuid",
93 workspace_id as "workspace_id: Uuid",
94 user_id as "user_id: Uuid",
95 role as "role: crate::models::UserRole",
96 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
97 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
98 FROM workspace_members
99 WHERE user_id = ?
100 ORDER BY last_activity DESC
101 "#,
102 user.id
103 )
104 .fetch_all(&self.db)
105 .await
106 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {}", e)))?;
107
108 let roles: Vec<String> = memberships.iter().map(|m| format!("{:?}", m.role)).collect();
110
111 let permissions: Vec<String> = memberships
113 .iter()
114 .flat_map(|m| match m.role {
115 crate::models::UserRole::Admin => vec![
116 "WorkspaceCreate".to_string(),
117 "WorkspaceRead".to_string(),
118 "WorkspaceUpdate".to_string(),
119 "WorkspaceDelete".to_string(),
120 "WorkspaceManageMembers".to_string(),
121 "MockCreate".to_string(),
122 "MockRead".to_string(),
123 "MockUpdate".to_string(),
124 "MockDelete".to_string(),
125 ],
126 crate::models::UserRole::Editor => vec![
127 "MockCreate".to_string(),
128 "MockRead".to_string(),
129 "MockUpdate".to_string(),
130 "MockDelete".to_string(),
131 ],
132 crate::models::UserRole::Viewer => vec!["MockRead".to_string()],
133 })
134 .collect();
135
136 let last_activity = memberships.iter().map(|m| m.last_activity).max();
138
139 let days_inactive = last_activity.map(|activity| {
141 let duration = Utc::now() - activity;
142 duration.num_days() as u64
143 });
144
145 let access_granted =
147 memberships.iter().map(|m| m.joined_at).min().unwrap_or(user.created_at);
148
149 user_access_list.push(UserAccessInfo {
150 user_id: user.id,
151 username: user.username,
152 email: user.email,
153 roles,
154 permissions,
155 last_login: last_activity, access_granted,
157 days_inactive,
158 is_active: user.is_active,
159 });
160 }
161
162 Ok(user_access_list)
163 }
164
165 async fn get_privileged_users(&self) -> Result<Vec<PrivilegedAccessInfo>, Error> {
166 let admin_members = sqlx::query_as!(
168 crate::models::WorkspaceMember,
169 r#"
170 SELECT
171 id as "id: Uuid",
172 workspace_id as "workspace_id: Uuid",
173 user_id as "user_id: Uuid",
174 role as "role: crate::models::UserRole",
175 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
176 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
177 FROM workspace_members
178 WHERE role = 'admin'
179 ORDER BY last_activity DESC
180 "#,
181 )
182 .fetch_all(&self.db)
183 .await
184 .map_err(|e| Error::Generic(format!("Failed to fetch privileged users: {}", e)))?;
185
186 use std::collections::HashMap;
188 let mut user_roles: HashMap<Uuid, Vec<String>> = HashMap::new();
189 let mut user_activities: HashMap<Uuid, Vec<DateTime<Utc>>> = HashMap::new();
190
191 for member in &admin_members {
192 user_roles
193 .entry(member.user_id)
194 .or_insert_with(Vec::new)
195 .push(format!("{:?}", member.role));
196 user_activities
197 .entry(member.user_id)
198 .or_insert_with(Vec::new)
199 .push(member.last_activity);
200 }
201
202 let mut privileged_list = Vec::new();
204
205 for (user_id, roles) in user_roles {
206 let user =
208 self.user_service.get_user(user_id).await.map_err(|e| {
209 Error::Generic(format!("Failed to get user {}: {}", user_id, e))
210 })?;
211
212 let activities = user_activities.get(&user_id).cloned().unwrap_or_default();
213 let last_activity = activities.iter().max().copied();
214
215 let mfa_enabled = if let Some(ref mfa_storage) = self.mfa_storage {
217 mfa_storage
218 .get_mfa_status(user_id)
219 .await
220 .ok()
221 .flatten()
222 .map(|s| s.enabled)
223 .unwrap_or(false)
224 } else {
225 false
226 };
227
228 let (justification, justification_expires) =
230 if let Some(ref just_storage) = self.justification_storage {
231 just_storage
232 .get_justification(user_id)
233 .await
234 .ok()
235 .flatten()
236 .map(|j| (Some(j.justification), j.expires_at))
237 .unwrap_or((None, None))
238 } else {
239 (None, None)
240 };
241
242 privileged_list.push(PrivilegedAccessInfo {
243 user_id,
244 username: user.username,
245 roles,
246 mfa_enabled,
247 justification,
248 justification_expires,
249 recent_actions_count: activities.len() as u64,
250 last_privileged_action: last_activity,
251 });
252 }
253
254 Ok(privileged_list)
255 }
256
257 async fn get_api_tokens(&self) -> Result<Vec<ApiTokenInfo>, Error> {
258 if let Some(ref storage) = self.token_storage {
259 storage.get_all_tokens().await
260 } else {
261 Ok(Vec::new())
263 }
264 }
265
266 async fn get_user(&self, user_id: Uuid) -> Result<Option<UserAccessInfo>, Error> {
267 let user = match self.user_service.get_user(user_id).await {
268 Ok(u) => u,
269 Err(_) => return Ok(None),
270 };
271
272 let memberships = sqlx::query_as!(
274 crate::models::WorkspaceMember,
275 r#"
276 SELECT
277 id as "id: Uuid",
278 workspace_id as "workspace_id: Uuid",
279 user_id as "user_id: Uuid",
280 role as "role: crate::models::UserRole",
281 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
282 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
283 FROM workspace_members
284 WHERE user_id = ?
285 "#,
286 user_id
287 )
288 .fetch_all(&self.db)
289 .await
290 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {}", e)))?;
291
292 let roles: Vec<String> = memberships.iter().map(|m| format!("{:?}", m.role)).collect();
293
294 let permissions: Vec<String> = memberships
295 .iter()
296 .flat_map(|m| match m.role {
297 crate::models::UserRole::Admin => vec![
298 "WorkspaceCreate".to_string(),
299 "WorkspaceRead".to_string(),
300 "WorkspaceUpdate".to_string(),
301 "WorkspaceDelete".to_string(),
302 "WorkspaceManageMembers".to_string(),
303 "MockCreate".to_string(),
304 "MockRead".to_string(),
305 "MockUpdate".to_string(),
306 "MockDelete".to_string(),
307 ],
308 crate::models::UserRole::Editor => vec![
309 "MockCreate".to_string(),
310 "MockRead".to_string(),
311 "MockUpdate".to_string(),
312 "MockDelete".to_string(),
313 ],
314 crate::models::UserRole::Viewer => vec!["MockRead".to_string()],
315 })
316 .collect();
317
318 let last_activity = memberships.iter().map(|m| m.last_activity).max();
319
320 let days_inactive = last_activity.map(|activity| {
321 let duration = Utc::now() - activity;
322 duration.num_days() as u64
323 });
324
325 let access_granted =
326 memberships.iter().map(|m| m.joined_at).min().unwrap_or(user.created_at);
327
328 Ok(Some(UserAccessInfo {
329 user_id: user.id,
330 username: user.username,
331 email: user.email,
332 roles,
333 permissions,
334 last_login: last_activity,
335 access_granted,
336 days_inactive,
337 is_active: user.is_active,
338 }))
339 }
340
341 async fn get_last_login(&self, user_id: Uuid) -> Result<Option<DateTime<Utc>>, Error> {
342 let result = sqlx::query!(
344 r#"
345 SELECT MAX(last_activity) as "last_activity: chrono::DateTime<chrono::Utc>"
346 FROM workspace_members
347 WHERE user_id = ?
348 "#,
349 user_id
350 )
351 .fetch_optional(&self.db)
352 .await
353 .map_err(|e| Error::Generic(format!("Failed to get last login: {}", e)))?;
354
355 Ok(result.and_then(|r| r.last_activity))
356 }
357
358 async fn revoke_user_access(&self, user_id: Uuid, reason: String) -> Result<(), Error> {
359 self.user_service
361 .deactivate_user(user_id)
362 .await
363 .map_err(|e| Error::Generic(format!("Failed to revoke access: {}", e)))?;
364
365 tracing::info!("Revoked access for user {}: {}", user_id, reason);
366
367 Ok(())
368 }
369
370 async fn update_user_permissions(
371 &self,
372 user_id: Uuid,
373 roles: Vec<String>,
374 permissions: Vec<String>,
375 ) -> Result<(), Error> {
376 tracing::warn!("update_user_permissions not yet fully implemented for user {}", user_id);
379 Ok(())
380 }
381}