use serde::{Deserialize, Serialize};
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Act {
Read,
Write,
Exec,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Effect {
Allow,
Ask,
Deny,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Rule {
pub act: Act,
pub effect: Effect,
pub pattern: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Layer {
pub name: String,
pub rules: Vec<Rule>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub struct Defaults {
pub read: Effect,
pub write: Effect,
pub exec: Effect,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Verdict {
pub effect: Effect,
pub rule: Option<String>,
pub layer: Option<String>,
}
const SECRET_PATTERNS: &[&str] = &[".env", "*.pem", "id_rsa", "id_ed25519", "*.key"];
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Policy {
pub layers: Vec<Layer>,
pub defaults: Defaults,
}
impl Default for Policy {
fn default() -> Self {
let mut rules = Vec::new();
for pat in SECRET_PATTERNS {
for act in [Act::Read, Act::Write] {
rules.push(Rule {
act,
effect: Effect::Deny,
pattern: (*pat).to_string(),
});
}
}
let exec = Layer {
name: "builtin-exec".into(),
rules: ["rustc", "<test-binary>"]
.iter()
.map(|p| Rule { act: Act::Exec, effect: Effect::Allow, pattern: (*p).to_string() })
.collect(),
};
Self {
layers: vec![
Layer { name: "builtin-secrets".into(), rules },
exec,
],
defaults: Defaults {
read: Effect::Allow,
write: Effect::Ask,
exec: Effect::Ask,
},
}
}
}
impl Policy {
pub fn permissive() -> Self {
Self {
layers: Vec::new(),
defaults: Defaults {
read: Effect::Allow,
write: Effect::Allow,
exec: Effect::Allow,
},
}
}
pub fn is_permissive(&self) -> bool {
self.layers.iter().all(|l| l.rules.is_empty())
&& self.defaults.read == Effect::Allow
&& self.defaults.write == Effect::Allow
&& self.defaults.exec == Effect::Allow
}
pub fn layer(mut self, name: impl Into<String>) -> Self {
self.layers.push(Layer {
name: name.into(),
rules: Vec::new(),
});
self
}
pub fn rule(mut self, act: Act, effect: Effect, pattern: impl Into<String>) -> Self {
if self.layers.is_empty() {
self = self.layer("policy");
}
let pattern = pattern.into();
let layer = self.layers.last_mut().expect("a layer exists");
layer.rules.push(Rule {
act,
effect,
pattern,
});
self
}
pub fn allow_read(self, pattern: impl Into<String>) -> Self {
self.rule(Act::Read, Effect::Allow, pattern)
}
pub fn deny_read(self, pattern: impl Into<String>) -> Self {
self.rule(Act::Read, Effect::Deny, pattern)
}
pub fn allow_write(self, pattern: impl Into<String>) -> Self {
self.rule(Act::Write, Effect::Allow, pattern)
}
pub fn deny_write(self, pattern: impl Into<String>) -> Self {
self.rule(Act::Write, Effect::Deny, pattern)
}
pub fn ask_write(self, pattern: impl Into<String>) -> Self {
self.rule(Act::Write, Effect::Ask, pattern)
}
pub fn allow_exec(self, pattern: impl Into<String>) -> Self {
self.rule(Act::Exec, Effect::Allow, pattern)
}
pub fn deny_exec(self, pattern: impl Into<String>) -> Self {
self.rule(Act::Exec, Effect::Deny, pattern)
}
pub fn merge(mut self, overlay: Policy) -> Self {
self.layers.extend(overlay.layers);
self.defaults = Defaults {
read: self.defaults.read.max(overlay.defaults.read),
write: self.defaults.write.max(overlay.defaults.write),
exec: self.defaults.exec.max(overlay.defaults.exec),
};
self
}
pub fn contain(&self, child: &Policy) -> Policy {
let mut layers = self.layers.clone();
for l in &child.layers {
let rules: Vec<Rule> = l
.rules
.iter()
.filter(|r| r.effect != Effect::Allow)
.cloned()
.collect();
if !rules.is_empty() {
layers.push(Layer {
name: l.name.clone(),
rules,
});
}
}
Policy {
layers,
defaults: Defaults {
read: self.defaults.read.max(child.defaults.read),
write: self.defaults.write.max(child.defaults.write),
exec: self.defaults.exec.max(child.defaults.exec),
},
}
}
pub fn explain(&self, act: Act, target: &str) -> Verdict {
for effect in [Effect::Deny, Effect::Ask, Effect::Allow] {
for layer in &self.layers {
for rule in &layer.rules {
if rule.act == act && rule.effect == effect && matches(&rule.pattern, target) {
return Verdict {
effect,
rule: Some(rule.pattern.clone()),
layer: Some(layer.name.clone()),
};
}
}
}
}
Verdict {
effect: match act {
Act::Read => self.defaults.read,
Act::Write => self.defaults.write,
Act::Exec => self.defaults.exec,
},
rule: None,
layer: None,
}
}
pub fn check(&self, act: Act, target: &str) -> Verdict {
self.explain(act, target)
}
}
fn matches(pattern: &str, target: &str) -> bool {
let target = target.replace('\\', "/");
let Ok(re) = glob_to_regex(pattern) else {
return false; };
if re.is_match(&target) {
return true;
}
match target.rsplit('/').next() {
Some(base) if base != target => re.is_match(base),
_ => false,
}
}
fn glob_to_regex(glob: &str) -> Result<regex::Regex> {
let mut re = String::from("(?s)^");
for ch in glob.chars() {
match ch {
'*' => re.push_str(".*"),
'?' => re.push('.'),
c => re.push_str(®ex::escape(&c.to_string())),
}
}
re.push('$');
regex::Regex::new(&re).map_err(|e| Error::Config(format!("bad policy glob: {e}")))
}
#[cfg(test)]
mod tests {
use super::*;
fn base() -> Policy {
Policy::default()
.layer("base")
.allow_read("src/*")
.allow_write("src/*")
.deny_read("secrets/*")
.deny_write("secrets/*")
}
#[test]
fn deny_beats_allow_even_when_both_match() {
let p = Policy::default()
.layer("l")
.allow_write("src/*")
.deny_write("src/generated/*");
assert_eq!(p.check(Act::Write, "src/a.rs").effect, Effect::Allow);
assert_eq!(
p.check(Act::Write, "src/generated/x.rs").effect,
Effect::Deny
);
}
#[test]
fn default_policy_denies_dotenv_read_inside_a_readable_tree() {
let p = Policy::default().layer("l").allow_read("*");
assert_eq!(p.check(Act::Read, "src/a.rs").effect, Effect::Allow);
assert_eq!(p.check(Act::Read, ".env").effect, Effect::Deny);
assert_eq!(p.check(Act::Read, "config/.env").effect, Effect::Deny);
assert_eq!(p.check(Act::Read, "keys/id_rsa").effect, Effect::Deny);
}
#[test]
fn serde_roundtrip_enforces_identically() {
let p = base();
let json = serde_json::to_string(&p).unwrap();
let back: Policy = serde_json::from_str(&json).unwrap();
for (act, path) in [
(Act::Write, "src/a.rs"),
(Act::Write, "secrets/key.txt"),
(Act::Read, "secrets/key.txt"),
(Act::Read, ".env"),
(Act::Exec, "rustc"),
] {
assert_eq!(
p.check(act, path).effect,
back.check(act, path).effect,
"{act:?} {path}"
);
}
}
#[test]
fn an_overlay_cannot_reallow_what_the_base_denied() {
let overlay = Policy::default().layer("app").allow_write("secrets/*");
let merged = base().merge(overlay);
assert_eq!(
merged.check(Act::Write, "secrets/key.txt").effect,
Effect::Deny
);
}
#[test]
fn an_overlay_only_allow_grants_capability_the_base_lacked() {
let overlay = Policy::default().layer("app").allow_write("docs/*");
let merged = base().merge(overlay);
assert_eq!(merged.check(Act::Write, "docs/x.md").effect, Effect::Allow);
assert_eq!(merged.check(Act::Write, "src/a.rs").effect, Effect::Allow);
}
#[test]
fn merging_is_deterministic_and_merging_a_layer_with_itself_changes_nothing() {
let a = base();
let probes = [
(Act::Write, "src/a.rs"),
(Act::Write, "secrets/k"),
(Act::Read, "docs/x"),
];
let once = a.clone().merge(base());
let twice = a.clone().merge(base()).merge(base());
for (act, path) in probes {
assert_eq!(a.check(act, path).effect, once.check(act, path).effect);
assert_eq!(once.check(act, path).effect, twice.check(act, path).effect);
}
}
#[test]
fn an_empty_overlay_leaves_the_base_unchanged() {
let merged = base().merge(Policy::default());
for (act, path) in [(Act::Write, "src/a.rs"), (Act::Write, "secrets/k")] {
assert_eq!(base().check(act, path).effect, merged.check(act, path).effect);
}
}
#[test]
fn explain_names_the_rule_and_the_layer_that_decided() {
let merged = base().merge(Policy::default().layer("app").allow_write("docs/*"));
let denied = merged.explain(Act::Write, "secrets/key.txt");
assert_eq!(denied.effect, Effect::Deny);
assert_eq!(denied.layer.as_deref(), Some("base"));
assert_eq!(denied.rule.as_deref(), Some("secrets/*"));
let allowed = merged.explain(Act::Write, "docs/x.md");
assert_eq!(allowed.effect, Effect::Allow);
assert_eq!(allowed.layer.as_deref(), Some("app"));
let fallback = merged.explain(Act::Write, "elsewhere/x");
assert_eq!(fallback.effect, Effect::Ask);
assert_eq!(fallback.layer, None);
}
#[test]
fn a_child_overlay_allow_cannot_reach_a_path_the_parent_denies() {
let parent = base(); let child = Policy::permissive()
.layer("child")
.allow_read("secrets/*")
.allow_write("secrets/*");
let contained = parent.contain(&child);
assert_eq!(contained.check(Act::Write, "secrets/key.txt").effect, Effect::Deny);
assert_eq!(contained.check(Act::Read, "secrets/key.txt").effect, Effect::Deny);
}
#[test]
fn a_child_overlay_allow_cannot_widen_a_parent_default() {
let parent = Policy::default().layer("parent").allow_write("src/*");
let child = Policy::permissive().layer("child").allow_write("docs/*");
assert_eq!(
parent.clone().merge(child.clone()).check(Act::Write, "docs/x.md").effect,
Effect::Allow,
"merge widens (0.4.0 behaviour)"
);
assert_eq!(
parent.contain(&child).check(Act::Write, "docs/x.md").effect,
Effect::Ask,
"contain does not widen"
);
}
#[test]
fn a_child_overlay_deny_narrows_the_parent() {
let parent = Policy::default().layer("parent").allow_write("src/*");
let child = Policy::permissive().layer("child").deny_write("src/generated/*");
let contained = parent.contain(&child);
assert_eq!(contained.check(Act::Write, "src/a.rs").effect, Effect::Allow);
assert_eq!(contained.check(Act::Write, "src/generated/x.rs").effect, Effect::Deny);
}
#[test]
fn containment_holds_downward_through_depth() {
let root = base(); let child = Policy::permissive().layer("child").deny_write("src/vendor/*");
let grandchild = Policy::permissive()
.layer("grandchild")
.allow_write("secrets/*") .allow_write("docs/*"); let effective = root.contain(&child).contain(&grandchild);
assert_eq!(effective.check(Act::Write, "secrets/key.txt").effect, Effect::Deny);
assert_eq!(effective.check(Act::Write, "docs/x.md").effect, Effect::Ask);
assert_eq!(effective.check(Act::Write, "src/vendor/x.rs").effect, Effect::Deny);
assert_eq!(effective.check(Act::Write, "src/a.rs").effect, Effect::Allow);
}
#[test]
fn explain_and_check_never_disagree() {
let p = base();
for (act, path) in [
(Act::Read, "src/a.rs"),
(Act::Write, "secrets/k"),
(Act::Write, "elsewhere/x"),
(Act::Read, ".env"),
(Act::Exec, "rustc"),
] {
assert_eq!(p.check(act, path).effect, p.explain(act, path).effect);
}
}
}