1use crate::{
4 ContextIn, DecideRequest, PolicyEngine, PrincipalIn, ResourceIn,
5 decision::{PolicyDecision, PolicyOutcome},
6 engine::EngineError,
7};
8use converge_core::{
9 FlowGateAuthorizer, FlowGateDecision, FlowGateError, FlowGateInput, FlowGateOutcome,
10};
11
12pub use converge_core::{FlowGateContext, FlowGatePrincipal, FlowGateResource};
13
14fn to_decide_request(input: &FlowGateInput) -> DecideRequest {
15 DecideRequest {
16 principal: PrincipalIn {
17 id: input.principal.id.clone(),
18 authority: input.principal.authority,
19 domains: input.principal.domains.clone(),
20 policy_version: input.principal.policy_version.clone(),
21 },
22 resource: ResourceIn {
23 id: input.resource.id.clone(),
24 resource_type: Some(input.resource.kind.clone()),
25 phase: Some(input.resource.phase),
26 gates_passed: Some(input.resource.gates_passed.clone()),
27 },
28 action: input.action,
29 context: Some(ContextIn {
30 commitment_type: input
31 .context
32 .commitment_type
33 .clone()
34 .or_else(|| Some(input.resource.kind.clone().into())),
35 amount: input.context.amount,
36 human_approval_present: input.context.human_approval_present,
37 required_gates_met: input.context.required_gates_met,
38 }),
39 delegation_b64: None,
40 }
41}
42
43impl PolicyEngine {
44 pub fn evaluate_flow(&self, input: &FlowGateInput) -> Result<PolicyDecision, EngineError> {
48 self.evaluate(&to_decide_request(input))
49 }
50}
51
52impl FlowGateAuthorizer for PolicyEngine {
53 fn decide(&self, input: &FlowGateInput) -> Result<FlowGateDecision, FlowGateError> {
54 let decision = self
55 .evaluate_flow(input)
56 .map_err(|err| FlowGateError::Authorizer(err.to_string()))?;
57
58 let outcome = match decision.outcome {
59 PolicyOutcome::Promote => FlowGateOutcome::Promote,
60 PolicyOutcome::Reject => FlowGateOutcome::Reject,
61 PolicyOutcome::Escalate => FlowGateOutcome::Escalate,
62 };
63
64 Ok(FlowGateDecision {
65 outcome,
66 reason: decision.reason,
67 source: Some("cedar".into()),
68 })
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75 use converge_core::FlowAction;
76
77 #[test]
78 fn flow_gate_input_projects_to_decide_request() {
79 let input = FlowGateInput {
80 principal: converge_core::FlowGatePrincipal {
81 id: "agent:finance".into(),
82 authority: converge_core::AuthorityLevel::Supervisory,
83 domains: vec!["finance".into()],
84 policy_version: Some("expense_v1".into()),
85 },
86 resource: converge_core::FlowGateResource {
87 id: "expense:001".into(),
88 kind: "expense".into(),
89 phase: converge_core::FlowPhase::Commitment,
90 gates_passed: vec!["receipt".into()],
91 },
92 action: FlowAction::Validate,
93 context: converge_core::FlowGateContext {
94 commitment_type: None,
95 amount: Some(1_250),
96 human_approval_present: Some(false),
97 required_gates_met: Some(true),
98 },
99 };
100
101 let request = to_decide_request(&input);
102 assert_eq!(request.action, FlowAction::Validate);
103 assert_eq!(request.principal.domains, vec!["finance"]);
104 assert_eq!(request.resource.gates_passed, Some(vec!["receipt".into()]));
105 assert_eq!(
106 request
107 .context
108 .as_ref()
109 .and_then(|ctx| ctx.commitment_type.as_deref()),
110 Some("expense")
111 );
112 }
113
114 #[test]
115 fn policy_engine_implements_neutral_authorizer_contract() {
116 let engine = PolicyEngine::from_policy_str(crate::EXPENSE_APPROVAL_POLICY)
117 .expect("policy should parse");
118
119 let input = FlowGateInput {
120 principal: converge_core::FlowGatePrincipal {
121 id: "agent:finance".into(),
122 authority: converge_core::AuthorityLevel::Supervisory,
123 domains: vec!["finance".into()],
124 policy_version: Some("expense_v1".into()),
125 },
126 resource: converge_core::FlowGateResource {
127 id: "expense:001".into(),
128 kind: "expense".into(),
129 phase: converge_core::FlowPhase::Commitment,
130 gates_passed: vec!["receipt".into()],
131 },
132 action: FlowAction::Validate,
133 context: converge_core::FlowGateContext {
134 commitment_type: Some("expense".into()),
135 amount: Some(1_250),
136 human_approval_present: Some(false),
137 required_gates_met: Some(true),
138 },
139 };
140
141 let decision = engine.decide(&input).expect("authorizer should succeed");
142 assert_eq!(decision.outcome, FlowGateOutcome::Promote);
143 assert_eq!(decision.source.as_deref(), Some("cedar"));
144 }
145}