1use std::sync::Arc;
28
29use crate::attributes::AttributeBag;
30use crate::evaluator::{evaluate_effects, evaluate_pipeline, Decision, FieldOutcome};
31use crate::pipeline::TaintEvent;
32use crate::rules::CompiledRoute;
33use crate::step::{DelegationInvoker, DispatchPhase, PdpResolver, PluginInvoker};
34
35#[derive(Debug, Clone)]
39pub struct RoutePayload {
40 pub args: serde_json::Value,
41 pub result: Option<serde_json::Value>,
42}
43
44impl RoutePayload {
45 pub fn new(args: serde_json::Value) -> Self {
46 Self { args, result: None }
47 }
48
49 pub fn with_result(args: serde_json::Value, result: serde_json::Value) -> Self {
50 Self {
51 args,
52 result: Some(result),
53 }
54 }
55}
56
57#[derive(Debug, Clone)]
59pub struct RouteDecision {
60 pub decision: Decision,
61 pub taints: Vec<TaintEvent>,
63 pub args_modified: bool,
65 pub result_modified: bool,
67}
68
69pub async fn evaluate_pre(
80 route: &CompiledRoute,
81 bag: &mut AttributeBag,
82 payload: &mut RoutePayload,
83 pdp: &Arc<dyn PdpResolver>,
84 plugins: &Arc<dyn PluginInvoker>,
85 delegations: &Arc<dyn DelegationInvoker>,
86) -> RouteDecision {
87 let mut taints: Vec<TaintEvent> = Vec::new();
88 let mut args_modified = false;
89
90 for rule in &route.args {
92 let Some(current) = get_dotted(&payload.args, &rule.field).cloned() else {
93 continue; };
95 let eval = evaluate_pipeline(
96 &rule.pipeline,
97 ¤t,
98 bag,
99 plugins,
100 &rule.field,
101 DispatchPhase::Pre,
102 )
103 .await;
104 taints.extend(eval.taints);
105 match eval.outcome {
106 FieldOutcome::Pass => {},
107 FieldOutcome::Replace(new_val) => {
108 if set_dotted(&mut payload.args, &rule.field, new_val) {
109 args_modified = true;
110 }
111 },
112 FieldOutcome::Omit => {
113 if remove_dotted(&mut payload.args, &rule.field) {
114 args_modified = true;
115 }
116 },
117 FieldOutcome::Deny {
118 reason,
119 stage_index: _,
120 } => {
121 return RouteDecision {
122 decision: Decision::Deny {
123 reason: Some(reason),
124 rule_source: rule.source.clone(),
125 },
126 taints,
127 args_modified,
128 result_modified: false,
129 };
130 },
131 }
132 }
133
134 let policy_eval = evaluate_effects(
136 &route.policy,
137 bag,
138 pdp,
139 plugins,
140 delegations,
141 DispatchPhase::Pre,
142 payload,
143 )
144 .await;
145 args_modified |= policy_eval.args_modified;
148 taints.extend(policy_eval.taints);
149 RouteDecision {
150 decision: policy_eval.decision,
151 taints,
152 args_modified,
153 result_modified: false,
154 }
155}
156
157pub async fn evaluate_post(
165 route: &CompiledRoute,
166 bag: &mut AttributeBag,
167 payload: &mut RoutePayload,
168 pdp: &Arc<dyn PdpResolver>,
169 plugins: &Arc<dyn PluginInvoker>,
170 delegations: &Arc<dyn DelegationInvoker>,
171) -> RouteDecision {
172 let mut taints: Vec<TaintEvent> = Vec::new();
173 let mut result_modified = false;
174
175 if let Some(result) = payload.result.as_mut() {
177 for rule in &route.result {
178 let Some(current) = get_dotted(result, &rule.field).cloned() else {
179 continue;
180 };
181 let eval = evaluate_pipeline(
182 &rule.pipeline,
183 ¤t,
184 bag,
185 plugins,
186 &rule.field,
187 DispatchPhase::Post,
188 )
189 .await;
190 taints.extend(eval.taints);
191 match eval.outcome {
192 FieldOutcome::Pass => {},
193 FieldOutcome::Replace(new_val) => {
194 if set_dotted(result, &rule.field, new_val) {
195 result_modified = true;
196 }
197 },
198 FieldOutcome::Omit => {
199 if remove_dotted(result, &rule.field) {
200 result_modified = true;
201 }
202 },
203 FieldOutcome::Deny {
204 reason,
205 stage_index: _,
206 } => {
207 return RouteDecision {
208 decision: Decision::Deny {
209 reason: Some(reason),
210 rule_source: rule.source.clone(),
211 },
212 taints,
213 args_modified: false,
214 result_modified,
215 };
216 },
217 }
218 }
219 }
220
221 let post_eval = evaluate_effects(
223 &route.post_policy,
224 bag,
225 pdp,
226 plugins,
227 delegations,
228 DispatchPhase::Post,
229 payload,
230 )
231 .await;
232 result_modified |= post_eval.result_modified;
235 taints.extend(post_eval.taints);
236
237 RouteDecision {
238 decision: post_eval.decision,
239 taints,
240 args_modified: false,
241 result_modified,
242 }
243}
244
245pub async fn evaluate_route(
256 route: &CompiledRoute,
257 bag: &mut AttributeBag,
258 payload: &mut RoutePayload,
259 pdp: &Arc<dyn PdpResolver>,
260 plugins: &Arc<dyn PluginInvoker>,
261 delegations: &Arc<dyn DelegationInvoker>,
262) -> RouteDecision {
263 let pre = evaluate_pre(route, bag, payload, pdp, plugins, delegations).await;
264 if matches!(pre.decision, Decision::Deny { .. }) {
265 return pre;
266 }
267 let post = evaluate_post(route, bag, payload, pdp, plugins, delegations).await;
268 let mut taints = pre.taints;
269 taints.extend(post.taints);
270 RouteDecision {
271 decision: post.decision,
272 taints,
273 args_modified: pre.args_modified,
274 result_modified: post.result_modified,
275 }
276}
277
278pub(crate) fn get_dotted<'a>(
285 root: &'a serde_json::Value,
286 path: &str,
287) -> Option<&'a serde_json::Value> {
288 let mut cur = root;
289 for seg in path.split('.') {
290 cur = cur.get(seg)?;
291 }
292 Some(cur)
293}
294
295pub(crate) fn set_dotted(
299 root: &mut serde_json::Value,
300 path: &str,
301 value: serde_json::Value,
302) -> bool {
303 let parts: Vec<&str> = path.split('.').collect();
304 let (leaf, parents) = match parts.split_last() {
305 Some(x) => x,
306 None => return false,
307 };
308 let mut cur = root;
309 for seg in parents {
310 let Some(next) = cur.get_mut(*seg) else {
311 return false;
312 };
313 if !next.is_object() {
314 return false;
315 }
316 cur = next;
317 }
318 if let serde_json::Value::Object(map) = cur {
319 map.insert((*leaf).to_string(), value);
320 true
321 } else {
322 false
323 }
324}
325
326pub(crate) fn remove_dotted(root: &mut serde_json::Value, path: &str) -> bool {
328 let parts: Vec<&str> = path.split('.').collect();
329 let (leaf, parents) = match parts.split_last() {
330 Some(x) => x,
331 None => return false,
332 };
333 let mut cur = root;
334 for seg in parents {
335 let Some(next) = cur.get_mut(*seg) else {
336 return false;
337 };
338 if !next.is_object() {
339 return false;
340 }
341 cur = next;
342 }
343 if let serde_json::Value::Object(map) = cur {
344 map.remove(*leaf).is_some()
345 } else {
346 false
347 }
348}
349
350#[cfg(test)]
351mod tests {
352 use super::*;
353 use crate::pipeline::{FieldRule, Pipeline, Stage, TaintScope, TypeCheck};
354 use crate::rules::{Effect, Expression, Rule};
355 use crate::step::{
356 NoopDelegationInvoker, PdpCall, PdpDecision, PdpDialect, PdpError, PluginError,
357 PluginInvocation, PluginOutcome,
358 };
359 use async_trait::async_trait;
360 use serde_json::json;
361
362 struct AllowPdp;
365 #[async_trait]
366 impl PdpResolver for AllowPdp {
367 fn dialect(&self) -> PdpDialect {
368 PdpDialect::Cedar
369 }
370 async fn evaluate(
371 &self,
372 _call: &PdpCall,
373 _bag: &AttributeBag,
374 ) -> Result<PdpDecision, PdpError> {
375 Ok(PdpDecision {
376 decision: Decision::Allow,
377 diagnostics: vec![],
378 })
379 }
380 }
381
382 struct NoPlugins;
383 #[async_trait]
384 impl PluginInvoker for NoPlugins {
385 async fn invoke(
386 &self,
387 name: &str,
388 _bag: &AttributeBag,
389 _invocation: PluginInvocation<'_>,
390 ) -> Result<PluginOutcome, PluginError> {
391 Err(PluginError::NotFound(name.into()))
392 }
393 }
394
395 fn pdp_arc() -> Arc<dyn PdpResolver> {
399 Arc::new(AllowPdp)
400 }
401 fn plugins() -> Arc<dyn PluginInvoker> {
402 Arc::new(NoPlugins)
403 }
404 fn delegations() -> Arc<dyn DelegationInvoker> {
405 Arc::new(NoopDelegationInvoker)
406 }
407
408 fn field_rule(field: &str, stages: Vec<Stage>) -> FieldRule {
409 FieldRule {
410 field: field.into(),
411 pipeline: Pipeline { stages },
412 source: format!("test.{}", field),
413 }
414 }
415
416 fn deny_rule(source: &str, reason: &str) -> Rule {
417 Rule::single(
418 Expression::Always,
419 Effect::Deny {
420 reason: Some(reason.into()),
421 code: None,
422 },
423 source,
424 )
425 }
426
427 #[tokio::test]
430 async fn empty_route_allows() {
431 let route = CompiledRoute::new("noop");
432 let mut bag = AttributeBag::new();
433 let mut payload = RoutePayload::new(json!({}));
434 let r = evaluate_route(
435 &route,
436 &mut bag,
437 &mut payload,
438 &pdp_arc(),
439 &plugins(),
440 &delegations(),
441 )
442 .await;
443 assert_eq!(r.decision, Decision::Allow);
444 assert!(!r.args_modified);
445 assert!(!r.result_modified);
446 assert!(r.taints.is_empty());
447 }
448
449 #[tokio::test]
450 async fn args_pipeline_mutates_payload() {
451 let mut route = CompiledRoute::new("ping");
452 route
453 .args
454 .push(field_rule("ssn", vec![Stage::Mask { keep_last: 4 }]));
455 let mut bag = AttributeBag::new();
456 let mut payload = RoutePayload::new(json!({ "ssn": "123-45-6789" }));
457 let r = evaluate_route(
458 &route,
459 &mut bag,
460 &mut payload,
461 &pdp_arc(),
462 &plugins(),
463 &delegations(),
464 )
465 .await;
466 assert_eq!(r.decision, Decision::Allow);
467 assert!(r.args_modified);
468 assert_eq!(payload.args["ssn"], json!("*******6789"));
469 }
470
471 #[tokio::test]
472 async fn args_deny_halts_route() {
473 let mut route = CompiledRoute::new("ping");
474 route.args.push(field_rule(
475 "amount",
476 vec![
477 Stage::Type(TypeCheck::Int),
478 Stage::Range {
479 min: Some(0),
480 max: Some(100),
481 },
482 ],
483 ));
484 route
488 .policy
489 .push(Effect::from(deny_rule("policy[0]", "policy denied too")));
490
491 let mut bag = AttributeBag::new();
492 let mut payload = RoutePayload::new(json!({ "amount": 200 }));
493 let r = evaluate_route(
494 &route,
495 &mut bag,
496 &mut payload,
497 &pdp_arc(),
498 &plugins(),
499 &delegations(),
500 )
501 .await;
502 match r.decision {
503 Decision::Deny { rule_source, .. } => {
504 assert!(
505 rule_source.contains("amount"),
506 "expected args rule source, got {}",
507 rule_source
508 );
509 },
510 d => panic!("expected Deny from args phase, got {:?}", d),
511 }
512 }
513
514 #[tokio::test]
515 async fn args_missing_field_is_skipped() {
516 let mut route = CompiledRoute::new("ping");
519 route.args.push(field_rule(
520 "compensation",
521 vec![Stage::Type(TypeCheck::Int)],
522 ));
523 let mut bag = AttributeBag::new();
524 let mut payload = RoutePayload::new(json!({ "other_field": 5 }));
525 let r = evaluate_route(
526 &route,
527 &mut bag,
528 &mut payload,
529 &pdp_arc(),
530 &plugins(),
531 &delegations(),
532 )
533 .await;
534 assert_eq!(r.decision, Decision::Allow);
535 assert!(!r.args_modified);
536 }
537
538 #[tokio::test]
539 async fn args_omit_drops_field() {
540 let mut route = CompiledRoute::new("ping");
541 route.args.push(field_rule("secret", vec![Stage::Omit]));
542 let mut bag = AttributeBag::new();
543 let mut payload = RoutePayload::new(json!({ "secret": "xyz", "keep": 1 }));
544 let r = evaluate_route(
545 &route,
546 &mut bag,
547 &mut payload,
548 &pdp_arc(),
549 &plugins(),
550 &delegations(),
551 )
552 .await;
553 assert_eq!(r.decision, Decision::Allow);
554 assert!(r.args_modified);
555 assert!(payload.args.get("secret").is_none());
556 assert_eq!(payload.args["keep"], json!(1));
557 }
558
559 #[tokio::test]
560 async fn policy_deny_halts_before_result() {
561 let mut route = CompiledRoute::new("ping");
562 route
563 .policy
564 .push(Effect::from(deny_rule("policy[0]", "blocked")));
565 route
567 .result
568 .push(field_rule("ssn", vec![Stage::Redact { condition: None }]));
569
570 let mut bag = AttributeBag::new();
571 let mut payload = RoutePayload::with_result(json!({}), json!({ "ssn": "123" }));
572 let r = evaluate_route(
573 &route,
574 &mut bag,
575 &mut payload,
576 &pdp_arc(),
577 &plugins(),
578 &delegations(),
579 )
580 .await;
581 match r.decision {
582 Decision::Deny { rule_source, .. } => assert_eq!(rule_source, "policy[0]"),
583 d => panic!("expected policy deny, got {:?}", d),
584 }
585 assert!(!r.result_modified);
586 assert_eq!(payload.result.as_ref().unwrap()["ssn"], json!("123"));
588 }
589
590 #[tokio::test]
591 async fn result_phase_skipped_when_no_response() {
592 let mut route = CompiledRoute::new("ping");
593 route
594 .result
595 .push(field_rule("ssn", vec![Stage::Redact { condition: None }]));
596 let mut bag = AttributeBag::new();
597 let mut payload = RoutePayload::new(json!({})); let r = evaluate_route(
599 &route,
600 &mut bag,
601 &mut payload,
602 &pdp_arc(),
603 &plugins(),
604 &delegations(),
605 )
606 .await;
607 assert_eq!(r.decision, Decision::Allow);
608 assert!(!r.result_modified);
609 }
610
611 #[tokio::test]
612 async fn result_pipeline_redacts_field() {
613 let mut route = CompiledRoute::new("ping");
614 route
615 .result
616 .push(field_rule("ssn", vec![Stage::Redact { condition: None }]));
617 let mut bag = AttributeBag::new();
618 let mut payload =
619 RoutePayload::with_result(json!({}), json!({ "ssn": "123-45-6789", "name": "alice" }));
620 let r = evaluate_route(
621 &route,
622 &mut bag,
623 &mut payload,
624 &pdp_arc(),
625 &plugins(),
626 &delegations(),
627 )
628 .await;
629 assert_eq!(r.decision, Decision::Allow);
630 assert!(r.result_modified);
631 let result = payload.result.as_ref().unwrap();
632 assert_eq!(result["ssn"], json!("[REDACTED]"));
633 assert_eq!(result["name"], json!("alice"));
634 }
635
636 #[tokio::test]
637 async fn taints_accumulate_across_phases() {
638 let mut route = CompiledRoute::new("ping");
639 route.args.push(field_rule(
641 "input",
642 vec![Stage::Taint {
643 label: "args_seen".into(),
644 scopes: vec![TaintScope::Session],
645 }],
646 ));
647 route.result.push(field_rule(
649 "output",
650 vec![Stage::Taint {
651 label: "result_seen".into(),
652 scopes: vec![TaintScope::Message],
653 }],
654 ));
655 let mut bag = AttributeBag::new();
656 let mut payload =
657 RoutePayload::with_result(json!({ "input": "hello" }), json!({ "output": "world" }));
658 let r = evaluate_route(
659 &route,
660 &mut bag,
661 &mut payload,
662 &pdp_arc(),
663 &plugins(),
664 &delegations(),
665 )
666 .await;
667 assert_eq!(r.decision, Decision::Allow);
668 let labels: Vec<&str> = r.taints.iter().map(|t| t.label.as_str()).collect();
669 assert_eq!(labels, vec!["args_seen", "result_seen"]);
670 }
671
672 #[tokio::test]
673 async fn nested_field_path_resolves_and_writes() {
674 let mut route = CompiledRoute::new("ping");
675 route.args.push(field_rule(
676 "user.profile.ssn",
677 vec![Stage::Mask { keep_last: 4 }],
678 ));
679 let mut bag = AttributeBag::new();
680 let mut payload = RoutePayload::new(json!({
681 "user": { "profile": { "ssn": "123-45-6789", "name": "alice" } }
682 }));
683 let r = evaluate_route(
684 &route,
685 &mut bag,
686 &mut payload,
687 &pdp_arc(),
688 &plugins(),
689 &delegations(),
690 )
691 .await;
692 assert_eq!(r.decision, Decision::Allow);
693 assert!(r.args_modified);
694 assert_eq!(payload.args["user"]["profile"]["ssn"], json!("*******6789"));
695 assert_eq!(payload.args["user"]["profile"]["name"], json!("alice"));
696 }
697
698 #[tokio::test]
699 async fn nested_field_missing_intermediate_is_skipped() {
700 let mut route = CompiledRoute::new("ping");
701 route.args.push(field_rule(
702 "user.profile.ssn",
703 vec![Stage::Mask { keep_last: 4 }],
704 ));
705 let mut bag = AttributeBag::new();
706 let mut payload = RoutePayload::new(json!({ "user": { "name": "alice" } }));
708 let r = evaluate_route(
709 &route,
710 &mut bag,
711 &mut payload,
712 &pdp_arc(),
713 &plugins(),
714 &delegations(),
715 )
716 .await;
717 assert_eq!(r.decision, Decision::Allow);
718 assert!(!r.args_modified);
719 }
720
721 #[tokio::test]
722 async fn post_policy_runs_after_result() {
723 let mut route = CompiledRoute::new("ping");
724 route
726 .result
727 .push(field_rule("ssn", vec![Stage::Redact { condition: None }]));
728 route
729 .post_policy
730 .push(Effect::from(deny_rule("post_policy[0]", "after-the-fact")));
731
732 let mut bag = AttributeBag::new();
733 let mut payload = RoutePayload::with_result(json!({}), json!({ "ssn": "123" }));
734 let r = evaluate_route(
735 &route,
736 &mut bag,
737 &mut payload,
738 &pdp_arc(),
739 &plugins(),
740 &delegations(),
741 )
742 .await;
743 match r.decision {
744 Decision::Deny { rule_source, .. } => assert_eq!(rule_source, "post_policy[0]"),
745 d => panic!("expected post_policy deny, got {:?}", d),
746 }
747 assert!(r.result_modified);
749 assert_eq!(payload.result.as_ref().unwrap()["ssn"], json!("[REDACTED]"));
750 }
751
752 #[test]
755 fn dotted_get_simple_and_nested() {
756 let v = json!({ "a": { "b": { "c": 7 } } });
757 assert_eq!(get_dotted(&v, "a.b.c"), Some(&json!(7)));
758 assert_eq!(get_dotted(&v, "a.b"), Some(&json!({ "c": 7 })));
759 assert!(get_dotted(&v, "a.b.x").is_none());
760 assert!(get_dotted(&v, "missing").is_none());
761 }
762
763 #[test]
764 fn dotted_set_overwrites_leaf() {
765 let mut v = json!({ "a": { "b": 1 } });
766 assert!(set_dotted(&mut v, "a.b", json!(99)));
767 assert_eq!(v["a"]["b"], json!(99));
768 }
769
770 #[test]
771 fn dotted_set_does_not_create_missing_parents() {
772 let mut v = json!({});
774 assert!(!set_dotted(&mut v, "a.b", json!(1)));
775 assert_eq!(v, json!({}));
776 }
777
778 #[test]
779 fn dotted_remove_leaf() {
780 let mut v = json!({ "a": { "b": 1, "c": 2 } });
781 assert!(remove_dotted(&mut v, "a.b"));
782 assert_eq!(v, json!({ "a": { "c": 2 } }));
783 assert!(!remove_dotted(&mut v, "a.b"));
785 }
786
787 #[tokio::test]
790 async fn evaluate_pre_runs_args_and_policy_only() {
791 let mut route = CompiledRoute::new("test");
795 route
796 .args
797 .push(field_rule("id", vec![Stage::Mask { keep_last: 2 }]));
798 route
799 .result
800 .push(field_rule("ssn", vec![Stage::Redact { condition: None }]));
801
802 let mut payload =
803 RoutePayload::with_result(json!({ "id": "ABCDEFGH" }), json!({ "ssn": "555-12-3456" }));
804 let mut bag = AttributeBag::new();
805 let r = evaluate_pre(
806 &route,
807 &mut bag,
808 &mut payload,
809 &pdp_arc(),
810 &plugins(),
811 &delegations(),
812 )
813 .await;
814 assert_eq!(r.decision, Decision::Allow);
815 assert!(
816 r.args_modified,
817 "args mask stage should have rewritten the field"
818 );
819 assert!(!r.result_modified, "evaluate_pre must not touch result");
820 assert_eq!(payload.args["id"], json!("******GH"));
822 assert_eq!(
824 payload.result.as_ref().unwrap()["ssn"],
825 json!("555-12-3456")
826 );
827 }
828
829 #[tokio::test]
830 async fn evaluate_post_runs_result_and_post_policy_only() {
831 let mut route = CompiledRoute::new("test");
834 route
835 .args
836 .push(field_rule("id", vec![Stage::Mask { keep_last: 2 }]));
837 route
838 .result
839 .push(field_rule("ssn", vec![Stage::Redact { condition: None }]));
840
841 let mut payload =
842 RoutePayload::with_result(json!({ "id": "ABCDEFGH" }), json!({ "ssn": "555-12-3456" }));
843 let mut bag = AttributeBag::new();
844 let r = evaluate_post(
845 &route,
846 &mut bag,
847 &mut payload,
848 &pdp_arc(),
849 &plugins(),
850 &delegations(),
851 )
852 .await;
853 assert_eq!(r.decision, Decision::Allow);
854 assert!(!r.args_modified, "evaluate_post must not touch args");
855 assert!(r.result_modified, "result redact should have fired");
856 assert_eq!(payload.args["id"], json!("ABCDEFGH"));
858 assert_eq!(payload.result.as_ref().unwrap()["ssn"], json!("[REDACTED]"));
860 }
861
862 #[tokio::test]
863 async fn evaluate_pre_deny_halts_before_policy() {
864 let mut route = CompiledRoute::new("test");
866 route
867 .args
868 .push(field_rule("id", vec![Stage::Type(TypeCheck::Uuid)]));
869 route.policy.push(Effect::from(Rule::single(
871 Expression::Always,
872 Effect::Deny {
873 reason: Some("policy_should_not_run".into()),
874 code: None,
875 },
876 "test.policy[0]",
877 )));
878
879 let mut payload = RoutePayload::new(json!({ "id": "not-a-uuid" }));
880 let mut bag = AttributeBag::new();
881 let r = evaluate_pre(
882 &route,
883 &mut bag,
884 &mut payload,
885 &pdp_arc(),
886 &plugins(),
887 &delegations(),
888 )
889 .await;
890 match r.decision {
891 Decision::Deny { rule_source, .. } => {
892 assert!(
893 rule_source.contains("test.id"),
894 "args denial got source {}",
895 rule_source
896 );
897 },
898 d => panic!("expected args-side Deny, got {:?}", d),
899 }
900 }
901
902 #[tokio::test]
903 async fn evaluate_route_skips_post_on_pre_deny() {
904 let mut route = CompiledRoute::new("test");
907 route.policy.push(Effect::from(Rule::single(
908 Expression::Always,
909 Effect::Deny {
910 reason: Some("policy_deny".into()),
911 code: None,
912 },
913 "test.policy[0]",
914 )));
915 route
916 .result
917 .push(field_rule("ssn", vec![Stage::Redact { condition: None }]));
918 route.post_policy.push(Effect::Taint {
919 label: "should_not_emit".into(),
920 scopes: vec![TaintScope::Session],
921 });
922
923 let mut payload = RoutePayload::with_result(json!({}), json!({ "ssn": "555-12-3456" }));
924 let mut bag = AttributeBag::new();
925 let r = evaluate_route(
926 &route,
927 &mut bag,
928 &mut payload,
929 &pdp_arc(),
930 &plugins(),
931 &delegations(),
932 )
933 .await;
934 assert!(matches!(r.decision, Decision::Deny { .. }));
935 assert!(!r.result_modified, "post must be skipped on pre-side Deny");
936 assert!(r.taints.is_empty());
938 assert_eq!(
940 payload.result.as_ref().unwrap()["ssn"],
941 json!("555-12-3456")
942 );
943 }
944}