1use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7use crate::types::AgentId;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum Role {
14 User,
16 Superuser,
18 Admin,
20}
21
22impl Role {
23 pub fn default_policy(&self) -> RbacPolicy {
25 match self {
26 Role::Admin => RbacPolicy {
27 role: Role::Admin,
28 allowed_actions: vec![
29 Action::UseTool("*".into()),
30 Action::AccessPath("*".into()),
31 Action::ManageAgents,
32 Action::ManagePrograms,
33 Action::ManageWorkspaces,
34 Action::ManageRBAC,
35 Action::ViewAuditLog,
36 Action::SystemConfig,
37 ]
38 .into_iter()
39 .collect(),
40 resource_patterns: vec!["*".into()],
41 max_concurrent_agents: usize::MAX,
42 },
43 Role::Superuser => RbacPolicy {
44 role: Role::Superuser,
45 allowed_actions: vec![
46 Action::UseTool("*".into()),
47 Action::AccessPath("/workspace/**".into()),
48 Action::ManageAgents,
49 Action::ManagePrograms,
50 Action::ManageWorkspaces,
51 Action::ViewAuditLog,
52 ]
53 .into_iter()
54 .collect(),
55 resource_patterns: vec!["/workspace/**".into(), "/tmp/**".into()],
56 max_concurrent_agents: 10,
57 },
58 Role::User => RbacPolicy {
59 role: Role::User,
60 allowed_actions: vec![
61 Action::UseTool("read".into()),
62 Action::UseTool("write".into()),
63 Action::UseTool("edit".into()),
64 Action::UseTool("bash".into()),
65 Action::UseTool("grep".into()),
66 Action::UseTool("find".into()),
67 Action::AccessPath("/workspace/**".into()),
68 Action::ManageAgents,
69 ]
70 .into_iter()
71 .collect(),
72 resource_patterns: vec!["/workspace/**".into()],
73 max_concurrent_agents: 2,
74 },
75 }
76 }
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub enum Subject {
82 User(String),
84 Agent(AgentId),
86 System,
88}
89
90impl std::fmt::Display for Subject {
91 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
92 match self {
93 Subject::User(name) => write!(f, "user:{}", name),
94 Subject::Agent(id) => write!(f, "agent:{}", id),
95 Subject::System => write!(f, "system"),
96 }
97 }
98}
99
100#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
102pub enum Action {
103 UseTool(String),
105 AccessPath(String),
107 ManageAgents,
109 ManagePrograms,
111 ManageWorkspaces,
113 ManageRBAC,
115 ViewAuditLog,
117 SystemConfig,
119}
120
121impl Action {
122 pub fn requires_approval(&self) -> bool {
124 match self {
125 Action::ManageRBAC | Action::SystemConfig => true,
126 Action::UseTool(t) => t == "*" || t == "osascript" || t == "rm",
127 _ => false,
128 }
129 }
130}
131
132#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct RbacPolicy {
135 pub role: Role,
137 pub allowed_actions: HashSet<Action>,
139 pub resource_patterns: Vec<String>,
141 pub max_concurrent_agents: usize,
143}
144
145impl RbacPolicy {
146 pub fn allows(&self, action: &Action) -> bool {
151 if self.allowed_actions.contains(action) {
153 return true;
154 }
155
156 match action {
158 Action::UseTool(tool_name) => {
159 self.allowed_actions
161 .iter()
162 .any(|a| matches!(a, Action::UseTool(w) if w == "*"))
163 || self.allowed_actions.contains(&Action::UseTool(tool_name.clone()))
165 }
166 Action::AccessPath(path) => {
167 self.allowed_actions
169 .iter()
170 .any(|a| matches!(a, Action::AccessPath(p) if p == "*"))
171 || self
172 .allowed_actions
173 .contains(&Action::AccessPath(path.clone()))
174 }
175 _ => false,
177 }
178 }
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct RbacAuditEntry {
184 pub timestamp: DateTime<Utc>,
186 pub subject: Subject,
188 pub action: Action,
190 pub resource: String,
192 pub allowed: bool,
194 pub reason: Option<String>,
196}
197
198impl RbacAuditEntry {
199 pub(crate) fn new(
201 subject: Subject,
202 action: Action,
203 resource: String,
204 allowed: bool,
205 reason: Option<String>,
206 ) -> Self {
207 Self {
208 timestamp: Utc::now(),
209 subject,
210 action,
211 resource,
212 allowed,
213 reason,
214 }
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct PendingApproval {
221 pub id: uuid::Uuid,
223 pub subject: Subject,
225 pub action: Action,
227 pub resource: String,
229 pub reason: String,
231 pub created_at: DateTime<Utc>,
233}
234
235#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237pub enum ApprovalStatus {
238 Pending,
240 Approved,
242 Rejected,
244 Expired,
246}
247
248#[derive(Debug, Clone)]
250pub struct RbacManager {
251 policies: HashMap<Role, RbacPolicy>,
252 subject_roles: HashMap<Subject, Role>,
253 audit_log: Vec<RbacAuditEntry>,
254 pending_approvals: Vec<(PendingApproval, ApprovalStatus)>,
255 max_audit_entries: usize,
256}
257
258impl RbacManager {
259 pub fn new() -> Self {
261 let mut this = Self {
262 policies: HashMap::new(),
263 subject_roles: HashMap::new(),
264 audit_log: Vec::new(),
265 pending_approvals: Vec::new(),
266 max_audit_entries: 10_000,
267 };
268 for role in [Role::User, Role::Superuser, Role::Admin] {
269 this.policies.insert(role, role.default_policy());
270 }
271 this
272 }
273
274 pub fn assign_role(&mut self, subject: Subject, role: Role) {
276 self.subject_roles.insert(subject.clone(), role);
277 }
278
279 pub fn revoke_role(&mut self, subject: &Subject) {
281 self.subject_roles.remove(subject);
282 }
283
284 pub fn get_role(&self, subject: &Subject) -> Option<Role> {
286 self.subject_roles.get(subject).copied()
287 }
288
289 pub fn check_permission(&mut self, subject: &Subject, action: &Action, resource: &str) -> bool {
291 if matches!(subject, Subject::System) {
292 return true;
293 }
294 let role = match self.subject_roles.get(subject) {
295 Some(r) => *r,
296 None => return false,
297 };
298 let policy = match self.policies.get(&role) {
299 Some(p) => p,
300 None => return false,
301 };
302 let allowed = policy.allows(action);
303 self.audit_log.push(RbacAuditEntry::new(
304 subject.clone(),
305 action.clone(),
306 resource.to_string(),
307 allowed,
308 if allowed {
309 None
310 } else {
311 Some(format!("role {:?} does not allow {:?}", role, action))
312 },
313 ));
314 if self.audit_log.len() > self.max_audit_entries {
315 self.audit_log
316 .drain(0..self.audit_log.len() - self.max_audit_entries);
317 }
318 allowed
319 }
320
321 pub fn request_approval(
323 &mut self,
324 subject: Subject,
325 action: Action,
326 resource: String,
327 reason: String,
328 ) -> uuid::Uuid {
329 let id = uuid::Uuid::new_v4();
330 self.pending_approvals.push((
331 PendingApproval {
332 id,
333 subject,
334 action,
335 resource,
336 reason,
337 created_at: Utc::now(),
338 },
339 ApprovalStatus::Pending,
340 ));
341 id
342 }
343
344 pub fn approve(&mut self, id: uuid::Uuid) -> bool {
346 if let Some((_, s)) = self
347 .pending_approvals
348 .iter_mut()
349 .find(|(p, s)| p.id == id && *s == ApprovalStatus::Pending)
350 {
351 *s = ApprovalStatus::Approved;
352 return true;
353 }
354 false
355 }
356
357 pub fn reject(&mut self, id: uuid::Uuid) -> bool {
359 if let Some((_, s)) = self
360 .pending_approvals
361 .iter_mut()
362 .find(|(p, s)| p.id == id && *s == ApprovalStatus::Pending)
363 {
364 *s = ApprovalStatus::Rejected;
365 return true;
366 }
367 false
368 }
369
370 pub fn pending_approvals(&self) -> Vec<&PendingApproval> {
372 self.pending_approvals
373 .iter()
374 .filter(|(_, s)| matches!(s, ApprovalStatus::Pending))
375 .map(|(p, _)| p)
376 .collect()
377 }
378
379 pub fn all_approvals(&self) -> &[(PendingApproval, ApprovalStatus)] {
381 &self.pending_approvals
382 }
383
384 pub fn audit_log(&self) -> &[RbacAuditEntry] {
386 &self.audit_log
387 }
388}
389
390impl Default for RbacManager {
391 fn default() -> Self {
392 Self::new()
393 }
394}
395
396#[cfg(test)]
397mod tests {
398 use super::*;
399
400 #[test]
401 fn test_default_policies_exist() {
402 let mgr = RbacManager::new();
403 assert!(mgr.policies.contains_key(&Role::User));
404 assert!(mgr.policies.contains_key(&Role::Superuser));
405 assert!(mgr.policies.contains_key(&Role::Admin));
406 }
407
408 #[test]
409 fn test_role_assignment() {
410 let mut mgr = RbacManager::new();
411 let subject = Subject::User("alice".into());
412 mgr.assign_role(subject.clone(), Role::Admin);
413 assert_eq!(mgr.get_role(&subject), Some(Role::Admin));
414
415 mgr.revoke_role(&subject);
416 assert_eq!(mgr.get_role(&subject), None);
417 }
418
419 #[test]
420 fn test_system_bypasses_rbac() {
421 let mut mgr = RbacManager::new();
422 let subject = Subject::System;
423 assert!(mgr.check_permission(&subject, &Action::ManageRBAC, "test"));
424 }
425
426 #[test]
427 fn test_unknown_subject_denied() {
428 let mut mgr = RbacManager::new();
429 let subject = Subject::User("nobody".into());
430 assert!(!mgr.check_permission(&subject, &Action::UseTool("read".into()), "test"));
431 }
432
433 #[test]
434 fn test_user_allowed_specific_tools() {
435 let mut mgr = RbacManager::new();
436 let subject = Subject::User("bob".into());
437 mgr.assign_role(subject.clone(), Role::User);
438
439 assert!(mgr.check_permission(&subject, &Action::UseTool("read".into()), "test"));
440 assert!(mgr.check_permission(&subject, &Action::UseTool("write".into()), "test"));
441 assert!(mgr.check_permission(&subject, &Action::UseTool("bash".into()), "test"));
442 }
443
444 #[test]
445 fn test_user_denied_admin_tools() {
446 let mut mgr = RbacManager::new();
447 let subject = Subject::User("bob".into());
448 mgr.assign_role(subject.clone(), Role::User);
449
450 assert!(!mgr.check_permission(&subject, &Action::ManageRBAC, "test"));
451 assert!(!mgr.check_permission(&subject, &Action::SystemConfig, "test"));
452 }
453
454 #[test]
455 fn test_admin_wildcard_allows_all_tools() {
456 let mut mgr = RbacManager::new();
457 let subject = Subject::User("admin".into());
458 mgr.assign_role(subject.clone(), Role::Admin);
459
460 assert!(mgr.check_permission(&subject, &Action::UseTool("any_tool".into()), "test"));
462 assert!(mgr.check_permission(&subject, &Action::UseTool("custom_thing".into()), "test"));
463 assert!(mgr.check_permission(&subject, &Action::UseTool("dangerous".into()), "test"));
464 }
465
466 #[test]
467 fn test_superuser_wildcard_allows_all_tools() {
468 let mut mgr = RbacManager::new();
469 let subject = Subject::User("super".into());
470 mgr.assign_role(subject.clone(), Role::Superuser);
471
472 assert!(mgr.check_permission(&subject, &Action::UseTool("custom".into()), "test"));
473 assert!(mgr.check_permission(&subject, &Action::UseTool("anything".into()), "test"));
474 }
475
476 #[test]
477 fn test_admin_all_paths_wildcard() {
478 let mut mgr = RbacManager::new();
479 let subject = Subject::User("admin".into());
480 mgr.assign_role(subject.clone(), Role::Admin);
481
482 assert!(mgr.check_permission(&subject, &Action::AccessPath("/any/path".into()), "test"));
483 assert!(mgr.check_permission(&subject, &Action::AccessPath("/secret/data".into()), "test"));
484 }
485
486 #[test]
487 fn test_policy_allows_exact_match() {
488 let policy = Role::User.default_policy();
489 assert!(policy.allows(&Action::UseTool("read".into())));
490 assert!(policy.allows(&Action::UseTool("bash".into())));
491 assert!(!policy.allows(&Action::UseTool("unknown_tool".into())));
492 }
493
494 #[test]
495 fn test_policy_allows_wildcard() {
496 let policy = Role::Admin.default_policy();
497 assert!(policy.allows(&Action::UseTool("literally_anything".into())));
498 assert!(policy.allows(&Action::AccessPath("/some/random/path".into())));
499 }
500
501 #[test]
502 fn test_approval_request_lifecycle() {
503 let mut mgr = RbacManager::new();
504 let id = mgr.request_approval(
505 Subject::User("alice".into()),
506 Action::ManageRBAC,
507 "rbac".into(),
508 "need admin".into(),
509 );
510
511 let pending = mgr.pending_approvals();
512 assert_eq!(pending.len(), 1);
513 assert_eq!(pending[0].id, id);
514
515 assert!(mgr.approve(id));
516 assert!(mgr.pending_approvals().is_empty());
517
518 assert!(!mgr.approve(id));
520 }
521
522 #[test]
523 fn test_approval_rejection() {
524 let mut mgr = RbacManager::new();
525 let id = mgr.request_approval(
526 Subject::User("alice".into()),
527 Action::SystemConfig,
528 "config".into(),
529 "need config".into(),
530 );
531
532 assert!(mgr.reject(id));
533 assert!(mgr.pending_approvals().is_empty());
534 }
535
536 #[test]
537 fn test_approval_nonexistent() {
538 let mut mgr = RbacManager::new();
539 assert!(!mgr.approve(uuid::Uuid::new_v4()));
540 assert!(!mgr.reject(uuid::Uuid::new_v4()));
541 }
542
543 #[test]
544 fn test_audit_log_recorded() {
545 let mut mgr = RbacManager::new();
546 let subject = Subject::User("alice".into());
547 mgr.assign_role(subject.clone(), Role::User);
548
549 mgr.check_permission(&subject, &Action::UseTool("read".into()), "test");
550 assert!(!mgr.audit_log().is_empty());
551
552 let entry = &mgr.audit_log()[0];
553 assert!(entry.allowed);
554 }
555
556 #[test]
557 fn test_audit_log_denied_recorded() {
558 let mut mgr = RbacManager::new();
559 let subject = Subject::User("alice".into());
560 mgr.assign_role(subject.clone(), Role::User);
561
562 mgr.check_permission(&subject, &Action::ManageRBAC, "test");
563 let denied_entries: Vec<_> = mgr.audit_log().iter().filter(|e| !e.allowed).collect();
564 assert_eq!(denied_entries.len(), 1);
565 }
566}