Skip to main content

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}