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| {
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 duration.num_days() as u64
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 user = match self.user_service.get_user(user_id).await {
248 Ok(u) => u,
249 Err(_) => return Ok(None),
250 };
251
252 let memberships = sqlx::query_as!(
254 crate::models::WorkspaceMember,
255 r#"
256 SELECT
257 id as "id: Uuid",
258 workspace_id as "workspace_id: Uuid",
259 user_id as "user_id: Uuid",
260 role as "role: crate::models::UserRole",
261 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
262 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
263 FROM workspace_members
264 WHERE user_id = ?
265 "#,
266 user_id
267 )
268 .fetch_all(&self.db)
269 .await
270 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {e}")))?;
271
272 let roles: Vec<String> = memberships.iter().map(|m| format!("{:?}", m.role)).collect();
273
274 let permissions: Vec<String> = memberships
275 .iter()
276 .flat_map(|m| match m.role {
277 crate::models::UserRole::Admin => vec![
278 "WorkspaceCreate".to_string(),
279 "WorkspaceRead".to_string(),
280 "WorkspaceUpdate".to_string(),
281 "WorkspaceDelete".to_string(),
282 "WorkspaceManageMembers".to_string(),
283 "MockCreate".to_string(),
284 "MockRead".to_string(),
285 "MockUpdate".to_string(),
286 "MockDelete".to_string(),
287 ],
288 crate::models::UserRole::Editor => vec![
289 "MockCreate".to_string(),
290 "MockRead".to_string(),
291 "MockUpdate".to_string(),
292 "MockDelete".to_string(),
293 ],
294 crate::models::UserRole::Viewer => vec!["MockRead".to_string()],
295 })
296 .collect();
297
298 let last_activity = memberships.iter().map(|m| m.last_activity).max();
299
300 let days_inactive = last_activity.map(|activity| {
301 let duration = Utc::now() - activity;
302 duration.num_days() as u64
303 });
304
305 let access_granted =
306 memberships.iter().map(|m| m.joined_at).min().unwrap_or(user.created_at);
307
308 Ok(Some(UserAccessInfo {
309 user_id: user.id,
310 username: user.username,
311 email: user.email,
312 roles,
313 permissions,
314 last_login: last_activity,
315 access_granted,
316 days_inactive,
317 is_active: user.is_active,
318 }))
319 }
320
321 async fn get_last_login(&self, user_id: Uuid) -> Result<Option<DateTime<Utc>>, Error> {
322 let result = sqlx::query!(
324 r#"
325 SELECT MAX(last_activity) as "last_activity: chrono::DateTime<chrono::Utc>"
326 FROM workspace_members
327 WHERE user_id = ?
328 "#,
329 user_id
330 )
331 .fetch_optional(&self.db)
332 .await
333 .map_err(|e| Error::Generic(format!("Failed to get last login: {e}")))?;
334
335 Ok(result.and_then(|r| r.last_activity))
336 }
337
338 async fn revoke_user_access(&self, user_id: Uuid, reason: String) -> Result<(), Error> {
339 self.user_service
341 .deactivate_user(user_id)
342 .await
343 .map_err(|e| Error::Generic(format!("Failed to revoke access: {e}")))?;
344
345 tracing::info!("Revoked access for user {}: {}", user_id, reason);
346
347 Ok(())
348 }
349
350 async fn update_user_permissions(
351 &self,
352 user_id: Uuid,
353 roles: Vec<String>,
354 permissions: Vec<String>,
355 ) -> Result<(), Error> {
356 let memberships = sqlx::query_as!(
358 crate::models::WorkspaceMember,
359 r#"
360 SELECT
361 id as "id: Uuid",
362 workspace_id as "workspace_id: Uuid",
363 user_id as "user_id: Uuid",
364 role as "role: crate::models::UserRole",
365 joined_at as "joined_at: chrono::DateTime<chrono::Utc>",
366 last_activity as "last_activity: chrono::DateTime<chrono::Utc>"
367 FROM workspace_members
368 WHERE user_id = ?
369 "#,
370 user_id
371 )
372 .fetch_all(&self.db)
373 .await
374 .map_err(|e| Error::Generic(format!("Failed to fetch memberships: {e}")))?;
375
376 if memberships.is_empty() {
377 tracing::warn!("No workspace memberships found for user {}", user_id);
378 return Ok(());
379 }
380
381 let target_role = if roles.iter().any(|r| r.eq_ignore_ascii_case("admin")) {
384 crate::models::UserRole::Admin
385 } else if roles.iter().any(|r| r.eq_ignore_ascii_case("editor")) {
386 crate::models::UserRole::Editor
387 } else if roles.iter().any(|r| r.eq_ignore_ascii_case("viewer")) {
388 crate::models::UserRole::Viewer
389 } else {
390 tracing::warn!(
392 "No valid role found in provided roles: {:?}, keeping existing roles",
393 roles
394 );
395 return Ok(());
396 };
397
398 for membership in &memberships {
402 if membership.role == target_role {
404 continue;
405 }
406
407 sqlx::query(
411 r#"
412 UPDATE workspace_members
413 SET role = ?
414 WHERE workspace_id = ? AND user_id = ?
415 "#,
416 )
417 .bind(target_role)
418 .bind(membership.workspace_id)
419 .bind(user_id)
420 .execute(&self.db)
421 .await
422 .map_err(|e| {
423 Error::Generic(format!(
424 "Failed to update role for workspace {}: {e}",
425 membership.workspace_id
426 ))
427 })?;
428
429 tracing::info!(
430 "Updated user {} role to {:?} in workspace {}",
431 user_id,
432 target_role,
433 membership.workspace_id
434 );
435 }
436
437 tracing::info!(
438 "Updated permissions for user {}: roles={:?}, permissions={:?}",
439 user_id,
440 roles,
441 permissions
442 );
443
444 Ok(())
445 }
446}