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 #[must_use]
29 pub fn new(
30 db: Pool<Sqlite>,
31 user_service: Arc<crate::user::UserService>,
32 workspace_service: Arc<crate::workspace::WorkspaceService>,
33 ) -> Self {
34 Self {
35 db,
36 user_service,
37 workspace_service,
38 token_storage: None,
39 mfa_storage: None,
40 justification_storage: None,
41 }
42 }
43
44 #[must_use]
46 pub fn with_storage(
47 db: Pool<Sqlite>,
48 user_service: Arc<crate::user::UserService>,
49 workspace_service: Arc<crate::workspace::WorkspaceService>,
50 token_storage: Option<Arc<dyn ApiTokenStorage>>,
51 mfa_storage: Option<Arc<dyn MfaStorage>>,
52 justification_storage: Option<Arc<dyn JustificationStorage>>,
53 ) -> Self {
54 Self {
55 db,
56 user_service,
57 workspace_service,
58 token_storage,
59 mfa_storage,
60 justification_storage,
61 }
62 }
63}
64
65#[async_trait::async_trait]
66impl UserDataProvider for CollabUserDataProvider {
67 async fn get_all_users(&self) -> Result<Vec<UserAccessInfo>, Error> {
68 let users = sqlx::query_as!(
70 crate::models::User,
71 r#"
72 SELECT id as "id: Uuid", username, email, password_hash, display_name, avatar_url,
73 created_at as "created_at: chrono::DateTime<chrono::Utc>",
74 updated_at as "updated_at: chrono::DateTime<chrono::Utc>",
75 is_active as "is_active: bool"
76 FROM users
77 WHERE is_active = TRUE
78 ORDER BY created_at
79 "#,
80 )
81 .fetch_all(&self.db)
82 .await
83 .map_err(|e| Error::Generic(format!("Failed to fetch users: {e}")))?;
84
85 let mut user_access_list = Vec::new();
87
88 for user in users {
89 let memberships = sqlx::query_as!(
91 crate::models::WorkspaceMember,
92 r#"
93 SELECT
94 id as "id: Uuid",
95 workspace_id as "workspace_id: Uuid",
96 user_id as "user_id: Uuid",
97 role as "role: crate::models::UserRole",
98 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
99 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
100 FROM workspace_members
101 WHERE user_id = ?
102 ORDER BY last_activity DESC
103 "#,
104 user.id
105 )
106 .fetch_all(&self.db)
107 .await
108 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {e}")))?;
109
110 let roles: Vec<String> = memberships.iter().map(|m| format!("{:?}", m.role)).collect();
112
113 let permissions: Vec<String> = memberships
115 .iter()
116 .flat_map(|m| match m.role {
117 crate::models::UserRole::Admin => vec![
118 "WorkspaceCreate".to_string(),
119 "WorkspaceRead".to_string(),
120 "WorkspaceUpdate".to_string(),
121 "WorkspaceDelete".to_string(),
122 "WorkspaceManageMembers".to_string(),
123 "MockCreate".to_string(),
124 "MockRead".to_string(),
125 "MockUpdate".to_string(),
126 "MockDelete".to_string(),
127 ],
128 crate::models::UserRole::Editor => vec![
129 "MockCreate".to_string(),
130 "MockRead".to_string(),
131 "MockUpdate".to_string(),
132 "MockDelete".to_string(),
133 ],
134 crate::models::UserRole::Viewer => vec!["MockRead".to_string()],
135 })
136 .collect();
137
138 let last_activity = memberships.iter().map(|m| m.last_activity).max();
140
141 let days_inactive = last_activity.map(|activity| {
143 let duration = Utc::now() - activity;
144 duration.num_days() as u64
145 });
146
147 let access_granted =
149 memberships.iter().map(|m| m.joined_at).min().unwrap_or(user.created_at);
150
151 user_access_list.push(UserAccessInfo {
152 user_id: user.id,
153 username: user.username,
154 email: user.email,
155 roles,
156 permissions,
157 last_login: last_activity, access_granted,
159 days_inactive,
160 is_active: user.is_active,
161 });
162 }
163
164 Ok(user_access_list)
165 }
166
167 async fn get_privileged_users(&self) -> Result<Vec<PrivilegedAccessInfo>, Error> {
168 let admin_members = sqlx::query_as!(
170 crate::models::WorkspaceMember,
171 r#"
172 SELECT
173 id as "id: Uuid",
174 workspace_id as "workspace_id: Uuid",
175 user_id as "user_id: Uuid",
176 role as "role: crate::models::UserRole",
177 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
178 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
179 FROM workspace_members
180 WHERE role = 'admin'
181 ORDER BY last_activity DESC
182 "#,
183 )
184 .fetch_all(&self.db)
185 .await
186 .map_err(|e| Error::Generic(format!("Failed to fetch privileged users: {e}")))?;
187
188 use std::collections::HashMap;
190 let mut user_roles: HashMap<Uuid, Vec<String>> = HashMap::new();
191 let mut user_activities: HashMap<Uuid, Vec<DateTime<Utc>>> = HashMap::new();
192
193 for member in &admin_members {
194 user_roles.entry(member.user_id).or_default().push(format!("{:?}", member.role));
195 user_activities.entry(member.user_id).or_default().push(member.last_activity);
196 }
197
198 let mut privileged_list = Vec::new();
200
201 for (user_id, roles) in user_roles {
202 let user = self
204 .user_service
205 .get_user(user_id)
206 .await
207 .map_err(|e| Error::Generic(format!("Failed to get user {user_id}: {e}")))?;
208
209 let activities = user_activities.get(&user_id).cloned().unwrap_or_default();
210 let last_activity = activities.iter().max().copied();
211
212 let mfa_enabled = if let Some(ref mfa_storage) = self.mfa_storage {
214 mfa_storage
215 .get_mfa_status(user_id)
216 .await
217 .ok()
218 .flatten()
219 .is_some_and(|s| s.enabled)
220 } else {
221 false
222 };
223
224 let (justification, justification_expires) =
226 if let Some(ref just_storage) = self.justification_storage {
227 just_storage
228 .get_justification(user_id)
229 .await
230 .ok()
231 .flatten()
232 .map_or((None, None), |j| (Some(j.justification), j.expires_at))
233 } else {
234 (None, None)
235 };
236
237 privileged_list.push(PrivilegedAccessInfo {
238 user_id,
239 username: user.username,
240 roles,
241 mfa_enabled,
242 justification,
243 justification_expires,
244 recent_actions_count: activities.len() as u64,
245 last_privileged_action: last_activity,
246 });
247 }
248
249 Ok(privileged_list)
250 }
251
252 async fn get_api_tokens(&self) -> Result<Vec<ApiTokenInfo>, Error> {
253 if let Some(ref storage) = self.token_storage {
254 storage.get_all_tokens().await
255 } else {
256 Ok(Vec::new())
258 }
259 }
260
261 async fn get_user(&self, user_id: Uuid) -> Result<Option<UserAccessInfo>, Error> {
262 let user = match self.user_service.get_user(user_id).await {
263 Ok(u) => u,
264 Err(_) => return Ok(None),
265 };
266
267 let memberships = sqlx::query_as!(
269 crate::models::WorkspaceMember,
270 r#"
271 SELECT
272 id as "id: Uuid",
273 workspace_id as "workspace_id: Uuid",
274 user_id as "user_id: Uuid",
275 role as "role: crate::models::UserRole",
276 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
277 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
278 FROM workspace_members
279 WHERE user_id = ?
280 "#,
281 user_id
282 )
283 .fetch_all(&self.db)
284 .await
285 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {e}")))?;
286
287 let roles: Vec<String> = memberships.iter().map(|m| format!("{:?}", m.role)).collect();
288
289 let permissions: Vec<String> = memberships
290 .iter()
291 .flat_map(|m| match m.role {
292 crate::models::UserRole::Admin => vec![
293 "WorkspaceCreate".to_string(),
294 "WorkspaceRead".to_string(),
295 "WorkspaceUpdate".to_string(),
296 "WorkspaceDelete".to_string(),
297 "WorkspaceManageMembers".to_string(),
298 "MockCreate".to_string(),
299 "MockRead".to_string(),
300 "MockUpdate".to_string(),
301 "MockDelete".to_string(),
302 ],
303 crate::models::UserRole::Editor => vec![
304 "MockCreate".to_string(),
305 "MockRead".to_string(),
306 "MockUpdate".to_string(),
307 "MockDelete".to_string(),
308 ],
309 crate::models::UserRole::Viewer => vec!["MockRead".to_string()],
310 })
311 .collect();
312
313 let last_activity = memberships.iter().map(|m| m.last_activity).max();
314
315 let days_inactive = last_activity.map(|activity| {
316 let duration = Utc::now() - activity;
317 duration.num_days() as u64
318 });
319
320 let access_granted =
321 memberships.iter().map(|m| m.joined_at).min().unwrap_or(user.created_at);
322
323 Ok(Some(UserAccessInfo {
324 user_id: user.id,
325 username: user.username,
326 email: user.email,
327 roles,
328 permissions,
329 last_login: last_activity,
330 access_granted,
331 days_inactive,
332 is_active: user.is_active,
333 }))
334 }
335
336 async fn get_last_login(&self, user_id: Uuid) -> Result<Option<DateTime<Utc>>, Error> {
337 let result = sqlx::query!(
339 r#"
340 SELECT MAX(last_activity) as "last_activity: chrono::DateTime<chrono::Utc>"
341 FROM workspace_members
342 WHERE user_id = ?
343 "#,
344 user_id
345 )
346 .fetch_optional(&self.db)
347 .await
348 .map_err(|e| Error::Generic(format!("Failed to get last login: {e}")))?;
349
350 Ok(result.and_then(|r| r.last_activity))
351 }
352
353 async fn revoke_user_access(&self, user_id: Uuid, reason: String) -> Result<(), Error> {
354 self.user_service
356 .deactivate_user(user_id)
357 .await
358 .map_err(|e| Error::Generic(format!("Failed to revoke access: {e}")))?;
359
360 tracing::info!("Revoked access for user {}: {}", user_id, reason);
361
362 Ok(())
363 }
364
365 async fn update_user_permissions(
366 &self,
367 user_id: Uuid,
368 roles: Vec<String>,
369 permissions: Vec<String>,
370 ) -> Result<(), Error> {
371 let memberships = sqlx::query_as!(
373 crate::models::WorkspaceMember,
374 r#"
375 SELECT
376 id as "id: Uuid",
377 workspace_id as "workspace_id: Uuid",
378 user_id as "user_id: Uuid",
379 role as "role: crate::models::UserRole",
380 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
381 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
382 FROM workspace_members
383 WHERE user_id = ?
384 "#,
385 user_id
386 )
387 .fetch_all(&self.db)
388 .await
389 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {e}")))?;
390
391 if memberships.is_empty() {
392 tracing::warn!("No workspace memberships found for user {}", user_id);
393 return Ok(());
394 }
395
396 let target_role = if roles.iter().any(|r| r.eq_ignore_ascii_case("admin")) {
399 crate::models::UserRole::Admin
400 } else if roles.iter().any(|r| r.eq_ignore_ascii_case("editor")) {
401 crate::models::UserRole::Editor
402 } else if roles.iter().any(|r| r.eq_ignore_ascii_case("viewer")) {
403 crate::models::UserRole::Viewer
404 } else {
405 tracing::warn!(
407 "No valid role found in provided roles: {:?}, keeping existing roles",
408 roles
409 );
410 return Ok(());
411 };
412
413 for membership in &memberships {
417 if membership.role == target_role {
419 continue;
420 }
421
422 sqlx::query(
426 r#"
427 UPDATE workspace_members
428 SET role = ?
429 WHERE workspace_id = ? AND user_id = ?
430 "#,
431 )
432 .bind(target_role)
433 .bind(membership.workspace_id)
434 .bind(user_id)
435 .execute(&self.db)
436 .await
437 .map_err(|e| {
438 Error::Generic(format!(
439 "Failed to update role for workspace {}: {e}",
440 membership.workspace_id
441 ))
442 })?;
443
444 tracing::info!(
445 "Updated user {} role to {:?} in workspace {}",
446 user_id,
447 target_role,
448 membership.workspace_id
449 );
450 }
451
452 tracing::info!(
453 "Updated permissions for user {}: roles={:?}, permissions={:?}",
454 user_id,
455 roles,
456 permissions
457 );
458
459 Ok(())
460 }
461}