use aethershell::env::Env;
use aethershell::safety::{effect_of, Effect};
use aethershell::value::Value;
use std::collections::BTreeMap;
use std::sync::Mutex;
static ENV: Mutex<()> = Mutex::new(());
fn call(name: &str, args: Vec<Value>) -> Result<Value, String> {
let mut env = Env::new();
aethershell::builtins::call(name, args, &mut env).map_err(|e| e.to_string())
}
fn s(v: &str) -> Value {
Value::Str(v.to_string())
}
fn agent_mode() -> std::path::PathBuf {
let ws = std::env::temp_dir().join("ae_rbac_legacy");
std::fs::create_dir_all(&ws).unwrap();
std::env::set_var("AETHER_MODE", "agent");
std::env::set_var("AETHER_WORKSPACE", &ws);
std::env::remove_var("AETHER_POLICY");
aethershell::safety::set_principal(None);
ws
}
fn everything() -> Value {
let mut rec = BTreeMap::new();
rec.insert("resource".to_string(), s("*"));
rec.insert("actions".to_string(), Value::Array(vec![s("*")]));
Value::Array(vec![Value::Record(rec)])
}
#[test]
fn the_two_grant_verbs_are_classified_differently_and_that_is_load_bearing() {
assert_eq!(
effect_of("rbac_grant"),
Effect::Privileged,
"rbac_grant writes into the store `guard` consults; if this is ever \
relaxed, tests/privilege_escalation.rs is the one that matters"
);
assert_eq!(
effect_of("role_grant"),
Effect::Pure,
"role_grant is unclassified because it writes into a store the guard \
never reads. That is only defensible while the stores stay separate — \
see this file's header before bridging them"
);
}
#[test]
fn a_role_granted_the_old_way_does_not_open_the_gate() {
let _l = ENV.lock().unwrap_or_else(|e| e.into_inner());
let ws = agent_mode();
let victim = ws.join("legacy_rbac_target.txt");
std::fs::write(&victim, "contents").unwrap();
let path = victim.to_string_lossy().to_string();
let before =
call("rm", vec![s(&path)]).expect_err("rm must be refused in agent mode with no principal");
assert!(
before.contains("approval"),
"the baseline refusal must be the gate, not something else:\n{before}"
);
call("role_create", vec![s("legacy_god"), everything()]).expect("role_create is not gated");
call("role_grant", vec![s("legacy_agent"), s("legacy_god")]).expect("role_grant is not gated");
let after = call("rm", vec![s(&path)]);
assert!(
after.is_err(),
"a role granted through the legacy store conferred real authority — the \
two RBAC stores have been bridged. `role_create`/`role_grant`/\
`role_revoke` must be classified in `safety::classified_effect` before \
that is safe: `rbac_grant` is `Privileged` for this exact reason, and \
without the same treatment `role_grant` is now an ungated spelling of \
a denied operation."
);
assert!(
after.unwrap_err().contains("approval"),
"still refused, but no longer by the gate — check what changed"
);
let _ = std::fs::remove_file(&victim);
}
#[test]
fn the_legacy_store_still_answers_its_own_question() {
let _l = ENV.lock().unwrap_or_else(|e| e.into_inner());
agent_mode();
let mut rec = BTreeMap::new();
rec.insert("resource".to_string(), s("config.toml"));
rec.insert("actions".to_string(), Value::Array(vec![s("write")]));
let perms = Value::Array(vec![Value::Record(rec)]);
call("role_create", vec![s("editor"), perms]).expect("role_create");
call("role_grant", vec![s("alice"), s("editor")]).expect("role_grant");
let allowed = call(
"check_permission",
vec![s("alice"), s("config.toml"), s("write")],
)
.expect("check_permission answers");
assert_eq!(
allowed,
Value::Bool(true),
"the legacy store must still answer the question the README shows"
);
let denied = call(
"check_permission",
vec![s("alice"), s("config.toml"), s("delete")],
)
.expect("check_permission answers");
assert_eq!(denied, Value::Bool(false), "and must still say no");
}