use crate::gateway::judge::glob_matches;
use crate::gateway::scope::applies;
use serde::Deserialize;
use serde_json::Value;
use std::collections::HashMap;
fn org_scope() -> String {
"org".to_string()
}
#[derive(Debug, Clone, Deserialize)]
pub struct TrustRule {
#[serde(default = "org_scope")]
pub scope: String,
pub intent: String,
#[serde(default, rename = "match")]
pub predicate: HashMap<String, String>,
pub level: String,
#[serde(default)]
pub after: Option<u32>,
}
pub fn evaluate(
imp: &str,
intent: &str,
payload: &Value,
default_level: &str,
rules: &[TrustRule],
executed: u32,
denied: u32,
) -> String {
for r in rules {
if applies(&r.scope, imp) && r.intent == intent && predicate_matches(&r.predicate, payload)
{
if r.level == "earned" {
let after = r.after.unwrap_or(5);
return if denied == 0 && executed >= after {
"auto".into()
} else {
"gate".into()
};
}
return r.level.clone();
}
}
default_level.to_string()
}
fn predicate_matches(preds: &HashMap<String, String>, payload: &Value) -> bool {
preds.iter().all(|(field, pat)| match payload.get(field) {
Some(Value::String(s)) => matches_value(pat, s),
Some(Value::Array(a)) => {
!a.is_empty()
&& a.iter()
.all(|e| e.as_str().map(|s| matches_value(pat, s)).unwrap_or(false))
}
_ => false,
})
}
fn matches_value(pat: &str, s: &str) -> bool {
glob_matches(pat, s.trim()) || glob_matches(pat, bare_address(s))
}
fn bare_address(s: &str) -> &str {
match (s.rfind('<'), s.rfind('>')) {
(Some(a), Some(b)) if b > a + 1 => s[a + 1..b].trim(),
_ => s.trim(),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn rules() -> Vec<TrustRule> {
vec![TrustRule {
scope: "org".into(),
intent: "email-send".into(),
predicate: [("to".to_string(), "*@ourco.com".to_string())].into(),
level: "auto".into(),
after: None,
}]
}
#[test]
fn internal_recipients_auto_external_gates() {
let rs = rules();
assert_eq!(
evaluate(
"org/yuko",
"email-send",
&json!({"to":["a@ourco.com","b@ourco.com"]}),
"gate",
&rs,
0,
0
),
"auto"
);
assert_eq!(
evaluate(
"org/yuko",
"email-send",
&json!({"to":["a@ourco.com","x@evil.com"]}),
"gate",
&rs,
0,
0
),
"gate"
);
}
#[test]
fn default_when_no_rule_matches() {
assert_eq!(
evaluate(
"org/yuko",
"discord-send",
&json!({}),
"gate",
&rules(),
0,
0
),
"gate"
);
assert_eq!(
evaluate("org/yuko", "message-user", &json!({}), "auto", &[], 0, 0),
"auto"
);
}
#[test]
fn matches_recipient_with_display_name() {
let rs = vec![TrustRule {
scope: "org".into(),
intent: "email-send".into(),
predicate: [("to".to_string(), "manasgarg@gmail.com".to_string())].into(),
level: "auto".into(),
after: None,
}];
assert_eq!(
evaluate(
"org/yuko",
"email-send",
&json!({"to":["manasgarg@gmail.com"]}),
"gate",
&rs,
0,
0
),
"auto"
);
assert_eq!(
evaluate(
"org/yuko",
"email-send",
&json!({"to":["Manas Garg <manasgarg@gmail.com>"]}),
"gate",
&rs,
0,
0
),
"auto"
);
assert_eq!(
evaluate(
"org/yuko",
"email-send",
&json!({"to":["Someone <other@gmail.com>"]}),
"gate",
&rs,
0,
0
),
"gate"
);
}
#[test]
fn scope_gates_out_of_scope_imps() {
let rs = vec![TrustRule {
scope: "org/w1".into(),
intent: "email-send".into(),
predicate: HashMap::new(),
level: "auto".into(),
after: None,
}];
assert_eq!(
evaluate("org/w1", "email-send", &json!({}), "gate", &rs, 0, 0),
"auto"
);
assert_eq!(
evaluate("org/w2", "email-send", &json!({}), "gate", &rs, 0, 0),
"gate"
);
}
#[test]
fn earned_promotes_after_threshold_and_resets_on_denial() {
let rs = vec![TrustRule {
scope: "org".into(),
intent: "email-send".into(),
predicate: HashMap::new(),
level: "earned".into(),
after: Some(3),
}];
let p = json!({"to":["x@out.com"]});
assert_eq!(
evaluate("org/yuko", "email-send", &p, "gate", &rs, 0, 0),
"gate"
); assert_eq!(
evaluate("org/yuko", "email-send", &p, "gate", &rs, 2, 0),
"gate"
); assert_eq!(
evaluate("org/yuko", "email-send", &p, "gate", &rs, 3, 0),
"auto"
); assert_eq!(
evaluate("org/yuko", "email-send", &p, "gate", &rs, 9, 1),
"gate"
); }
}