1use serde::{Deserialize, Serialize};
8use std::collections::{HashMap, HashSet};
9use uuid::Uuid;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
17#[serde(rename_all = "snake_case")]
18pub enum Role {
19 Owner,
21 Admin,
23 Member,
25 Viewer,
27 Guest,
29}
30
31impl Role {
32 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 pub fn has_permission(&self, permission: Permission) -> bool {
75 self.default_permissions().contains(&permission)
76 }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
81#[serde(rename_all = "snake_case")]
82pub enum Permission {
83 ViewTeam,
85 EditTeam,
86 DeleteTeam,
87 TransferOwnership,
88
89 ViewMembers,
91 InviteMembers,
92 RemoveMembers,
93 EditMemberRoles,
94
95 ViewSessions,
97 CreateSession,
98 EditOwnSessions,
99 EditAllSessions,
100 DeleteOwnSessions,
101 DeleteAllSessions,
102 ShareSessions,
103 ExportSessions,
104
105 AddComments,
107 EditOwnComments,
108 EditAllComments,
109 DeleteOwnComments,
110 DeleteAllComments,
111
112 ViewAnalytics,
114 ExportAnalytics,
115 ConfigureAnalytics,
116
117 ViewActivityFeed,
119
120 EditTeamSettings,
122 ManageIntegrations,
123 ManageWebhooks,
124
125 ViewAuditLog,
127 ManageRetentionPolicy,
128}
129
130impl Permission {
131 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
210pub struct RoleAssignment {
211 pub user_id: Uuid,
213 pub team_id: Uuid,
215 pub role: Role,
217 pub granted_permissions: HashSet<Permission>,
219 pub revoked_permissions: HashSet<Permission>,
221}
222
223impl RoleAssignment {
224 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 pub fn effective_permissions(&self) -> HashSet<Permission> {
237 let mut perms = self.role.default_permissions();
238
239 for perm in &self.granted_permissions {
241 perms.insert(*perm);
242 }
243
244 for perm in &self.revoked_permissions {
246 perms.remove(perm);
247 }
248
249 perms
250 }
251
252 pub fn has_permission(&self, permission: Permission) -> bool {
254 self.effective_permissions().contains(&permission)
255 }
256
257 pub fn grant(&mut self, permission: Permission) {
259 self.revoked_permissions.remove(&permission);
260 self.granted_permissions.insert(permission);
261 }
262
263 pub fn revoke(&mut self, permission: Permission) {
265 self.granted_permissions.remove(&permission);
266 self.revoked_permissions.insert(permission);
267 }
268}
269
270#[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#[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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
301pub enum AccessDecision {
302 Allow,
303 Deny,
304}
305
306pub struct AccessControl {
308 assignments: HashMap<(Uuid, Uuid), RoleAssignment>,
310}
311
312impl AccessControl {
313 pub fn new() -> Self {
315 Self {
316 assignments: HashMap::new(),
317 }
318 }
319
320 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 pub fn remove_assignment(&mut self, team_id: Uuid, user_id: Uuid) {
330 self.assignments.remove(&(team_id, user_id));
331 }
332
333 pub fn get_assignment(&self, team_id: Uuid, user_id: Uuid) -> Option<&RoleAssignment> {
335 self.assignments.get(&(team_id, user_id))
336 }
337
338 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 let assignment = match self.get_assignment(team_id, user_id) {
351 Some(a) => a,
352 None => return AccessDecision::Deny,
353 };
354
355 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 fn get_required_permission(
367 &self,
368 user_id: Uuid,
369 resource: &Resource,
370 action: Action,
371 ) -> Permission {
372 match (resource, action) {
373 (Resource::Team { .. }, Action::View) => Permission::ViewTeam,
375 (Resource::Team { .. }, Action::Edit) => Permission::EditTeam,
376 (Resource::Team { .. }, Action::Delete) => Permission::DeleteTeam,
377
378 (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 (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 (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 (Resource::Analytics { .. }, Action::View) => Permission::ViewAnalytics,
423 (Resource::Analytics { .. }, Action::Export) => Permission::ExportAnalytics,
424 (Resource::Analytics { .. }, Action::Manage) => Permission::ConfigureAnalytics,
425
426 (Resource::Settings { .. }, Action::View) => Permission::ViewTeam,
428 (Resource::Settings { .. }, Action::Edit) => Permission::EditTeamSettings,
429
430 _ => Permission::ViewTeam, }
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 assignment.grant(Permission::EditAllSessions);
468 assert!(assignment.has_permission(Permission::EditAllSessions));
469
470 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 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 assert_eq!(ac.check(user_id, &resource, Action::Edit), AccessDecision::Deny);
494
495 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}