mockforge_collab/
access_review_provider.rs

1//! Access review data provider for collaboration system
2//!
3//! This module provides a `UserDataProvider` implementation that integrates
4//! the access review system with the collaboration database.
5
6use 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
16/// User data provider for collaboration system
17pub 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    /// Create a new user data provider
28    #[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    /// Create with optional storage backends
45    #[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        // Fetch all active users
69        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        // For each user, get their workspace memberships and roles
86        let mut user_access_list = Vec::new();
87
88        for user in users {
89            // Get all workspace memberships for this user
90            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            // Collect roles and permissions
111            let roles: Vec<String> = memberships.iter().map(|m| format!("{:?}", m.role)).collect();
112
113            // Map roles to permissions (simplified - in reality would use PermissionChecker)
114            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            // Get most recent activity
139            let last_activity = memberships.iter().map(|m| m.last_activity).max();
140
141            // Calculate days inactive
142            let days_inactive = last_activity.map(|activity| {
143                let duration = Utc::now() - activity;
144                duration.num_days() as u64
145            });
146
147            // Access granted date is the earliest membership join date
148            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, // Using last_activity as proxy for last login
158                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        // Get all users with admin role in any workspace
169        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        // Group by user_id and collect roles
189        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        // Get user details
199        let mut privileged_list = Vec::new();
200
201        for (user_id, roles) in user_roles {
202            // Get user details
203            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            // Check MFA status
213            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            // Get justification
225            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            // No token storage configured, return empty list
257            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        // Get memberships
268        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        // Use last_activity from workspace_members as proxy for last login
338        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        // Deactivate the user
355        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        // Get all workspace memberships for this user
372        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        // Determine target role based on provided roles
397        // Priority: Admin > Editor > Viewer
398        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            // If no valid role found, keep existing roles or default to viewer
406            tracing::warn!(
407                "No valid role found in provided roles: {:?}, keeping existing roles",
408                roles
409            );
410            return Ok(());
411        };
412
413        // Update all workspace memberships to the target role
414        // Note: In a more sophisticated implementation, we might want to update
415        // roles per-workspace, but the access review system provides roles at the user level
416        for membership in &memberships {
417            // Skip if role is already the target
418            if membership.role == target_role {
419                continue;
420            }
421
422            // Use workspace service to change role (requires admin permissions)
423            // For access review, we'll directly update the database
424            // In production, this should go through proper permission checks
425            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}