1use 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}