use std::collections::BTreeMap;
use evorule_reactor::{CallerRole, IoCallContext};
use evorule_tcb::JsonValue;
use serde::{Deserialize, Serialize};
use super::condition::ConditionEvaluator;
use super::entry::{Effect, PermissionEntry, PermissionError, Subject};
use super::tcb_to_serde;
use crate::shared_facts_log::SharedFactsLog;
pub const ENTRY_PREFIX: &str = "shared.__permission__.entry.";
pub const DEFAULT_POLICY_PATH: &str = "shared.__permission__.default_policy";
const TOMBSTONE_KEY: &str = "__deleted";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case", default)]
pub struct DefaultPolicy {
pub human: Effect,
pub llm: Effect,
pub unknown: Effect,
}
impl Default for DefaultPolicy {
fn default() -> Self {
Self {
human: Effect::Allow,
llm: Effect::Deny,
unknown: Effect::Deny,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Verdict {
Allow,
Deny,
Candidate,
}
#[derive(Debug, Clone)]
pub struct PermissionTable {
entries: Vec<PermissionEntry>,
default_policy: DefaultPolicy,
version: u64,
}
impl PermissionTable {
pub fn snapshot_at(
log: &SharedFactsLog,
v_shared: u64,
) -> Result<PermissionTable, PermissionError> {
let mut latest: BTreeMap<String, (u64, JsonValue)> = BTreeMap::new();
for fact in log.facts_by_path_prefix(ENTRY_PREFIX) {
if fact.version > v_shared {
continue;
}
let id = fact.path.trim_start_matches(ENTRY_PREFIX).to_string();
match latest.get(&id) {
Some((prev_v, _)) if *prev_v >= fact.version => {}
_ => {
latest.insert(id, (fact.version, fact.value.clone()));
}
}
}
let mut entries = Vec::with_capacity(latest.len());
for (id, (fact_version, value)) in latest {
let serde_value = tcb_to_serde(&value);
if serde_value
.as_object()
.and_then(|o| o.get(TOMBSTONE_KEY))
.and_then(|v| v.as_bool())
== Some(true)
{
continue;
}
let mut entry: PermissionEntry = serde_json::from_value(serde_value)?;
entry.id = id;
entry.version = fact_version;
entries.push(entry);
}
let default_policy = load_default_policy(log, v_shared);
Ok(PermissionTable {
entries,
default_policy,
version: v_shared,
})
}
pub fn empty() -> Self {
Self {
entries: Vec::new(),
default_policy: DefaultPolicy::default(),
version: 0,
}
}
pub const fn version(&self) -> u64 {
self.version
}
pub fn entries(&self) -> &[PermissionEntry] {
&self.entries
}
pub fn get(&self, id: &str) -> Option<&PermissionEntry> {
self.entries.iter().find(|e| e.id == id)
}
pub fn evaluate(
&self,
ctx: &IoCallContext,
resource: &str,
action: &str,
payload: Option<&JsonValue>,
) -> Verdict {
let role_key = ctx.caller_role.as_str();
let empty_payload = JsonValue::Null;
let payload = payload.unwrap_or(&empty_payload);
let mut candidate = false;
let mut allowed = false;
for e in &self.entries {
if e.state.is_candidate() {
if subject_matches(&e.subject, role_key)
&& action_matches(&e.action, action)
&& resource_matches(&e.resource, resource)
{
candidate = true;
}
continue;
}
if !e.state.is_active() {
continue;
}
if !subject_matches(&e.subject, role_key)
|| !action_matches(&e.action, action)
|| !resource_matches(&e.resource, resource)
{
continue;
}
let cond_ok = match &e.conditions {
None => true,
Some(c) => {
let evaluator = ConditionEvaluator;
match evaluator.evaluate(c, ctx, payload) {
Ok(v) => v,
Err(err) => {
tracing::warn!(
"权限条件求值失败,按 Deny fail-closed 处理(entry={}, 条件={}): {}",
e.id, c, err
);
return Verdict::Deny;
}
}
}
};
if !cond_ok {
continue;
}
match e.effect {
Effect::Deny => return Verdict::Deny,
Effect::Allow => allowed = true,
}
}
if allowed {
return Verdict::Allow;
}
if candidate {
return Verdict::Candidate;
}
let effect = match ctx.caller_role {
CallerRole::Human => self.default_policy.human,
CallerRole::Llm => self.default_policy.llm,
CallerRole::Unknown => self.default_policy.unknown,
};
match effect {
Effect::Allow => Verdict::Allow,
Effect::Deny => Verdict::Deny,
}
}
pub fn store_entry(
log: &SharedFactsLog,
entry: &PermissionEntry,
session_id: u64,
) -> Result<u64, PermissionError> {
let value = super::serde_to_tcb(&serde_json::to_value(entry)?);
log.append(&path_for(ENTRY_PREFIX, &entry.id), value, session_id)
.map_err(|e| PermissionError::Store(e.to_string()))
}
pub fn remove(
log: &SharedFactsLog,
id: &str,
session_id: u64,
) -> Result<(), PermissionError> {
let mut tomb = serde_json::Map::new();
tomb.insert(TOMBSTONE_KEY.to_string(), serde_json::Value::Bool(true));
let value = super::serde_to_tcb(&serde_json::Value::Object(tomb));
log.append(&path_for(ENTRY_PREFIX, id), value, session_id)
.map_err(|e| PermissionError::Store(e.to_string()))?;
Ok(())
}
pub fn set_default_policy(
log: &SharedFactsLog,
policy: &DefaultPolicy,
session_id: u64,
) -> Result<u64, PermissionError> {
let value = super::serde_to_tcb(&serde_json::to_value(policy)?);
log.append(DEFAULT_POLICY_PATH, value, session_id)
.map_err(|e| PermissionError::Store(e.to_string()))
}
}
fn path_for(prefix: &str, id: &str) -> String {
format!("{prefix}{id}")
}
fn load_default_policy(log: &SharedFactsLog, v_shared: u64) -> DefaultPolicy {
let latest = log
.facts_by_path_prefix(DEFAULT_POLICY_PATH)
.into_iter()
.filter(|f| f.version <= v_shared && f.path == DEFAULT_POLICY_PATH)
.max_by_key(|f| f.version);
match latest {
Some(f) => match serde_json::from_value::<DefaultPolicy>(tcb_to_serde(&f.value)) {
Ok(p) => p,
Err(e) => {
tracing::warn!(
"默认策略反序列化失败(路径 {}),回退 DefaultPolicy::default(): {e}",
DEFAULT_POLICY_PATH
);
DefaultPolicy::default()
}
},
None => DefaultPolicy::default(),
}
}
fn subject_matches(subject: &Subject, role_key: &str) -> bool {
subject.subject_type == super::entry::SubjectType::Any || subject.id == role_key
}
fn action_matches(rule_action: &str, action: &str) -> bool {
rule_action == "*" || rule_action == action
}
fn resource_matches(resource: &super::entry::Resource, target: &str) -> bool {
let pattern = resource.path.as_str();
if pattern.is_empty() {
return false;
}
if let Some(prefix) = pattern.strip_suffix('*') {
target.starts_with(prefix)
} else {
target == pattern
}
}