use crate::config::Config;
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrantStatus {
Granted,
NotGranted,
Undetermined,
}
impl GrantStatus {
pub fn label(self) -> &'static str {
match self {
Self::Granted => "granted",
Self::NotGranted => "NOT granted",
Self::Undetermined => "cannot be checked from the pattern alone",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EntryStatus {
pub raw: String,
pub status: GrantStatus,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GrantReport {
pub agent: String,
pub allow_blueprint: bool,
pub entries: Vec<EntryStatus>,
}
pub fn build(
blueprint: &leviath_core::Blueprint,
config: &Config,
workdir: &Path,
) -> Option<Result<GrantReport, String>> {
let rp = blueprint
.read_paths
.as_ref()
.filter(|rp| !rp.allow.is_empty())?;
Some(report_entries(
&blueprint.name,
&rp.allow,
config,
workdir,
leviath_core::home_dir().as_deref(),
cfg!(windows),
))
}
fn report_entries(
agent: &str,
declared: &[String],
config: &Config,
workdir: &Path,
home: Option<&Path>,
windows: bool,
) -> Result<GrantReport, String> {
let grant_entries = config.read_path_grants_for_agent(agent);
let grants = leviath_core::ReadPathSet::compile(&grant_entries, workdir, home, windows)
.map_err(|e| format!("read_paths grant in your config.toml: {e}"))?;
let allow_blueprint = config.security.allow_blueprint_read_paths;
let entries = declared
.iter()
.map(|raw| EntryStatus {
raw: raw.clone(),
status: entry_status(raw, &grants, allow_blueprint, workdir, home, windows),
})
.collect();
Ok(GrantReport {
agent: agent.to_string(),
allow_blueprint,
entries,
})
}
fn entry_status(
raw: &str,
grants: &leviath_core::ReadPathSet,
allow_blueprint: bool,
workdir: &Path,
home: Option<&Path>,
windows: bool,
) -> GrantStatus {
if allow_blueprint {
return GrantStatus::Granted;
}
if grants.is_empty() {
return GrantStatus::NotGranted;
}
let one = [raw.to_string()];
let sample = leviath_core::ReadPathSet::compile(&one, workdir, home, windows)
.ok()
.and_then(|set| set.entries().first().and_then(|e| e.sample_path()));
match sample {
Some(sample) if grants.matches_lexically(&sample) => GrantStatus::Granted,
Some(_) => GrantStatus::NotGranted,
None => GrantStatus::Undetermined,
}
}
impl GrantReport {
pub fn declared(&self) -> usize {
self.entries.len()
}
pub fn granted(&self) -> usize {
self.entries
.iter()
.filter(|e| e.status == GrantStatus::Granted)
.count()
}
pub fn ungranted(&self) -> Vec<&str> {
self.entries
.iter()
.filter(|e| e.status == GrantStatus::NotGranted)
.map(|e| e.raw.as_str())
.collect()
}
pub fn has_ungranted(&self) -> bool {
!self.ungranted().is_empty()
}
pub fn summary(&self) -> String {
format!("{} declared, {} granted", self.declared(), self.granted())
}
pub fn grant_stanza(&self) -> Vec<String> {
let ungranted = self.ungranted();
if ungranted.is_empty() {
return Vec::new();
}
let listed = ungranted
.iter()
.map(|e| format!("\"{e}\""))
.collect::<Vec<_>>()
.join(", ");
vec![
format!("[agent_read_paths.{}]", self.agent),
format!("allow = [{listed}]"),
]
}
pub fn warning_line(&self) -> Option<String> {
self.has_ungranted().then(|| {
format!(
"warning: agent '{}' declares [read_paths] your config does not grant ({}); \
reads outside the workdir will be refused",
self.agent,
self.summary()
)
})
}
}
#[cfg(test)]
mod tests;