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}