bulwark_wasm_sdk_macros/lib.rs
1use proc_macro::TokenStream;
2use proc_macro2::Span;
3use quote::{quote, quote_spanned};
4use syn::{
5 parse_macro_input, parse_quote, punctuated::Punctuated, spanned::Spanned, Attribute, Ident,
6 ItemFn, ItemImpl, LitBool, LitStr, ReturnType, Signature, Visibility,
7};
8extern crate proc_macro;
9
10/// The `bulwark_plugin` attribute generates default implementations for all handler traits in a module
11/// and produces friendly errors for common mistakes.
12///
13/// All trait functions for `Handlers` are optional when used in conjunction with this macro. A no-op
14/// implementation will be automatically generated if a handler function has not been defined. Handler
15/// functions are called in sequence, in the order below. All `*_decision` handlers render an updated
16/// decision. In the case of a `restricted` outcome, no further processing will occur. Otherwise,
17/// processing will continue to the next handler.
18///
19/// # Trait Functions
20/// - `on_init` - Not typically used. Called when the plugin is first loaded. If defined, overrides the
21/// default macro behavior of calling
22/// [`receive_request_body(true)`](https://docs.rs/bulwark-wasm-sdk/latest/bulwark_wasm_sdk/fn.receive_request_body.html)
23/// or [`receive_response_body(true)`](https://docs.rs/bulwark-wasm-sdk/latest/bulwark_wasm_sdk/fn.receive_response_body.html)
24/// when the corresponding handlers have been defined.
25/// - `on_request` - This handler is called for every incoming request, before any decision-making will occur.
26/// It is typically used to perform enrichment tasks with the
27/// [`set_param_value`](https://docs.rs/bulwark-wasm-sdk/latest/bulwark_wasm_sdk/fn.set_param_value.html) function.
28/// The request body will not yet be available when this handler is called.
29/// - `on_request_decision` - This handler is called to make an initial decision.
30/// - `on_request_body_decision` - This handler is called once the request body is available. The decision may be updated
31/// with any new evidence found in the request body.
32/// - `on_response_decision` - This handler is called once the interior service has received the request, processed it, and
33/// returned a response, but prior to that response being sent onwards to the original exterior client. Notably, a `restricted`
34/// outcome here does not cancel any actions or side-effects from the interior service that may have taken place already.
35/// This handler is often used to process response status codes.
36/// - `on_response_body_decision` - This handler is called once the response body is available. The decision may be updated
37/// with any new evidence found in the response body.
38/// - `on_decision_feedback` - This handler is called once a final verdict has been reached. The combined decision
39/// of all plugins is available here, not just the decision of the currently executing plugin. This handler may be
40/// used for any form of feedback loop, counter-based detections, or to train a model. Additionally, in the case of a
41/// `restricted` outcome, this handler may be used to perform logouts or otherwise cancel or attempt to roll back undesired
42/// side-effects that could have occurred prior to the verdict being rendered.
43///
44/// # Example
45///
46/// ```no_compile
47/// use bulwark_wasm_sdk::*;
48///
49/// struct ExamplePlugin;
50///
51/// #[bulwark_plugin]
52/// impl Handlers for ExamplePlugin {
53/// fn on_request_decision() -> Result {
54/// println!("hello world");
55/// // implement detection logic here
56/// Ok(())
57/// }
58/// }
59/// ```
60#[proc_macro_attribute]
61pub fn bulwark_plugin(_: TokenStream, input: TokenStream) -> TokenStream {
62 // Parse the input token stream as an impl, or return an error.
63 let raw_impl = parse_macro_input!(input as ItemImpl);
64
65 // The trait must be specified by the developer even though there's only one valid value.
66 // If we inject it, that leads to a very surprising result when developers try to define helper functions
67 // in the same struct impl and can't because it's really a trait impl.
68 if let Some((_, path, _)) = raw_impl.trait_ {
69 let trait_name = path.get_ident().map_or(String::new(), |id| id.to_string());
70 if &trait_name != "Handlers" {
71 return syn::Error::new(
72 path.span(),
73 format!(
74 "`bulwark_plugin` encountered unexpected trait `{}` for the impl",
75 trait_name
76 ),
77 )
78 .to_compile_error()
79 .into();
80 }
81 } else {
82 return syn::Error::new(
83 raw_impl.self_ty.span(),
84 "`bulwark_plugin` requires an impl for the `Guest` trait",
85 )
86 .to_compile_error()
87 .into();
88 }
89
90 let struct_type = &raw_impl.self_ty;
91 let mut init_handler_found = false;
92 let mut request_body_handler_found = false;
93 let mut response_body_handler_found = false;
94
95 let mut handlers = vec![
96 "on_request",
97 "on_request_decision",
98 "on_request_body_decision",
99 "on_response_decision",
100 "on_response_body_decision",
101 "on_decision_feedback",
102 ];
103
104 let mut new_items = Vec::with_capacity(raw_impl.items.len());
105 for item in &raw_impl.items {
106 if let syn::ImplItem::Fn(iifn) = item {
107 match iifn.sig.ident.to_string().as_str() {
108 "on_init" => init_handler_found = true,
109 "on_request_body_decision" => request_body_handler_found = true,
110 "on_response_body_decision" => response_body_handler_found = true,
111 _ => {}
112 }
113 let initial_len = handlers.len();
114 // Find and record the implemented handlers, removing any we find from the list above.
115 handlers.retain(|h| *h != iifn.sig.ident.to_string().as_str());
116 // Verify that any functions with a handler name we find have set the `handler` attribute.
117 let mut use_original_item = true;
118 if handlers.len() < initial_len {
119 let mut handler_attr_found = false;
120 for attr in &iifn.attrs {
121 if let Some(ident) = attr.meta.path().get_ident() {
122 if ident.to_string().as_str() == "handler" {
123 handler_attr_found = true;
124 break;
125 }
126 }
127 }
128 if !handler_attr_found {
129 use_original_item = false;
130 let mut new_iifn = iifn.clone();
131 new_iifn.attrs.push(parse_quote! {
132 #[handler]
133 });
134 new_items.push(syn::ImplItem::Fn(new_iifn));
135 }
136 }
137 if use_original_item {
138 new_items.push(item.clone());
139 }
140 } else {
141 new_items.push(item.clone());
142 }
143 }
144
145 // Define the missing handlers with no-op defaults
146 let noop_handlers = handlers
147 .iter()
148 .map(|handler_name| {
149 let handler_ident = Ident::new(handler_name, Span::call_site());
150 quote! {
151 #[handler]
152 fn #handler_ident() -> Result {
153 Ok(())
154 }
155 }
156 })
157 .collect::<Vec<proc_macro2::TokenStream>>();
158
159 let init_handler = if init_handler_found {
160 // Empty token stream if an init handler was already defined, we'll generate nothing and use that instead
161 quote! {}
162 } else {
163 let receive_request_body = LitBool::new(request_body_handler_found, Span::call_site());
164 let receive_response_body = LitBool::new(response_body_handler_found, Span::call_site());
165 quote! {
166 #[handler]
167 fn on_init() -> Result {
168 receive_request_body(#receive_request_body);
169 receive_response_body(#receive_response_body);
170 Ok(())
171 }
172 }
173 };
174
175 let output = quote! {
176 mod handlers {
177 use super::#struct_type;
178
179 wit_bindgen::generate!({
180 world: "bulwark:plugin/handlers",
181 exports: {
182 world: #struct_type
183 }
184 });
185 }
186
187 use handlers::Guest as Handlers;
188 impl Handlers for #struct_type {
189 #init_handler
190 #(#new_items)*
191 #(#noop_handlers)*
192 }
193 };
194
195 output.into()
196}
197
198/// The `handler` attribute makes the associated function into a Bulwark event handler.
199///
200/// The `handler` attribute is normally applied automatically by the `bulwark_plugin` macro and
201/// need not be specified explicitly.
202///
203/// The associated function must take no parameters and return a `bulwark_wasm_sdk::Result`. It may only be
204/// named one of the following:
205/// - `on_init`
206/// - `on_request`
207/// - `on_request_decision`
208/// - `on_request_body_decision`
209/// - `on_response_decision`
210/// - `on_response_body_decision`
211/// - `on_decision_feedback`
212#[doc(hidden)]
213#[proc_macro_attribute]
214pub fn handler(_: TokenStream, input: TokenStream) -> TokenStream {
215 // Parse the input token stream as a free-standing function, or return an error.
216 let raw_handler = parse_macro_input!(input as ItemFn);
217
218 // Check that the function signature looks okay-ish. If we have the wrong number of arguments,
219 // or no return type is specified , print a friendly spanned error with the expected signature.
220 if !check_impl_signature(&raw_handler.sig) {
221 return syn::Error::new(
222 raw_handler.sig.span(),
223 format!(
224 "`handler` expects a function such as:
225
226#[handler]
227fn {}() -> Result {{
228 ...
229}}
230",
231 raw_handler.sig.ident
232 ),
233 )
234 .to_compile_error()
235 .into();
236 }
237
238 // Get the attributes and signature of the outer function. Then, update the
239 // attributes and visibility of the inner function that we will inline.
240 let (attrs, sig) = outer_handler_info(&raw_handler);
241 let (name, inner_fn) = inner_fn_info(raw_handler);
242 let name_str = LitStr::new(name.to_string().as_str(), Span::call_site());
243
244 let output;
245
246 match name.to_string().as_str() {
247 "on_init"
248 | "on_request"
249 | "on_request_decision"
250 | "on_request_body_decision"
251 | "on_response_decision"
252 | "on_response_body_decision"
253 | "on_decision_feedback" => {
254 output = quote_spanned! {inner_fn.span() =>
255 #(#attrs)*
256 #sig {
257 // Declares the inlined inner function, calls it, then performs very
258 // basic error handling on the result
259 #[inline(always)]
260 #inner_fn
261 let result = #name().map_err(|e| {
262 eprintln!("error in '{}' handler: {}", #name_str, e);
263 append_tags(["error"]);
264 // Absorbs the error, returning () to match desired signature
265 });
266 #[allow(unused_must_use)]
267 {
268 // Apparently we can exit the guest environment before IO is flushed,
269 // causing it to never be captured? This ensures IO is flushed and captured.
270 use std::io::Write;
271 std::io::stdout().flush();
272 std::io::stderr().flush();
273 }
274 result
275 }
276 }
277 }
278 _ => {
279 return syn::Error::new(
280 inner_fn.sig.span(),
281 "`handler` expects a function named one of:
282
283- `on_init`
284- `on_request`
285- `on_request_decision`
286- `on_request_body_decision`
287- `on_response_decision`
288- `on_response_body_decision`
289- `on_decision_feedback`
290",
291 )
292 .to_compile_error()
293 .into()
294 }
295 }
296
297 output.into()
298}
299
300/// Check if the signature of the `#[handler]` function seems correct.
301///
302/// Unfortunately, precisely typecheck in a procedural macro attribute is not possible, because we
303/// are dealing with [`TokenStream`]s. This checks that our signature takes one input, and has a
304/// return type. Specific type errors are caught later, after the macro has been expanded.
305///
306/// This is used by the [`handler`] procedural macro attribute to help provide friendly errors
307/// when given a function with the incorrect signature.
308///
309/// [`TokenStream`]: proc_macro/struct.TokenStream.html
310fn check_impl_signature(sig: &Signature) -> bool {
311 if sig.inputs.iter().len() != 0 {
312 false // Return false if the signature takes no inputs, or more than one input.
313 } else if let ReturnType::Default = sig.output {
314 false // Return false if the signature's output type is empty.
315 } else {
316 true
317 }
318}
319
320/// Returns a 2-tuple containing the attributes and signature of our outer `handler`.
321///
322/// The outer handler function will use the same attributes and visibility as our raw handler
323/// function.
324///
325/// The signature of the outer function will be changed to have inputs and outputs of the form
326/// `fn handler_name() -> Result<(), ()>`. The name of the outer handler function will be the same
327/// as the inlined function.
328fn outer_handler_info(inner_handler: &ItemFn) -> (Vec<Attribute>, Signature) {
329 let attrs = inner_handler.attrs.clone();
330 let name = inner_handler.sig.ident.to_string();
331 let sig = {
332 let mut sig = inner_handler.sig.clone();
333 sig.ident = Ident::new(&name, Span::call_site());
334 sig.inputs = Punctuated::new();
335 sig.output = parse_quote!(-> ::std::result::Result<(), ()>);
336 sig
337 };
338
339 (attrs, sig)
340}
341
342/// Prepare our inner function to be inlined into our outer handler function.
343///
344/// This changes its visibility to [`Inherited`], and removes [`no_mangle`] from the attributes of
345/// the inner function if it is there.
346///
347/// This function returns a 2-tuple of the inner function's identifier and the function itself.
348///
349/// [`Inherited`]: syn/enum.Visibility.html#variant.Inherited
350/// [`no_mangle`]: https://doc.rust-lang.org/reference/abi.html#the-no_mangle-attribute
351fn inner_fn_info(mut inner_handler: ItemFn) -> (Ident, ItemFn) {
352 let name = inner_handler.sig.ident.clone();
353 inner_handler.vis = Visibility::Inherited;
354 inner_handler
355 .attrs
356 .retain(|attr| !attr.path().is_ident("no_mangle"));
357 (name, inner_handler)
358}