use std::collections::HashMap;
use std::path::Path;
use std::sync::{Mutex, PoisonError};
use std::time::Instant;
use anyhow::{Context as _, Result, bail};
use openmoat_audit::{Event, EventId, Store};
use openmoat_core::{CompiledPolicy, EvalContext, Policy, Taint, Verdict};
use openmoat_hosts::Host;
use openmoat_proxy::TaintLookup;
use super::{Pass, SessionTaint, fold};
use crate::context;
use crate::home::Home;
pub const ISSUED_RULE: &str = "proxy-credential";
const MAX_SESSIONS: usize = 1024;
pub struct IssuedTaint {
home: Home,
state: Mutex<State>,
}
#[derive(Default)]
struct State {
store: Option<Store>,
sessions: HashMap<String, Followed>,
}
struct Followed {
host: Host,
ctx: EvalContext,
policy: Policy,
upto: EventId,
taint: SessionTaint,
used: Instant,
}
impl IssuedTaint {
pub fn new(home: Home) -> Self {
Self {
home,
state: Mutex::new(State::default()),
}
}
fn fold(&self, session: &str) -> Result<Taint> {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
let state = &mut *state;
if state.store.is_none() {
state.store = Some(self.home.open_audit()?);
}
let Some(store) = &state.store else {
bail!("the audit log is not open");
};
if !state.sessions.contains_key(session) {
let events = store.session_after(session, EventId(0))?;
let followed = self.follow(session, &events)?;
if state.sessions.len() >= MAX_SESSIONS
&& let Some(oldest) = state
.sessions
.iter()
.min_by_key(|(_, f)| f.used)
.map(|(id, _)| id.clone())
{
state.sessions.remove(&oldest);
}
state.sessions.insert(session.to_owned(), followed);
}
let Some(followed) = state.sessions.get_mut(session) else {
bail!("session {session} is not followed");
};
followed.used = Instant::now();
let events = store.session_after(session, followed.upto)?;
if let Some(last) = events.last() {
let compiled = CompiledPolicy::compile(&followed.policy, &followed.ctx)?;
let cwd = followed.ctx.cwd.clone();
fold(
&mut followed.taint,
&events,
followed.host,
&compiled,
&cwd,
&mut Pass::default(),
)?;
followed.upto = last.id;
}
Ok(followed.taint.taint.clone())
}
fn follow(&self, session: &str, events: &[Event]) -> Result<Followed> {
let Some(issued) = events
.iter()
.find(|e| e.verdict == Verdict::Allow && e.rules.iter().any(|r| r == ISSUED_RULE))
else {
bail!("no proxy credential was issued to session {session}");
};
let host: Host = issued
.host
.parse()
.map_err(|_| anyhow::Error::msg(format!("unknown host `{}`", issued.host)))?;
let cwd = issued
.cwd
.as_deref()
.context("the issue row names no directory")?;
let ctx = context::eval_context(&self.home, Some(Path::new(cwd)), None)?;
let policy = crate::repo::effective_policy(&self.home, &ctx)?;
Ok(Followed {
host,
ctx,
policy,
upto: EventId(0),
taint: SessionTaint::default(),
used: Instant::now(),
})
}
}
impl TaintLookup for IssuedTaint {
fn taint(&self, session: &str) -> Result<Taint, String> {
self.fold(session).map_err(|e| format!("{e:#}"))
}
fn version(&self) -> Option<i64> {
let mut state = self.state.lock().unwrap_or_else(PoisonError::into_inner);
if state.store.is_none() {
state.store = self.home.open_audit().ok();
}
let id = state.store.as_ref()?.latest_id().ok()?;
Some(id.map_or(0, |id| id.0))
}
}
#[cfg(test)]
mod tests;