guard_core_engine/payload.rs
1//! The data contracts the reference passes between the configuration
2//! surface, the block hook, and the response-side pass.
3//!
4//! - [`BlockPayload`] is the reference `on_block` payload, key for key
5//! (`build_block_payload`, which stays facade-side next to the
6//! redaction it runs the path through).
7//! - [`RequestBits`] / [`ResponseBits`] are the plain views the response
8//! processor reads and the adapter fill in; [`ResponseModifierFn`] is
9//! the reference `custom_response_modifier` seam over them.
10//!
11//! The types live in the engine so the unified `SecurityConfig` surface
12//! (the next slice of the fieldization train) can type every hook field
13//! without reaching into a framework layer; the facade re-exports them
14//! under their historical paths.
15
16use std::collections::BTreeMap;
17use std::sync::Arc;
18
19/// The check names the reference never fires the block hook for
20/// (`ON_BLOCK_EXCLUDED_CHECK_NAMES`): the two application-authored checks
21/// and the HTTPS redirect, which is not a block.
22pub const ON_BLOCK_EXCLUDED_CHECK_NAMES: [&str; 3] =
23 ["custom_request", "custom_validators", "https_enforcement"];
24
25/// The reference block payload, key for key
26/// (`build_block_payload`). `path` arrives pre-redacted;
27/// `status_code` is `None` on the passive-mode path, where no response is
28/// ever sent.
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct BlockPayload {
31 /// The emitting check (`rate_limit`, `ip_security`, ...).
32 pub check_name: String,
33 /// Why, the reference reason string.
34 pub reason: String,
35 /// The trigger description when the check carries one.
36 pub trigger_info: String,
37 /// Whether the pipeline runs in passive (log-only) mode.
38 pub passive_mode: bool,
39 /// The resolved client identity.
40 pub client_ip: String,
41 /// The redacted request path.
42 pub path: String,
43 /// The HTTP method.
44 pub method: String,
45 /// The block answer's status, `None` on the passive path.
46 pub status_code: Option<u16>,
47}
48
49/// The `on_block` callback type: sync, best-effort, never fatal
50/// (`SecurityConfig.on_block`).
51pub type OnBlockHook = Arc<dyn Fn(&BlockPayload) + Send + Sync>;
52
53/// The `on_error` callback type (`SecurityConfig.on_error`, the reference
54/// `Callable[[str, BaseException, dict], None]`).
55///
56/// Arguments: the pipeline stage name, the error message, and the metadata
57/// pairs. Sync, best-effort, never fatal - a panicking hook is the
58/// caller's `catch_unwind`, exactly like the block hook.
59pub type OnErrorFn = Arc<dyn Fn(&str, &str, &BTreeMap<String, String>) + Send + Sync>;
60
61/// The plain request view the response-side pass reads: method, path,
62/// resolved client identity, and the `Origin` header when the request
63/// carried one.
64#[derive(Debug, Clone, Default)]
65pub struct RequestBits {
66 /// The HTTP method.
67 pub method: String,
68 /// The request path (query included when present).
69 pub url_path: String,
70 /// The resolved client identity (empty when the connection carries
71 /// none, e.g. a unix-socket listener).
72 pub client_ip: String,
73 /// The `Origin` header value.
74 pub origin: Option<String>,
75}
76
77/// The mutable response view the response-side pass and the
78/// `custom_response_modifier` seam write.
79///
80/// The status and body pass through untouched, the headers gain the
81/// security-header set and the CORS verdict; `body` is `None` when the
82/// response carries none or it is not buffered.
83#[derive(Debug, Clone, Default)]
84pub struct ResponseBits {
85 /// The response status code.
86 pub status: u16,
87 /// The buffered body prefix when one is available.
88 pub body: Option<String>,
89 /// The headers the pass adds (security headers, CORS verdict).
90 pub headers: BTreeMap<String, String>,
91}
92
93/// The `custom_response_modifier` callback type
94/// (`SecurityConfig.custom_response_modifier`).
95///
96/// The reference `Callable[[GuardResponse], Awaitable[GuardResponse]]`:
97/// mutates the forwarded response's view in place before it leaves the
98/// pipeline.
99pub type ResponseModifierFn = Arc<dyn Fn(&mut ResponseBits) + Send + Sync>;
100
101#[cfg(test)]
102mod tests {
103 use super::*;
104
105 #[test]
106 fn payload_and_bits_are_constructible_and_default() {
107 let payload = BlockPayload {
108 check_name: String::from("rate_limit"),
109 reason: String::from("Rate limit exceeded"),
110 trigger_info: String::new(),
111 passive_mode: false,
112 client_ip: String::from("192.0.2.1"),
113 path: String::from("/login"),
114 method: String::from("GET"),
115 status_code: Some(429),
116 };
117 assert_eq!(payload.status_code, Some(429));
118
119 let bits = ResponseBits::default();
120 assert_eq!(bits.status, 0);
121 assert!(bits.body.is_none());
122 assert!(bits.headers.is_empty());
123
124 let bits = RequestBits::default();
125 assert!(bits.client_ip.is_empty());
126 assert!(bits.origin.is_none());
127 }
128
129 #[test]
130 fn hooks_receive_their_views() {
131 let seen = Arc::new(std::sync::Mutex::new(Vec::new()));
132 let sink = Arc::clone(&seen);
133 let hook: OnBlockHook = Arc::new(move |payload: &BlockPayload| {
134 sink.lock().expect("sink").push(payload.check_name.clone());
135 });
136 let payload = BlockPayload {
137 check_name: String::from("ip_security"),
138 reason: String::new(),
139 trigger_info: String::new(),
140 passive_mode: true,
141 client_ip: String::new(),
142 path: String::new(),
143 method: String::new(),
144 status_code: None,
145 };
146 hook(&payload);
147 assert_eq!(seen.lock().expect("sink").as_slice(), ["ip_security"]);
148
149 let errors = Arc::new(std::sync::Mutex::new(Vec::new()));
150 let sink = Arc::clone(&errors);
151 let on_error: OnErrorFn = Arc::new(move |stage: &str, message: &str, _| {
152 sink.lock()
153 .expect("sink")
154 .push((stage.to_owned(), message.to_owned()));
155 });
156 let mut metadata = BTreeMap::new();
157 metadata.insert(String::from("k"), String::from("v"));
158 on_error("cloud_fetch", "boom", &metadata);
159 assert_eq!(
160 errors.lock().expect("sink").as_slice(),
161 [("cloud_fetch".to_owned(), "boom".to_owned())]
162 );
163
164 let modifier: ResponseModifierFn = Arc::new(|bits: &mut ResponseBits| {
165 bits.headers
166 .insert(String::from("x-modified"), String::from("1"));
167 });
168 let mut bits = ResponseBits::default();
169 modifier(&mut bits);
170 assert_eq!(
171 bits.headers.get("x-modified").map(String::as_str),
172 Some("1")
173 );
174 }
175}