Skip to main content

torsh_package/
access_control.rs

1//! Role-Based Access Control (RBAC) for package distribution
2//!
3//! This module provides fine-grained access control for package operations
4//! including publishing, downloading, modifying, and deleting packages.
5
6use std::collections::{HashMap, HashSet};
7
8use chrono::{DateTime, Utc};
9use serde::{Deserialize, Serialize};
10use torsh_core::error::{Result, TorshError};
11
12/// Permission for package operations
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub enum Permission {
15    /// Read package metadata
16    ReadMetadata,
17    /// Download package
18    Download,
19    /// Publish new package
20    Publish,
21    /// Update existing package
22    Update,
23    /// Delete package
24    Delete,
25    /// Manage package versions
26    ManageVersions,
27    /// Yank package version
28    Yank,
29    /// Un-yank package version
30    Unyank,
31    /// Manage package owners
32    ManageOwners,
33    /// View package statistics
34    ViewStats,
35    /// Modify package security settings
36    ManageSecurity,
37    /// Custom permission
38    Custom(String),
39}
40
41/// Role with a set of permissions
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct Role {
44    /// Role name
45    pub name: String,
46    /// Role description
47    pub description: String,
48    /// Permissions granted to this role
49    pub permissions: HashSet<Permission>,
50    /// Whether this is a built-in role
51    pub builtin: bool,
52}
53
54/// User identity
55#[derive(Debug, Clone, Serialize, Deserialize)]
56pub struct User {
57    /// User ID (unique identifier)
58    pub id: String,
59    /// Username
60    pub username: String,
61    /// Email address
62    pub email: String,
63    /// Roles assigned to this user
64    pub roles: HashSet<String>,
65    /// Whether the user is active
66    pub active: bool,
67    /// When the user was created
68    pub created_at: DateTime<Utc>,
69}
70
71/// Organization for package management
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub struct Organization {
74    /// Organization ID
75    pub id: String,
76    /// Organization name
77    pub name: String,
78    /// Organization description
79    pub description: Option<String>,
80    /// Members of the organization
81    pub members: HashMap<String, OrganizationMembership>,
82    /// Created timestamp
83    pub created_at: DateTime<Utc>,
84}
85
86/// Organization membership
87#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct OrganizationMembership {
89    /// User ID
90    pub user_id: String,
91    /// Roles within the organization
92    pub roles: HashSet<String>,
93    /// When the user joined
94    pub joined_at: DateTime<Utc>,
95}
96
97/// Package ownership
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct PackageOwnership {
100    /// Package name
101    pub package_name: String,
102    /// Owners (user IDs or organization IDs)
103    pub owners: HashSet<String>,
104    /// Permissions for specific users
105    pub user_permissions: HashMap<String, HashSet<Permission>>,
106    /// Public access level
107    pub public_access: AccessLevel,
108}
109
110/// Access level for public packages
111#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
112pub enum AccessLevel {
113    /// Public - anyone can read
114    Public,
115    /// Restricted - only authenticated users can read
116    Restricted,
117    /// Private - only owners can access
118    Private,
119}
120
121/// Access control list (ACL) entry
122#[derive(Debug, Clone, Serialize, Deserialize)]
123pub struct AclEntry {
124    /// Principal (user or organization ID)
125    pub principal: String,
126    /// Permissions granted
127    pub permissions: HashSet<Permission>,
128    /// Expiration time (if any)
129    pub expires_at: Option<DateTime<Utc>>,
130}
131
132/// Access control manager
133pub struct AccessControlManager {
134    /// Role definitions
135    roles: HashMap<String, Role>,
136    /// Users
137    users: HashMap<String, User>,
138    /// Organizations
139    organizations: HashMap<String, Organization>,
140    /// Package ownership
141    package_ownership: HashMap<String, PackageOwnership>,
142}
143
144/// Access check result
145#[derive(Debug, Clone)]
146pub struct AccessCheckResult {
147    /// Whether access is granted
148    pub granted: bool,
149    /// Reason for denial (if not granted)
150    pub denial_reason: Option<String>,
151    /// Permissions that were checked
152    pub checked_permissions: HashSet<Permission>,
153}
154
155impl Role {
156    /// Create a new role
157    pub fn new(name: String, description: String) -> Self {
158        Self {
159            name,
160            description,
161            permissions: HashSet::new(),
162            builtin: false,
163        }
164    }
165
166    /// Add a permission to the role
167    pub fn add_permission(&mut self, permission: Permission) {
168        self.permissions.insert(permission);
169    }
170
171    /// Check if role has a permission
172    pub fn has_permission(&self, permission: &Permission) -> bool {
173        self.permissions.contains(permission)
174    }
175
176    /// Admin role (all permissions)
177    pub fn admin() -> Self {
178        let mut role = Self {
179            name: "admin".to_string(),
180            description: "Administrator with all permissions".to_string(),
181            permissions: HashSet::new(),
182            builtin: true,
183        };
184
185        role.permissions.insert(Permission::ReadMetadata);
186        role.permissions.insert(Permission::Download);
187        role.permissions.insert(Permission::Publish);
188        role.permissions.insert(Permission::Update);
189        role.permissions.insert(Permission::Delete);
190        role.permissions.insert(Permission::ManageVersions);
191        role.permissions.insert(Permission::Yank);
192        role.permissions.insert(Permission::Unyank);
193        role.permissions.insert(Permission::ManageOwners);
194        role.permissions.insert(Permission::ViewStats);
195        role.permissions.insert(Permission::ManageSecurity);
196
197        role
198    }
199
200    /// Maintainer role (can publish and update)
201    pub fn maintainer() -> Self {
202        let mut role = Self {
203            name: "maintainer".to_string(),
204            description: "Maintainer can publish and update packages".to_string(),
205            permissions: HashSet::new(),
206            builtin: true,
207        };
208
209        role.permissions.insert(Permission::ReadMetadata);
210        role.permissions.insert(Permission::Download);
211        role.permissions.insert(Permission::Publish);
212        role.permissions.insert(Permission::Update);
213        role.permissions.insert(Permission::ManageVersions);
214        role.permissions.insert(Permission::Yank);
215        role.permissions.insert(Permission::Unyank);
216
217        role
218    }
219
220    /// Contributor role (can only download)
221    pub fn contributor() -> Self {
222        let mut role = Self {
223            name: "contributor".to_string(),
224            description: "Contributor can download packages".to_string(),
225            permissions: HashSet::new(),
226            builtin: true,
227        };
228
229        role.permissions.insert(Permission::ReadMetadata);
230        role.permissions.insert(Permission::Download);
231
232        role
233    }
234
235    /// Viewer role (can only view metadata)
236    pub fn viewer() -> Self {
237        let mut role = Self {
238            name: "viewer".to_string(),
239            description: "Viewer can only view metadata".to_string(),
240            permissions: HashSet::new(),
241            builtin: true,
242        };
243
244        role.permissions.insert(Permission::ReadMetadata);
245
246        role
247    }
248}
249
250impl User {
251    /// Create a new user
252    pub fn new(id: String, username: String, email: String) -> Self {
253        Self {
254            id,
255            username,
256            email,
257            roles: HashSet::new(),
258            active: true,
259            created_at: Utc::now(),
260        }
261    }
262
263    /// Add a role to the user
264    pub fn add_role(&mut self, role: String) {
265        self.roles.insert(role);
266    }
267
268    /// Check if user has a role
269    pub fn has_role(&self, role: &str) -> bool {
270        self.roles.contains(role)
271    }
272
273    /// Deactivate user
274    pub fn deactivate(&mut self) {
275        self.active = false;
276    }
277}
278
279impl Organization {
280    /// Create a new organization
281    pub fn new(id: String, name: String) -> Self {
282        Self {
283            id,
284            name,
285            description: None,
286            members: HashMap::new(),
287            created_at: Utc::now(),
288        }
289    }
290
291    /// Add a member to the organization
292    pub fn add_member(&mut self, user_id: String, roles: HashSet<String>) {
293        let membership = OrganizationMembership {
294            user_id: user_id.clone(),
295            roles,
296            joined_at: Utc::now(),
297        };
298        self.members.insert(user_id, membership);
299    }
300
301    /// Remove a member from the organization
302    pub fn remove_member(&mut self, user_id: &str) {
303        self.members.remove(user_id);
304    }
305
306    /// Check if user is a member
307    pub fn is_member(&self, user_id: &str) -> bool {
308        self.members.contains_key(user_id)
309    }
310
311    /// Get member's roles
312    pub fn get_member_roles(&self, user_id: &str) -> Option<&HashSet<String>> {
313        self.members.get(user_id).map(|m| &m.roles)
314    }
315}
316
317impl PackageOwnership {
318    /// Create new package ownership
319    pub fn new(package_name: String) -> Self {
320        Self {
321            package_name,
322            owners: HashSet::new(),
323            user_permissions: HashMap::new(),
324            public_access: AccessLevel::Public,
325        }
326    }
327
328    /// Add an owner
329    pub fn add_owner(&mut self, owner_id: String) {
330        self.owners.insert(owner_id);
331    }
332
333    /// Remove an owner
334    pub fn remove_owner(&mut self, owner_id: &str) {
335        self.owners.remove(owner_id);
336    }
337
338    /// Check if principal is an owner
339    pub fn is_owner(&self, principal_id: &str) -> bool {
340        self.owners.contains(principal_id)
341    }
342
343    /// Grant permission to a user
344    pub fn grant_permission(&mut self, user_id: String, permission: Permission) {
345        self.user_permissions
346            .entry(user_id)
347            .or_default()
348            .insert(permission);
349    }
350
351    /// Revoke permission from a user
352    pub fn revoke_permission(&mut self, user_id: &str, permission: &Permission) {
353        if let Some(perms) = self.user_permissions.get_mut(user_id) {
354            perms.remove(permission);
355        }
356    }
357}
358
359impl Default for AccessControlManager {
360    fn default() -> Self {
361        Self::new()
362    }
363}
364
365impl AccessControlManager {
366    /// Create a new access control manager
367    pub fn new() -> Self {
368        let mut manager = Self {
369            roles: HashMap::new(),
370            users: HashMap::new(),
371            organizations: HashMap::new(),
372            package_ownership: HashMap::new(),
373        };
374
375        // Register built-in roles
376        manager.register_role(Role::admin());
377        manager.register_role(Role::maintainer());
378        manager.register_role(Role::contributor());
379        manager.register_role(Role::viewer());
380
381        manager
382    }
383
384    /// Register a role
385    pub fn register_role(&mut self, role: Role) {
386        self.roles.insert(role.name.clone(), role);
387    }
388
389    /// Create a user
390    pub fn create_user(&mut self, id: String, username: String, email: String) -> Result<User> {
391        if self.users.contains_key(&id) {
392            return Err(TorshError::InvalidArgument(format!(
393                "User already exists: {}",
394                id
395            )));
396        }
397
398        let user = User::new(id.clone(), username, email);
399        self.users.insert(id, user.clone());
400        Ok(user)
401    }
402
403    /// Get a user
404    pub fn get_user(&self, user_id: &str) -> Option<&User> {
405        self.users.get(user_id)
406    }
407
408    /// Create an organization
409    pub fn create_organization(&mut self, id: String, name: String) -> Result<Organization> {
410        if self.organizations.contains_key(&id) {
411            return Err(TorshError::InvalidArgument(format!(
412                "Organization already exists: {}",
413                id
414            )));
415        }
416
417        let org = Organization::new(id.clone(), name);
418        self.organizations.insert(id, org.clone());
419        Ok(org)
420    }
421
422    /// Get an organization
423    pub fn get_organization(&self, org_id: &str) -> Option<&Organization> {
424        self.organizations.get(org_id)
425    }
426
427    /// Set package ownership
428    pub fn set_package_ownership(&mut self, ownership: PackageOwnership) {
429        self.package_ownership
430            .insert(ownership.package_name.clone(), ownership);
431    }
432
433    /// Check if user has permission for a package operation
434    pub fn check_access(
435        &self,
436        user_id: &str,
437        package_name: &str,
438        permission: &Permission,
439    ) -> AccessCheckResult {
440        // Get user
441        let user = match self.get_user(user_id) {
442            Some(u) => u,
443            None => {
444                return AccessCheckResult {
445                    granted: false,
446                    denial_reason: Some("User not found".to_string()),
447                    checked_permissions: HashSet::from([permission.clone()]),
448                };
449            }
450        };
451
452        // Check if user is active
453        if !user.active {
454            return AccessCheckResult {
455                granted: false,
456                denial_reason: Some("User is not active".to_string()),
457                checked_permissions: HashSet::from([permission.clone()]),
458            };
459        }
460
461        // Get package ownership
462        let ownership = match self.package_ownership.get(package_name) {
463            Some(o) => o,
464            None => {
465                return AccessCheckResult {
466                    granted: false,
467                    denial_reason: Some("Package not found".to_string()),
468                    checked_permissions: HashSet::from([permission.clone()]),
469                };
470            }
471        };
472
473        // Check if user is owner
474        if ownership.is_owner(user_id) {
475            return AccessCheckResult {
476                granted: true,
477                denial_reason: None,
478                checked_permissions: HashSet::from([permission.clone()]),
479            };
480        }
481
482        // Check user-specific permissions
483        if let Some(perms) = ownership.user_permissions.get(user_id) {
484            if perms.contains(permission) {
485                return AccessCheckResult {
486                    granted: true,
487                    denial_reason: None,
488                    checked_permissions: HashSet::from([permission.clone()]),
489                };
490            }
491        }
492
493        // Check role-based permissions
494        for role_name in &user.roles {
495            if let Some(role) = self.roles.get(role_name) {
496                if role.has_permission(permission) {
497                    return AccessCheckResult {
498                        granted: true,
499                        denial_reason: None,
500                        checked_permissions: HashSet::from([permission.clone()]),
501                    };
502                }
503            }
504        }
505
506        // Check public access for read operations
507        if ownership.public_access == AccessLevel::Public
508            && (permission == &Permission::ReadMetadata || permission == &Permission::Download)
509        {
510            return AccessCheckResult {
511                granted: true,
512                denial_reason: None,
513                checked_permissions: HashSet::from([permission.clone()]),
514            };
515        }
516
517        AccessCheckResult {
518            granted: false,
519            denial_reason: Some("Insufficient permissions".to_string()),
520            checked_permissions: HashSet::from([permission.clone()]),
521        }
522    }
523
524    /// Grant role to user
525    pub fn grant_role(&mut self, user_id: &str, role_name: &str) -> Result<()> {
526        // Check if role exists
527        if !self.roles.contains_key(role_name) {
528            return Err(TorshError::InvalidArgument(format!(
529                "Role not found: {}",
530                role_name
531            )));
532        }
533
534        // Get user and add role
535        let user = self
536            .users
537            .get_mut(user_id)
538            .ok_or_else(|| TorshError::InvalidArgument(format!("User not found: {}", user_id)))?;
539
540        user.add_role(role_name.to_string());
541        Ok(())
542    }
543
544    /// Revoke role from user
545    pub fn revoke_role(&mut self, user_id: &str, role_name: &str) -> Result<()> {
546        let user = self
547            .users
548            .get_mut(user_id)
549            .ok_or_else(|| TorshError::InvalidArgument(format!("User not found: {}", user_id)))?;
550
551        user.roles.remove(role_name);
552        Ok(())
553    }
554
555    /// Add user to organization
556    pub fn add_to_organization(
557        &mut self,
558        user_id: &str,
559        org_id: &str,
560        roles: HashSet<String>,
561    ) -> Result<()> {
562        // Verify user exists
563        if !self.users.contains_key(user_id) {
564            return Err(TorshError::InvalidArgument(format!(
565                "User not found: {}",
566                user_id
567            )));
568        }
569
570        // Get organization and add member
571        let org = self.organizations.get_mut(org_id).ok_or_else(|| {
572            TorshError::InvalidArgument(format!("Organization not found: {}", org_id))
573        })?;
574
575        org.add_member(user_id.to_string(), roles);
576        Ok(())
577    }
578}
579
580impl AccessCheckResult {
581    /// Create a granted result
582    pub fn granted() -> Self {
583        Self {
584            granted: true,
585            denial_reason: None,
586            checked_permissions: HashSet::new(),
587        }
588    }
589
590    /// Create a denied result
591    pub fn denied(reason: String) -> Self {
592        Self {
593            granted: false,
594            denial_reason: Some(reason),
595            checked_permissions: HashSet::new(),
596        }
597    }
598}
599
600#[cfg(test)]
601mod tests {
602    use super::*;
603
604    #[test]
605    fn test_role_creation() {
606        let mut role = Role::new("test-role".to_string(), "Test role".to_string());
607        assert_eq!(role.name, "test-role");
608
609        role.add_permission(Permission::ReadMetadata);
610        assert!(role.has_permission(&Permission::ReadMetadata));
611        assert!(!role.has_permission(&Permission::Publish));
612    }
613
614    #[test]
615    fn test_builtin_roles() {
616        let admin = Role::admin();
617        assert!(admin.has_permission(&Permission::Delete));
618        assert!(admin.builtin);
619
620        let viewer = Role::viewer();
621        assert!(viewer.has_permission(&Permission::ReadMetadata));
622        assert!(!viewer.has_permission(&Permission::Publish));
623    }
624
625    #[test]
626    fn test_user_creation() {
627        let mut user = User::new(
628            "user1".to_string(),
629            "testuser".to_string(),
630            "test@example.com".to_string(),
631        );
632
633        assert_eq!(user.username, "testuser");
634        assert!(user.active);
635
636        user.add_role("admin".to_string());
637        assert!(user.has_role("admin"));
638
639        user.deactivate();
640        assert!(!user.active);
641    }
642
643    #[test]
644    fn test_organization() {
645        let mut org = Organization::new("org1".to_string(), "Test Org".to_string());
646
647        let roles = HashSet::from(["maintainer".to_string()]);
648        org.add_member("user1".to_string(), roles);
649
650        assert!(org.is_member("user1"));
651        assert!(!org.is_member("user2"));
652
653        org.remove_member("user1");
654        assert!(!org.is_member("user1"));
655    }
656
657    #[test]
658    fn test_package_ownership() {
659        let mut ownership = PackageOwnership::new("test-package".to_string());
660
661        ownership.add_owner("user1".to_string());
662        assert!(ownership.is_owner("user1"));
663        assert!(!ownership.is_owner("user2"));
664
665        ownership.grant_permission("user2".to_string(), Permission::Download);
666        assert!(ownership
667            .user_permissions
668            .get("user2")
669            .unwrap()
670            .contains(&Permission::Download));
671
672        ownership.revoke_permission("user2", &Permission::Download);
673        assert!(!ownership
674            .user_permissions
675            .get("user2")
676            .unwrap()
677            .contains(&Permission::Download));
678    }
679
680    #[test]
681    fn test_access_control_manager() {
682        let mut acl = AccessControlManager::new();
683
684        // Create user
685        let user = acl
686            .create_user(
687                "user1".to_string(),
688                "testuser".to_string(),
689                "test@example.com".to_string(),
690            )
691            .unwrap();
692        assert_eq!(user.username, "testuser");
693
694        // Create organization
695        let org = acl
696            .create_organization("org1".to_string(), "Test Org".to_string())
697            .unwrap();
698        assert_eq!(org.name, "Test Org");
699
700        // Grant role
701        acl.grant_role("user1", "maintainer").unwrap();
702        assert!(acl.get_user("user1").unwrap().has_role("maintainer"));
703    }
704
705    #[test]
706    fn test_access_check() {
707        let mut acl = AccessControlManager::new();
708
709        // Create user
710        acl.create_user(
711            "user1".to_string(),
712            "testuser".to_string(),
713            "test@example.com".to_string(),
714        )
715        .unwrap();
716
717        // Set up package ownership
718        let mut ownership = PackageOwnership::new("test-package".to_string());
719        ownership.public_access = AccessLevel::Public;
720        acl.set_package_ownership(ownership);
721
722        // Check read access (should be granted for public package)
723        let result = acl.check_access("user1", "test-package", &Permission::ReadMetadata);
724        assert!(result.granted);
725
726        // Check publish access (should be denied)
727        let result = acl.check_access("user1", "test-package", &Permission::Publish);
728        assert!(!result.granted);
729    }
730
731    #[test]
732    fn test_owner_access() {
733        let mut acl = AccessControlManager::new();
734
735        // Create user
736        acl.create_user(
737            "user1".to_string(),
738            "testuser".to_string(),
739            "test@example.com".to_string(),
740        )
741        .unwrap();
742
743        // Set up package ownership with user1 as owner
744        let mut ownership = PackageOwnership::new("test-package".to_string());
745        ownership.add_owner("user1".to_string());
746        acl.set_package_ownership(ownership);
747
748        // Check publish access (should be granted for owner)
749        let result = acl.check_access("user1", "test-package", &Permission::Publish);
750        assert!(result.granted);
751
752        // Check delete access (should be granted for owner)
753        let result = acl.check_access("user1", "test-package", &Permission::Delete);
754        assert!(result.granted);
755    }
756}