protect-endpoints

repository·main·Indexed 42 days ago

https://github.com/ddtkey/protect-endpoints

A collection of Rust crates providing authorization and endpoint protection for popular web frameworks, including actix-web (actix-web-grants 4.1.2), poem (poem-grants), axum (protect-axum), salvo (protect-salvo), and rocket (rocket-grants). It enables declarative protection via proc-macros, guards, or manual checks using authority extractors and middleware.

Tokens
20.3K
Snippets
53
Records
66
Agent score
52%

What's inside protect-endpoints

  1. Overview of protect-endpoints crates

    main

    The protect-endpoints project provides a collection of crates designed to authorize requests by protecting endpoints in various Rust web frameworks. It offers specialized integration crates for the following frameworks:

    • actix-web (via actix-web-grants)
    • rocket (via rocket-grants)
    • poem (via poem-grants)
    • axum (via protect-axum)
    • salvo (via protect-salvo)

    Developers can choose the crate that matches their web framework to implement authorization logic.

  2. Advanced protection with ABAC-like attributes

    main

    The protect macro supports advanced logic using expr and ty attributes for Attribute-Based Access Control (ABAC) style checks.

    • expr: Allows including logic checks based on function parameters. You can combine these with authorities using all or any.
    • ty: Allows specifying a custom type for authorities (requires the middleware to be configured for that type).

    Examples

    Single authority with expression check:

    #[poem_grants::protect("ADMIN", expr = "*user_id == user.id", ty = "Role")]
    #[poem::handler]
    async fn macro_secured(user_id: web::Path<i32>, user: web::Data<User>) -> Response { ... }

    Logical OR with expression check:

    #[poem_grants::protect(any("ADMIN", expr = "user.is_super_user()"), ty = "Role")]
    #[poem::handler]
    async fn admin_or_super_user(user_id: web::Path<i32>, user: web::Data<User>) -> Response { ... }
    #[poem_grants::protect("ADMIN", expr = "*user_id == user.id", ty = "Role")]
    #[poem::handler]
    async fn macro_secured(user_id: web::Path<i32>, user: web::Data<User>) -> Response {
        Response::builder().status(StatusCode::OK).body("some secured response")
    }
  3. Set up rocket-grants for authorization

    main

    To use rocket-grants to authorize requests in your Rocket application, follow these three steps:

    1. Define an authority extraction function: Create an async function that takes a &rocket::Request<'_> and returns an Option<HashSet<String>> (or a custom type). This function is responsible for determining which authorities/permissions are associated with the current request.

    2. Attach the GrantsFairing: Register the fairing in your Rocket build process, passing your extraction function to GrantsFairing::with_extractor_fn. Since the extractor is async, you must wrap the call in Box::pin.

    3. Protect endpoints: Use either the #[rocket_grants::protect] procedural macro or manual checks within your route handlers.

    // 1. Define extractor
    async fn extract(req: &rocket::Request<'_>) -> Option<HashSet<String>> {
        // logic to extract permissions
        None
    }
    
    // 2. Attach fairing
    #[rocket::launch]
    fn rocket() -> _ {
        rocket::build()
            .mount("/api", rocket::routes![endpoint])
            .attach(GrantsFairing::with_extractor_fn(|req| {
                Box::pin(extract(req))
            }))
    }
  4. Protect endpoints using proc-macros

    main

    You can protect endpoints declaratively using the #[poem_grants::protect(...)] macro. This is the most convenient way to enforce specific authorities on a handler.

    Standard Poem Handlers

    Apply the macro directly above your #[poem::handler]:

    #[poem_grants::protect("OP_READ_SECURED_INFO")]
    #[poem::handler]
    async fn macro_secured() -> Response { ... }

    Poem-OpenAPI Handlers

    When using poem-openapi, you must use the #[poem_grants::open_api] macro, and it must be placed above the #[OpenApi] attribute:

    #[poem_grants::open_api]
    #[OpenApi]
    impl Api {
        #[protect("OP_READ_ADMIN_INFO")]
        #[oai(path = "/admin", method = "get")]
        async fn macro_secured(&self) -> PlainText<String> { ... }
    }
    #[poem_grants::protect("OP_READ_SECURED_INFO")]
    #[poem::handler]
    async fn macro_secured() -> Response {
        Response::builder().status(StatusCode::OK).body("ADMIN_RESPONSE")
    }
  5. Set up poem-grants authorization

    main

    To use poem-grants for endpoint protection, follow these three steps:

    1. Define an authorities extractor: Create an asynchronous function that extracts user permissions from the request. The simplest signature returns a HashSet<String>:

      async fn extract(req: &poem::Request) -> poem::Result<HashSet<String>>
    2. Register the middleware: Add the GrantsMiddleware to your application routes using the extractor defined in step 1:

      Route::new()
          .at("/endpoint", your_endpoint)
          .with(GrantsMiddleware::with_extractor(extract))
    3. Protect endpoints: Use either procedural macros (declarative) or manual checks (imperative) to enforce authorization.

    async fn extract(req: &poem::Request) -> poem::Result<HashSet<String>>
    
    Route::new()
        .at("/endpoint", your_endpoint)
        .with(GrantsMiddleware::with_extractor(extract))
  6. Set up actix-web-grants

    main

    To use actix-web-grants for endpoint protection, follow these three steps:

    1. Define an authority extractor: Create an asynchronous function that extracts user authorities from a ServiceRequest. The function should return a Result containing a collection of authorities (e.g., HashSet<String>) or an actix_web::Error.
    2. Register the middleware: Add the GrantsMiddleware to your App using the extractor defined in step 1.
    3. Protect endpoints: Use one of the three protection methods: proc-macro, Guard, or manual checks.

    Note: You can integrate this with third-party libraries like actix-web-httpauth or use custom middleware instead of the standard extractor setup.

    use actix_web::{dev::ServiceRequest, Error};
    use std::collections::HashSet;
    
    // 1. Define the extractor
    async fn extract(req: &ServiceRequest) -> Result<HashSet<String>, Error> {
        // implementation logic to get authorities
        Ok(HashSet::new())
    }
    
    // 2. Add middleware to the App
    App::new()
        .wrap(GrantsMiddleware::with_extractor(extract))
        // 3. Protect endpoints (see methods below)
  7. Set up protect-axum for Axum

    main

    To use protect-axum, you must first define an authority extractor and then apply it to your Axum Router using GrantsLayer.

    1. Define an authority extractor: Create an async function that takes a mutable reference to an axum::extract::Request and returns a Result containing a collection of authorities (e.g., HashSet<String>) or an axum::response::Response on failure.
    2. Apply the middleware: Use GrantsLayer::with_extractor(your_extractor_function) as a layer on your Router.
    use axum::extract::Request;
    use axum::response::Response;
    use std::collections::HashSet;
    use axum::routing::get;
    use axum::Router;
    use protect_axum::GrantsLayer;
    
    // 1. Define the extractor
    pub async fn extract(req: &mut Request) -> Result<HashSet<String>, Response> {
        // Implementation logic to extract authorities from request
        let mut authorities = HashSet::new();
        authorities.insert("USER_ROLE".to_string());
        Ok(authorities)
    }
    
    // 2. Add the layer to your Router
    let app = Router::new()
        .route("/endpoint", get(your_handler))
        .layer(GrantsLayer::with_extractor(extract));
  8. Add protect-salvo middleware to a Salvo application

    main

    Once an extractor is defined, add the GrantsLayer to your Salvo router using the .hoop() method. To ensure compatibility with Salvo's routing, use TowerLayerCompat from protect_salvo::salvo_extra (or salvo_extra directly) to wrap the layer.

    // You can use [`salvo_extra`] directly for Tower compatibility or re-exported one
    use protect_salvo::salvo_extra::TowerLayerCompat;
    
    Router::with_path("/")
        .hoop(GrantsLayer::with_extractor(extract).compat())
        .push(Router::with_path("/endpoint").get(your_handler))
  9. Protect endpoints using the `#[rocket_grants::protect]` macro

    main

    The procedural macro #[rocket_grants::protect] is the easiest way to secure routes. It checks if the authorities extracted by your fairing match the required permissions.

    Basic Usage

    Pass a string literal representing the required authority.

    Advanced Logic with any, all, and expr

    • any(...) / all(...): Combine multiple authority checks.
    • expr: Add attribute-based access control (ABAC) by including logic based on function parameters. For example, you can ensure a user_id in the path matches the id of the user object in the request body.
    • Custom Types: If your extractor returns a custom type instead of String, you can use the ty attribute (refer to the enum-role example in the repository for implementation details).
    // Basic protection
    #[rocket_grants::protect("OP_READ_SECURED_INFO")]
    #[rocket::get("/")]
    async fn macro_secured() -> &'static str {
       "ADMIN_RESPONSE"
    }
    
    // ABAC-like protection using `expr` and `any`
    #[rocket_grants::protect(any("ADMIN", expr = "user.is_super_user()"))]
    #[rocket::post("/secure/admin/<user_id>", data = "<user>")]
    async fn admin_or_super_user(user_id: i32, user: Json<User>) -> &'static str {
       "some secured info with parameters"
    }
  10. Implement an authority extractor for protect-salvo

    main

    To use protect-salvo, you must first define how user authorities are extracted from an incoming request. The simplest way is to implement an asynchronous function that matches the following signature. This function must return a Result containing a HashSet<String> of authorities or a salvo::hyper::Response on failure.

    Note: You can use a custom type instead of String for authorities, but you must use hyper's Request and Response types because the integration is based on tower.

    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>>
  11. Integrate existing authentication with `AttachAuthorities`

    main
    If you already have an authentication system (a fairing) in your Rocket application, you can integrate it with rocket-grants by implementing the AttachAuthorities trait. This allows the library to extract permissions from your existing authentication mechanism and use them for endpoint protection.
  12. Configure endpoint protection conditions

    main

    When using the protection macros, you can specify authorization requirements using several types of conditions. The macro supports logical grouping and different ways to express authority requirements:

    • String Literals: A simple string literal (e.g., "admin") checks if the user has that specific authority.
    • any(...): A logical OR. The condition is met if the user has any of the specified authorities or if any of the nested expressions evaluate to true.
    • all(...): A logical AND. The condition is met only if the user has all of the specified authorities or if all nested expressions evaluate to true.
    • expr: Allows passing a custom Rust expression to evaluate the condition.

    If multiple conditions are provided at the top level, they are treated as an all (AND) condition by default.

    // Example conceptual usage of the macro logic:
    #[protect(any("admin", "editor"))]
    // OR
    #[protect(all(any("user"), expr(is_active)))]
    // OR
    #[protect("super_user")]