Skip to main content

oxios_kernel/kernel_handle/
security_api.rs

1//! Security API — authentication, audit trail, RBAC, approvals.
2
3use crate::access_manager::{
4    AccessManager, AgentPermissions, ApprovalStatus, PendingApproval, PermissionUpdate,
5};
6use crate::auth::AuthManager;
7use crate::state_store::StateStore;
8use oxi_sdk::observability::{AuditAction, AuditTrail, TrailEntry};
9use std::collections::HashMap;
10use std::sync::Arc;
11
12/// A one-time ticket for WebSocket authentication.
13struct WsTicket {
14    created_at: std::time::Instant,
15}
16
17/// How long a [`WsTicket`] is considered valid during `validate_ws_ticket`.
18/// Single-use: the ticket is removed from the map on first validation.
19const WS_TICKET_TTL_SECS: u64 = 30;
20/// Prune threshold used inside `generate_ws_ticket`. Slightly longer than
21/// [`WS_TICKET_TTL_SECS`] so an expired-but-not-yet-consumed ticket is
22/// cleared from memory on the next generate, rather than lingering until
23/// process exit.
24const WS_TICKET_PRUNE_AFTER_SECS: u64 = 60;
25
26/// Security system calls.
27pub struct SecurityApi {
28    pub(crate) auth_manager: Arc<parking_lot::Mutex<AuthManager>>,
29    pub(crate) audit_trail: Arc<AuditTrail>,
30    pub(crate) access_manager: Arc<parking_lot::Mutex<AccessManager>>,
31    pub(crate) state_store: Arc<StateStore>,
32    ws_tickets: Arc<parking_lot::Mutex<HashMap<String, WsTicket>>>,
33}
34
35impl SecurityApi {
36    /// Create a new SecurityApi.
37    pub fn new(
38        auth_manager: Arc<parking_lot::Mutex<AuthManager>>,
39        audit_trail: Arc<AuditTrail>,
40        access_manager: Arc<parking_lot::Mutex<AccessManager>>,
41        state_store: Arc<StateStore>,
42    ) -> Self {
43        Self {
44            auth_manager,
45            audit_trail,
46            access_manager,
47            state_store,
48            ws_tickets: Arc::new(parking_lot::Mutex::new(HashMap::new())),
49        }
50    }
51
52    /// Generate a one-time WebSocket ticket.
53    ///
54    /// The ticket is valid for [`WS_TICKET_TTL_SECS`] seconds (single-use).
55    /// Pruning removes entries older than [`WS_TICKET_PRUNE_AFTER_SECS`]
56    /// seconds — the prune window is intentionally a bit longer than the
57    /// validate window so a ticket that has just expired is still cleared
58    /// from memory on the next generate.
59    pub fn generate_ws_ticket(&self) -> String {
60        let bytes: [u8; 16] = *uuid::Uuid::new_v4().as_bytes();
61        let ticket = format!("wst_{}", hex::encode(bytes));
62        let mut tickets = self.ws_tickets.lock();
63        // Prune expired tickets.
64        tickets.retain(|_, t| t.created_at.elapsed().as_secs() < WS_TICKET_PRUNE_AFTER_SECS);
65        tickets.insert(
66            ticket.clone(),
67            WsTicket {
68                created_at: std::time::Instant::now(),
69            },
70        );
71        ticket
72    }
73
74    /// Validate and consume a one-time WebSocket ticket. Returns false if
75    /// invalid/expired/already-used.
76    pub fn validate_ws_ticket(&self, ticket: &str) -> bool {
77        let mut tickets = self.ws_tickets.lock();
78        if let Some(t) = tickets.remove(ticket) {
79            t.created_at.elapsed().as_secs() < WS_TICKET_TTL_SECS
80        } else {
81            false
82        }
83    }
84
85    /// Audit an action.
86    pub fn audit(&self, actor: &str, action: AuditAction, resource: &str) -> String {
87        self.audit_trail
88            .append(actor.to_string(), action, resource.to_string())
89    }
90
91    /// Verify audit chain integrity.
92    pub fn verify_chain(&self) -> anyhow::Result<bool> {
93        self.audit_trail
94            .verify()
95            .map_err(|e| anyhow::anyhow!("audit verify failed: {e:?}"))
96    }
97
98    /// Query audit entries by sequence range.
99    pub fn query_audit(&self, from_seq: u64, to_seq: u64) -> Vec<TrailEntry> {
100        self.audit_trail.entries(from_seq, to_seq)
101    }
102
103    /// Query audit entries whose agent/subject matches `agent_id`.
104    /// Field access is serde-based so this is robust to `TrailEntry` field
105    /// renames in oxi-sdk.
106    pub fn query_audit_by_agent(&self, agent_id: &str) -> Vec<TrailEntry> {
107        self.audit_trail
108            .entries(0, u64::MAX)
109            .into_iter()
110            .filter(|e| {
111                serde_json::to_value(e)
112                    .ok()
113                    .and_then(|v| {
114                        v.get("agent")
115                            .or_else(|| v.get("subject"))
116                            .or_else(|| v.get("agent_id"))
117                            .and_then(|s| s.as_str())
118                            .map(|s| s == agent_id)
119                    })
120                    .unwrap_or(false)
121            })
122            .collect()
123    }
124
125    /// Get audit entry count.
126    pub fn audit_count(&self) -> usize {
127        self.audit_trail.len()
128    }
129
130    /// Flush audit trail to disk and commit to git.
131    ///
132    /// Persists all in-memory audit entries to the state store,
133    /// then commits the audit file to git for versioning.
134    pub fn flush(&self, git: &crate::git_layer::GitLayer) -> anyhow::Result<()> {
135        // 1. Persist entries to state store via AuditPersistence trait
136        self.audit_trail.flush_to(self.state_store.as_ref())?;
137        // 2. Commit to git. Unlike best-effort commits in save_and_commit
138        //    (where the on-disk save already succeeded), audit trail commits
139        //    are compliance-relevant: surface the failure so operators know
140        //    the audit record is not versioned.
141        if git.is_enabled() {
142            git.commit_file("audit", "audit trail flush")?;
143        }
144        Ok(())
145    }
146
147    /// Validate a bearer token.
148    pub fn validate_token(&self, token: &str) -> bool {
149        self.auth_manager.lock().validate(token)
150    }
151
152    /// Get audit log entries from access manager.
153    pub fn get_audit_log(&self) -> Vec<crate::access_manager::AuditEntry> {
154        self.access_manager.lock().audit_log().to_vec()
155    }
156
157    /// Get permissions for an agent.
158    pub fn get_permissions(&self, agent: &str) -> Option<AgentPermissions> {
159        self.access_manager.lock().get_permissions(agent).cloned()
160    }
161
162    /// Ensure permissions exist for an agent (get or create).
163    pub fn ensure_permissions(&self, agent: &str) -> AgentPermissions {
164        self.access_manager
165            .lock()
166            .get_or_create_permissions(agent)
167            .clone()
168    }
169
170    /// Update permissions for an agent.
171    pub fn update_permissions(&self, agent: &str, update: PermissionUpdate) -> anyhow::Result<()> {
172        self.access_manager.lock().update_permissions(agent, update)
173    }
174
175    /// Log an audit action.
176    pub fn log_action(&self, agent_name: &str, action: &str, resource: &str) {
177        let mut am = self.access_manager.lock();
178        am.log_access(agent_name, action, resource, true, None);
179    }
180
181    /// List all pending approvals.
182    pub fn list_approvals(&self) -> Vec<(PendingApproval, ApprovalStatus)> {
183        self.access_manager
184            .lock()
185            .rbac_manager()
186            .all_approvals()
187            .to_vec()
188    }
189
190    /// Approve a pending request.
191    pub fn approve(&self, id: uuid::Uuid) -> bool {
192        self.access_manager.lock().rbac_manager_mut().approve(id)
193    }
194
195    /// Reject a pending request.
196    pub fn reject(&self, id: uuid::Uuid) -> bool {
197        self.access_manager.lock().rbac_manager_mut().reject(id)
198    }
199}