Skip to main content

chasm/teams/
rbac.rs

1// Copyright (c) 2024-2027 Nervosys LLC
2// SPDX-License-Identifier: AGPL-3.0-only
3//! Role-Based Access Control (RBAC) module
4//!
5//! Provides roles, permissions, and access control for team workspaces.
6
7use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use uuid::Uuid;
10
11// ============================================================================
12// Roles and Permissions
13// ============================================================================
14
15/// Team role
16#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum Role {
19    /// Team owner - full access
20    Owner,
21    /// Administrator - can manage team and members
22    Admin,
23    /// Regular member - can view and contribute
24    Member,
25    /// Viewer - read-only access
26    Viewer,
27    /// Guest - limited access to specific resources
28    Guest,
29}
30
31impl Role {
32    /// Get default permissions for this role
33    pub fn default_permissions(&self) -> HashSet<Permission> {
34        match self {
35            Role::Owner => Permission::all(),
36            Role::Admin => {
37                let mut perms = Permission::all();
38                perms.remove(&Permission::DeleteTeam);
39                perms.remove(&Permission::TransferOwnership);
40                perms
41            }
42            Role::Member => {
43                let mut perms = HashSet::new();
44                perms.insert(Permission::ViewTeam);
45                perms.insert(Permission::ViewMembers);
46                perms.insert(Permission::ViewSessions);
47                perms.insert(Permission::CreateSession);
48                perms.insert(Permission::EditOwnSessions);
49                perms.insert(Permission::DeleteOwnSessions);
50                perms.insert(Permission::ShareSessions);
51                perms.insert(Permission::AddComments);
52                perms.insert(Permission::ViewAnalytics);
53                perms.insert(Permission::ViewActivityFeed);
54                perms
55            }
56            Role::Viewer => {
57                let mut perms = HashSet::new();
58                perms.insert(Permission::ViewTeam);
59                perms.insert(Permission::ViewMembers);
60                perms.insert(Permission::ViewSessions);
61                perms.insert(Permission::ViewAnalytics);
62                perms.insert(Permission::ViewActivityFeed);
63                perms
64            }
65            Role::Guest => {
66                let mut perms = HashSet::new();
67                perms.insert(Permission::ViewSessions);
68                perms
69            }
70        }
71    }
72
73    /// Check if this role has a permission
74    pub fn has_permission(&self, permission: Permission) -> bool {
75        self.default_permissions().contains(&permission)
76    }
77}
78
79/// Granular permission
80#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum Permission {
83    // Team management
84    ViewTeam,
85    EditTeam,
86    DeleteTeam,
87    TransferOwnership,
88
89    // Member management
90    ViewMembers,
91    InviteMembers,
92    RemoveMembers,
93    EditMemberRoles,
94
95    // Session management
96    ViewSessions,
97    CreateSession,
98    EditOwnSessions,
99    EditAllSessions,
100    DeleteOwnSessions,
101    DeleteAllSessions,
102    ShareSessions,
103    ExportSessions,
104
105    // Collaboration
106    AddComments,
107    EditOwnComments,
108    EditAllComments,
109    DeleteOwnComments,
110    DeleteAllComments,
111
112    // Analytics
113    ViewAnalytics,
114    ExportAnalytics,
115    ConfigureAnalytics,
116
117    // Activity
118    ViewActivityFeed,
119
120    // Settings
121    EditTeamSettings,
122    ManageIntegrations,
123    ManageWebhooks,
124
125    // Admin
126    ViewAuditLog,
127    ManageRetentionPolicy,
128}
129
130impl Permission {
131    /// Get all permissions
132    pub fn all() -> HashSet<Permission> {
133        use Permission::*;
134        [
135            ViewTeam,
136            EditTeam,
137            DeleteTeam,
138            TransferOwnership,
139            ViewMembers,
140            InviteMembers,
141            RemoveMembers,
142            EditMemberRoles,
143            ViewSessions,
144            CreateSession,
145            EditOwnSessions,
146            EditAllSessions,
147            DeleteOwnSessions,
148            DeleteAllSessions,
149            ShareSessions,
150            ExportSessions,
151            AddComments,
152            EditOwnComments,
153            EditAllComments,
154            DeleteOwnComments,
155            DeleteAllComments,
156            ViewAnalytics,
157            ExportAnalytics,
158            ConfigureAnalytics,
159            ViewActivityFeed,
160            EditTeamSettings,
161            ManageIntegrations,
162            ManageWebhooks,
163            ViewAuditLog,
164            ManageRetentionPolicy,
165        ]
166        .into_iter()
167        .collect()
168    }
169
170    /// Get permission description
171    pub fn description(&self) -> &'static str {
172        use Permission::*;
173        match self {
174            ViewTeam => "View team information",
175            EditTeam => "Edit team name and description",
176            DeleteTeam => "Delete the team",
177            TransferOwnership => "Transfer team ownership",
178            ViewMembers => "View team members",
179            InviteMembers => "Invite new members",
180            RemoveMembers => "Remove members from team",
181            EditMemberRoles => "Change member roles",
182            ViewSessions => "View sessions",
183            CreateSession => "Create new sessions",
184            EditOwnSessions => "Edit own sessions",
185            EditAllSessions => "Edit any session",
186            DeleteOwnSessions => "Delete own sessions",
187            DeleteAllSessions => "Delete any session",
188            ShareSessions => "Share sessions with team",
189            ExportSessions => "Export sessions",
190            AddComments => "Add comments",
191            EditOwnComments => "Edit own comments",
192            EditAllComments => "Edit any comment",
193            DeleteOwnComments => "Delete own comments",
194            DeleteAllComments => "Delete any comment",
195            ViewAnalytics => "View analytics",
196            ExportAnalytics => "Export analytics data",
197            ConfigureAnalytics => "Configure analytics settings",
198            ViewActivityFeed => "View activity feed",
199            EditTeamSettings => "Edit team settings",
200            ManageIntegrations => "Manage integrations",
201            ManageWebhooks => "Manage webhooks",
202            ViewAuditLog => "View audit log",
203            ManageRetentionPolicy => "Manage data retention",
204        }
205    }
206}
207
208/// Role assignment for a user
209#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct RoleAssignment {
211    /// User ID
212    pub user_id: Uuid,
213    /// Team ID
214    pub team_id: Uuid,
215    /// Assigned role
216    pub role: Role,
217    /// Custom permission overrides (additions)
218    pub granted_permissions: HashSet<Permission>,
219    /// Custom permission overrides (removals)
220    pub revoked_permissions: HashSet<Permission>,
221}
222
223impl RoleAssignment {
224    /// Create a new role assignment
225    pub fn new(user_id: Uuid, team_id: Uuid, role: Role) -> Self {
226        Self {
227            user_id,
228            team_id,
229            role,
230            granted_permissions: HashSet::new(),
231            revoked_permissions: HashSet::new(),
232        }
233    }
234
235    /// Get effective permissions
236    pub fn effective_permissions(&self) -> HashSet<Permission> {
237        let mut perms = self.role.default_permissions();
238
239        // Add granted permissions
240        for perm in &self.granted_permissions {
241            perms.insert(*perm);
242        }
243
244        // Remove revoked permissions
245        for perm in &self.revoked_permissions {
246            perms.remove(perm);
247        }
248
249        perms
250    }
251
252    /// Check if user has a specific permission
253    pub fn has_permission(&self, permission: Permission) -> bool {
254        self.effective_permissions().contains(&permission)
255    }
256
257    /// Grant an additional permission
258    pub fn grant(&mut self, permission: Permission) {
259        self.revoked_permissions.remove(&permission);
260        self.granted_permissions.insert(permission);
261    }
262
263    /// Revoke a permission
264    pub fn revoke(&mut self, permission: Permission) {
265        self.granted_permissions.remove(&permission);
266        self.revoked_permissions.insert(permission);
267    }
268}
269
270// ============================================================================
271// Access Control
272// ============================================================================
273
274/// Resource being accessed
275#[derive(Debug, Clone, Serialize, Deserialize)]
276#[serde(tag = "type", rename_all = "snake_case")]
277pub enum Resource {
278    Team { team_id: Uuid },
279    Member { team_id: Uuid, member_id: Uuid },
280    Session { team_id: Uuid, session_id: String, owner_id: Uuid },
281    Comment { team_id: Uuid, comment_id: String, author_id: Uuid },
282    Analytics { team_id: Uuid },
283    Settings { team_id: Uuid },
284}
285
286/// Action being performed
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "snake_case")]
289pub enum Action {
290    View,
291    Create,
292    Edit,
293    Delete,
294    Share,
295    Export,
296    Manage,
297}
298
299/// Access decision
300#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum AccessDecision {
302    Allow,
303    Deny,
304}
305
306/// Access control manager
307pub struct AccessControl {
308    /// Role assignments by (team_id, user_id)
309    assignments: HashMap<(Uuid, Uuid), RoleAssignment>,
310}
311
312impl AccessControl {
313    /// Create a new access control manager
314    pub fn new() -> Self {
315        Self {
316            assignments: HashMap::new(),
317        }
318    }
319
320    /// Add a role assignment
321    pub fn assign_role(&mut self, assignment: RoleAssignment) {
322        self.assignments.insert(
323            (assignment.team_id, assignment.user_id),
324            assignment,
325        );
326    }
327
328    /// Remove a role assignment
329    pub fn remove_assignment(&mut self, team_id: Uuid, user_id: Uuid) {
330        self.assignments.remove(&(team_id, user_id));
331    }
332
333    /// Get role assignment
334    pub fn get_assignment(&self, team_id: Uuid, user_id: Uuid) -> Option<&RoleAssignment> {
335        self.assignments.get(&(team_id, user_id))
336    }
337
338    /// Check if a user can perform an action on a resource
339    pub fn check(&self, user_id: Uuid, resource: &Resource, action: Action) -> AccessDecision {
340        let team_id = match resource {
341            Resource::Team { team_id } => *team_id,
342            Resource::Member { team_id, .. } => *team_id,
343            Resource::Session { team_id, .. } => *team_id,
344            Resource::Comment { team_id, .. } => *team_id,
345            Resource::Analytics { team_id } => *team_id,
346            Resource::Settings { team_id } => *team_id,
347        };
348
349        // Get user's role assignment
350        let assignment = match self.get_assignment(team_id, user_id) {
351            Some(a) => a,
352            None => return AccessDecision::Deny,
353        };
354
355        // Map resource and action to required permission
356        let required_permission = self.get_required_permission(user_id, resource, action);
357
358        if assignment.has_permission(required_permission) {
359            AccessDecision::Allow
360        } else {
361            AccessDecision::Deny
362        }
363    }
364
365    /// Get the required permission for a resource/action combination
366    fn get_required_permission(
367        &self,
368        user_id: Uuid,
369        resource: &Resource,
370        action: Action,
371    ) -> Permission {
372        match (resource, action) {
373            // Team
374            (Resource::Team { .. }, Action::View) => Permission::ViewTeam,
375            (Resource::Team { .. }, Action::Edit) => Permission::EditTeam,
376            (Resource::Team { .. }, Action::Delete) => Permission::DeleteTeam,
377
378            // Members
379            (Resource::Member { .. }, Action::View) => Permission::ViewMembers,
380            (Resource::Member { .. }, Action::Create) => Permission::InviteMembers,
381            (Resource::Member { .. }, Action::Delete) => Permission::RemoveMembers,
382            (Resource::Member { .. }, Action::Edit) => Permission::EditMemberRoles,
383
384            // Sessions
385            (Resource::Session { owner_id: _, .. }, Action::View) => Permission::ViewSessions,
386            (Resource::Session { .. }, Action::Create) => Permission::CreateSession,
387            (Resource::Session { owner_id, .. }, Action::Edit) => {
388                if *owner_id == user_id {
389                    Permission::EditOwnSessions
390                } else {
391                    Permission::EditAllSessions
392                }
393            }
394            (Resource::Session { owner_id, .. }, Action::Delete) => {
395                if *owner_id == user_id {
396                    Permission::DeleteOwnSessions
397                } else {
398                    Permission::DeleteAllSessions
399                }
400            }
401            (Resource::Session { .. }, Action::Share) => Permission::ShareSessions,
402            (Resource::Session { .. }, Action::Export) => Permission::ExportSessions,
403
404            // Comments
405            (Resource::Comment { author_id: _, .. }, Action::Create) => Permission::AddComments,
406            (Resource::Comment { author_id, .. }, Action::Edit) => {
407                if *author_id == user_id {
408                    Permission::EditOwnComments
409                } else {
410                    Permission::EditAllComments
411                }
412            }
413            (Resource::Comment { author_id, .. }, Action::Delete) => {
414                if *author_id == user_id {
415                    Permission::DeleteOwnComments
416                } else {
417                    Permission::DeleteAllComments
418                }
419            }
420
421            // Analytics
422            (Resource::Analytics { .. }, Action::View) => Permission::ViewAnalytics,
423            (Resource::Analytics { .. }, Action::Export) => Permission::ExportAnalytics,
424            (Resource::Analytics { .. }, Action::Manage) => Permission::ConfigureAnalytics,
425
426            // Settings
427            (Resource::Settings { .. }, Action::View) => Permission::ViewTeam,
428            (Resource::Settings { .. }, Action::Edit) => Permission::EditTeamSettings,
429
430            // Default deny
431            _ => Permission::ViewTeam, // Will be denied if user doesn't have it
432        }
433    }
434}
435
436impl Default for AccessControl {
437    fn default() -> Self {
438        Self::new()
439    }
440}
441
442#[cfg(test)]
443mod tests {
444    use super::*;
445
446    #[test]
447    fn test_role_permissions() {
448        assert!(Role::Owner.has_permission(Permission::DeleteTeam));
449        assert!(Role::Admin.has_permission(Permission::InviteMembers));
450        assert!(!Role::Admin.has_permission(Permission::DeleteTeam));
451        assert!(Role::Member.has_permission(Permission::ViewSessions));
452        assert!(!Role::Member.has_permission(Permission::EditAllSessions));
453        assert!(Role::Viewer.has_permission(Permission::ViewSessions));
454        assert!(!Role::Viewer.has_permission(Permission::CreateSession));
455    }
456
457    #[test]
458    fn test_role_assignment() {
459        let user_id = Uuid::new_v4();
460        let team_id = Uuid::new_v4();
461        let mut assignment = RoleAssignment::new(user_id, team_id, Role::Member);
462
463        assert!(assignment.has_permission(Permission::CreateSession));
464        assert!(!assignment.has_permission(Permission::EditAllSessions));
465
466        // Grant additional permission
467        assignment.grant(Permission::EditAllSessions);
468        assert!(assignment.has_permission(Permission::EditAllSessions));
469
470        // Revoke a default permission
471        assignment.revoke(Permission::CreateSession);
472        assert!(!assignment.has_permission(Permission::CreateSession));
473    }
474
475    #[test]
476    fn test_access_control() {
477        let mut ac = AccessControl::new();
478        let user_id = Uuid::new_v4();
479        let owner_id = Uuid::new_v4();
480        let team_id = Uuid::new_v4();
481
482        ac.assign_role(RoleAssignment::new(user_id, team_id, Role::Member));
483
484        // Can view sessions
485        let resource = Resource::Session {
486            team_id,
487            session_id: "session-1".to_string(),
488            owner_id,
489        };
490        assert_eq!(ac.check(user_id, &resource, Action::View), AccessDecision::Allow);
491
492        // Cannot edit others' sessions
493        assert_eq!(ac.check(user_id, &resource, Action::Edit), AccessDecision::Deny);
494
495        // Can edit own sessions
496        let own_resource = Resource::Session {
497            team_id,
498            session_id: "session-2".to_string(),
499            owner_id: user_id,
500        };
501        assert_eq!(ac.check(user_id, &own_resource, Action::Edit), AccessDecision::Allow);
502    }
503}