use std::fs;
use std::io::ErrorKind;
use std::path::{Path, PathBuf};
use anyhow::{Context as _, Result, bail};
use openmoat_core::{EvalContext, Policy, RepoPolicy};
use crate::context;
use crate::home::{self, Home};
use crate::integrity::{Lock, sha256_hex};
use crate::trust::Trust;
pub struct Found {
pub path: PathBuf,
pub root: String,
pub digest: String,
pub text: String,
pub policy: RepoPolicy,
}
fn signature_path(found: &Found) -> std::path::PathBuf {
found.path.with_file_name("policy.yaml.sig")
}
pub fn signed_by(home: &Home, user: &Policy, found: &Found) -> Result<Option<String>> {
let signature = signature_path(found);
if !user.require_signed_repo_policy || fs::symlink_metadata(&signature).is_err() {
return Ok(None);
}
let (Some(program), Some(signers)) = (
crate::signing::locate(home),
crate::signing::trusted_signers(home),
) else {
return Ok(None);
};
crate::signing::verify(&program, &signers, found.text.as_bytes(), &signature)
.with_context(|| format!("checking the signature {}", signature.display()))
}
pub fn root_key(root: &Path) -> Result<String> {
let real = fs::canonicalize(root).with_context(|| format!("resolving {}", root.display()))?;
Ok(context::path_string(&real))
}
pub fn find(home: &Home, root: &Path) -> Result<Option<Found>> {
let path = root.join(home::DIR_NAME).join("policy.yaml");
let meta = match fs::metadata(&path) {
Err(e) if matches!(e.kind(), ErrorKind::NotFound | ErrorKind::NotADirectory) => {
return Ok(None);
}
meta => meta.with_context(|| format!("reading repository policy {}", path.display()))?,
};
let canonical = |p: &Path| fs::canonicalize(p).ok();
if path.parent().and_then(canonical) == canonical(home.root()) {
return Ok(None);
}
if !meta.is_file() {
bail!("repository policy {} is not a regular file", path.display());
}
let text = context::read_policy(&path)?;
let policy = RepoPolicy::parse(&text)
.with_context(|| format!("invalid repository policy {}", path.display()))?;
Ok(Some(Found {
root: root_key(root)?,
digest: sha256_hex(text.as_bytes()),
text,
path,
policy,
}))
}
pub fn trust(home: &Home) -> Result<Trust> {
let path = home.trust_path();
if !Lock::load(&home.lock_path()).is_ok_and(|lock| lock.pins(&path)) {
return Ok(Trust::default());
}
Trust::load(&path)
}
pub fn is_trusted(home: &Home, user: &Policy, found: &Found) -> Result<bool> {
if user.require_signed_repo_policy {
return Ok(signed_by(home, user, found)?.is_some());
}
Ok(trust(home)?.trusts(&found.root, &found.digest))
}
pub fn effective_policy(home: &Home, ctx: &EvalContext) -> Result<Policy> {
let mut user = home.load_policy()?;
crate::install::self_paths::protect_recorded(home, &mut user)?;
let Some(root) = &ctx.project else {
return Ok(user);
};
let Some(found) = find(home, Path::new(root))? else {
return Ok(user);
};
let trusted = is_trusted(home, &user, &found)?;
found
.policy
.merge(&user, trusted)
.with_context(|| format!("merging repository policy {}", found.path.display()))
}
pub fn summary(home: &Home) -> Result<Option<String>> {
let Some(root) = context::eval_context(home, None, None)?.project else {
return Ok(None);
};
let Some(found) = find(home, Path::new(&root))? else {
return Ok(None);
};
let p = &found.policy;
let rules = format!(
"{} deny, {} ask, {} allow",
p.deny.len(),
p.ask.len(),
p.allow.len()
);
let (signer, signed_only) = match home.load_policy() {
Ok(user) => (
signed_by(home, &user, &found)?,
user.require_signed_repo_policy,
),
Err(_) => (None, false),
};
let form = match (trust(home)?.repos.get(&found.root), signer) {
(_, Some(signer)) => format!("trusted: signed by {signer} ({rules})"),
_ if signed_only && p.allow.is_empty() => format!("tightening only ({rules})"),
_ if signed_only => {
format!("not signed by a trusted key: tightening only, allow rules ignored ({rules})")
}
(Some(digest), _) if *digest == found.digest => format!("trusted ({rules})"),
(Some(_), None) => {
format!("changed since `moat trust`: tightening only, allow rules ignored ({rules})")
}
(None, None) if p.allow.is_empty() => format!("tightening only ({rules})"),
(None, None) => format!("not trusted: tightening only, allow rules ignored ({rules})"),
};
Ok(Some(format!(
"{} sha256:{} {form}",
found.path.display(),
found.digest
)))
}
#[cfg(test)]
mod tests;