Skip to main content

oxios_kernel/access_manager/
rbac.rs

1//! RBAC types and manager — role-based access control with HitL approvals.
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use std::collections::{HashMap, HashSet};
6
7use crate::types::AgentId;
8
9// ─── RBAC Types ───────────────────────────────────────────────────────────────
10
11/// Roles for role-based access control (3-tier model).
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
13pub enum Role {
14    /// Basic user — can use agents, limited permissions.
15    User,
16    /// Superuser — can manage programs, skills, workspaces.
17    Superuser,
18    /// Admin — full system access, can modify RBAC.
19    Admin,
20}
21
22impl Role {
23    /// Returns the default policy for this role.
24    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/// Subject — who is accessing the system.
80#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
81pub enum Subject {
82    /// A named user.
83    User(String),
84    /// An agent acting on behalf of a user.
85    Agent(AgentId),
86    /// System-level operations (bypass RBAC).
87    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/// Actions that can be authorized by RBAC.
101#[derive(Debug, Clone, Hash, Eq, PartialEq, Serialize, Deserialize)]
102pub enum Action {
103    /// Use a specific tool (name or * for all).
104    UseTool(String),
105    /// Access a specific path pattern (glob).
106    AccessPath(String),
107    /// Manage agents (fork/exec/kill).
108    ManageAgents,
109    /// Manage programs (install/uninstall).
110    ManagePrograms,
111    /// Manage workspaces (create/start/stop/remove).
112    ManageWorkspaces,
113    /// Modify RBAC policies and role assignments.
114    ManageRBAC,
115    /// View the audit log.
116    ViewAuditLog,
117    /// Modify system-level configuration.
118    SystemConfig,
119}
120
121impl Action {
122    /// Returns true if this action is considered high-risk and needs HitL approval.
123    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/// RBAC policy defining what a role can do.
133#[derive(Debug, Clone, Serialize, Deserialize)]
134pub struct RbacPolicy {
135    /// The role this policy applies to.
136    pub role: Role,
137    /// Set of actions this role is allowed to perform.
138    pub allowed_actions: HashSet<Action>,
139    /// Glob patterns for accessible resources.
140    pub resource_patterns: Vec<String>,
141    /// Maximum number of concurrent agents for this role.
142    pub max_concurrent_agents: usize,
143}
144
145impl RbacPolicy {
146    /// Checks whether this policy allows the given action.
147    ///
148    /// Supports wildcard matching for `UseTool("*")` (matches any tool name)
149    /// and `AccessPath("*")` (matches any path).
150    pub fn allows(&self, action: &Action) -> bool {
151        // First, try exact match.
152        if self.allowed_actions.contains(action) {
153            return true;
154        }
155
156        // Then, check wildcard patterns.
157        match action {
158            Action::UseTool(tool_name) => {
159                // Check if policy has UseTool("*") wildcard.
160                self.allowed_actions
161                    .iter()
162                    .any(|a| matches!(a, Action::UseTool(w) if w == "*"))
163                    // Also check if the specific tool name is listed.
164                    || self.allowed_actions.contains(&Action::UseTool(tool_name.clone()))
165            }
166            Action::AccessPath(path) => {
167                // Check if policy has AccessPath("*") wildcard.
168                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            // Non-parameterized actions: exact match only (already checked above).
176            _ => false,
177        }
178    }
179}
180
181/// RBAC audit entry — records authorization decisions.
182#[derive(Debug, Clone, Serialize, Deserialize)]
183pub struct RbacAuditEntry {
184    /// When the authorization decision was made.
185    pub timestamp: DateTime<Utc>,
186    /// Who performed the action.
187    pub subject: Subject,
188    /// What action was attempted.
189    pub action: Action,
190    /// Which resource was involved.
191    pub resource: String,
192    /// Whether the action was allowed.
193    pub allowed: bool,
194    /// Optional reason for the decision.
195    pub reason: Option<String>,
196}
197
198impl RbacAuditEntry {
199    /// Creates a new RBAC audit entry.
200    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/// Human-in-the-loop approval request.
219#[derive(Debug, Clone, Serialize, Deserialize)]
220pub struct PendingApproval {
221    /// Unique identifier for this approval request.
222    pub id: uuid::Uuid,
223    /// Who is requesting the action.
224    pub subject: Subject,
225    /// What action is being requested.
226    pub action: Action,
227    /// Which resource is involved.
228    pub resource: String,
229    /// Why the action needs approval.
230    pub reason: String,
231    /// When the request was created.
232    pub created_at: DateTime<Utc>,
233}
234
235/// Status of a HitL approval request.
236#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237pub enum ApprovalStatus {
238    /// Awaiting user decision.
239    Pending,
240    /// User approved the request.
241    Approved,
242    /// User rejected the request.
243    Rejected,
244    /// Request timed out.
245    Expired,
246}
247
248/// RBAC Manager — manages roles, permissions, and HitL approvals.
249#[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    /// Creates a new RBAC manager with default policies for all roles.
260    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    /// Assigns a role to a subject.
275    pub fn assign_role(&mut self, subject: Subject, role: Role) {
276        self.subject_roles.insert(subject.clone(), role);
277    }
278
279    /// Revokes the role from a subject.
280    pub fn revoke_role(&mut self, subject: &Subject) {
281        self.subject_roles.remove(subject);
282    }
283
284    /// Returns the role assigned to a subject, if any.
285    pub fn get_role(&self, subject: &Subject) -> Option<Role> {
286        self.subject_roles.get(subject).copied()
287    }
288
289    /// Checks whether a subject has permission for the given action on a resource.
290    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    /// Creates a new approval request for a high-risk action.
322    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    /// Approves a pending approval request.
345    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    /// Rejects a pending approval request.
358    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    /// Returns all currently pending approval requests.
371    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    /// Returns all approval requests (pending + history) with their status.
380    pub fn all_approvals(&self) -> &[(PendingApproval, ApprovalStatus)] {
381        &self.pending_approvals
382    }
383
384    /// Returns the RBAC audit log.
385    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        // Admin should be able to use ANY tool via wildcard.
461        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        // Already approved
519        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}