mockforge_core/workspace/
rbac.rs

1//! Environment-scoped RBAC for workspace environments
2//!
3//! Extends the base RBAC system to support environment-specific permissions,
4//! allowing fine-grained control over who can modify settings in dev/test/prod.
5
6use crate::workspace::mock_environment::MockEnvironmentName;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9
10/// Permission type - using string for now since mockforge_collab may not be available
11/// In the future, this could be replaced with mockforge_collab::permissions::Permission
12pub type Permission = String;
13
14/// Environment permission policy
15///
16/// Defines which roles are allowed to perform specific actions in specific environments.
17#[derive(Debug, Clone, Serialize, Deserialize)]
18pub struct EnvironmentPermissionPolicy {
19    /// Unique identifier
20    pub id: String,
21    /// Organization ID (optional, for org-wide policies)
22    pub org_id: Option<String>,
23    /// Workspace ID (optional, for workspace-specific policies)
24    pub workspace_id: Option<String>,
25    /// Environment this policy applies to
26    pub environment: MockEnvironmentName,
27    /// Permission this policy controls
28    pub permission: String, // Permission name as string (e.g., "ManageSettings")
29    /// Roles allowed to perform this action in this environment
30    pub allowed_roles: Vec<String>, // Role names as strings (e.g., "admin", "platform", "qa")
31    /// Created timestamp
32    pub created_at: chrono::DateTime<chrono::Utc>,
33}
34
35impl EnvironmentPermissionPolicy {
36    /// Create a new environment permission policy
37    pub fn new(
38        environment: MockEnvironmentName,
39        permission: Permission,
40        allowed_roles: Vec<String>,
41    ) -> Self {
42        Self {
43            id: uuid::Uuid::new_v4().to_string(),
44            org_id: None,
45            workspace_id: None,
46            environment,
47            permission: permission.to_string(),
48            allowed_roles,
49            created_at: chrono::Utc::now(),
50        }
51    }
52
53    /// Check if a role is allowed for this policy
54    pub fn allows_role(&self, role: &str) -> bool {
55        self.allowed_roles.iter().any(|r| r.eq_ignore_ascii_case(role))
56    }
57}
58
59/// Environment permission checker
60///
61/// Checks if a user has permission to perform an action in a specific environment,
62/// considering both base permissions and environment-specific policies.
63pub struct EnvironmentPermissionChecker {
64    /// Environment-specific policies indexed by (environment, permission)
65    policies: HashMap<(MockEnvironmentName, String), Vec<EnvironmentPermissionPolicy>>,
66}
67
68impl EnvironmentPermissionChecker {
69    /// Create a new environment permission checker
70    pub fn new() -> Self {
71        Self {
72            policies: HashMap::new(),
73        }
74    }
75
76    /// Add a policy
77    pub fn add_policy(&mut self, policy: EnvironmentPermissionPolicy) {
78        let key = (policy.environment, policy.permission.clone());
79        self.policies.entry(key).or_default().push(policy);
80    }
81
82    /// Check if a role has permission in an environment
83    ///
84    /// Returns true if:
85    /// 1. There's no environment-specific policy (fallback to base permission check)
86    /// 2. There's a policy and the role is allowed
87    pub fn has_permission(
88        &self,
89        role: &str,
90        permission: Permission,
91        environment: MockEnvironmentName,
92    ) -> bool {
93        let key = (environment, permission.to_string());
94
95        if let Some(policies) = self.policies.get(&key) {
96            // Check if any policy allows this role
97            policies.iter().any(|policy| policy.allows_role(role))
98        } else {
99            // No environment-specific policy, fallback to base permission check
100            // This should be handled by the base RBAC system
101            true
102        }
103    }
104
105    /// Get policies for an environment
106    pub fn get_policies_for_environment(
107        &self,
108        environment: MockEnvironmentName,
109    ) -> Vec<&EnvironmentPermissionPolicy> {
110        self.policies
111            .iter()
112            .filter_map(|((env, _), policies)| {
113                if *env == environment {
114                    Some(policies.iter())
115                } else {
116                    None
117                }
118            })
119            .flatten()
120            .collect()
121    }
122
123    /// Get policies for a permission across all environments
124    pub fn get_policies_for_permission(
125        &self,
126        permission: Permission,
127    ) -> Vec<&EnvironmentPermissionPolicy> {
128        let perm_str = permission.to_string();
129        self.policies
130            .iter()
131            .filter_map(|((_, perm), policies)| {
132                if *perm == perm_str {
133                    Some(policies.iter())
134                } else {
135                    None
136                }
137            })
138            .flatten()
139            .collect()
140    }
141}
142
143impl Default for EnvironmentPermissionChecker {
144    fn default() -> Self {
145        Self::new()
146    }
147}
148
149/// Helper function to check environment-scoped permissions
150///
151/// This function combines base permission checking with environment-specific policies.
152/// It should be called after the base permission check passes.
153pub fn check_environment_permission(
154    checker: &EnvironmentPermissionChecker,
155    role: &str,
156    permission: Permission,
157    environment: Option<MockEnvironmentName>,
158) -> bool {
159    // If no environment is specified, use base permission check only
160    let env = match environment {
161        Some(e) => e,
162        None => return true, // No environment restriction
163    };
164
165    // Check environment-specific policy
166    checker.has_permission(role, permission, env)
167}