confium_signatif/
conditions.rs1use serde_json::Value;
18
19use crate::error::{SignatifError, SignatifResult};
20
21#[derive(Debug, Clone)]
24pub struct ConditionContext {
25 pub root: Value,
28}
29
30impl ConditionContext {
31 pub fn new(
37 payload: &Value,
38 artifact_id: &str,
39 signer_cert_ref: &str,
40 dimension: &str,
41 attested_at: &str,
42 ) -> Self {
43 Self {
44 root: serde_json::json!({
45 "payload": payload,
46 "artifact_id": artifact_id,
47 "signer_cert_ref": signer_cert_ref,
48 "dimension": dimension,
49 "attested_at": attested_at,
50 }),
51 }
52 }
53}
54
55pub fn evaluate_condition(expr: &Value, ctx: &ConditionContext) -> SignatifResult<bool> {
70 let obj = expr
71 .as_object()
72 .ok_or_else(|| SignatifError::Encoding("condition must be an object".into()))?;
73 let (op, args) = obj
74 .iter()
75 .next()
76 .ok_or_else(|| SignatifError::Encoding("condition object is empty".into()))?;
77 match op.as_str() {
78 "var" => Err(SignatifError::Encoding(
79 "var is only valid as an operand, not a top-level condition".into(),
80 )),
81 "!" => Ok(!evaluate_condition(args, ctx)?),
82 "!!" => evaluate_condition(args, ctx),
83 "and" => {
84 let list = args
85 .as_array()
86 .ok_or_else(|| SignatifError::Encoding("and expects an array".into()))?;
87 let mut ok = true;
88 for sub in list {
89 ok = ok && evaluate_condition(sub, ctx)?;
90 }
91 Ok(ok)
92 }
93 "or" => {
94 let list = args
95 .as_array()
96 .ok_or_else(|| SignatifError::Encoding("or expects an array".into()))?;
97 let mut ok = false;
98 for sub in list {
99 ok = ok || evaluate_condition(sub, ctx)?;
100 }
101 Ok(ok)
102 }
103 ">=" | ">" | "<=" | "<" | "==" | "!=" => {
104 let list = args
105 .as_array()
106 .ok_or_else(|| SignatifError::Encoding(format!("{op} expects an array")))?;
107 if list.len() != 2 {
108 return Err(SignatifError::Encoding(format!(
109 "{op} expects two operands"
110 )));
111 }
112 let a = resolve_operand(&list[0], ctx)?;
113 let b = resolve_operand(&list[1], ctx)?;
114 let cmp = compare(&a, &b);
115 Ok(match op.as_str() {
116 ">=" => cmp != std::cmp::Ordering::Less,
117 ">" => cmp == std::cmp::Ordering::Greater,
118 "<=" => cmp != std::cmp::Ordering::Greater,
119 "<" => cmp == std::cmp::Ordering::Less,
120 "==" => cmp == std::cmp::Ordering::Equal,
121 _ => cmp != std::cmp::Ordering::Equal,
122 })
123 }
124 other => Err(SignatifError::Encoding(format!(
125 "unsupported condition operator {other}"
126 ))),
127 }
128}
129
130pub fn evaluate_all(conditions: &[Value], ctx: &ConditionContext) -> SignatifResult<()> {
137 for (i, expr) in conditions.iter().enumerate() {
138 let ok = evaluate_condition(expr, ctx)?;
139 if !ok {
140 return Err(SignatifError::HardCheck(format!(
141 "scope_condition[{i}] not satisfied"
142 )));
143 }
144 }
145 Ok(())
146}
147
148fn resolve_operand(v: &Value, ctx: &ConditionContext) -> SignatifResult<Value> {
149 match v {
150 Value::Object(obj) => {
151 if let Some(Value::String(path)) = obj.get("var") {
152 let mut current = &ctx.root;
153 for segment in path.split('.') {
154 current = current.get(segment).ok_or_else(|| {
155 SignatifError::Encoding(format!("var path `{path}` not found"))
156 })?;
157 }
158 Ok(current.clone())
159 } else {
160 Ok(Value::Bool(evaluate_condition(v, ctx)?))
162 }
163 }
164 other => Ok(other.clone()),
165 }
166}
167
168fn compare(a: &Value, b: &Value) -> std::cmp::Ordering {
169 if let (Some(x), Some(y)) = (a.as_f64(), b.as_f64()) {
170 return x.partial_cmp(&y).unwrap_or(std::cmp::Ordering::Equal);
171 }
172 if let (Some(x), Some(y)) = (a.as_str(), b.as_str()) {
173 return x.cmp(y);
174 }
175 if a == b {
176 std::cmp::Ordering::Equal
177 } else {
178 std::cmp::Ordering::Greater
181 }
182}
183
184#[cfg(test)]
185mod tests {
186 use super::*;
187 use serde_json::json;
188
189 fn ctx() -> ConditionContext {
190 ConditionContext::new(
191 &json!({"quantity": 50000, "product": "vaccine-batch-A", "batch": {"id": "LOT-1"}}),
192 "art-1",
193 "end-1",
194 "data",
195 "2026-08-18T00:00:00Z",
196 )
197 }
198
199 #[test]
200 fn value_within_range() {
201 let c = evaluate_condition(&json!({">=": [{"var": "payload.quantity"}, 10000]}), &ctx())
202 .unwrap();
203 assert!(c);
204 let c = evaluate_condition(&json!({"<": [{"var": "payload.quantity"}, 10000]}), &ctx())
205 .unwrap();
206 assert!(!c);
207 }
208
209 #[test]
210 fn nested_paths_and_boolean_combinators() {
211 let c = evaluate_condition(
212 &json!({"and": [
213 {"==": [{"var": "payload.product"}, "vaccine-batch-A"]},
214 {"or": [
215 {">=": [{"var": "payload.quantity"}, 10000]},
216 {"==": [{"var": "payload.batch.id"}, "LOT-9"]}
217 ]},
218 ]}),
219 &ctx(),
220 )
221 .unwrap();
222 assert!(c);
223 }
224
225 #[test]
226 fn signer_and_dimension_visible() {
227 let c = evaluate_condition(
228 &json!({"==": [{"var": "signer_cert_ref"}, "end-1"]}),
229 &ctx(),
230 )
231 .unwrap();
232 assert!(c);
233 let c = evaluate_condition(&json!({"==": [{"var": "dimension"}, "data"]}), &ctx()).unwrap();
234 assert!(c);
235 }
236
237 #[test]
238 fn failing_condition_hard_fails() {
239 let expr = serde_json::json!({ ">=": [ {"var": "payload.quantity"}, 999999 ] });
240 let err = evaluate_all(&[expr], &ctx()).unwrap_err();
241 assert!(err.to_string().contains("scope_condition[0]"));
242 }
243
244 #[test]
245 fn malformed_expressions_fail_closed() {
246 assert!(evaluate_condition(&json!("nope"), &ctx()).is_err());
247 assert!(evaluate_condition(&json!({"+": [1, 1]}), &ctx()).is_err());
248 assert!(
249 evaluate_condition(&json!({">=": [{"var": "payload.missing"}, 1]}), &ctx()).is_err()
250 );
251 }
252
253 #[test]
254 fn deterministic_same_context_same_result() {
255 let expr = json!({">=": [{"var": "payload.quantity"}, 1]});
256 assert_eq!(
257 evaluate_condition(&expr, &ctx()).unwrap(),
258 evaluate_condition(&expr, &ctx()).unwrap()
259 );
260 }
261}