1use std::{collections::HashMap, sync::Arc};
30
31use cel::{
32 Context, Program, Value,
33 objects::{Key, Map},
34};
35
36use crate::validation::values::json_to_cel;
37
38#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
40pub struct GroupVersionKind {
41 pub group: String,
43 pub version: String,
45 pub kind: String,
47}
48
49#[derive(Clone, Debug, Default, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
51pub struct GroupVersionResource {
52 pub group: String,
54 pub version: String,
56 pub resource: String,
58}
59
60#[derive(Clone, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
67#[serde(rename_all = "camelCase")]
68pub struct AdmissionRequest {
69 pub operation: String,
71 pub username: String,
73 pub uid: String,
75 pub groups: Vec<String>,
77 pub name: String,
79 pub namespace: String,
81 pub dry_run: bool,
83 pub kind: GroupVersionKind,
85 pub resource: GroupVersionResource,
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
91#[serde(rename_all = "camelCase")]
92pub struct VapExpression {
93 pub expression: String,
95 pub message: Option<String>,
97 pub message_expression: Option<String>,
100}
101
102#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
107#[non_exhaustive]
108pub struct VapResult {
109 pub expression: String,
111 pub passed: bool,
113 pub message: Option<String>,
115}
116
117pub struct CompiledVapExpression {
122 program: Program,
123 expression: String,
124 message: Option<String>,
125 message_program: Option<Program>,
126}
127
128impl std::fmt::Debug for CompiledVapExpression {
129 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
130 f.debug_struct("CompiledVapExpression")
131 .field("expression", &self.expression)
132 .field("message", &self.message)
133 .field("has_message_program", &self.message_program.is_some())
134 .finish_non_exhaustive()
135 }
136}
137
138#[derive(Debug)]
148#[non_exhaustive]
149pub struct VapError {
150 pub expression: String,
152 source: Box<dyn std::error::Error + Send + Sync>,
153}
154
155impl std::fmt::Display for VapError {
156 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
157 write!(
158 f,
159 "failed to compile VAP expression \"{}\": {}",
160 self.expression, self.source
161 )
162 }
163}
164
165impl std::error::Error for VapError {
166 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
167 Some(&*self.source)
168 }
169}
170
171#[derive(Debug)]
179pub struct VapEvaluator {
180 object: serde_json::Value,
181 old_object: Option<serde_json::Value>,
182 request: AdmissionRequest,
183 params: Option<serde_json::Value>,
184 namespace_object: Option<serde_json::Value>,
185}
186
187#[derive(Debug, Default)]
189pub struct VapEvaluatorBuilder {
190 object: Option<serde_json::Value>,
191 old_object: Option<serde_json::Value>,
192 request: AdmissionRequest,
193 params: Option<serde_json::Value>,
194 namespace_object: Option<serde_json::Value>,
195}
196
197impl VapEvaluatorBuilder {
198 pub fn object(mut self, obj: serde_json::Value) -> Self {
200 self.object = Some(obj);
201 self
202 }
203
204 pub fn old_object(mut self, obj: serde_json::Value) -> Self {
207 self.old_object = Some(obj);
208 self
209 }
210
211 pub fn request(mut self, req: AdmissionRequest) -> Self {
213 self.request = req;
214 self
215 }
216
217 pub fn params(mut self, p: serde_json::Value) -> Self {
220 self.params = Some(p);
221 self
222 }
223
224 pub fn namespace_object(mut self, ns: serde_json::Value) -> Self {
227 self.namespace_object = Some(ns);
228 self
229 }
230
231 pub fn build(self) -> VapEvaluator {
233 VapEvaluator {
234 object: self.object.unwrap_or(serde_json::Value::Null),
235 old_object: self.old_object,
236 request: self.request,
237 params: self.params,
238 namespace_object: self.namespace_object,
239 }
240 }
241}
242
243impl VapEvaluator {
244 pub fn builder() -> VapEvaluatorBuilder {
246 VapEvaluatorBuilder::default()
247 }
248
249 fn build_context(&self) -> Context<'static> {
251 let mut ctx = Context::default();
252 crate::register_all(&mut ctx);
253
254 let _ = ctx.add_variable("object", json_to_cel(&self.object));
256
257 let old_object_val = match &self.old_object {
259 Some(v) => json_to_cel(v),
260 None => Value::Null,
261 };
262 let _ = ctx.add_variable("oldObject", old_object_val);
263
264 let _ = ctx.add_variable("request", request_to_cel(&self.request));
266
267 if let Some(params) = &self.params {
269 let _ = ctx.add_variable("params", json_to_cel(params));
270 }
271
272 if let Some(ns) = &self.namespace_object {
274 let _ = ctx.add_variable("namespaceObject", json_to_cel(ns));
275 }
276
277 ctx
278 }
279
280 #[must_use]
287 pub fn compile_expressions(
288 &self,
289 expressions: &[VapExpression],
290 ) -> Vec<Result<CompiledVapExpression, VapError>> {
291 expressions
292 .iter()
293 .map(|expr| {
294 let program = Program::compile(&expr.expression).map_err(|e| VapError {
295 expression: expr.expression.clone(),
296 source: Box::new(e),
297 })?;
298 let message_program = match expr.message_expression.as_deref() {
302 Some(me) => Some(Program::compile(me).map_err(|e| VapError {
303 expression: me.to_string(),
304 source: Box::new(e),
305 })?),
306 None => None,
307 };
308 Ok(CompiledVapExpression {
309 program,
310 expression: expr.expression.clone(),
311 message: expr.message.clone(),
312 message_program,
313 })
314 })
315 .collect()
316 }
317
318 #[must_use]
324 pub fn evaluate_compiled(&self, compiled: &[Result<CompiledVapExpression, VapError>]) -> Vec<VapResult> {
325 let ctx = self.build_context();
326
327 compiled
328 .iter()
329 .map(|c| match c {
330 Ok(ce) => match ce.program.execute(&ctx) {
331 Ok(Value::Bool(true)) => VapResult {
332 expression: ce.expression.clone(),
333 passed: true,
334 message: None,
335 },
336 Ok(Value::Bool(false)) => {
337 let msg = ce
338 .message_program
339 .as_ref()
340 .and_then(|prog| match prog.execute(&ctx) {
341 Ok(Value::String(s)) => Some((*s).clone()),
342 _ => None,
343 })
344 .or_else(|| ce.message.clone())
345 .unwrap_or_else(|| {
346 format!("validation expression '{}' evaluated to false", ce.expression)
347 });
348 VapResult {
349 expression: ce.expression.clone(),
350 passed: false,
351 message: Some(msg),
352 }
353 }
354 Ok(other) => VapResult {
355 expression: ce.expression.clone(),
356 passed: false,
357 message: Some(format!("expression returned non-boolean: {other:?}")),
358 },
359 Err(e) => VapResult {
360 expression: ce.expression.clone(),
361 passed: false,
362 message: Some(format!("evaluation error: {e}")),
363 },
364 },
365 Err(e) => VapResult {
366 expression: e.expression.clone(),
367 passed: false,
368 message: Some(e.to_string()),
369 },
370 })
371 .collect()
372 }
373
374 #[must_use]
380 pub fn evaluate(&self, expressions: &[VapExpression]) -> Vec<VapResult> {
381 let compiled = self.compile_expressions(expressions);
382 self.evaluate_compiled(&compiled)
383 }
384}
385
386fn request_to_cel(req: &AdmissionRequest) -> Value {
401 let mut map: HashMap<Key, Value> = HashMap::new();
402
403 map.insert(
404 Key::String(Arc::new("operation".into())),
405 Value::String(Arc::new(req.operation.clone())),
406 );
407 map.insert(
408 Key::String(Arc::new("name".into())),
409 Value::String(Arc::new(req.name.clone())),
410 );
411 map.insert(
412 Key::String(Arc::new("namespace".into())),
413 Value::String(Arc::new(req.namespace.clone())),
414 );
415 map.insert(Key::String(Arc::new("dryRun".into())), Value::Bool(req.dry_run));
416
417 let mut kind_map: HashMap<Key, Value> = HashMap::new();
419 kind_map.insert(
420 Key::String(Arc::new("group".into())),
421 Value::String(Arc::new(req.kind.group.clone())),
422 );
423 kind_map.insert(
424 Key::String(Arc::new("version".into())),
425 Value::String(Arc::new(req.kind.version.clone())),
426 );
427 kind_map.insert(
428 Key::String(Arc::new("kind".into())),
429 Value::String(Arc::new(req.kind.kind.clone())),
430 );
431 map.insert(
432 Key::String(Arc::new("kind".into())),
433 Value::Map(Map {
434 map: Arc::new(kind_map),
435 }),
436 );
437
438 let mut resource_map: HashMap<Key, Value> = HashMap::new();
440 resource_map.insert(
441 Key::String(Arc::new("group".into())),
442 Value::String(Arc::new(req.resource.group.clone())),
443 );
444 resource_map.insert(
445 Key::String(Arc::new("version".into())),
446 Value::String(Arc::new(req.resource.version.clone())),
447 );
448 resource_map.insert(
449 Key::String(Arc::new("resource".into())),
450 Value::String(Arc::new(req.resource.resource.clone())),
451 );
452 map.insert(
453 Key::String(Arc::new("resource".into())),
454 Value::Map(Map {
455 map: Arc::new(resource_map),
456 }),
457 );
458
459 let groups_list: Vec<Value> = req
461 .groups
462 .iter()
463 .map(|g| Value::String(Arc::new(g.clone())))
464 .collect();
465 let mut user_info_map: HashMap<Key, Value> = HashMap::new();
466 user_info_map.insert(
467 Key::String(Arc::new("username".into())),
468 Value::String(Arc::new(req.username.clone())),
469 );
470 user_info_map.insert(
471 Key::String(Arc::new("uid".into())),
472 Value::String(Arc::new(req.uid.clone())),
473 );
474 user_info_map.insert(
475 Key::String(Arc::new("groups".into())),
476 Value::List(Arc::new(groups_list)),
477 );
478 map.insert(
479 Key::String(Arc::new("userInfo".into())),
480 Value::Map(Map {
481 map: Arc::new(user_info_map),
482 }),
483 );
484
485 Value::Map(Map { map: Arc::new(map) })
486}
487
488#[cfg(test)]
489mod tests {
490 use super::*;
491 use serde_json::json;
492
493 #[test]
494 fn vap_basic_validation_passes() {
495 let evaluator = VapEvaluator::builder()
496 .object(json!({"metadata": {"name": "test"}, "spec": {"replicas": 3}}))
497 .request(AdmissionRequest {
498 operation: "CREATE".into(),
499 ..Default::default()
500 })
501 .build();
502 let results = evaluator.evaluate(&[VapExpression {
503 expression: "object.spec.replicas >= 0".into(),
504 message: Some("replicas must be non-negative".into()),
505 message_expression: None,
506 }]);
507 assert_eq!(results.len(), 1);
508 assert!(results[0].passed);
509 }
510
511 #[test]
512 fn compile_failure_yields_vap_error_with_cause() {
513 use std::error::Error;
514 let evaluator = VapEvaluator::builder().build();
515 let compiled = evaluator.compile_expressions(&[VapExpression {
516 expression: "this is ((( not valid".into(),
517 message: None,
518 message_expression: None,
519 }]);
520 let err = compiled[0].as_ref().unwrap_err();
521 assert_eq!(err.expression, "this is ((( not valid");
522 assert!(
523 err.source().is_some(),
524 "VapError should chain the cel parse error"
525 );
526
527 let results = evaluator.evaluate_compiled(&compiled);
529 assert!(!results[0].passed);
530 assert_eq!(results[0].expression, "this is ((( not valid");
531 }
532
533 #[test]
534 fn invalid_message_expression_fails_closed() {
535 use std::error::Error;
536 let evaluator = VapEvaluator::builder().object(json!({})).build();
540 let compiled = evaluator.compile_expressions(&[VapExpression {
541 expression: "true".into(),
542 message: Some("fallback".into()),
543 message_expression: Some("invalid >=".into()),
544 }]);
545 let err = compiled[0].as_ref().unwrap_err();
546 assert_eq!(err.expression, "invalid >=");
548 assert!(err.source().is_some(), "VapError should chain the cel cause");
549
550 let results = evaluator.evaluate_compiled(&compiled);
552 assert!(!results[0].passed);
553 }
554
555 #[test]
556 fn vap_validation_fails_with_message() {
557 let evaluator = VapEvaluator::builder()
558 .object(json!({"spec": {"replicas": -1}}))
559 .request(AdmissionRequest {
560 operation: "CREATE".into(),
561 ..Default::default()
562 })
563 .build();
564 let results = evaluator.evaluate(&[VapExpression {
565 expression: "object.spec.replicas >= 0".into(),
566 message: Some("replicas must be non-negative".into()),
567 message_expression: None,
568 }]);
569 assert!(!results[0].passed);
570 assert_eq!(
571 results[0].message.as_deref(),
572 Some("replicas must be non-negative")
573 );
574 }
575
576 #[test]
577 fn vap_request_variables_accessible() {
578 let evaluator = VapEvaluator::builder()
579 .object(json!({"spec": {}}))
580 .request(AdmissionRequest {
581 operation: "CREATE".into(),
582 username: "admin".into(),
583 namespace: "default".into(),
584 ..Default::default()
585 })
586 .build();
587 let results = evaluator.evaluate(&[VapExpression {
588 expression: "request.operation == 'CREATE' && request.userInfo.username == 'admin'".into(),
589 message: None,
590 message_expression: None,
591 }]);
592 assert!(results[0].passed);
593 }
594
595 #[test]
596 fn vap_old_object_null_on_create() {
597 let evaluator = VapEvaluator::builder()
598 .object(json!({"spec": {}}))
599 .request(AdmissionRequest {
600 operation: "CREATE".into(),
601 ..Default::default()
602 })
603 .build();
604 let results = evaluator.evaluate(&[VapExpression {
605 expression: "oldObject == null".into(),
606 message: None,
607 message_expression: None,
608 }]);
609 assert!(results[0].passed);
610 }
611
612 #[test]
613 fn vap_params_accessible() {
614 let evaluator = VapEvaluator::builder()
615 .object(json!({"spec": {"replicas": 5}}))
616 .params(json!({"maxReplicas": 10}))
617 .request(AdmissionRequest::default())
618 .build();
619 let results = evaluator.evaluate(&[VapExpression {
620 expression: "object.spec.replicas <= params.maxReplicas".into(),
621 message: None,
622 message_expression: None,
623 }]);
624 assert!(results[0].passed);
625 }
626
627 #[test]
628 fn vap_message_expression() {
629 let evaluator = VapEvaluator::builder()
630 .object(json!({"spec": {"replicas": -1}}))
631 .request(AdmissionRequest::default())
632 .build();
633 let results = evaluator.evaluate(&[VapExpression {
634 expression: "object.spec.replicas >= 0".into(),
635 message: Some("static fallback".into()),
636 message_expression: Some("'replicas is ' + string(object.spec.replicas)".into()),
637 }]);
638 assert!(!results[0].passed);
639 assert_eq!(results[0].message.as_deref(), Some("replicas is -1"));
640 }
641
642 #[test]
643 fn vap_compiled_expressions_reusable() {
644 let evaluator = VapEvaluator::builder()
645 .object(json!({"spec": {"replicas": 3}}))
646 .request(AdmissionRequest {
647 operation: "CREATE".into(),
648 ..Default::default()
649 })
650 .build();
651
652 let expressions = vec![VapExpression {
653 expression: "object.spec.replicas >= 0".into(),
654 message: Some("bad".into()),
655 message_expression: None,
656 }];
657
658 let compiled = evaluator.compile_expressions(&expressions);
659 assert!(compiled[0].is_ok());
660
661 let r1 = evaluator.evaluate_compiled(&compiled);
662 let r2 = evaluator.evaluate_compiled(&compiled);
663 assert!(r1[0].passed);
664 assert!(r2[0].passed);
665 }
666
667 #[test]
668 fn vap_compiled_error_preserved() {
669 let evaluator = VapEvaluator::builder()
670 .object(json!({}))
671 .request(AdmissionRequest::default())
672 .build();
673
674 let expressions = vec![VapExpression {
675 expression: "invalid >=".into(),
676 message: None,
677 message_expression: None,
678 }];
679
680 let compiled = evaluator.compile_expressions(&expressions);
681 assert!(compiled[0].is_err());
682
683 let results = evaluator.evaluate_compiled(&compiled);
684 assert!(!results[0].passed);
685 assert!(
686 results[0]
687 .message
688 .as_ref()
689 .unwrap()
690 .contains("failed to compile VAP expression")
691 );
692 }
693
694 #[test]
695 fn vap_multiple_expressions() {
696 let evaluator = VapEvaluator::builder()
697 .object(json!({"spec": {"replicas": -1, "name": ""}}))
698 .request(AdmissionRequest::default())
699 .build();
700 let results = evaluator.evaluate(&[
701 VapExpression {
702 expression: "object.spec.replicas >= 0".into(),
703 message: Some("bad replicas".into()),
704 message_expression: None,
705 },
706 VapExpression {
707 expression: "object.spec.name.size() > 0".into(),
708 message: Some("name required".into()),
709 message_expression: None,
710 },
711 ]);
712 assert_eq!(results.len(), 2);
713 assert!(!results[0].passed);
714 assert!(!results[1].passed);
715 }
716}