How to use casbin authorization in your rust web-app [Part - 3] 
Tags: casbin, authorization, rust
Published: 2021-06-12 04:17:21

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` - 
```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"] }
```
Include the `casbin.conf`. (see repo)
And the `preset_policy.csv`. (see the repo)
Create a `.env` - 
```.env
APP_HOST=127.0.0.1
APP_PORT=8080
DATABASE_URL=postgres://databasename:password@127.0.0.1:5432/test
POOL_SIZE=8
HASH_ROUNDS=12
```

Then, in the src folder, modify the `main.rs` - 
Import external crates first and modules(to be made later) - 
```rs
#![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;
```

We spawn a server using `HttpServer`.
All default values such as `APP_HOST` are defined in the `.env`.
We define a connection pool -
```rs
let pool = config::db::migrate_and_config_db(&database_url, pool_size);
```
With a default pool size of 8.
We import our casbin model - 
```rs
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!()
        }
    }
```
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
```
.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
```
This creates the `schema.rs` in the `src/` dir. You can check that out.
In the `config/` dir created before, define the database config - 
```rs
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
}
```
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 -
```rs
#![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},
};
```
Then we'll create a public struct -
```rs
pub struct Authentication;
```
Now implement a trait `Transform` (see docs) - 
```rs
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)),
        })
    }
}
```
`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` - 
```rs
pub struct AuthenticationMiddleware<S> {
    service: Rc<RefCell<S>>,
}
```
Implement `Service` for `AuthenticationMiddleware` (see docs) -
```rs
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>>>>;
..
..
```
`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
```rs
fn call(&mut self, mut req: ServiceRequest) -> Self::Future {
..
..
}
```
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` -
```rs
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"),
);
```
This is the main logic in this file - 
```rs
        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");
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
```
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 - 
```rs
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(),
                ))
            })
        }
```
We then use this `authn.rs` in our `main.rs` when we spawn the our http server - 
```rs
    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?;
```
That's it.
This is how casbin can be used in an actix-web app.