Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: support salvo #96

Merged
merged 7 commits into from
May 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ members = [
"core",
"proc-macro",
"actix-web-grants",
"protect-axum",
"poem-grants",
"protect-axum",
"protect-salvo",
"rocket-grants",
]

Expand Down
9 changes: 7 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
[![dependency status](https://deps.rs/repo/github/DDtKey/protect-endpoints/status.svg)](https://deps.rs/repo/github/DDtKey/protect-endpoints)
[![CI](/~https://github.com/DDtKey/protect-endpoints/workflows/CI/badge.svg)](/~https://github.com/DDtKey/protect-endpoints/actions)

> Extension for [`actix-web`], [`rocket`] and [`poem`] to authorize requests.
> Extension for [`actix-web`], [`rocket`], [`poem`], [`axum`] and [`salvo`] to authorize requests.

<p align="center">
<a href="/~https://github.com/DDtKey/protect-endpoints/tree/main/actix-web-grants"><img alt="actix-web-grants" src="/~https://github.com/DDtKey/protect-endpoints/raw/main/actix-web-grants/logo.png"></a>
Expand All @@ -12,10 +12,15 @@
</p>
<p align="center">
<a href="/~https://github.com/DDtKey/protect-endpoints/tree/main/protect-axum"><img alt="protect-axum" src="protect-axum/logo.png"></a>
<a href="/~https://github.com/DDtKey/protect-endpoints/tree/main/protect-salvo"><img alt="protect-axum" src="protect-salvo/logo.png"></a>
</p>

[`actix-web`]: /~https://github.com/actix/actix-web

[`rocket`]: /~https://github.com/SergioBenitez/Rocket
[`axum`]:/~https://github.com/tokio-rs/axum

[`poem`]: /~https://github.com/poem-web/poem

[`rocket`]: /~https://github.com/SergioBenitez/Rocket

[`salvo`]: /~https://github.com/salvo-rs/salvo
3 changes: 3 additions & 0 deletions proc-macro/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ actix-web = []
axum = []
poem = []
rocket = []
salvo = []

[dependencies]
darling = "0.20.3"
Expand All @@ -35,3 +36,5 @@ poem-openapi = { version = "5.0.0" }
rocket = { version = "0.5.0", features = ["json"] }
rocket-grants = { path = "../rocket-grants" }
serde = { version = "1.0", features = ["derive"] }
protect-salvo = { path = "../protect-salvo" }
salvo = { version = "0.67.2" }
6 changes: 6 additions & 0 deletions proc-macro/src/expand.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ mod axum;
mod poem;
#[cfg(feature = "rocket")]
mod rocket;
#[cfg(feature = "salvo")]
mod salvo;

#[derive(Debug, Copy, Clone)]
pub(crate) enum Framework {
Expand All @@ -23,6 +25,8 @@ pub(crate) enum Framework {
Poem,
#[cfg(feature = "rocket")]
Rocket,
#[cfg(feature = "salvo")]
Salvo,
}

#[derive(Debug, Clone)]
Expand Down Expand Up @@ -77,6 +81,8 @@ impl ToTokens for ProtectEndpoint {
Framework::Rocket => self.to_tokens_rocket(output),
#[cfg(feature = "axum")]
Framework::Axum => self.to_tokens_axum(output),
#[cfg(feature = "salvo")]
Framework::Salvo => self.to_tokens_salvo(output),
}
}
}
Expand Down
65 changes: 65 additions & 0 deletions proc-macro/src/expand/salvo.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
use crate::expand::ProtectEndpoint;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens};
use syn::{parse_quote, ReturnType};

impl ProtectEndpoint {
pub(super) fn to_tokens_salvo(&self, output: &mut TokenStream2) {
let func_vis = &self.func.vis();
let func_block = &self.func.block();

let fn_sig = &self.func.sig();
let fn_attrs = &self.func.attrs();
let fn_name = &fn_sig.ident;
let fn_generics = &fn_sig.generics;
let fn_async = &fn_sig.asyncness.expect("only async handlers are supported");
let fn_output = match &fn_sig.output {
ReturnType::Type(ref _arrow, ref ty) => ty.to_token_stream(),
ReturnType::Default => {
quote! {()}
}
};

let ty = self
.args
.ty
.as_ref()
.map(ToTokens::to_token_stream)
.unwrap_or(quote! {String});

let mut fn_args = fn_sig.inputs.clone();
let auth_details = format!("_auth_details_{}", fn_args.len());
let auth_details: Ident = Ident::new(&auth_details, Span::call_site());

fn_args.push(parse_quote!(#auth_details: protect_salvo::authorities::AuthDetails<#ty>));

let condition = self
.args
.cond
.to_tokens(&auth_details, self.args.ty.is_some());
let condition = quote!(if #condition);

let err_resp = if let Some(expr) = &self.args.error_fn {
quote!(#expr())
} else {
quote!(salvo::prelude::StatusCode::FORBIDDEN)
};

let stream = quote! {
#(#fn_attrs)*
#func_vis #fn_async fn #fn_name #fn_generics(
#fn_args
) -> Result<#fn_output, impl salvo::Writer + Send + std::fmt::Debug + 'static> {
use protect_salvo::authorities::AuthoritiesCheck;
#condition {
let f = || async move #func_block;
Ok(f().await)
} else {
Err(#err_resp)
}
}
};

output.extend(stream);
}
}
54 changes: 51 additions & 3 deletions proc-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -109,11 +109,11 @@ pub fn protect_rocket(args: TokenStream, input: TokenStream) -> TokenStream {
protect_endpoint(Framework::Rocket, args, input)
}

/// Macro to сheck that the user has all the specified permissions.
/// Macro to check that the user has all the specified permissions.
/// Allow to add a conditional restriction based on handlers parameters.
/// Add the `expr` attribute followed by the the boolean expression to validate based on parameters
/// Add the `expr` attribute followed by the boolean expression to validate based on parameters
///
/// Also you can use you own types instead of Strings, just add `ty` attribute with path to type
/// Also, you can use you own types instead of Strings, just add `ty` attribute with path to type
/// # Examples
/// ```rust,no_run
/// use poem::web::Json;
Expand Down Expand Up @@ -220,6 +220,54 @@ pub fn open_api(_args: TokenStream, input: TokenStream) -> TokenStream {
res.into()
}

/// Macro to check that the user has all the specified permissions.
/// Allow to add a conditional restriction based on handlers parameters.
/// Add the `expr` attribute followed by the boolean expression to validate based on parameters
///
/// Also, you can use you own types instead of Strings, just add `ty` attribute with path to type
/// # Examples
/// ```rust,no_run
/// use salvo::prelude::*;
///
/// // User should be ADMIN with OP_GET_SECRET permission
/// #[protect_salvo::protect("ROLE_ADMIN", "OP_GET_SECRET")]
/// async fn macro_secured() -> &'static str {
/// "some secured info"
/// }
///
/// // User should be ADMIN with OP_GET_SECRET permission and the user.id param should be equal
/// // to the path parameter {user_id}
/// #[derive(serde::Deserialize, Extractible)]
/// #[salvo(extract(default_source(from = "body")))]
/// struct User {id: i32}
/// #[derive(serde::Deserialize, Extractible)]
/// #[salvo(extract(default_source(from = "param")))]
/// struct UserParams { user_id: i32 }
///
/// #[protect_salvo::protect("ROLE_ADMIN", "OP_GET_SECRET", expr="params.user_id == user.id")]
/// async fn macro_secured_params(params: UserParams, user: User, req: &mut Request) -> &'static str {
/// "some secured info with user_id path equal to user.id"
///}
///
/// #[derive(Hash, PartialEq, Eq)]
/// enum MyPermissionEnum {
/// OpGetSecret
/// }
///
/// // User must have MyPermissionEnum::OpGetSecret (you own enum example)
/// #[protect_salvo::protect("MyPermissionEnum::OpGetSecret", ty = MyPermissionEnum)]
/// async fn macro_enum_secured() -> &'static str {
/// "some secured info"
/// }
///
///```
#[cfg(feature = "salvo")]
#[cfg_attr(docsrs, doc(cfg(feature = "salvo")))]
#[proc_macro_attribute]
pub fn protect_salvo(args: TokenStream, input: TokenStream) -> TokenStream {
protect_endpoint(Framework::Salvo, args, input)
}

fn protect_endpoint(framework: Framework, args: TokenStream, input: TokenStream) -> TokenStream {
let args = match NestedMeta::parse_meta_list(args.into()) {
Ok(v) => v,
Expand Down
36 changes: 36 additions & 0 deletions protect-salvo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "protect-salvo"
version = "0.1.0"
description = "Authorization extension `salvo` to protect your endpoints"
readme = "README.md"
keywords = ["salvo", "auth", "security", "grants", "permissions"]
authors.workspace = true
repository.workspace = true
homepage.workspace = true
categories.workspace = true
license.workspace = true
edition.workspace = true

[lib]
name = "protect_salvo"
path = "src/lib.rs"

[features]
default = ["macro-check"]
macro-check = ["protect-endpoints-proc-macro"]

[dependencies]
salvo = { version = "0.67.2", default-features = false, features = ["tower-compat"] }
protect-endpoints-core = { workspace = true, features = ["tower"] }
protect-endpoints-proc-macro = { workspace = true, features = ["salvo"], optional = true }
tower = { version = "0.4.13", default-features = false }

[dev-dependencies]
chrono = "0.4.35"
http-body-util = "0.1.0"
jsonwebtoken = "9.1.0"
parse-display = "0.9.0"
salvo = { version = "0.67.2", default-features = false, features = ["test"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0.64"
tokio = { version = "1.34.0", features = ["rt-multi-thread"] }
103 changes: 103 additions & 0 deletions protect-salvo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# protect-salvo

<p align="center">
<img alt="protect-salvo" src="/~https://github.com/DDtKey/protect-endpoints/raw/main/protect-salvo/logo.png">
</p>

> Authorization extension for [`salvo`] to protect your endpoints.

[![Crates.io Downloads Badge](https://img.shields.io/crates/d/protect-salvo)](https://crates.io/crates/protect-salvo)
[![crates.io](https://img.shields.io/crates/v/protect-salvo)](https://crates.io/crates/protect-salvo)
[![Documentation](https://docs.rs/protect-salvo/badge.svg)](https://docs.rs/protect-salvo)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/protect-salvo)

To check user access to specific services, you can use built-in `proc-macro` or manual.

The library can also be integrated with third-party solutions (e.g. jwt-middlewares).

## How to use

1. Add `tower-compat` feature to [`salvo`] dependency in your `Cargo.toml`

2. Declare your
own [authority extractor](https://docs.rs/protect-endpoints-core/latest/protect_endpoints_core/authorities/extractor/trait.AuthoritiesExtractor.html)

The easiest way is to declare a function with the following signature (trait is already implemented for such Fn):

```rust,ignore
use salvo::prelude::*;

// You can use custom type instead of String
// It requires to use hyper's `Request` & `Response` types, because integration is based on `tower`
pub async fn extract(req: &mut salvo::hyper::Request<ReqBody>) -> Result<HashSet<String>, salvo::hyper::Response<ResBody>>
```

3. Add middleware to your application using the extractor defined in step 1

```rust,ignore
Router::with_path("/")
.hoop(GrantsLayer::with_extractor(extract).compat())
.push(Router::with_path("/endpoint").get(your_handler))
```

> Steps 2 and 3 can be replaced by custom middleware or integration with another libraries.

4. Protect your endpoints in any convenient way from the examples below:

### Example of `proc-macro` way protection

```rust,ignore
#[protect_salvo::protect("ROLE_ADMIN")]
#[handler]
async fn macro_secured() -> &'static str {
return "Hello, World!";
}
```

<details>

<summary> <b><i> Example of ABAC-like protection and custom authority type </i></b></summary>
<br/>


Here is an example using the `ty` and `expr` attributes. But these are independent features.

`expr` allows you to include some checks in the macro based on function params, it can be combined with authorities by
using `all`/`any`.

`ty` allows you to use a custom type for th authorities (then the middleware needs to be configured).

```rust,ignore
use enums::Role::{self, ADMIN};
use dto::User;

#[post("/info/{user_id}")]
#[protect_salvo::protect(any("ADMIN", expr = "user.is_super_user()"), ty = "Role")]
async fn admin_or_super_user(user: User) -> &'static str {
"some secured response"
}
```

</details>

### Example of manual way protection

```rust,ignore
use protect_salvo::authorities::{AuthDetails, AuthoritiesCheck};

async fn manual_secure(details: AuthDetails) -> &'static str {
if details.has_authority(ROLE_ADMIN) {
return "ADMIN_RESPONSE";
}
"OTHER_RESPONSE"
}
```

You can find more [`examples`] in the git repository folder and [`documentation`].


[`examples`]: /~https://github.com/DDtKey/protect-endpoints/tree/main/protect-salvo/examples

[`documentation`]: https://docs.rs/protect-salvo

[`salvo`]: https://salvo.rs/
Binary file added protect-salvo/logo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading