Skip to main content

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            // Use canonical role-permission mapping from RolePermissions
114            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            // Get most recent activity
124            let last_activity = memberships.iter().map(|m| m.last_activity).max();
125
126            // Calculate days inactive
127            let days_inactive = last_activity.map(|activity| {
128                let duration = Utc::now() - activity;
129                duration.num_days() as u64
130            });
131
132            // Access granted date is the earliest membership join date
133            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, // Using last_activity as proxy for last login
143                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        // Get all users with admin role in any workspace
154        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        // Group by user_id and collect roles
174        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        // Get user details
184        let mut privileged_list = Vec::new();
185
186        for (user_id, roles) in user_roles {
187            // Get user details
188            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            // Check MFA status
198            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            // Get justification
210            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            // No token storage configured, return empty list
242            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        // Get memberships
253        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        // Use last_activity from workspace_members as proxy for last login
323        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        // Deactivate the user
340        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        // Get all workspace memberships for this user
357        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        // Determine target role based on provided roles
382        // Priority: Admin > Editor > Viewer
383        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            // If no valid role found, keep existing roles or default to viewer
391            tracing::warn!(
392                "No valid role found in provided roles: {:?}, keeping existing roles",
393                roles
394            );
395            return Ok(());
396        };
397
398        // Update all workspace memberships to the target role
399        // Note: In a more sophisticated implementation, we might want to update
400        // roles per-workspace, but the access review system provides roles at the user level
401        for membership in &memberships {
402            // Skip if role is already the target
403            if membership.role == target_role {
404                continue;
405            }
406
407            // Use workspace service to change role (requires admin permissions)
408            // For access review, we'll directly update the database
409            // In production, this should go through proper permission checks
410            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}