Skip to main content

mockforge_collab/
permissions.rs

1//! Permission checking and role-based access control
2
3use crate::error::{CollabError, Result};
4use crate::models::UserRole;
5use serde::{Deserialize, Serialize};
6
7/// Permission types in the system
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
9pub enum Permission {
10    /// Create workspaces
11    WorkspaceCreate,
12    /// Read workspace data
13    WorkspaceRead,
14    /// Update workspace settings
15    WorkspaceUpdate,
16    /// Delete workspaces
17    WorkspaceDelete,
18    /// Archive workspaces
19    WorkspaceArchive,
20    /// Manage workspace members
21    WorkspaceManageMembers,
22
23    /// Create mock routes
24    MockCreate,
25    /// Read mock routes
26    MockRead,
27    /// Update mock routes
28    MockUpdate,
29    /// Delete mock routes
30    MockDelete,
31
32    /// Invite members to workspace
33    InviteMembers,
34    /// Remove members from workspace
35    RemoveMembers,
36    /// Change member roles
37    ChangeRoles,
38
39    /// View workspace history
40    ViewHistory,
41    /// Create named snapshots
42    CreateSnapshot,
43    /// Restore from snapshots
44    RestoreSnapshot,
45
46    /// Manage workspace settings
47    ManageSettings,
48    /// Manage integrations
49    ManageIntegrations,
50
51    // Scenario-specific permissions
52    /// Modify chaos rules for scenarios (typically QA only)
53    ScenarioModifyChaosRules,
54    /// Change reality-level defaults for scenarios (typically Platform team only)
55    ScenarioModifyRealityDefaults,
56    /// Promote scenarios between environments
57    ScenarioPromote,
58    /// Approve scenario promotions
59    ScenarioApprove,
60    /// Modify drift budgets for scenarios
61    ScenarioModifyDriftBudgets,
62}
63
64impl std::fmt::Display for Permission {
65    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66        match self {
67            Self::WorkspaceCreate => write!(f, "WorkspaceCreate"),
68            Self::WorkspaceRead => write!(f, "WorkspaceRead"),
69            Self::WorkspaceUpdate => write!(f, "WorkspaceUpdate"),
70            Self::WorkspaceDelete => write!(f, "WorkspaceDelete"),
71            Self::WorkspaceArchive => write!(f, "WorkspaceArchive"),
72            Self::WorkspaceManageMembers => write!(f, "WorkspaceManageMembers"),
73            Self::MockCreate => write!(f, "MockCreate"),
74            Self::MockRead => write!(f, "MockRead"),
75            Self::MockUpdate => write!(f, "MockUpdate"),
76            Self::MockDelete => write!(f, "MockDelete"),
77            Self::InviteMembers => write!(f, "InviteMembers"),
78            Self::RemoveMembers => write!(f, "RemoveMembers"),
79            Self::ChangeRoles => write!(f, "ChangeRoles"),
80            Self::ViewHistory => write!(f, "ViewHistory"),
81            Self::CreateSnapshot => write!(f, "CreateSnapshot"),
82            Self::RestoreSnapshot => write!(f, "RestoreSnapshot"),
83            Self::ManageSettings => write!(f, "ManageSettings"),
84            Self::ManageIntegrations => write!(f, "ManageIntegrations"),
85            Self::ScenarioModifyChaosRules => write!(f, "ScenarioModifyChaosRules"),
86            Self::ScenarioModifyRealityDefaults => write!(f, "ScenarioModifyRealityDefaults"),
87            Self::ScenarioPromote => write!(f, "ScenarioPromote"),
88            Self::ScenarioApprove => write!(f, "ScenarioApprove"),
89            Self::ScenarioModifyDriftBudgets => write!(f, "ScenarioModifyDriftBudgets"),
90        }
91    }
92}
93
94/// Role permissions mapping
95pub struct RolePermissions;
96
97impl RolePermissions {
98    /// Get all permissions for a role
99    #[must_use]
100    pub fn get_permissions(role: UserRole) -> Vec<Permission> {
101        match role {
102            UserRole::Admin => vec![
103                // Full access to everything
104                Permission::WorkspaceCreate,
105                Permission::WorkspaceRead,
106                Permission::WorkspaceUpdate,
107                Permission::WorkspaceDelete,
108                Permission::WorkspaceArchive,
109                Permission::WorkspaceManageMembers,
110                Permission::MockCreate,
111                Permission::MockRead,
112                Permission::MockUpdate,
113                Permission::MockDelete,
114                Permission::InviteMembers,
115                Permission::RemoveMembers,
116                Permission::ChangeRoles,
117                Permission::ViewHistory,
118                Permission::CreateSnapshot,
119                Permission::RestoreSnapshot,
120                Permission::ManageSettings,
121                Permission::ManageIntegrations,
122                // Scenario permissions - admins have all
123                Permission::ScenarioModifyChaosRules,
124                Permission::ScenarioModifyRealityDefaults,
125                Permission::ScenarioPromote,
126                Permission::ScenarioApprove,
127                Permission::ScenarioModifyDriftBudgets,
128            ],
129            UserRole::Editor => vec![
130                // Can edit but not manage workspace or members
131                Permission::WorkspaceRead,
132                Permission::MockCreate,
133                Permission::MockRead,
134                Permission::MockUpdate,
135                Permission::MockDelete,
136                Permission::ViewHistory,
137                Permission::CreateSnapshot,
138                // Editors can promote scenarios but not approve or modify sensitive configs
139                Permission::ScenarioPromote,
140            ],
141            UserRole::Viewer => vec![
142                // Read-only access
143                Permission::WorkspaceRead,
144                Permission::MockRead,
145                Permission::ViewHistory,
146            ],
147        }
148    }
149
150    /// Check if a role has a specific permission
151    #[must_use]
152    pub fn has_permission(role: UserRole, permission: Permission) -> bool {
153        Self::get_permissions(role).contains(&permission)
154    }
155}
156
157/// Permission checker for authorization
158pub struct PermissionChecker;
159
160impl PermissionChecker {
161    /// Check if a user has permission to perform an action
162    ///
163    /// # Errors
164    ///
165    /// Returns an error if the role lacks the required permission.
166    pub fn check(user_role: UserRole, required_permission: Permission) -> Result<()> {
167        if RolePermissions::has_permission(user_role, required_permission) {
168            Ok(())
169        } else {
170            Err(CollabError::AuthorizationFailed(format!(
171                "Role {user_role:?} does not have permission {required_permission:?}"
172            )))
173        }
174    }
175
176    /// Check multiple permissions (must have all)
177    ///
178    /// # Errors
179    ///
180    /// Returns an error if the role lacks any required permission.
181    pub fn check_all(user_role: UserRole, required_permissions: &[Permission]) -> Result<()> {
182        for permission in required_permissions {
183            Self::check(user_role, *permission)?;
184        }
185        Ok(())
186    }
187
188    /// Check multiple permissions (must have at least one)
189    ///
190    /// # Errors
191    ///
192    /// Returns an error if the role lacks all required permissions.
193    pub fn check_any(user_role: UserRole, required_permissions: &[Permission]) -> Result<()> {
194        for permission in required_permissions {
195            if RolePermissions::has_permission(user_role, *permission) {
196                return Ok(());
197            }
198        }
199        Err(CollabError::AuthorizationFailed(format!(
200            "Role {user_role:?} does not have any of the required permissions"
201        )))
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    #[test]
210    fn test_admin_permissions() {
211        let permissions = RolePermissions::get_permissions(UserRole::Admin);
212        assert!(permissions.contains(&Permission::WorkspaceDelete));
213        assert!(permissions.contains(&Permission::MockCreate));
214        assert!(permissions.contains(&Permission::ChangeRoles));
215    }
216
217    #[test]
218    fn test_editor_permissions() {
219        let permissions = RolePermissions::get_permissions(UserRole::Editor);
220        assert!(permissions.contains(&Permission::MockCreate));
221        assert!(permissions.contains(&Permission::MockUpdate));
222        assert!(!permissions.contains(&Permission::WorkspaceDelete));
223        assert!(!permissions.contains(&Permission::ChangeRoles));
224    }
225
226    #[test]
227    fn test_viewer_permissions() {
228        let permissions = RolePermissions::get_permissions(UserRole::Viewer);
229        assert!(permissions.contains(&Permission::WorkspaceRead));
230        assert!(permissions.contains(&Permission::MockRead));
231        assert!(!permissions.contains(&Permission::MockCreate));
232        assert!(!permissions.contains(&Permission::MockUpdate));
233    }
234
235    #[test]
236    fn test_permission_check() {
237        assert!(PermissionChecker::check(UserRole::Admin, Permission::WorkspaceDelete).is_ok());
238        assert!(PermissionChecker::check(UserRole::Editor, Permission::MockCreate).is_ok());
239        assert!(PermissionChecker::check(UserRole::Viewer, Permission::MockCreate).is_err());
240    }
241
242    #[test]
243    fn test_check_all() {
244        let permissions = vec![Permission::MockRead, Permission::MockCreate];
245        assert!(PermissionChecker::check_all(UserRole::Editor, &permissions).is_ok());
246        assert!(PermissionChecker::check_all(UserRole::Viewer, &permissions).is_err());
247    }
248
249    #[test]
250    fn test_check_any() {
251        let permissions = vec![Permission::MockCreate, Permission::WorkspaceDelete];
252        assert!(PermissionChecker::check_any(UserRole::Editor, &permissions).is_ok());
253
254        let admin_only = vec![Permission::WorkspaceDelete, Permission::ChangeRoles];
255        assert!(PermissionChecker::check_any(UserRole::Viewer, &admin_only).is_err());
256    }
257}