use anyhow::{anyhow, Result};
use easy_error::{bail, Error as BundError};
use rust_multistackvm::multistackvm::VM;
use serde::{Deserialize, Serialize};
use std::collections::HashSet;
#[allow(dead_code)]
pub mod category {
pub const STORE_READ: &str = "store_read";
pub const STORE_WRITE: &str = "store_write";
pub const FS_READ: &str = "fs_read";
pub const FS_WRITE: &str = "fs_write";
pub const NET: &str = "net";
pub const SHELL: &str = "shell";
pub const CODE_EVAL: &str = "code_eval";
pub const KEYMAP: &str = "keymap";
}
pub const DEFAULT_DENIED_CATEGORIES: &[&str] = &[
category::FS_WRITE,
category::NET,
category::SHELL,
category::CODE_EVAL,
category::KEYMAP,
];
pub const WORD_CATEGORIES: &[(&str, &str)] = &[
("ink.node.list", category::STORE_READ),
("ink.node.get", category::STORE_READ),
("ink.node.children", category::STORE_READ),
("ink.paragraph.text", category::STORE_READ),
("ink.search.text", category::STORE_READ),
("ink.snapshot.list", category::STORE_READ),
("ink.key.bind", category::KEYMAP),
("ink.key.bind_lambda", category::KEYMAP),
("ink.key.unbind", category::KEYMAP),
("ink.key.list", category::KEYMAP),
];
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Policy {
#[serde(default)]
pub disabled_categories: Vec<String>,
#[serde(default)]
pub disabled_words: Vec<String>,
#[serde(default)]
pub enabled_words: Vec<String>,
#[serde(default)]
pub enabled_categories: Vec<String>,
#[serde(default)]
pub no_default_deny: bool,
#[serde(default)]
pub bootstrap: String,
}
impl Default for Policy {
fn default() -> Self {
Self {
disabled_categories: Vec::new(),
disabled_words: Vec::new(),
enabled_words: Vec::new(),
enabled_categories: Vec::new(),
no_default_deny: false,
bootstrap: String::new(),
}
}
}
impl Policy {
pub fn is_open(&self) -> bool {
self.disabled_categories.is_empty()
&& self.disabled_words.is_empty()
&& self.no_default_deny
}
fn effective_denied_categories(&self) -> HashSet<&str> {
let mut s: HashSet<&str> = HashSet::new();
if !self.no_default_deny {
for c in DEFAULT_DENIED_CATEGORIES {
s.insert(*c);
}
}
for c in &self.disabled_categories {
s.insert(c.as_str());
}
for c in &self.enabled_categories {
s.remove(c.as_str());
}
s
}
}
pub fn apply_policy(vm: &mut VM, policy: &Policy) -> Result<()> {
let denied_categories = policy.effective_denied_categories();
let enabled: HashSet<&str> = policy.enabled_words.iter().map(String::as_str).collect();
let denied_words: HashSet<&str> =
policy.disabled_words.iter().map(String::as_str).collect();
for (word, cat) in WORD_CATEGORIES {
if enabled.contains(*word) {
continue; }
let cat_denied = denied_categories.contains(*cat);
let word_denied = denied_words.contains(*word);
if cat_denied || word_denied {
tracing::warn!(
target: "inkhaven::scripting::policy",
"denying {} (category {})",
word,
cat
);
vm.register_inline(word.to_string(), denied_stub)
.map_err(|e| anyhow!("policy: re-register {word} as denied: {e}"))?;
}
}
Ok(())
}
fn denied_stub(_vm: &mut VM) -> std::result::Result<&mut VM, BundError> {
bail!(
"script denied by inkhaven policy — earlier log lines name the offending word"
);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_policy_is_conservative() {
let p = Policy::default();
let denied = p.effective_denied_categories();
assert!(denied.contains(category::FS_WRITE));
assert!(denied.contains(category::NET));
assert!(denied.contains(category::SHELL));
assert!(denied.contains(category::CODE_EVAL));
assert!(!denied.contains(category::STORE_READ));
assert!(!denied.contains(category::FS_READ));
}
#[test]
fn no_default_deny_clears_baseline() {
let p = Policy {
no_default_deny: true,
..Policy::default()
};
assert!(p.effective_denied_categories().is_empty());
}
#[test]
fn enabled_words_override_category_deny() {
let p = Policy {
disabled_categories: vec![category::STORE_READ.into()],
enabled_words: vec!["ink.node.list".into()],
..Policy::default()
};
let denied_cats = p.effective_denied_categories();
let enabled: HashSet<&str> = p.enabled_words.iter().map(String::as_str).collect();
for (word, cat) in WORD_CATEGORIES {
if *cat == category::STORE_READ {
let cat_denied = denied_cats.contains(*cat);
let effectively_denied = cat_denied && !enabled.contains(*word);
if *word == "ink.node.list" {
assert!(!effectively_denied, "ink.node.list should be allowed");
} else {
assert!(effectively_denied, "{word} should be denied");
}
}
}
}
}