![](/style/images/good.png)
![](/style/images/bad.png)
How to use casbin authorization in your rust web-app [Part - 3]
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"] }
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
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;
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);
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!()
}
}
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 -
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 -
#![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 -
pub struct Authentication;
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)),
})
}
}
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>>,
}
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>>>>;
..
..
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 {
..
..
}
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"),
);
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");
}
}
}
}
}
}
}
}
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(),
))
})
}
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?;
That's it.
This is how casbin can be used in an actix-web app.
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK