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("*".into()),
48 Action::ManageAgents,
49 Action::ManagePrograms,
50 Action::ManageWorkspaces,
51 Action::ViewAuditLog,
52 ]
53 .into_iter()
54 .collect(),
55 resource_patterns: vec!["*".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 if self
169 .allowed_actions
170 .iter()
171 .any(|a| matches!(a, Action::AccessPath(p) if p == "*"))
172 || self
173 .allowed_actions
174 .contains(&Action::AccessPath(path.clone()))
175 {
176 return true;
177 }
178 for pattern in &self.resource_patterns {
181 if pattern == "*" {
182 return true;
183 }
184 if let Ok(p) = glob::Pattern::new(pattern)
185 && p.matches(path)
186 {
187 return true;
188 }
189 }
190 false
191 }
192 _ => false,
194 }
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct RbacAuditEntry {
201 pub timestamp: DateTime<Utc>,
203 pub subject: Subject,
205 pub action: Action,
207 pub resource: String,
209 pub allowed: bool,
211 pub reason: Option<String>,
213}
214
215impl RbacAuditEntry {
216 pub(crate) fn new(
218 subject: Subject,
219 action: Action,
220 resource: String,
221 allowed: bool,
222 reason: Option<String>,
223 ) -> Self {
224 Self {
225 timestamp: Utc::now(),
226 subject,
227 action,
228 resource,
229 allowed,
230 reason,
231 }
232 }
233}
234
235#[derive(Debug, Clone, Serialize, Deserialize)]
237pub struct PendingApproval {
238 pub id: uuid::Uuid,
240 pub subject: Subject,
242 pub action: Action,
244 pub resource: String,
246 pub reason: String,
248 pub created_at: DateTime<Utc>,
250}
251
252#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
254pub enum ApprovalStatus {
255 Pending,
257 Approved,
259 Rejected,
261 Expired,
263}
264
265#[derive(Debug, Clone)]
267pub struct RbacManager {
268 policies: HashMap<Role, RbacPolicy>,
269 subject_roles: HashMap<Subject, Role>,
270 audit_log: Vec<RbacAuditEntry>,
271 pending_approvals: Vec<(PendingApproval, ApprovalStatus)>,
272 max_audit_entries: usize,
273}
274
275impl RbacManager {
276 pub fn new() -> Self {
278 let mut this = Self {
279 policies: HashMap::new(),
280 subject_roles: HashMap::new(),
281 audit_log: Vec::new(),
282 pending_approvals: Vec::new(),
283 max_audit_entries: 10_000,
284 };
285 for role in [Role::User, Role::Superuser, Role::Admin] {
286 this.policies.insert(role, role.default_policy());
287 }
288 this
289 }
290
291 pub fn assign_role(&mut self, subject: Subject, role: Role) {
293 self.subject_roles.insert(subject.clone(), role);
294 }
295
296 pub fn revoke_role(&mut self, subject: &Subject) {
298 self.subject_roles.remove(subject);
299 }
300
301 pub fn get_role(&self, subject: &Subject) -> Option<Role> {
303 self.subject_roles.get(subject).copied()
304 }
305
306 pub fn check_permission(&mut self, subject: &Subject, action: &Action, resource: &str) -> bool {
308 if matches!(subject, Subject::System) {
309 self.audit_log.push(RbacAuditEntry::new(
312 subject.clone(),
313 action.clone(),
314 resource.to_string(),
315 true,
316 Some("system subject bypass".to_string()),
317 ));
318 if self.audit_log.len() > self.max_audit_entries {
319 self.audit_log
320 .drain(0..self.audit_log.len() - self.max_audit_entries);
321 }
322 return true;
323 }
324 let role = match self.subject_roles.get(subject) {
325 Some(r) => *r,
326 None => return false,
327 };
328 let policy = match self.policies.get(&role) {
329 Some(p) => p,
330 None => return false,
331 };
332 let allowed = policy.allows(action);
333 self.audit_log.push(RbacAuditEntry::new(
334 subject.clone(),
335 action.clone(),
336 resource.to_string(),
337 allowed,
338 if allowed {
339 None
340 } else {
341 Some(format!("role {role:?} does not allow {action:?}"))
342 },
343 ));
344 if self.audit_log.len() > self.max_audit_entries {
345 self.audit_log
346 .drain(0..self.audit_log.len() - self.max_audit_entries);
347 }
348 allowed
349 }
350
351 pub fn request_approval(
353 &mut self,
354 subject: Subject,
355 action: Action,
356 resource: String,
357 reason: String,
358 ) -> uuid::Uuid {
359 let id = uuid::Uuid::new_v4();
360 self.pending_approvals.push((
361 PendingApproval {
362 id,
363 subject,
364 action,
365 resource,
366 reason,
367 created_at: Utc::now(),
368 },
369 ApprovalStatus::Pending,
370 ));
371 id
372 }
373
374 pub fn approve(&mut self, id: uuid::Uuid) -> bool {
376 if let Some((_, s)) = self
377 .pending_approvals
378 .iter_mut()
379 .find(|(p, s)| p.id == id && *s == ApprovalStatus::Pending)
380 {
381 *s = ApprovalStatus::Approved;
382 return true;
383 }
384 false
385 }
386
387 pub fn reject(&mut self, id: uuid::Uuid) -> bool {
389 if let Some((_, s)) = self
390 .pending_approvals
391 .iter_mut()
392 .find(|(p, s)| p.id == id && *s == ApprovalStatus::Pending)
393 {
394 *s = ApprovalStatus::Rejected;
395 return true;
396 }
397 false
398 }
399
400 pub fn pending_approvals(&self) -> Vec<&PendingApproval> {
402 self.pending_approvals
403 .iter()
404 .filter(|(_, s)| matches!(s, ApprovalStatus::Pending))
405 .map(|(p, _)| p)
406 .collect()
407 }
408
409 pub fn all_approvals(&self) -> &[(PendingApproval, ApprovalStatus)] {
411 &self.pending_approvals
412 }
413
414 pub fn audit_log(&self) -> &[RbacAuditEntry] {
416 &self.audit_log
417 }
418}
419
420impl Default for RbacManager {
421 fn default() -> Self {
422 Self::new()
423 }
424}
425
426#[cfg(test)]
427mod tests {
428 use super::*;
429
430 #[test]
431 fn test_default_policies_exist() {
432 let mgr = RbacManager::new();
433 assert!(mgr.policies.contains_key(&Role::User));
434 assert!(mgr.policies.contains_key(&Role::Superuser));
435 assert!(mgr.policies.contains_key(&Role::Admin));
436 }
437
438 #[test]
439 fn test_role_assignment() {
440 let mut mgr = RbacManager::new();
441 let subject = Subject::User("alice".into());
442 mgr.assign_role(subject.clone(), Role::Admin);
443 assert_eq!(mgr.get_role(&subject), Some(Role::Admin));
444
445 mgr.revoke_role(&subject);
446 assert_eq!(mgr.get_role(&subject), None);
447 }
448
449 #[test]
450 fn test_system_bypasses_rbac() {
451 let mut mgr = RbacManager::new();
452 let subject = Subject::System;
453 assert!(mgr.check_permission(&subject, &Action::ManageRBAC, "test"));
454 }
455
456 #[test]
457 fn test_unknown_subject_denied() {
458 let mut mgr = RbacManager::new();
459 let subject = Subject::User("nobody".into());
460 assert!(!mgr.check_permission(&subject, &Action::UseTool("read".into()), "test"));
461 }
462
463 #[test]
464 fn test_user_allowed_specific_tools() {
465 let mut mgr = RbacManager::new();
466 let subject = Subject::User("bob".into());
467 mgr.assign_role(subject.clone(), Role::User);
468
469 assert!(mgr.check_permission(&subject, &Action::UseTool("read".into()), "test"));
470 assert!(mgr.check_permission(&subject, &Action::UseTool("write".into()), "test"));
471 assert!(mgr.check_permission(&subject, &Action::UseTool("bash".into()), "test"));
472 }
473
474 #[test]
475 fn test_user_denied_admin_tools() {
476 let mut mgr = RbacManager::new();
477 let subject = Subject::User("bob".into());
478 mgr.assign_role(subject.clone(), Role::User);
479
480 assert!(!mgr.check_permission(&subject, &Action::ManageRBAC, "test"));
481 assert!(!mgr.check_permission(&subject, &Action::SystemConfig, "test"));
482 }
483
484 #[test]
485 fn test_admin_wildcard_allows_all_tools() {
486 let mut mgr = RbacManager::new();
487 let subject = Subject::User("admin".into());
488 mgr.assign_role(subject.clone(), Role::Admin);
489
490 assert!(mgr.check_permission(&subject, &Action::UseTool("any_tool".into()), "test"));
492 assert!(mgr.check_permission(&subject, &Action::UseTool("custom_thing".into()), "test"));
493 assert!(mgr.check_permission(&subject, &Action::UseTool("dangerous".into()), "test"));
494 }
495
496 #[test]
497 fn test_superuser_wildcard_allows_all_tools() {
498 let mut mgr = RbacManager::new();
499 let subject = Subject::User("super".into());
500 mgr.assign_role(subject.clone(), Role::Superuser);
501
502 assert!(mgr.check_permission(&subject, &Action::UseTool("custom".into()), "test"));
503 assert!(mgr.check_permission(&subject, &Action::UseTool("anything".into()), "test"));
504 }
505
506 #[test]
507 fn test_admin_all_paths_wildcard() {
508 let mut mgr = RbacManager::new();
509 let subject = Subject::User("admin".into());
510 mgr.assign_role(subject.clone(), Role::Admin);
511
512 assert!(mgr.check_permission(&subject, &Action::AccessPath("/any/path".into()), "test"));
513 assert!(mgr.check_permission(&subject, &Action::AccessPath("/secret/data".into()), "test"));
514 }
515
516 #[test]
517 fn test_policy_allows_exact_match() {
518 let policy = Role::User.default_policy();
519 assert!(policy.allows(&Action::UseTool("read".into())));
520 assert!(policy.allows(&Action::UseTool("bash".into())));
521 assert!(!policy.allows(&Action::UseTool("unknown_tool".into())));
522 }
523
524 #[test]
525 fn test_policy_allows_wildcard() {
526 let policy = Role::Admin.default_policy();
527 assert!(policy.allows(&Action::UseTool("literally_anything".into())));
528 assert!(policy.allows(&Action::AccessPath("/some/random/path".into())));
529 }
530
531 #[test]
532 fn test_approval_request_lifecycle() {
533 let mut mgr = RbacManager::new();
534 let id = mgr.request_approval(
535 Subject::User("alice".into()),
536 Action::ManageRBAC,
537 "rbac".into(),
538 "need admin".into(),
539 );
540
541 let pending = mgr.pending_approvals();
542 assert_eq!(pending.len(), 1);
543 assert_eq!(pending[0].id, id);
544
545 assert!(mgr.approve(id));
546 assert!(mgr.pending_approvals().is_empty());
547
548 assert!(!mgr.approve(id));
550 }
551
552 #[test]
553 fn test_approval_rejection() {
554 let mut mgr = RbacManager::new();
555 let id = mgr.request_approval(
556 Subject::User("alice".into()),
557 Action::SystemConfig,
558 "config".into(),
559 "need config".into(),
560 );
561
562 assert!(mgr.reject(id));
563 assert!(mgr.pending_approvals().is_empty());
564 }
565
566 #[test]
567 fn test_approval_nonexistent() {
568 let mut mgr = RbacManager::new();
569 assert!(!mgr.approve(uuid::Uuid::new_v4()));
570 assert!(!mgr.reject(uuid::Uuid::new_v4()));
571 }
572
573 #[test]
574 fn test_audit_log_recorded() {
575 let mut mgr = RbacManager::new();
576 let subject = Subject::User("alice".into());
577 mgr.assign_role(subject.clone(), Role::User);
578
579 mgr.check_permission(&subject, &Action::UseTool("read".into()), "test");
580 assert!(!mgr.audit_log().is_empty());
581
582 let entry = &mgr.audit_log()[0];
583 assert!(entry.allowed);
584 }
585
586 #[test]
587 fn test_audit_log_denied_recorded() {
588 let mut mgr = RbacManager::new();
589 let subject = Subject::User("alice".into());
590 mgr.assign_role(subject.clone(), Role::User);
591
592 mgr.check_permission(&subject, &Action::ManageRBAC, "test");
593 let denied_entries: Vec<_> = mgr.audit_log().iter().filter(|e| !e.allowed).collect();
594 assert_eq!(denied_entries.len(), 1);
595 }
596}