Skip to content

Add flexible, framework-invoked authorization logic#647

Draft
juv wants to merge 3 commits into
rapina-rs:mainfrom
juv:feat/pluggable-authorization
Draft

Add flexible, framework-invoked authorization logic#647
juv wants to merge 3 commits into
rapina-rs:mainfrom
juv:feat/pluggable-authorization

Conversation

@juv

@juv juv commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a new #[authorize] macro that adds flexible authorization for any endpoint handler.
See /examples/jwt-validation for a full example.

Related Issues

Closes #116

Checklist

  • cargo fmt passes
  • cargo clippy has no warnings
  • Tests pass
  • Documentation updated (if needed)

@github-actions github-actions Bot added area: core Core framework area: macros Proc macros labels Jun 17, 2026
Comment thread rapina/examples/jwt-validation/src/main.rs Outdated
Comment thread rapina-macros/src/lib.rs
Comment on lines +229 to +248
#[proc_macro_attribute]
pub fn authorize(_attr: TokenStream, item: TokenStream) -> TokenStream {
let func: ItemFn =
syn::parse2(item.clone().into()).expect("#[authorize] must be applied to a function");

if let Some(attr) = func
.attrs
.iter()
.find(|attr| attr.path().is_ident("public"))
{
return syn::Error::new(
attr.span(),
"#[authorize] contradicts #[public]. A public handler must not include authorization.",
)
.to_compile_error()
.into();
}

item
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question on how the two attributes compose, since I want to be sure I'm reading the expansion right. Attribute macros expand top-down and each one consumes its own attribute, so with the documented order

#[get("/email")]
#[authorize(authz::authorize(JsonWebToken))]

so #[get] expands first and strips #[authorize] itself. Does this authorize proc macro ever actually run in that case? And if it doesn't, what's enforcing the #[public] conflict for the order users will actually write, can #[get] #[public] #[authorize(...)] compile into a route that's both public and authorized?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now the other way around:

#[authorize(authz::authorize(JsonWebToken))]
#[get("/email")]

Here authorize expands first and returns item without its own attribute, so by the time #[get] runs there's nothing left to find. Is the route still protected? I don't think it is, and that's the part that worries me. For an auth feature, getting the attribute order wrong shouldn't silently drop the check, it should fail to compile. Could we move all of this into route_macro_core (it already extracts the attr), put the #[public] conflict check there so it actually runs, and make a bare #[authorize] a hard error?

Comment thread rapina-macros/src/lib.rs
if let Some(attr) = &contains_authorize {
let auth_args = attr
.parse_args::<AuthorizeArgs>()
.expect("#[authorize] args must be parseable");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we expect() here instead of surfacing a syn::Error? When the attribute args don't parse this panics, and a proc-macro panic renders as proc macro panicked with the message buried under it..

Comment thread rapina-macros/src/lib.rs

if normalize_type(ty) == dep_norm {
let ident = extract_ident(pat)?;
found = Some(quote!(&#ident));

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We hand the dependency to the auth fn two different ways depending on the branch: when a handler param matches, here and when we extract a fresh one, #tmp by value. The auth fn has a single signature for that parameter though, so which one is it meant to be?

Comment thread rapina-macros/src/lib.rs
}

fn normalize_type(ty: &Type) -> String {
ty.to_token_stream().to_string().replace(' ', "")

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is this comparison really pulling its weight?

Comment thread rapina-macros/src/lib.rs
Comment on lines +523 to +525
let args = attr
.parse_args::<AuthorizeArgs>()
.expect("#[authorize] args must be parseable");

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why this gets parsed twice? We could parse once and thread it through, minor though.

Comment thread rapina-macros/src/lib.rs
content.parse_terminated(Type::parse, Token![,])?;
parsed.into_iter().collect()
} else {
input.parse::<Token![,]>()?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the comma form deliberate, or a leftover?

Comment thread rapina-macros/src/lib.rs
let (__rapina_parts, _) = __rapina_req.into_parts();
#(#header_extractions)*

//call authorization handler

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a nit, these comments skip the space. fmt won't catch it.

.state(jwks_client)
.state(jwks_validation)
.discover()
.router(router)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.router(router)

@arferreira arferreira left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work @juv! A few things need sorting before it can land.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: core Core framework area: macros Proc macros

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Add pluggable authorization for endpoints

2 participants