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: 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: 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| {
117 crate::permissions::RolePermissions::get_permissions(m.role)
118 .into_iter()
119 .map(|p| format!("{p:?}"))
120 })
121 .collect();
122
123 let last_activity = memberships.iter().map(|m| m.last_activity).max();
125
126 let days_inactive = last_activity.map(|activity| {
128 let duration = Utc::now() - activity;
129 u64::try_from(duration.num_days()).unwrap_or(0)
130 });
131
132 let access_granted =
134 memberships.iter().map(|m| m.joined_at).min().unwrap_or(user.created_at);
135
136 user_access_list.push(UserAccessInfo {
137 user_id: user.id,
138 username: user.username,
139 email: user.email,
140 roles,
141 permissions,
142 last_login: last_activity, access_granted,
144 days_inactive,
145 is_active: user.is_active,
146 });
147 }
148
149 Ok(user_access_list)
150 }
151
152 async fn get_privileged_users(&self) -> Result<Vec<PrivilegedAccessInfo>, Error> {
153 let admin_members = sqlx::query_as!(
155 crate::models::WorkspaceMember,
156 r#"
157 SELECT
158 id as "id: Uuid",
159 workspace_id as "workspace_id: Uuid",
160 user_id as "user_id: Uuid",
161 role as "role: crate::models::UserRole",
162 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
163 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
164 FROM workspace_members
165 WHERE role = 'admin'
166 ORDER BY last_activity DESC
167 "#,
168 )
169 .fetch_all(&self.db)
170 .await
171 .map_err(|e| Error::Generic(format!("Failed to fetch privileged users: {e}")))?;
172
173 use std::collections::HashMap;
175 let mut user_roles: HashMap<Uuid, Vec<String>> = HashMap::new();
176 let mut user_activities: HashMap<Uuid, Vec<DateTime<Utc>>> = HashMap::new();
177
178 for member in &admin_members {
179 user_roles.entry(member.user_id).or_default().push(format!("{:?}", member.role));
180 user_activities.entry(member.user_id).or_default().push(member.last_activity);
181 }
182
183 let mut privileged_list = Vec::new();
185
186 for (user_id, roles) in user_roles {
187 let user = self
189 .user_service
190 .get_user(user_id)
191 .await
192 .map_err(|e| Error::Generic(format!("Failed to get user {user_id}: {e}")))?;
193
194 let activities = user_activities.get(&user_id).cloned().unwrap_or_default();
195 let last_activity = activities.iter().max().copied();
196
197 let mfa_enabled = if let Some(ref mfa_storage) = self.mfa_storage {
199 mfa_storage
200 .get_mfa_status(user_id)
201 .await
202 .ok()
203 .flatten()
204 .is_some_and(|s| s.enabled)
205 } else {
206 false
207 };
208
209 let (justification, justification_expires) =
211 if let Some(ref just_storage) = self.justification_storage {
212 just_storage
213 .get_justification(user_id)
214 .await
215 .ok()
216 .flatten()
217 .map_or((None, None), |j| (Some(j.justification), j.expires_at))
218 } else {
219 (None, None)
220 };
221
222 privileged_list.push(PrivilegedAccessInfo {
223 user_id,
224 username: user.username,
225 roles,
226 mfa_enabled,
227 justification,
228 justification_expires,
229 recent_actions_count: activities.len() as u64,
230 last_privileged_action: last_activity,
231 });
232 }
233
234 Ok(privileged_list)
235 }
236
237 async fn get_api_tokens(&self) -> Result<Vec<ApiTokenInfo>, Error> {
238 if let Some(ref storage) = self.token_storage {
239 storage.get_all_tokens().await
240 } else {
241 Ok(Vec::new())
243 }
244 }
245
246 async fn get_user(&self, user_id: Uuid) -> Result<Option<UserAccessInfo>, Error> {
247 let Ok(user) = self.user_service.get_user(user_id).await else {
248 return Ok(None);
249 };
250
251 let memberships = sqlx::query_as!(
253 crate::models::WorkspaceMember,
254 r#"
255 SELECT
256 id as "id: Uuid",
257 workspace_id as "workspace_id: Uuid",
258 user_id as "user_id: Uuid",
259 role as "role: crate::models::UserRole",
260 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
261 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
262 FROM workspace_members
263 WHERE user_id = ?
264 "#,
265 user_id
266 )
267 .fetch_all(&self.db)
268 .await
269 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {e}")))?;
270
271 let roles: Vec<String> = memberships.iter().map(|m| format!("{:?}", m.role)).collect();
272
273 let permissions: Vec<String> = memberships
274 .iter()
275 .flat_map(|m| match m.role {
276 crate::models::UserRole::Admin => vec![
277 "WorkspaceCreate".to_string(),
278 "WorkspaceRead".to_string(),
279 "WorkspaceUpdate".to_string(),
280 "WorkspaceDelete".to_string(),
281 "WorkspaceManageMembers".to_string(),
282 "MockCreate".to_string(),
283 "MockRead".to_string(),
284 "MockUpdate".to_string(),
285 "MockDelete".to_string(),
286 ],
287 crate::models::UserRole::Editor => vec![
288 "MockCreate".to_string(),
289 "MockRead".to_string(),
290 "MockUpdate".to_string(),
291 "MockDelete".to_string(),
292 ],
293 crate::models::UserRole::Viewer => vec!["MockRead".to_string()],
294 })
295 .collect();
296
297 let last_activity = memberships.iter().map(|m| m.last_activity).max();
298
299 let days_inactive = last_activity.map(|activity| {
300 let duration = Utc::now() - activity;
301 u64::try_from(duration.num_days()).unwrap_or(0)
302 });
303
304 let access_granted =
305 memberships.iter().map(|m| m.joined_at).min().unwrap_or(user.created_at);
306
307 Ok(Some(UserAccessInfo {
308 user_id: user.id,
309 username: user.username,
310 email: user.email,
311 roles,
312 permissions,
313 last_login: last_activity,
314 access_granted,
315 days_inactive,
316 is_active: user.is_active,
317 }))
318 }
319
320 async fn get_last_login(&self, user_id: Uuid) -> Result<Option<DateTime<Utc>>, Error> {
321 let result = sqlx::query!(
323 r#"
324 SELECT MAX(last_activity) as "last_activity: chrono::DateTime<chrono::Utc>"
325 FROM workspace_members
326 WHERE user_id = ?
327 "#,
328 user_id
329 )
330 .fetch_optional(&self.db)
331 .await
332 .map_err(|e| Error::Generic(format!("Failed to get last login: {e}")))?;
333
334 Ok(result.and_then(|r| r.last_activity))
335 }
336
337 async fn revoke_user_access(&self, user_id: Uuid, reason: String) -> Result<(), Error> {
338 self.user_service
340 .deactivate_user(user_id)
341 .await
342 .map_err(|e| Error::Generic(format!("Failed to revoke access: {e}")))?;
343
344 tracing::info!("Revoked access for user {}: {}", user_id, reason);
345
346 Ok(())
347 }
348
349 async fn update_user_permissions(
350 &self,
351 user_id: Uuid,
352 roles: Vec<String>,
353 permissions: Vec<String>,
354 ) -> Result<(), Error> {
355 let memberships = sqlx::query_as!(
357 crate::models::WorkspaceMember,
358 r#"
359 SELECT
360 id as "id: Uuid",
361 workspace_id as "workspace_id: Uuid",
362 user_id as "user_id: Uuid",
363 role as "role: crate::models::UserRole",
364 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
365 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
366 FROM workspace_members
367 WHERE user_id = ?
368 "#,
369 user_id
370 )
371 .fetch_all(&self.db)
372 .await
373 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {e}")))?;
374
375 if memberships.is_empty() {
376 tracing::warn!("No workspace memberships found for user {}", user_id);
377 return Ok(());
378 }
379
380 let target_role = if roles.iter().any(|r| r.eq_ignore_ascii_case("admin")) {
383 crate::models::UserRole::Admin
384 } else if roles.iter().any(|r| r.eq_ignore_ascii_case("editor")) {
385 crate::models::UserRole::Editor
386 } else if roles.iter().any(|r| r.eq_ignore_ascii_case("viewer")) {
387 crate::models::UserRole::Viewer
388 } else {
389 tracing::warn!(
391 "No valid role found in provided roles: {:?}, keeping existing roles",
392 roles
393 );
394 return Ok(());
395 };
396
397 for membership in &memberships {
401 if membership.role == target_role {
403 continue;
404 }
405
406 sqlx::query(
410 r"
411 UPDATE workspace_members
412 SET role = ?
413 WHERE workspace_id = ? AND user_id = ?
414 ",
415 )
416 .bind(target_role)
417 .bind(membership.workspace_id)
418 .bind(user_id)
419 .execute(&self.db)
420 .await
421 .map_err(|e| {
422 Error::Generic(format!(
423 "Failed to update role for workspace {}: {e}",
424 membership.workspace_id
425 ))
426 })?;
427
428 tracing::info!(
429 "Updated user {} role to {:?} in workspace {}",
430 user_id,
431 target_role,
432 membership.workspace_id
433 );
434 }
435
436 tracing::info!(
437 "Updated permissions for user {}: roles={:?}, permissions={:?}",
438 user_id,
439 roles,
440 permissions
441 );
442
443 Ok(())
444 }
445}