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: 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: 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                u64::try_from(duration.num_days()).unwrap_or(0)
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 Ok(user) = self.user_service.get_user(user_id).await else {
248            return Ok(None);
249        };
250
251        // Get memberships
252        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        // Use last_activity from workspace_members as proxy for last login
322        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        // Deactivate the user
339        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        // Get all workspace memberships for this user
356        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        // Determine target role based on provided roles
381        // Priority: Admin > Editor > Viewer
382        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            // If no valid role found, keep existing roles or default to viewer
390            tracing::warn!(
391                "No valid role found in provided roles: {:?}, keeping existing roles",
392                roles
393            );
394            return Ok(());
395        };
396
397        // Update all workspace memberships to the target role
398        // Note: In a more sophisticated implementation, we might want to update
399        // roles per-workspace, but the access review system provides roles at the user level
400        for membership in &memberships {
401            // Skip if role is already the target
402            if membership.role == target_role {
403                continue;
404            }
405
406            // Use workspace service to change role (requires admin permissions)
407            // For access review, we'll directly update the database
408            // In production, this should go through proper permission checks
409            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}