use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
pub const ASK_TIMEOUT: Duration = Duration::from_secs(300);
#[derive(Default)]
pub struct PermissionGate {
pending: parking_lot::Mutex<HashMap<String, tokio::sync::oneshot::Sender<String>>>,
always: parking_lot::Mutex<HashMap<String, Vec<String>>>,
hydrated: parking_lot::Mutex<std::collections::HashSet<String>>,
requests: parking_lot::Mutex<HashMap<String, Value>>,
}
pub struct PendingGuard {
gate: Arc<PermissionGate>,
id: String,
}
impl Drop for PendingGuard {
fn drop(&mut self) {
self.gate.pending.lock().remove(&self.id);
self.gate.requests.lock().remove(&self.id);
}
}
impl PermissionGate {
pub fn new() -> Arc<Self> {
Arc::new(Self::default())
}
pub fn register(
self: Arc<Self>,
id: &str,
request: Value,
) -> (tokio::sync::oneshot::Receiver<String>, PendingGuard) {
let (tx, rx) = tokio::sync::oneshot::channel();
self.pending.lock().insert(id.to_string(), tx);
self.requests.lock().insert(id.to_string(), request);
let guard = PendingGuard {
gate: self,
id: id.to_string(),
};
(rx, guard)
}
pub fn hydrate(&self, db: &std::path::Path, session_id: &str) -> anyhow::Result<()> {
if self.hydrated.lock().contains(session_id) {
return Ok(());
}
let keys = ocserve_store::session_always_keys(db, session_id)?;
if !keys.is_empty() {
let mut always = self.always.lock();
let entry = always.entry(session_id.to_string()).or_default();
for k in keys {
if !entry.contains(&k) {
entry.push(k);
}
}
}
self.hydrated.lock().insert(session_id.to_string());
Ok(())
}
pub fn pending_len(&self) -> usize {
self.pending.lock().len()
}
pub fn reply(&self, id: &str, reply: &str) -> bool {
self.requests.lock().remove(id);
match self.pending.lock().remove(id) {
Some(tx) => tx.send(reply.to_string()).is_ok(),
None => false,
}
}
pub async fn wait(&self, id: &str, rx: tokio::sync::oneshot::Receiver<String>) -> String {
match tokio::time::timeout(ASK_TIMEOUT, rx).await {
Ok(Ok(reply)) => reply,
_ => {
self.requests.lock().remove(id);
self.pending.lock().remove(id);
"reject".to_string()
}
}
}
pub fn grant_always(&self, session_id: &str, key: &str) {
self.always
.lock()
.entry(session_id.to_string())
.or_default()
.push(key.to_string());
}
pub fn check_always(&self, session_id: &str, key: &str) -> bool {
self.always
.lock()
.get(session_id)
.map(|v| v.iter().any(|k| k == key))
.unwrap_or(false)
}
pub fn list(&self) -> Vec<Value> {
self.requests.lock().values().cloned().collect()
}
}
#[cfg(test)]
mod guard_tests {
use super::*;
#[test]
fn dropping_pending_guard_clears_entries() {
let gate = Arc::new(PermissionGate::default());
let (rx, guard) = gate
.clone()
.register("perm_t", serde_json::json!({"tool":"bash"}));
assert_eq!(gate.pending_len(), 1);
drop(guard); assert_eq!(gate.pending_len(), 0, "guard must clear pending (A2)");
assert!(!gate.reply("perm_t", "once"), "already cleared");
drop(rx);
}
#[test]
fn reply_then_guard_drop_is_idempotent() {
let gate = Arc::new(PermissionGate::default());
let (rx, guard) = gate.clone().register("perm_r", serde_json::json!({}));
assert!(gate.reply("perm_r", "allow"));
assert_eq!(gate.pending_len(), 0);
drop(guard); assert_eq!(gate.pending_len(), 0);
drop(rx);
}
}