1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6use uuid::Uuid;
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
10pub enum Role {
11 User,
13 Superuser,
15 Admin,
17}
18
19impl Role {
20 pub fn default_policy(&self) -> RbacPolicy {
22 match self {
23 Role::Admin => RbacPolicy {
24 role: Role::Admin,
25 allowed_actions: vec![
26 Action::UseTool("*".into()),
27 Action::AccessPath("*".into()),
28 Action::ManageAgents,
29 Action::ManagePrograms,
30 Action::ManageWorkspaces,
31 Action::ManageRBAC,
32 Action::ViewAuditLog,
33 Action::SystemConfig,
34 ]
35 .into_iter()
36 .collect(),
37 resource_patterns: vec!["*".into()],
38 max_concurrent_agents: usize::MAX,
39 },
40 Role::Superuser => RbacPolicy {
41 role: Role::Superuser,
42 allowed_actions: vec![
43 Action::UseTool("*".into()),
44 Action::AccessPath("/workspace/**".into()),
45 Action::ManageAgents,
46 Action::ManagePrograms,
47 Action::ManageWorkspaces,
48 Action::ViewAuditLog,
49 ]
50 .into_iter()
51 .collect(),
52 resource_patterns: vec!["/workspace/**".into(), "/tmp/**".into()],
53 max_concurrent_agents: 10,
54 },
55 Role::User => RbacPolicy {
56 role: Role::User,
57 allowed_actions: vec![
58 Action::UseTool("read".into()),
59 Action::UseTool("write".into()),
60 Action::UseTool("edit".into()),
61 Action::UseTool("bash".into()),
62 Action::UseTool("grep".into()),
63 Action::UseTool("find".into()),
64 Action::AccessPath("/workspace/**".into()),
65 Action::ManageAgents,
66 ]
67 .into_iter()
68 .collect(),
69 resource_patterns: vec!["/workspace/**".into()],
70 max_concurrent_agents: 2,
71 },
72 }
73 }
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
78pub enum Subject {
79 User(String),
81 Agent(Uuid),
83 System,
85}
86
87impl std::fmt::Display for Subject {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 match self {
90 Subject::User(name) => write!(f, "user:{name}"),
91 Subject::Agent(id) => write!(f, "agent:{id}"),
92 Subject::System => write!(f, "system"),
93 }
94 }
95}
96
97#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
99pub enum Action {
100 UseTool(String),
102 AccessPath(String),
104 ManageAgents,
106 ManagePrograms,
108 ManageWorkspaces,
110 ManageRBAC,
112 ViewAuditLog,
114 SystemConfig,
116}
117
118impl Action {
119 pub fn requires_approval(&self) -> bool {
121 match self {
122 Action::ManageRBAC | Action::SystemConfig => true,
123 Action::UseTool(t) => t == "*" || t == "osascript" || t == "rm",
124 _ => false,
125 }
126 }
127}
128
129#[derive(Debug, Clone, Serialize, Deserialize)]
131pub struct RbacPolicy {
132 pub role: Role,
134 pub allowed_actions: HashSet<Action>,
136 pub resource_patterns: Vec<String>,
138 pub max_concurrent_agents: usize,
140}
141
142impl RbacPolicy {
143 pub fn allows(&self, action: &Action) -> bool {
145 if self.allowed_actions.contains(action) {
146 return true;
147 }
148 match action {
149 Action::UseTool(tool_name) => {
150 self.allowed_actions
151 .iter()
152 .any(|a| matches!(a, Action::UseTool(w) if w == "*"))
153 || self
154 .allowed_actions
155 .contains(&Action::UseTool(tool_name.clone()))
156 }
157 Action::AccessPath(path) => {
158 self.allowed_actions
159 .iter()
160 .any(|a| matches!(a, Action::AccessPath(p) if p == "*"))
161 || self
162 .allowed_actions
163 .contains(&Action::AccessPath(path.clone()))
164 }
165 _ => false,
166 }
167 }
168}
169
170#[derive(Debug, Clone, Serialize, Deserialize)]
172pub struct RbacAuditEntry {
173 pub timestamp: DateTime<Utc>,
175 pub subject: Subject,
177 pub action: Action,
179 pub resource: String,
181 pub allowed: bool,
183 pub reason: Option<String>,
185}
186
187#[derive(Debug, Clone, Serialize, Deserialize)]
189pub struct PendingApproval {
190 pub id: Uuid,
192 pub subject: Subject,
194 pub action: Action,
196 pub resource: String,
198 pub reason: String,
200 pub created_at: DateTime<Utc>,
202}
203
204#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
206pub enum ApprovalStatus {
207 Pending,
209 Approved,
211 Rejected,
213 Expired,
215}
216
217#[derive(Debug, Clone)]
219pub struct RbacManager {
220 policies: HashMap<Role, RbacPolicy>,
221 subject_roles: HashMap<Subject, Role>,
222 audit_log: Vec<RbacAuditEntry>,
223 pending_approvals: Vec<(PendingApproval, ApprovalStatus)>,
224 max_audit_entries: usize,
225}
226
227impl RbacManager {
228 pub fn new() -> Self {
230 let mut this = Self {
231 policies: HashMap::new(),
232 subject_roles: HashMap::new(),
233 audit_log: Vec::new(),
234 pending_approvals: Vec::new(),
235 max_audit_entries: 10_000,
236 };
237 for role in [Role::User, Role::Superuser, Role::Admin] {
238 this.policies.insert(role, role.default_policy());
239 }
240 this
241 }
242
243 pub fn assign_role(&mut self, subject: Subject, role: Role) {
245 self.subject_roles.insert(subject, role);
246 }
247
248 pub fn revoke_role(&mut self, subject: &Subject) {
250 self.subject_roles.remove(subject);
251 }
252
253 pub fn get_role(&self, subject: &Subject) -> Option<Role> {
255 self.subject_roles.get(subject).copied()
256 }
257
258 pub fn check_permission(&mut self, subject: &Subject, action: &Action, resource: &str) -> bool {
260 if matches!(subject, Subject::System) {
261 return true;
262 }
263 let role = match self.subject_roles.get(subject) {
264 Some(r) => *r,
265 None => return false,
266 };
267 let policy = match self.policies.get(&role) {
268 Some(p) => p,
269 None => return false,
270 };
271 let allowed = policy.allows(action);
272 self.audit_log.push(RbacAuditEntry {
273 timestamp: Utc::now(),
274 subject: subject.clone(),
275 action: action.clone(),
276 resource: resource.to_string(),
277 allowed,
278 reason: if allowed {
279 None
280 } else {
281 Some(format!("role {role:?} does not allow {action:?}"))
282 },
283 });
284 if self.audit_log.len() > self.max_audit_entries {
285 self.audit_log
286 .drain(0..self.audit_log.len() - self.max_audit_entries);
287 }
288 allowed
289 }
290
291 pub fn request_approval(
293 &mut self,
294 subject: Subject,
295 action: Action,
296 resource: String,
297 reason: String,
298 ) -> Uuid {
299 let id = Uuid::new_v4();
300 self.pending_approvals.push((
301 PendingApproval {
302 id,
303 subject,
304 action,
305 resource,
306 reason,
307 created_at: Utc::now(),
308 },
309 ApprovalStatus::Pending,
310 ));
311 id
312 }
313
314 pub fn approve(&mut self, id: Uuid) -> bool {
316 if let Some((_, s)) = self
317 .pending_approvals
318 .iter_mut()
319 .find(|(p, s)| p.id == id && *s == ApprovalStatus::Pending)
320 {
321 *s = ApprovalStatus::Approved;
322 return true;
323 }
324 false
325 }
326
327 pub fn reject(&mut self, id: Uuid) -> bool {
329 if let Some((_, s)) = self
330 .pending_approvals
331 .iter_mut()
332 .find(|(p, s)| p.id == id && *s == ApprovalStatus::Pending)
333 {
334 *s = ApprovalStatus::Rejected;
335 return true;
336 }
337 false
338 }
339
340 pub fn pending_approvals(&self) -> Vec<&PendingApproval> {
342 self.pending_approvals
343 .iter()
344 .filter(|(_, s)| matches!(s, ApprovalStatus::Pending))
345 .map(|(p, _)| p)
346 .collect()
347 }
348
349 pub fn all_approvals(&self) -> &[(PendingApproval, ApprovalStatus)] {
351 &self.pending_approvals
352 }
353
354 pub fn audit_log(&self) -> &[RbacAuditEntry] {
356 &self.audit_log
357 }
358}
359
360impl Default for RbacManager {
361 fn default() -> Self {
362 Self::new()
363 }
364}
365
366#[cfg(test)]
367mod tests {
368 use super::*;
369
370 #[test]
371 fn role_assignment() {
372 let mut mgr = RbacManager::new();
373 let s = Subject::User("alice".into());
374 mgr.assign_role(s.clone(), Role::Admin);
375 assert_eq!(mgr.get_role(&s), Some(Role::Admin));
376 mgr.revoke_role(&s);
377 assert_eq!(mgr.get_role(&s), None);
378 }
379
380 #[test]
381 fn system_bypasses() {
382 let mut mgr = RbacManager::new();
383 assert!(mgr.check_permission(&Subject::System, &Action::ManageRBAC, "test"));
384 }
385
386 #[test]
387 fn unknown_denied() {
388 let mut mgr = RbacManager::new();
389 assert!(!mgr.check_permission(
390 &Subject::User("nobody".into()),
391 &Action::UseTool("read".into()),
392 "test"
393 ));
394 }
395
396 #[test]
397 fn admin_wildcard() {
398 let mut mgr = RbacManager::new();
399 let s = Subject::User("admin".into());
400 mgr.assign_role(s.clone(), Role::Admin);
401 assert!(mgr.check_permission(&s, &Action::UseTool("anything".into()), "test"));
402 }
403
404 #[test]
405 fn approval_lifecycle() {
406 let mut mgr = RbacManager::new();
407 let id = mgr.request_approval(
408 Subject::User("alice".into()),
409 Action::ManageRBAC,
410 "rbac".into(),
411 "need admin".into(),
412 );
413 assert_eq!(mgr.pending_approvals().len(), 1);
414 assert!(mgr.approve(id));
415 assert!(mgr.pending_approvals().is_empty());
416 assert!(!mgr.approve(id)); }
418}