actrpc-interceptor 0.1.0

Concrete interceptors for ActRPC.
Documentation
use crate::interceptors::policy::{
    config::{MatchExpr, PolicyApply, PolicyCondition, PolicyConfig, PolicyEffect, PolicyRule},
    error::PolicyError,
    fact::PolicyFacts,
    matcher::CompiledPolicyMatcher,
};
use actrpc_core::interception::InterceptionRequest;
use std::collections::HashSet;

#[derive(Debug, Clone)]
pub struct PolicyEngine {
    rules: Vec<CompiledPolicyRule>,
}

#[derive(Debug, Clone)]
struct CompiledPolicyRule {
    name: String,
    match_expr: CompiledMatchExpr,
    apply: PolicyApply,
}

#[derive(Debug, Clone)]
enum CompiledMatchExpr {
    All(Vec<CompiledMatchExpr>),
    Any(Vec<CompiledMatchExpr>),
    Condition(CompiledPolicyCondition),
}

#[derive(Debug, Clone)]
struct CompiledPolicyCondition {
    condition: PolicyCondition,
    matcher: CompiledPolicyMatcher,
}

#[derive(Debug, Clone, Default)]
pub struct PolicyDecision {
    pub matched_rules: Vec<MatchedPolicyRule>,
}

#[derive(Debug, Clone)]
pub struct MatchedPolicyRule {
    pub name: String,
    pub apply: PolicyApply,
}

impl PolicyEngine {
    pub fn compile(config: PolicyConfig) -> Result<Self, PolicyError> {
        validate_config(&config)?;

        let mut rules = Vec::with_capacity(config.rules.len());

        for rule in config.rules {
            rules.push(CompiledPolicyRule {
                name: rule.name.clone(),
                match_expr: compile_match_expr(&rule.name, rule.match_expr)?,
                apply: rule.apply,
            });
        }

        Ok(Self { rules })
    }

    pub fn evaluate(&self, request: &InterceptionRequest) -> PolicyDecision {
        let facts = PolicyFacts::new(request);
        let mut decision = PolicyDecision::default();

        for rule in &self.rules {
            if !rule.match_expr.matches(&facts) {
                continue;
            }

            decision.matched_rules.push(MatchedPolicyRule {
                name: rule.name.clone(),
                apply: rule.apply.clone(),
            });
        }

        decision
    }
}

impl CompiledMatchExpr {
    fn matches(&self, facts: &PolicyFacts<'_>) -> bool {
        match self {
            CompiledMatchExpr::All(expressions) => expressions
                .iter()
                .all(|expression| expression.matches(facts)),

            CompiledMatchExpr::Any(expressions) => expressions
                .iter()
                .any(|expression| expression.matches(facts)),

            CompiledMatchExpr::Condition(condition) => {
                let value = facts.get_string(&condition.condition.fact);
                condition.matcher.matches(value.as_deref())
            }
        }
    }
}

fn compile_match_expr(rule: &str, expr: MatchExpr) -> Result<CompiledMatchExpr, PolicyError> {
    match expr {
        MatchExpr::All { all } => Ok(CompiledMatchExpr::All(
            all.into_iter()
                .map(|expr| compile_match_expr(rule, expr))
                .collect::<Result<Vec<_>, _>>()?,
        )),

        MatchExpr::Any { any } => Ok(CompiledMatchExpr::Any(
            any.into_iter()
                .map(|expr| compile_match_expr(rule, expr))
                .collect::<Result<Vec<_>, _>>()?,
        )),

        MatchExpr::Condition { condition } => {
            let matcher = CompiledPolicyMatcher::compile(rule, &condition.matcher)?;

            Ok(CompiledMatchExpr::Condition(CompiledPolicyCondition {
                condition,
                matcher,
            }))
        }
    }
}

fn validate_config(config: &PolicyConfig) -> Result<(), PolicyError> {
    let mut names = HashSet::new();

    for rule in &config.rules {
        validate_rule(rule)?;

        if !names.insert(rule.name.clone()) {
            return Err(PolicyError::DuplicateRuleName {
                name: rule.name.clone(),
            });
        }
    }

    Ok(())
}

fn validate_rule(rule: &PolicyRule) -> Result<(), PolicyError> {
    if rule.name.trim().is_empty() {
        return Err(PolicyError::InvalidConfig {
            message: "policy rule name cannot be empty".to_owned(),
        });
    }

    validate_match_expr(&rule.name, &rule.match_expr)?;

    if rule.apply.immediate.is_empty() && rule.apply.review.is_none() {
        return Err(PolicyError::InvalidConfig {
            message: format!("policy rule {} has no effects", rule.name),
        });
    }

    for effect in &rule.apply.immediate {
        validate_effect(&rule.name, effect)?;
    }

    if let Some(review) = &rule.apply.review {
        for effect in &review.on_approve {
            validate_effect(&rule.name, effect)?;
        }

        for effect in &review.on_deny {
            validate_effect(&rule.name, effect)?;
        }
    }

    Ok(())
}

fn validate_match_expr(rule: &str, expr: &MatchExpr) -> Result<(), PolicyError> {
    match expr {
        MatchExpr::All { all } => {
            if all.is_empty() {
                return Err(PolicyError::InvalidConfig {
                    message: format!("policy rule {rule} contains empty all expression"),
                });
            }

            for expr in all {
                validate_match_expr(rule, expr)?;
            }
        }

        MatchExpr::Any { any } => {
            if any.is_empty() {
                return Err(PolicyError::InvalidConfig {
                    message: format!("policy rule {rule} contains empty any expression"),
                });
            }

            for expr in any {
                validate_match_expr(rule, expr)?;
            }
        }

        MatchExpr::Condition { condition } => {
            if condition.fact.as_str().trim().is_empty() {
                return Err(PolicyError::InvalidConfig {
                    message: format!("policy rule {rule} has empty fact path"),
                });
            }
        }
    }

    Ok(())
}

fn validate_effect(rule: &str, effect: &PolicyEffect) -> Result<(), PolicyError> {
    match effect {
        PolicyEffect::ExcludeInterceptors {
            exclude_interceptors,
        } => {
            if exclude_interceptors.names.is_empty() {
                return Err(PolicyError::InvalidConfig {
                    message: format!(
                        "policy rule {rule} has exclude_interceptors effect with empty names"
                    ),
                });
            }
        }

        PolicyEffect::RejectCall { .. } => {}
    }

    Ok(())
}