use anyhow::{Result, bail};
use openmoat_core::{Decision, Verdict};
use crate::home::Home;
use crate::integrity::{self, Drift, INTEGRITY_RULE, Lock};
const CONFIG_CHANGE_RULE: &str = "config-change";
pub(super) fn decision(
home: &Home,
action: Option<&openmoat_core::Action>,
source: &str,
change: &str,
) -> Result<Decision> {
let lock_path = home.lock_path();
if !lock_path.is_file() {
bail!("no policy lock at {}; run `moat init`", lock_path.display());
}
let lock = Lock::load(&lock_path)?;
let drift = lock.verify();
let relaxed = !drift.is_empty();
if relaxed && integrity::split(home, &lock, drift.clone()).deny.is_some() {
return Ok(drift_blocks_change(&drift));
}
let mut decision = Decision::new(Verdict::Allow);
decision.rules.push(CONFIG_CHANGE_RULE.to_owned());
let path = match action {
Some(openmoat_core::Action::FsWrite { path }) => std::path::Path::new(path),
Some(_) => bail!("config change for something other than a file"),
None => {
let pins = if relaxed {
"files pinned by OpenMoat differ only in settings that change no decision"
} else {
"every file pinned by OpenMoat matches the policy lock"
};
decision
.reasons
.push(format!("{source} {change} (no file named); {pins}"));
return Ok(decision);
}
};
if let Some(target) = path.to_str().and_then(openmoat_hosts::proposal_target)
&& lock.pins(std::path::Path::new(&target))
{
if let Some(drift) = lock.verify_proposal(path, std::path::Path::new(&target)) {
let mut deny = Decision::new(Verdict::Deny);
deny.rules.push(INTEGRITY_RULE.to_owned());
deny.reasons.push(format!(
"{drift}; OpenMoat pinned {target}, so the proposal is not applied. Edit the file yourself and run `moat doctor --accept` to change it"
));
return Ok(deny);
}
decision
.reasons
.push(format!("{} leaves {target} as pinned", path.display()));
return Ok(decision);
}
let pinned = if lock.verify_one(path).is_some() {
"differs from the policy lock only in settings that change no decision; `moat doctor --accept` re-pins"
} else if lock.pins(path) {
"matches the policy lock"
} else {
"not pinned by OpenMoat"
};
decision
.reasons
.push(format!("{} {change}; {pinned}", path.display()));
Ok(decision)
}
fn drift_blocks_change(drift: &[Drift]) -> Decision {
let mut decision = Decision::new(Verdict::Deny);
decision.rules.push(INTEGRITY_RULE.to_owned());
for d in drift {
decision.reasons.push(format!(
"{d} outside OpenMoat; the change is not loaded into this session. Run `moat doctor` to inspect, `moat doctor --accept` to accept it"
));
}
decision
}