17

How to use casbin authorization in your rust web-app [Part - 3]

 3 years ago
source link: https://dev.to/smrpn/how-to-use-casbin-authorization-in-your-rust-web-app-part-3-4g2f
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

How to use casbin authorization in your rust web-app [Part - 3]

Jun 12

・6 min read

In this blog we'll make a new project in which we'll use the authorization model talked about in the previous blog.
Here is the link to the github repository for reference -
https://github.com/casbin-rs/examples/tree/master/actix-middleware-example

We'll make a simple anonynous forum app using Actix-web, Casbin and Diesel, with JWT support.
There will be 2 roles in this app - admin and user

So, let's start.
First, configure the Cargo.toml -

[dependencies]
http = "0.2.1"
actix =  "0.11.0"
actix-web = "3.3.2"
actix-service = "2.0.0"
actix-rt = "1.1.1"
actix-cors = "0.4.0"
futures = "0.3.5"
failure = "0.1.8"
serde = "1.0.116"
serde_derive = "1.0.116"
serde_json = "1.0.57"
derive_more = "0.99.10"
chrono = { version = "0.4.18", features = ["serde"] }
diesel = { version = "1.4.5", features = ["postgres","r2d2", "chrono"] }
diesel_migrations = "1.4.0"
dotenv = "0.15.0"
env_logger = "0.8.1"
log = "0.4.11"
jsonwebtoken = "7.2.0"
bcrypt = "0.9.0"
csv = "1.1.3"
walkdir = "2.3.1"
actix-casbin= {version = "0.4.2", default-features = false, features = [ "runtime-async-std" ]}
actix-casbin-auth = {version = "0.4.4", default-features = false, features = [ "runtime-async-std" ]}
diesel-adapter = { version = "0.8.1", default-features = false, features = ["postgres","runtime-async-std"] }
uuid = {version = "0.8.1", features = ["v4"] }
Enter fullscreen modeExit fullscreen mode

Include the casbin.conf. (see repo)
And the preset_policy.csv. (see the repo)
Create a .env -

APP_HOST=127.0.0.1
APP_PORT=8080
DATABASE_URL=postgres://databasename:[email protected]:5432/test
POOL_SIZE=8
HASH_ROUNDS=12
Enter fullscreen modeExit fullscreen mode

Then, in the src folder, modify the main.rs -
Import external crates first and modules(to be made later) -

#![allow(proc_macro_derive_resolution_fallback)]

#[macro_use]
extern crate diesel;
#[macro_use]
extern crate log;
#[macro_use]
extern crate diesel_migrations;
#[macro_use]
extern crate serde_derive;
#[macro_use]
extern crate serde_json;

use crate::utils::csv_utils::{load_csv, walk_csv};
use actix::Supervisor;
use actix_casbin::casbin::{
    function_map::key_match2, CachedEnforcer, CoreApi, DefaultModel, MgmtApi, Result,
};
use actix_casbin::CasbinActor;
use actix_casbin_auth::CasbinService;
use actix_cors::Cors;
use actix_web::middleware::normalize::TrailingSlash;
use actix_web::middleware::Logger;
use actix_web::middleware::NormalizePath;
use actix_web::{App, HttpServer};
use diesel_adapter::DieselAdapter;
use std::env;

mod api;
mod config;
mod constants;
mod errors;
mod middleware;
mod models;
mod routers;
mod schema;
mod services;
mod utils;
Enter fullscreen modeExit fullscreen mode

We spawn a server using HttpServer.
All default values such as APP_HOST are defined in the .env.
We define a connection pool -

let pool = config::db::migrate_and_config_db(&database_url, pool_size);
Enter fullscreen modeExit fullscreen mode

With a default pool size of 8.
We import our casbin model -

let model = DefaultModel::from_file("casbin.conf").await?;
    let adapter = DieselAdapter::new(database_url, pool_size)?;
    let mut casbin_middleware = CasbinService::new(model, adapter).await.unwrap();
    casbin_middleware
        .write()
        .await
        .get_role_manager()
        .write()
        .unwrap()
        .matching_fn(Some(key_match2), None);

    let share_enforcer = casbin_middleware.get_enforcer();
    let clone_enforcer = share_enforcer.clone();
    let casbin_actor = CasbinActor::<CachedEnforcer>::set_enforcer(share_enforcer)?;
    let started_actor = Supervisor::start(|_| casbin_actor);
 let preset_rules = load_csv(walk_csv("."));
    for mut policy in preset_rules {
        let ptype = policy.remove(0);
        if ptype.starts_with('p') {
            match clone_enforcer.write().await.add_policy(policy).await {
                Ok(_) => info!("Preset policies(p) add successfully"),
                Err(err) => error!("Preset policies(p) add error: {}", err.to_string()),
            };
            continue;
        } else if ptype.starts_with('g') {
            match clone_enforcer
                .write()
                .await
                .add_named_grouping_policy(&ptype, policy)
                .await
            {
                Ok(_) => info!("Preset policies(p) add successfully"),
                Err(err) => error!("Preset policies(g) add error: {}", err.to_string()),
            };
            continue;
        } else {
            unreachable!()
        }
    }
Enter fullscreen modeExit fullscreen mode

Then we can define our modules.
Inside the src/ dir, make the following dirs - api/, config/, middleware/, models/, routers/, services/ and utils/.
Also make these files - constants.rs and errors.rs.
Create models in models/ -
post.rs, response.rs, user.rs and user_token.rs

Run the following command in the root of the project -

cargo install diesel_cli --no-default-features --features postgres
Enter fullscreen modeExit fullscreen mode

.env DATABASE_URL property that Diesel will use to get the connection details of your Postgres instance.
Now run diesel setup in the project root folder . Diesel will create a new database (confessions), as well as a set of empty migrations.

Now run -

diesel migration generate casbin_rules post users ⏎
diesel migration run
Enter fullscreen modeExit fullscreen mode

This creates the schema.rs in the src/ dir. You can check that out.
In the config/ dir created before, define the database config -

pub fn migrate_and_config_db(url: &str, pool_size: u32) -> Pool {
    info!("Migrating and configurating database...");
    let manager = ConnectionManager::<Connection>::new(url);
    let pool = r2d2::Pool::builder()
        .connection_timeout(Duration::from_secs(10))
        .max_size(pool_size)
        .build(manager)
        .expect("Failed to create pool.");
    embedded_migrations::run(&pool.get().expect("Failed to migrate."))
        .expect("Failed to migrate.");

    pool
}
Enter fullscreen modeExit fullscreen mode

Now, let's write the middleware. In the middleware/ dir, make a file authn.rs.
This is where we implement role-Based HTTP authorization.
Import the external crates and libs -

#![allow(clippy::type_complexity)]
use crate::{
    config::db::Pool, constants, models::response::ResponseBody, utils::token_utils,
};

use actix_casbin_auth::CasbinVals;
use actix_service::{Service, Transform};
use actix_web::{
    dev::{ServiceRequest, ServiceResponse},
    http::{HeaderName, HeaderValue, Method},
    web::Data,
    Error, HttpMessage, HttpResponse,
};
use futures::{
    future::{ok, Ready},
    Future,
};
use std::cell::RefCell;
use std::rc::Rc;
use std::{
    pin::Pin,
    task::{Context, Poll},
};
Enter fullscreen modeExit fullscreen mode

Then we'll create a public struct -

pub struct Authentication;
Enter fullscreen modeExit fullscreen mode

Now implement a trait Transform (see docs) -

impl<S, B> Transform<S, ServiceRequest> for Authentication
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    B: MessageBody,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = AuthenticationMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ok(AuthenticationMiddleware {
            service: Rc::new(RefCell::new(service)),
        })
    }
}
Enter fullscreen modeExit fullscreen mode

Response, Error, InitError, Transform and Future are all associated types defined in the default implementations of the trait Transform.
The new_transform function returns a Future.

We make another public struct AuthenticationMiddleware -

pub struct AuthenticationMiddleware<S> {
    service: Rc<RefCell<S>>,
}
Enter fullscreen modeExit fullscreen mode

Implement Service for AuthenticationMiddleware (see docs) -

impl<S, B> Service<ServiceRequest> for AuthenticationMiddleware<S>
where 
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    B: MessageBody,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;
..
..
Enter fullscreen modeExit fullscreen mode

poll_ready is an underlying method that makes a Future work, similar to the regular poll on the Future trait.
Define a function call inside the Service impl

fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
..
..
}
Enter fullscreen modeExit fullscreen mode

Inside the call function, we define certain variables.
We store the casbin service which is a smart pointer in rust - Rc<RefCell<S>>.
Why do we use Rc<RefCell<>>? - Well, that is because the the actix actor is single-threaded, whereas our casbin enforcer is multi-threaded, hence the service is a pointer to each thread.
Then we have authenticate_pass, public_route and authenticate_username -

let mut srv = self.service.clone();
let mut authenticate_pass: bool = false;
let mut public_route: bool = false;
let mut authenticate_username: String = String::from("");

// Bypass some account routes
let headers = req.headers_mut();
headers.append(
    HeaderName::from_static("content-length"),
    HeaderValue::from_static("true"),
);
Enter fullscreen modeExit fullscreen mode

This is the main logic in this file -

        if Method::OPTIONS == *req.method() {
            authenticate_pass = true;
        } else {
            for ignore_route in constants::IGNORE_ROUTES.iter() {
                if req.path().starts_with(ignore_route) {
                    authenticate_pass = true;
                    public_route = true;
                }
            }
            if !authenticate_pass {
                if let Some(pool) = req.app_data::<Data<Pool>>() {
                    info!("Connecting to database...");
                    if let Some(authen_header) =
                        req.headers().get(constants::AUTHORIZATION)
                    {
                        info!("Parsing authorization header...");
                        if let Ok(authen_str) = authen_header.to_str() {
                            if authen_str.starts_with("bearer")
                                || authen_str.starts_with("Bearer")
                            {
                                info!("Parsing token...");
                                let token = authen_str[6..].trim();
                                if let Ok(token_data) =
                                    token_utils::decode_token(token.to_string())
                                {
                                    info!("Decoding token...");
                                    if token_utils::verify_token(&token_data, pool)
                                        .is_ok()
                                    {
                                        info!("Valid token");
                                        authenticate_username = token_data.claims.user;
                                        authenticate_pass = true;
                                    } else {
                                        error!("Invalid token");
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
Enter fullscreen modeExit fullscreen mode

Pretty straightforward - connect to db, get auth token from headers, parse token, decode token, verify token, authenticate.
Then casbin checks if the particular user is authorized to access the route -

if authenticate_pass {
            if public_route {
                let vals = CasbinVals {
                    subject: "anonymous".to_string(),
                    domain: None,
                };
                req.extensions_mut().insert(vals);
                Box::pin(async move { srv.call(req).await })
            } else {
                let vals = CasbinVals {
                    subject: authenticate_username,
                    domain: None,
                };
                req.extensions_mut().insert(vals);
                Box::pin(async move { srv.clone().call(req).await })
            }
        } else {
            Box::pin(async move {
                Ok(req.into_response(
                    HttpResponse::Unauthorized()
                        .json(ResponseBody::new(
                            constants::MESSAGE_INVALID_TOKEN,
                            constants::EMPTY,
                        ))
                        .into_body(),
                ))
            })
        }
Enter fullscreen modeExit fullscreen mode

We then use this authn.rs in our main.rs when we spawn the our http server -

    HttpServer::new(move || {
        App::new()
            .data(pool.clone())
            .data(started_actor.clone())
            .wrap(
                Cors::new()
                    .send_wildcard()
                    .allowed_methods(vec!["GET", "POST", "DELETE"])
                    .allowed_headers(vec![
                        http::header::AUTHORIZATION,
                        http::header::ACCEPT,
                    ])
                    .allowed_header(http::header::CONTENT_TYPE)
                    .max_age(3600)
                    .finish(),
            )
            .wrap(NormalizePath::new(TrailingSlash::Trim))
            .wrap(Logger::default())
            .wrap(casbin_middleware.clone())
            .wrap(crate::middleware::authn::Authentication)
            .configure(routers::routes)
    })
    .bind(&app_url)?
    .run()
    .await?;
Enter fullscreen modeExit fullscreen mode

That's it.
This is how casbin can be used in an actix-web app.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK