use areev_cal::governance::{GovernanceHost, LoopInfo, ReviewDecision, RunLoopOptions};
use areev_cal::{CalStoreFacade, AreevFacade};
use areev_core::error::{AreevError, Result};
use areev_loop::{Decision, Engine, Policy, RunOptions};
pub struct LoopGovernance {
policy: Option<Policy>,
}
impl LoopGovernance {
fn scopes_over(
&self,
facade: &areev_cal::AreevFacade,
sub: &mut crate::BorrowedSubstrate<'_>,
rec_hash: &str,
) -> Result<areev_loop::ScopeSet> {
let authz = facade.authz();
let scope = self
.engine()
.recommendations(sub, None)
.ok()
.and_then(|recs| recs.into_iter().find(|r| r.hash == rec_hash))
.map(|r| r.scope)
.unwrap_or_default();
if !crate::covers_rec(&authz, &scope) {
return Err(Self::wrap(areev_loop::Error::NotFound(rec_hash.into())));
}
Ok(crate::scopes_for_rec(&authz, &scope))
}
pub fn new() -> Self {
LoopGovernance { policy: None }
}
pub fn with_policy(policy: Policy) -> Self {
LoopGovernance { policy: Some(policy) }
}
fn engine(&self) -> Engine {
match &self.policy {
Some(p) => Engine::with_builtins().with_policy(p.clone()),
None => Engine::with_builtins(),
}
}
fn facade<'a>(&self, store: &'a dyn CalStoreFacade) -> Result<&'a AreevFacade> {
store
.as_any()
.and_then(|a| a.downcast_ref::<AreevFacade>())
.ok_or_else(|| {
AreevError::Internal(
"governance requires the Areev facade — this store cannot host the loop"
.into(),
)
})
}
fn wrap(e: areev_loop::Error) -> AreevError {
AreevError::Validation(e.to_string())
}
}
impl Default for LoopGovernance {
fn default() -> Self {
Self::new()
}
}
impl GovernanceHost for LoopGovernance {
fn run_loop(
&self,
store: &dyn CalStoreFacade,
opts: &RunLoopOptions,
) -> Result<serde_json::Value> {
let facade = self.facade(store)?;
facade
.authz()
.check(areev_core::authz::Verb::LoopRun, areev_loop::LOOP_NS)?;
let mut sub = crate::BorrowedSubstrate::new(facade);
let run_opts = RunOptions {
min_new: opts.min_new,
if_stale_ms: opts.if_stale_ms,
full_sweep: opts.full_sweep,
triggering_actor: Some(facade.authz().principal().to_string()),
..RunOptions::default()
};
let res = self
.engine()
.run(&mut sub, &run_opts, crate::now_ms())
.map_err(Self::wrap)?;
serde_json::to_value(&res).map_err(|e| AreevError::Internal(e.to_string()))
}
fn review(
&self,
store: &dyn CalStoreFacade,
rec_hash: &str,
decision: ReviewDecision,
because: &str,
) -> Result<()> {
let facade = self.facade(store)?;
let actor = facade.authz().principal().to_string();
let observer = crate::observer_for_principal(&actor);
let mut sub = crate::BorrowedSubstrate::new(facade);
let scopes = self.scopes_over(facade, &mut sub, rec_hash)?;
let d = match decision {
ReviewDecision::Approve => Decision::Approve,
ReviewDecision::Reject => Decision::Reject,
};
self.engine()
.review(&mut sub, rec_hash, d, &actor, observer, &scopes, because, crate::now_ms())
.map_err(Self::wrap)
}
fn apply(
&self,
store: &dyn CalStoreFacade,
rec_hash: &str,
because: &str,
destructive_cap: bool,
) -> Result<bool> {
let facade = self.facade(store)?;
let authz = facade.authz();
let actor = authz.principal().to_string();
let observer = crate::observer_for_principal(&actor);
let allow_destructive = destructive_cap
&& (authz.allows(areev_core::authz::Verb::Delete, "*")
|| authz.allows(areev_core::authz::Verb::Erase, "*"));
let mut sub = crate::BorrowedSubstrate::new(facade);
let scopes = self.scopes_over(facade, &mut sub, rec_hash)?;
let applied = self
.engine()
.apply(
&mut sub,
rec_hash,
&actor,
observer,
&scopes,
because,
allow_destructive,
crate::now_ms(),
)
.map_err(Self::wrap)?;
Ok(applied.rollbackable)
}
fn rollback(&self, store: &dyn CalStoreFacade, rec_hash: &str, because: &str) -> Result<()> {
let facade = self.facade(store)?;
let actor = facade.authz().principal().to_string();
let observer = crate::observer_for_principal(&actor);
let mut sub = crate::BorrowedSubstrate::new(facade);
let scopes = self.scopes_over(facade, &mut sub, rec_hash)?;
self.engine()
.rollback(&mut sub, rec_hash, &actor, observer, &scopes, because, crate::now_ms())
.map_err(Self::wrap)
}
fn describe(&self, store: &dyn CalStoreFacade, what: LoopInfo) -> Result<serde_json::Value> {
let facade = self.facade(store)?;
facade
.authz()
.check(areev_core::authz::Verb::Read, areev_loop::LOOP_NS)?;
let authz = facade.authz();
let sub = crate::BorrowedSubstrate::new(facade);
let engine = self.engine();
let v = match what {
LoopInfo::Loop => {
let health = engine.health(&sub, crate::now_ms()).map_err(Self::wrap)?;
let pending = crate::visible_recommendations(
&engine,
&sub,
&authz,
Some(areev_loop::RecStatus::Pending),
)
.map_err(Self::wrap)?;
serde_json::to_value(health).map(|mut v| {
v["pending_recommendations"] = serde_json::Value::Array(
pending
.iter()
.map(|r| {
serde_json::json!({
"hash": r.hash,
"analyzer": r.analyzer,
"severity": format!("{:?}", r.severity).to_lowercase(),
"summary": r.summary.render(),
})
})
.collect(),
);
v
})
}
LoopInfo::Analyzers => Ok(serde_json::Value::Array(
engine
.analyzers()
.iter()
.map(|a| {
let m = a.manifest();
serde_json::json!({
"id": m.id,
"family": m.family(),
"default_on": m.default_on,
})
})
.collect(),
)),
LoopInfo::Outcomes => {
serde_json::to_value(engine.outcomes(&sub).map_err(Self::wrap)?)
}
LoopInfo::Policy => Ok(match &self.policy {
None => serde_json::json!({
"attached": false,
"note": "no host policy attached — the closed default: nothing \
auto-applies, nothing is denied",
}),
Some(p) => serde_json::json!({
"attached": true,
"auto_apply_enabled": p.auto_apply_enabled,
"denied_families": p.deny,
}),
}),
};
v.map_err(|e| AreevError::Internal(e.to_string()))
}
}