Skip to main content

cratestack_policy/
eval.rs

1//! Procedure-policy evaluation entrypoints and helpers.
2
3use cratestack_core::{CoolContext, CoolError, Value};
4
5use crate::procedure_types::{
6    ProcedureArgs, ProcedurePolicy, ProcedurePolicyExpr, ProcedurePolicyLiteral, ProcedurePredicate,
7};
8
9pub fn authorize_procedure<A: ProcedureArgs + ?Sized>(
10    allow_policies: &[ProcedurePolicy],
11    deny_policies: &[ProcedurePolicy],
12    args: &A,
13    ctx: &CoolContext,
14) -> Result<(), CoolError> {
15    if allow_policies.is_empty() {
16        return Err(CoolError::Forbidden(
17            "procedure policy denied this operation".to_owned(),
18        ));
19    }
20
21    if deny_policies
22        .iter()
23        .any(|policy| procedure_policy_expr_matches(policy.expr, args, ctx))
24    {
25        return Err(CoolError::Forbidden(
26            "procedure policy denied this operation".to_owned(),
27        ));
28    }
29
30    if allow_policies
31        .iter()
32        .any(|policy| procedure_policy_expr_matches(policy.expr, args, ctx))
33    {
34        Ok(())
35    } else {
36        Err(CoolError::Forbidden(
37            "procedure policy denied this operation".to_owned(),
38        ))
39    }
40}
41
42pub fn context_has_role(ctx: &CoolContext, role: &str) -> bool {
43    ctx.auth_field("role")
44        .or_else(|| ctx.auth_field("actor.role"))
45        .is_some_and(|value| matches!(value, Value::String(candidate) if candidate == role))
46}
47
48pub fn context_in_tenant(ctx: &CoolContext, tenant_id: &str) -> bool {
49    ctx.auth_field("tenant.id")
50        .is_some_and(|value| matches!(value, Value::String(candidate) if candidate == tenant_id))
51}
52
53fn procedure_policy_expr_matches<A: ProcedureArgs + ?Sized>(
54    expr: ProcedurePolicyExpr,
55    args: &A,
56    ctx: &CoolContext,
57) -> bool {
58    match expr {
59        ProcedurePolicyExpr::Predicate(predicate) => {
60            procedure_predicate_matches(predicate, args, ctx)
61        }
62        ProcedurePolicyExpr::And(exprs) => exprs
63            .iter()
64            .copied()
65            .all(|expr| procedure_policy_expr_matches(expr, args, ctx)),
66        ProcedurePolicyExpr::Or(exprs) => exprs
67            .iter()
68            .copied()
69            .any(|expr| procedure_policy_expr_matches(expr, args, ctx)),
70    }
71}
72
73fn procedure_predicate_matches<A: ProcedureArgs + ?Sized>(
74    predicate: ProcedurePredicate,
75    args: &A,
76    ctx: &CoolContext,
77) -> bool {
78    match predicate {
79        ProcedurePredicate::AuthNotNull => ctx.is_authenticated(),
80        ProcedurePredicate::AuthIsNull => !ctx.is_authenticated(),
81        ProcedurePredicate::HasRole { role } => context_has_role(ctx, role),
82        ProcedurePredicate::InTenant { tenant_id } => context_in_tenant(ctx, tenant_id),
83        ProcedurePredicate::AuthFieldEqLiteral { auth_field, value } => ctx
84            .auth_field(auth_field)
85            .is_some_and(|candidate| value_matches_literal(candidate, value)),
86        ProcedurePredicate::AuthFieldNeLiteral { auth_field, value } => ctx
87            .auth_field(auth_field)
88            .is_some_and(|candidate| !value_matches_literal(candidate, value)),
89        ProcedurePredicate::InputFieldIsTrue { field } => args
90            .procedure_arg_value(field)
91            .is_some_and(|value| value == Value::Bool(true)),
92        ProcedurePredicate::InputFieldEqLiteral { field, value } => args
93            .procedure_arg_value(field)
94            .is_some_and(|candidate| value_matches_literal(&candidate, value)),
95        ProcedurePredicate::InputFieldNeLiteral { field, value } => args
96            .procedure_arg_value(field)
97            .is_some_and(|candidate| !value_matches_literal(&candidate, value)),
98        ProcedurePredicate::InputFieldEqAuth { field, auth_field } => {
99            match (args.procedure_arg_value(field), ctx.auth_field(auth_field)) {
100                (Some(left), Some(right)) => &left == right,
101                _ => false,
102            }
103        }
104        ProcedurePredicate::InputFieldNeAuth { field, auth_field } => {
105            match (args.procedure_arg_value(field), ctx.auth_field(auth_field)) {
106                (Some(left), Some(right)) => &left != right,
107                _ => false,
108            }
109        }
110        ProcedurePredicate::InputFieldEqInput { field, other_field } => {
111            match (
112                args.procedure_arg_value(field),
113                args.procedure_arg_value(other_field),
114            ) {
115                (Some(left), Some(right)) => left == right,
116                _ => false,
117            }
118        }
119        ProcedurePredicate::InputFieldNeInput { field, other_field } => {
120            match (
121                args.procedure_arg_value(field),
122                args.procedure_arg_value(other_field),
123            ) {
124                (Some(left), Some(right)) => left != right,
125                _ => false,
126            }
127        }
128    }
129}
130
131fn value_matches_literal(value: &Value, literal: ProcedurePolicyLiteral) -> bool {
132    match (value, literal) {
133        (Value::Bool(left), ProcedurePolicyLiteral::Bool(right)) => *left == right,
134        (Value::Int(left), ProcedurePolicyLiteral::Int(right)) => *left == right,
135        (Value::String(left), ProcedurePolicyLiteral::String(right)) => left == right,
136        _ => false,
137    }
138}