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}