sentinel_proxy/agents/
decision.rs1use std::collections::HashMap;
4
5use sentinel_agent_protocol::{AgentResponse, AuditMetadata, BodyMutation, Decision, HeaderOp};
6
7#[derive(Debug, Clone)]
9pub struct AgentDecision {
10 pub action: AgentAction,
12 pub request_headers: Vec<HeaderOp>,
14 pub response_headers: Vec<HeaderOp>,
16 pub audit: Vec<AuditMetadata>,
18 pub routing_metadata: HashMap<String, String>,
20 pub needs_more: bool,
22 pub request_body_mutation: Option<BodyMutation>,
24 pub response_body_mutation: Option<BodyMutation>,
26}
27
28#[derive(Debug, Clone)]
30pub enum AgentAction {
31 Allow,
33 Block {
35 status: u16,
36 body: Option<String>,
37 headers: Option<HashMap<String, String>>,
38 },
39 Redirect { url: String, status: u16 },
41 Challenge {
43 challenge_type: String,
44 params: HashMap<String, String>,
45 },
46}
47
48impl AgentDecision {
49 pub fn default_allow() -> Self {
51 Self {
52 action: AgentAction::Allow,
53 request_headers: Vec::new(),
54 response_headers: Vec::new(),
55 audit: Vec::new(),
56 routing_metadata: HashMap::new(),
57 needs_more: false,
58 request_body_mutation: None,
59 response_body_mutation: None,
60 }
61 }
62
63 pub fn block(status: u16, message: &str) -> Self {
65 Self {
66 action: AgentAction::Block {
67 status,
68 body: Some(message.to_string()),
69 headers: None,
70 },
71 request_headers: Vec::new(),
72 response_headers: Vec::new(),
73 audit: Vec::new(),
74 routing_metadata: HashMap::new(),
75 needs_more: false,
76 request_body_mutation: None,
77 response_body_mutation: None,
78 }
79 }
80
81 pub fn is_allow(&self) -> bool {
83 matches!(self.action, AgentAction::Allow)
84 }
85
86 pub fn merge(&mut self, other: AgentDecision) {
91 if !other.is_allow() {
93 self.action = other.action;
94 }
95
96 self.request_headers.extend(other.request_headers);
98 self.response_headers.extend(other.response_headers);
99
100 self.audit.extend(other.audit);
102
103 self.routing_metadata.extend(other.routing_metadata);
105
106 if other.needs_more {
108 self.needs_more = true;
109 }
110
111 if other.request_body_mutation.is_some() {
113 self.request_body_mutation = other.request_body_mutation;
114 }
115 if other.response_body_mutation.is_some() {
116 self.response_body_mutation = other.response_body_mutation;
117 }
118 }
119}
120
121impl From<AgentResponse> for AgentDecision {
122 fn from(response: AgentResponse) -> Self {
123 let action = match response.decision {
124 Decision::Allow => AgentAction::Allow,
125 Decision::Block {
126 status,
127 body,
128 headers,
129 } => AgentAction::Block {
130 status,
131 body,
132 headers,
133 },
134 Decision::Redirect { url, status } => AgentAction::Redirect { url, status },
135 Decision::Challenge {
136 challenge_type,
137 params,
138 } => AgentAction::Challenge {
139 challenge_type,
140 params,
141 },
142 };
143
144 Self {
145 action,
146 request_headers: response.request_headers,
147 response_headers: response.response_headers,
148 audit: vec![response.audit],
149 routing_metadata: response.routing_metadata,
150 needs_more: response.needs_more,
151 request_body_mutation: response.request_body_mutation,
152 response_body_mutation: response.response_body_mutation,
153 }
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn test_agent_decision_merge() {
163 let mut decision1 = AgentDecision::default_allow();
164 decision1.request_headers.push(HeaderOp::Set {
165 name: "X-Test".to_string(),
166 value: "1".to_string(),
167 });
168
169 let decision2 = AgentDecision::block(403, "Forbidden");
170
171 decision1.merge(decision2);
172 assert!(!decision1.is_allow());
173 }
174}