use std::sync::Arc;
use evorule_reactor::{CallerRoleResolver, IoCallContext};
use evorule_tcb::JsonValue;
use super::table::{PermissionTable, Verdict};
use crate::shared_facts_log::SharedFactsLog;
#[derive(Clone)]
pub struct PermissionGate {
shared_log: Arc<SharedFactsLog>,
resource_prefix: Arc<str>,
caller_role_resolver: Option<CallerRoleResolver>,
}
impl PermissionGate {
pub fn new(shared_log: Arc<SharedFactsLog>) -> Self {
Self {
shared_log,
resource_prefix: Arc::from("io:"),
caller_role_resolver: None,
}
}
pub fn with_caller_role_resolver(mut self, resolver: CallerRoleResolver) -> Self {
self.caller_role_resolver = Some(resolver);
self
}
pub fn with_resource_prefix(mut self, prefix: impl Into<Arc<str>>) -> Self {
self.resource_prefix = prefix.into();
self
}
pub fn check(
&self,
ctx: &mut IoCallContext,
io_type: &str,
payload: Option<&JsonValue>,
) -> Verdict {
if let Some(resolver) = &self.caller_role_resolver {
ctx.caller_role = resolver(ctx);
}
ctx.v_trigger = self.shared_log.version();
let Ok(table) = PermissionTable::snapshot_at(&self.shared_log, ctx.v_trigger) else {
return Verdict::Deny;
};
let resource = format!("{}{}", self.resource_prefix, io_type);
table.evaluate(ctx, &resource, "io", payload)
}
pub fn version(&self) -> u64 {
self.shared_log.version()
}
}
#[cfg(test)]
mod tests {
#![allow(clippy::unwrap_used, clippy::expect_used)]
use std::sync::Arc;
use evorule_reactor::{CallerRole, FactId};
use super::*;
fn empty_gate() -> PermissionGate {
PermissionGate::new(Arc::new(SharedFactsLog::new()))
}
#[test]
fn resolver_overrides_caller_role_and_changes_verdict() {
let mut ctx = IoCallContext::new(FactId(1), 0, None);
let gate = empty_gate().with_caller_role_resolver(Arc::new(|_| CallerRole::Human));
assert_eq!(gate.check(&mut ctx, "fake_call", None), Verdict::Allow);
}
#[test]
fn resolver_llm_is_fail_closed_on_default() {
let mut ctx = IoCallContext::new(FactId(1), 0, None);
let gate = empty_gate().with_caller_role_resolver(Arc::new(|_| CallerRole::Llm));
assert_eq!(gate.check(&mut ctx, "fake_call", None), Verdict::Deny);
}
#[test]
fn no_resolver_keeps_ctx_unknown_deny() {
let mut ctx = IoCallContext::new(FactId(1), 0, None);
let gate = empty_gate();
assert_eq!(gate.check(&mut ctx, "fake_call", None), Verdict::Deny);
}
#[test]
fn broken_deny_condition_is_fail_closed() {
use crate::permission::{
Effect, PermissionEntry, PermissionState, Resource, ResourceType, Subject,
};
let log = Arc::new(SharedFactsLog::new());
let mut entry = PermissionEntry::new(
"broken-deny",
Subject::any(),
Resource {
resource_type: ResourceType::IoAction,
path: "io:fake_call".to_string(),
},
Effect::Deny,
);
entry.state = PermissionState::Active;
entry.conditions = Some(serde_json::json!({ "type": "not_a_real_domain_op" }));
PermissionTable::store_entry(&log, &entry, 0).expect("store entry");
let gate = PermissionGate::new(log);
let mut ctx = IoCallContext::new(FactId(1), 0, None);
let verdict = gate.check(&mut ctx, "fake_call", None);
assert_eq!(
verdict,
Verdict::Deny,
"条件求值失败必须 fail-closed Deny,不得静默跳过 Deny 条目造成 fail-open"
);
}
}