poem_grants_proc_macro/lib.rs
1extern crate proc_macro;
2use proc_macro::TokenStream;
3use quote::ToTokens;
4use syn::{parse_macro_input, AttributeArgs, ImplItem, ImplItemMethod, ItemFn, ItemImpl, Meta};
5
6use crate::expand::{FnType, ProtectEndpoint};
7
8mod expand;
9
10const HAS_AUTHORITIES: &str = "has_permissions";
11const HAS_ANY_AUTHORITY: &str = "has_any_permission";
12
13const HAS_ROLES: &str = "has_roles";
14const HAS_ANY_ROLE: &str = "has_any_role";
15
16macro_rules! unwrap_result {
17 ($result:expr) => {
18 match $result {
19 Ok(result) => result,
20 Err(err) => return syn::Error::from(err).to_compile_error().into(),
21 }
22 };
23}
24
25/// Macro to сheck that the user has all the specified permissions.
26/// Allow to add a conditional restriction based on handlers parameters.
27/// Add the `secure` attribute followed by the the boolean expression to validate based on parameters
28///
29/// Attention: these macros have to be above of (`[poem::handler]`) or `oai`
30///
31/// Also you can use you own types instead of Strings, just add `type` attribute with path to type
32/// # Examples
33/// ```
34/// use poem::{Response, http::StatusCode};
35///
36/// // User should be ADMIN with OP_GET_SECRET permission
37/// #[poem_grants::has_permissions["ROLE_ADMIN", "OP_GET_SECRET"]]
38/// #[poem::handler]
39/// async fn macro_secured() -> Response {
40/// Response::builder().status(StatusCode::OK).body("some secured info")
41/// }
42///
43/// // User should be ADMIN with OP_GET_SECRET permission and the user.id param should be equal
44/// // to the path parameter {user_id}
45/// struct User {id: i32}
46/// #[poem_grants::has_permissions["ROLE_ADMIN", "OP_GET_SECRET", secure="*user_id == user.id"]]
47/// #[poem::handler]
48/// async fn macro_secured_params(user_id: web::Path<i32>, user: web::Data<User>) -> Response {
49/// Response::builder().status(StatusCode::OK).body("some secured info with user_id path equal to user.id")
50///}
51///
52/// // User must have MyPermissionEnum::OP_GET_SECRET (you own enum example)
53/// #[poem_grants::has_permissions["OP_GET_SECRET", type = "MyPermissionEnum"]]
54/// #[poem::handler]
55/// async fn macro_enum_secured() -> Response {
56/// Response::builder().status(StatusCode::OK).body("some secured info")
57/// }
58///
59///```
60#[proc_macro_attribute]
61pub fn has_permissions(args: TokenStream, input: TokenStream) -> TokenStream {
62 check_permissions(HAS_AUTHORITIES, args, input)
63}
64
65/// Macro to сheck that the user has any of the specified permissions.
66///
67/// # Examples
68/// ```
69/// use poem::{Response, http::StatusCode};
70///
71/// // User should be ADMIN or MANAGER
72/// #[poem_grants::has_any_permission["ROLE_ADMIN", "ROLE_MANAGER"]]
73/// #[poem::handler]
74/// async fn macro_secured() -> Response {
75/// Response::builder().status(StatusCode::OK).body("some secured info")
76/// }
77/// ```
78#[proc_macro_attribute]
79pub fn has_any_permission(args: TokenStream, input: TokenStream) -> TokenStream {
80 check_permissions(HAS_ANY_AUTHORITY, args, input)
81}
82
83/// Macro to сheck that the user has all the specified roles.
84/// Role - is permission with prefix "ROLE_".
85///
86/// # Examples
87/// ```
88/// use poem::{Response, http::StatusCode};
89///
90/// // User should be ADMIN and MANAGER
91/// #[poem_grants::has_roles["ADMIN", "MANAGER"]]
92/// #[poem::handler]
93/// async fn macro_secured() -> Response {
94/// Response::builder().status(StatusCode::OK).body("some secured info")
95/// }
96/// ```
97#[proc_macro_attribute]
98pub fn has_roles(args: TokenStream, input: TokenStream) -> TokenStream {
99 check_permissions(HAS_ROLES, args, input)
100}
101
102/// Macro to сheck that the user has any the specified roles.
103/// Role - is permission with prefix "ROLE_".
104///
105/// # Examples
106/// ```
107/// use poem::{Response, http::StatusCode};
108///
109/// // User should be ADMIN or MANAGER
110/// #[poem_grants::has_any_role["ADMIN", "MANAGER"]]
111/// #[poem::handler]
112/// async fn macro_secured() -> Response {
113/// Response::builder().status(StatusCode::OK).body("some secured info")
114/// }
115/// ```
116#[proc_macro_attribute]
117pub fn has_any_role(args: TokenStream, input: TokenStream) -> TokenStream {
118 check_permissions(HAS_ANY_ROLE, args, input)
119}
120
121fn check_permissions(check_fn_name: &str, args: TokenStream, input: TokenStream) -> TokenStream {
122 let args = parse_macro_input!(args as AttributeArgs);
123 let func = parse_macro_input!(input as ItemFn);
124
125 unwrap_result!(ProtectEndpoint::new(check_fn_name, args, FnType::Fn(func)))
126 .into_token_stream()
127 .into()
128}
129
130/// Macro for `poem-openapi` support
131/// Add macro `#[poem_grants::open_api]` above of `#[poem_openapi::OpenApi]` and mark all needed methods with necessary security-methods:
132/// One of [`has_permissions`, `has_any_permission`, `has_roles`, `has_any_role`]
133///
134/// # Examples
135/// ```
136/// use poem_openapi::payload::PlainText;
137///
138/// struct Api;
139///
140/// #[poem_grants::open_api]
141/// #[poem_openapi::OpenApi]
142/// impl Api {
143/// // An example of protection via `proc-macro`.
144/// // Just use the necessary name of macro provided by `poem-grants` without crate-name:
145/// #[has_permissions("OP_READ_ADMIN_INFO")]
146/// #[oai(path = "/admin", method = "get")]
147/// async fn macro_secured(&self) -> PlainText<String> {
148/// PlainText("ADMIN_RESPONSE".to_string())
149/// }
150/// }
151/// ```
152#[proc_macro_attribute]
153pub fn open_api(_args: TokenStream, input: TokenStream) -> TokenStream {
154 let mut item_impl = parse_macro_input!(input as ItemImpl);
155 let mut methods = Vec::new();
156 for (idx, item) in item_impl.items.iter().enumerate() {
157 if let ImplItem::Method(method) = item {
158 for grants_attr in method
159 .attrs
160 .iter()
161 .filter(|attr| is_poem_grants_attr(*attr))
162 {
163 let args = match unwrap_result!(grants_attr.parse_meta()) {
164 Meta::List(list) => list.nested.into_iter().collect::<Vec<syn::NestedMeta>>(),
165 _ => {
166 return syn::Error::new_spanned(
167 grants_attr,
168 "Expected endpoint-attribute to be a list",
169 )
170 .to_compile_error()
171 .into()
172 }
173 };
174
175 let generated = unwrap_result!(ProtectEndpoint::new(
176 &grants_attr
177 .path
178 .get_ident()
179 .expect("validated by condition above")
180 .to_string(),
181 args,
182 FnType::Method(method.clone()),
183 ))
184 .into_token_stream()
185 .into();
186
187 let mut gen_method = parse_macro_input!(generated as ImplItemMethod);
188
189 gen_method.attrs.retain(|attr| attr != grants_attr);
190
191 methods.push((idx, gen_method));
192 }
193 }
194 }
195
196 for (idx, method) in methods {
197 let _ = std::mem::replace(&mut item_impl.items[idx], ImplItem::Method(method));
198 }
199
200 let res = quote::quote! {
201 #item_impl
202 };
203
204 res.into()
205}
206
207fn is_poem_grants_attr(attr: &syn::Attribute) -> bool {
208 attr.path.is_ident(HAS_ANY_AUTHORITY)
209 || attr.path.is_ident(HAS_AUTHORITIES)
210 || attr.path.is_ident(HAS_ANY_ROLE)
211 || attr.path.is_ident(HAS_ROLES)
212}