Skip to main content

fraiseql_auth/
operation_rbac.rs

1//! Operation-level Role-Based Access Control (RBAC).
2//!
3//! Defines the [`OperationPermission`] enum, the [`Role`] type that bundles a set
4//! of permissions, and the [`RBACPolicy`] engine that evaluates authorization
5//! decisions for authenticated users.
6
7use std::collections::HashMap;
8
9use serde::{Deserialize, Serialize};
10
11use crate::{AuthError, error::Result, middleware::AuthenticatedUser};
12
13/// A discrete permission that can be granted to a [`Role`].
14///
15/// Each variant maps to one or more GraphQL mutations or system operations.
16/// The string representation returned by [`OperationPermission::as_str`] is used
17/// when storing role-permission mappings in configuration or databases.
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
19#[non_exhaustive]
20pub enum OperationPermission {
21    /// Create a new observer rule.
22    CreateRule,
23    /// Modify an existing observer rule.
24    UpdateRule,
25    /// Remove an observer rule.
26    DeleteRule,
27    /// Trigger immediate execution of an observer rule.
28    ExecuteRule,
29
30    /// Create a new action definition.
31    CreateAction,
32    /// Modify an existing action definition.
33    UpdateAction,
34    /// Remove an action definition.
35    DeleteAction,
36    /// Trigger execution of an action.
37    ExecuteAction,
38
39    /// Create, update, or remove webhook subscriptions.
40    ManageWebhooks,
41    /// Read or rotate application secrets.
42    ManageSecrets,
43    /// Create, modify, or disable user accounts.
44    ManageUsers,
45    /// Define and assign roles within the system.
46    ManageRoles,
47    /// Create or modify multi-tenant isolation boundaries.
48    ManageTenants,
49
50    /// Export data records from the system.
51    ExportData,
52    /// Import data records into the system.
53    ImportData,
54    /// Permanently delete data records.
55    DeleteData,
56
57    /// Read the security audit trail.
58    ViewAuditLogs,
59    /// Modify system-wide configuration settings.
60    ManageConfiguration,
61    /// Configure third-party integrations and connectors.
62    ManageIntegrations,
63}
64
65impl OperationPermission {
66    /// Human-readable name for the permission
67    #[must_use]
68    pub const fn name(&self) -> &'static str {
69        match self {
70            Self::CreateRule => "Create Observer Rule",
71            Self::UpdateRule => "Update Observer Rule",
72            Self::DeleteRule => "Delete Observer Rule",
73            Self::ExecuteRule => "Execute Observer Rule",
74            Self::CreateAction => "Create Action",
75            Self::UpdateAction => "Update Action",
76            Self::DeleteAction => "Delete Action",
77            Self::ExecuteAction => "Execute Action",
78            Self::ManageWebhooks => "Manage Webhooks",
79            Self::ManageSecrets => "Manage Secrets",
80            Self::ManageUsers => "Manage Users",
81            Self::ManageRoles => "Manage Roles",
82            Self::ManageTenants => "Manage Tenants",
83            Self::ExportData => "Export Data",
84            Self::ImportData => "Import Data",
85            Self::DeleteData => "Delete Data",
86            Self::ViewAuditLogs => "View Audit Logs",
87            Self::ManageConfiguration => "Manage Configuration",
88            Self::ManageIntegrations => "Manage Integrations",
89        }
90    }
91
92    /// Convert to string for policy storage
93    #[must_use]
94    pub const fn as_str(&self) -> &'static str {
95        match self {
96            Self::CreateRule => "create_rule",
97            Self::UpdateRule => "update_rule",
98            Self::DeleteRule => "delete_rule",
99            Self::ExecuteRule => "execute_rule",
100            Self::CreateAction => "create_action",
101            Self::UpdateAction => "update_action",
102            Self::DeleteAction => "delete_action",
103            Self::ExecuteAction => "execute_action",
104            Self::ManageWebhooks => "manage_webhooks",
105            Self::ManageSecrets => "manage_secrets",
106            Self::ManageUsers => "manage_users",
107            Self::ManageRoles => "manage_roles",
108            Self::ManageTenants => "manage_tenants",
109            Self::ExportData => "export_data",
110            Self::ImportData => "import_data",
111            Self::DeleteData => "delete_data",
112            Self::ViewAuditLogs => "view_audit_logs",
113            Self::ManageConfiguration => "manage_configuration",
114            Self::ManageIntegrations => "manage_integrations",
115        }
116    }
117}
118
119/// Predefined roles with their associated permissions
120#[derive(Debug, Clone)]
121pub struct Role {
122    /// Role name (e.g., `"admin"`, `"viewer"`)
123    pub name:        String,
124    /// Set of operations this role is allowed to perform
125    pub permissions: Vec<OperationPermission>,
126}
127
128impl Role {
129    /// Create a new role with specified permissions
130    #[must_use]
131    pub const fn new(name: String, permissions: Vec<OperationPermission>) -> Self {
132        Self { name, permissions }
133    }
134
135    /// Check if role has a specific permission
136    #[must_use]
137    pub fn has_permission(&self, permission: OperationPermission) -> bool {
138        self.permissions.contains(&permission)
139    }
140
141    /// Get all permissions for this role
142    #[must_use]
143    pub fn get_permissions(&self) -> &[OperationPermission] {
144        &self.permissions
145    }
146}
147
148/// RBAC policy engine
149#[derive(Debug, Clone)]
150pub struct RBACPolicy {
151    roles: HashMap<String, Role>,
152}
153
154impl Default for RBACPolicy {
155    fn default() -> Self {
156        Self::new()
157    }
158}
159
160impl RBACPolicy {
161    /// Create a new RBAC policy with default roles
162    #[must_use]
163    pub fn new() -> Self {
164        let mut roles = HashMap::new();
165
166        // Admin role - full permissions
167        roles.insert(
168            "admin".to_string(),
169            Role::new(
170                "admin".to_string(),
171                vec![
172                    OperationPermission::CreateRule,
173                    OperationPermission::UpdateRule,
174                    OperationPermission::DeleteRule,
175                    OperationPermission::ExecuteRule,
176                    OperationPermission::CreateAction,
177                    OperationPermission::UpdateAction,
178                    OperationPermission::DeleteAction,
179                    OperationPermission::ExecuteAction,
180                    OperationPermission::ManageWebhooks,
181                    OperationPermission::ManageSecrets,
182                    OperationPermission::ManageUsers,
183                    OperationPermission::ManageRoles,
184                    OperationPermission::ManageTenants,
185                    OperationPermission::ExportData,
186                    OperationPermission::ImportData,
187                    OperationPermission::DeleteData,
188                    OperationPermission::ViewAuditLogs,
189                    OperationPermission::ManageConfiguration,
190                    OperationPermission::ManageIntegrations,
191                ],
192            ),
193        );
194
195        // Operator role - can modify rules and actions, view logs
196        roles.insert(
197            "operator".to_string(),
198            Role::new(
199                "operator".to_string(),
200                vec![
201                    OperationPermission::CreateRule,
202                    OperationPermission::UpdateRule,
203                    OperationPermission::DeleteRule,
204                    OperationPermission::ExecuteRule,
205                    OperationPermission::CreateAction,
206                    OperationPermission::UpdateAction,
207                    OperationPermission::DeleteAction,
208                    OperationPermission::ExecuteAction,
209                    OperationPermission::ManageWebhooks,
210                    OperationPermission::ExportData,
211                    OperationPermission::ViewAuditLogs,
212                ],
213            ),
214        );
215
216        // Viewer role - read-only access
217        roles.insert(
218            "viewer".to_string(),
219            Role::new(
220                "viewer".to_string(),
221                vec![
222                    OperationPermission::ExportData,
223                    OperationPermission::ViewAuditLogs,
224                ],
225            ),
226        );
227
228        Self { roles }
229    }
230
231    /// Register a custom role
232    pub fn register_role(&mut self, role: Role) {
233        self.roles.insert(role.name.clone(), role);
234    }
235
236    /// Check if a user has permission to perform an operation
237    ///
238    /// # Errors
239    ///
240    /// Returns `AuthError::Forbidden` if the user lacks the required permission.
241    pub fn authorize(
242        &self,
243        user: &AuthenticatedUser,
244        permission: OperationPermission,
245    ) -> Result<()> {
246        // Get user's roles (can be single role or array of roles)
247        let user_roles = self.extract_user_roles(user);
248
249        // Check if any of user's roles has the permission
250        for role_name in user_roles {
251            if let Some(role) = self.roles.get(&role_name) {
252                if role.has_permission(permission) {
253                    return Ok(());
254                }
255            }
256        }
257
258        Err(AuthError::Forbidden {
259            message: format!(
260                "User {} does not have permission to: {}",
261                user.user_id,
262                permission.name()
263            ),
264        })
265    }
266
267    /// Check multiple permissions at once
268    ///
269    /// # Errors
270    ///
271    /// Returns `AuthError::Forbidden` if the user lacks all of the given permissions.
272    pub fn authorize_any(
273        &self,
274        user: &AuthenticatedUser,
275        permissions: &[OperationPermission],
276    ) -> Result<()> {
277        for permission in permissions {
278            if self.authorize(user, *permission).is_ok() {
279                return Ok(());
280            }
281        }
282
283        Err(AuthError::Forbidden {
284            message: format!("User {} does not have any of the required permissions", user.user_id),
285        })
286    }
287
288    /// Check that user has all permissions
289    ///
290    /// # Errors
291    ///
292    /// Returns `AuthError::Forbidden` if the user lacks any of the required permissions.
293    pub fn authorize_all(
294        &self,
295        user: &AuthenticatedUser,
296        permissions: &[OperationPermission],
297    ) -> Result<()> {
298        for permission in permissions {
299            self.authorize(user, *permission)?;
300        }
301        Ok(())
302    }
303
304    /// Get all permissions for a user
305    #[must_use]
306    pub fn get_user_permissions(&self, user: &AuthenticatedUser) -> Vec<OperationPermission> {
307        let user_roles = self.extract_user_roles(user);
308        let mut permissions = Vec::new();
309
310        for role_name in user_roles {
311            if let Some(role) = self.roles.get(&role_name) {
312                permissions.extend(role.get_permissions());
313            }
314        }
315
316        // Remove duplicates
317        permissions.sort_by_key(|p| *p as u32);
318        permissions.dedup();
319
320        permissions
321    }
322
323    /// Extract user's roles from their claims
324    fn extract_user_roles(&self, user: &AuthenticatedUser) -> Vec<String> {
325        let mut roles = Vec::new();
326
327        // Check for single role claim
328        if let Some(serde_json::Value::String(role)) = user.get_custom_claim("role") {
329            roles.push(role.clone());
330        }
331
332        // Check for roles array
333        if let Some(serde_json::Value::Array(role_array)) = user.get_custom_claim("roles") {
334            for role_val in role_array {
335                if let serde_json::Value::String(role_name) = role_val {
336                    roles.push(role_name.clone());
337                }
338            }
339        }
340
341        // Check for standard claim name variations
342        if let Some(serde_json::Value::Array(role_array)) = user.get_custom_claim("fraiseql_roles")
343        {
344            for role_val in role_array {
345                if let serde_json::Value::String(role_name) = role_val {
346                    roles.push(role_name.clone());
347                }
348            }
349        }
350
351        // Remove duplicates
352        roles.sort();
353        roles.dedup();
354
355        roles
356    }
357}