use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail};
use openmoat_core::{Decision, Verdict};
use openmoat_hosts::{HookRequest, ProjectSettingsSpec};
use crate::home::Home;
use crate::integrity::{self, Drift, INTEGRITY_RULE, Lock};
use crate::project_settings;
const CONFIG_CHANGE_RULE: &str = "config-change";
fn project_file(
request: &HookRequest,
source: &str,
) -> Option<(&'static ProjectSettingsSpec, &'static str)> {
let format = request.host.spec().speaks.unwrap_or(request.host);
let spec = format.spec().project_settings.as_ref()?;
match source {
"project_settings" => Some((spec, spec.shared)),
"local_settings" => Some((spec, spec.local?)),
_ => None,
}
}
pub(super) fn decision(
home: &Home,
request: &HookRequest,
source: &str,
change: &str,
) -> Result<Decision> {
let lock_path = home.lock_path();
if !lock_path.is_file() {
bail!(
"no policy lock at {}; run `openmoat init`",
lock_path.display()
);
}
let lock = Lock::load(&lock_path)?;
let (drift, checked) = integrity::verify_state(home, &lock);
let relaxed = !drift.is_empty();
if relaxed
&& integrity::split(&checked, &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 &request.action {
Some(openmoat_core::Action::FsWrite { path }) => Path::new(path),
Some(_) => bail!("config change for something other than a file"),
None => {
if let Some((spec, name)) = project_file(request, source) {
for file in unnamed_project_files(request.cwd.as_deref(), spec, name)? {
if !lock.pins(&file)
&& let Some(blocked) = project_change(&file)
{
return Ok(blocked);
}
}
}
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);
}
};
let target = path.to_str().and_then(openmoat_hosts::proposal_target);
if let Some(target) = &target
&& lock.pins(Path::new(target))
{
if let Some(drift) = lock.verify_proposal(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 `openmoat doctor --accept` to change it"
));
return Ok(deny);
}
decision
.reasons
.push(format!("{} leaves {target} as pinned", path.display()));
return Ok(decision);
}
if project_file(request, source).is_some()
&& !lock.pins(path)
&& let Some(blocked) = project_change(path)
{
return Ok(blocked);
}
let pinned = if lock.verify_one(path).is_some() {
"differs from the policy lock only in settings that change no decision; `openmoat 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 unnamed_project_files(
cwd: Option<&str>,
spec: &ProjectSettingsSpec,
name: &str,
) -> Result<Vec<PathBuf>> {
let cwd = match cwd {
Some(dir) => PathBuf::from(dir),
None => std::env::current_dir().context("resolving working directory")?,
};
Ok(project_settings::candidates(
&cwd,
&crate::home::user_home()?,
spec,
name,
))
}
fn project_change(path: &Path) -> Option<Decision> {
let found = project_settings::check_unchanged(path)?;
let mut deny = Decision::new(Verdict::Deny);
deny.rules.push(project_settings::RULE.to_owned());
deny.reasons.push(format!(
"{} (project settings) {found}; the change is not loaded into this session. Review the file and remove what you did not add; a new session loads the file as it is",
path.display()
));
Some(deny)
}
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 `openmoat doctor` to inspect, `openmoat doctor --accept` to accept it"
));
}
decision
}
#[cfg(test)]
mod tests;