

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
-
155
Basic Role-Based HTTP Authorization in Go with Casbin Authentication and Authorization are essential parts of any secured Web Application. I recently finished writing my first serious web application in Go and t...
-
173
Iris + Casbin 权限控制实战
-
119
README.md Gin Admin 基于 Gin + GORM + Casbin + Ant Design React 实现的RBAC权限管理脚手架,目的是提供一套轻量的中后台开发框架,方便、快速的完成业务...
-
61
简介 Casbin可以做到: 支持自定义请求的格式,默认的请求格式为{subject, object, action}。 具有访问控制模型model和策略policy两个核心概念。 支持RBAC中的多层角色继承,不止主体可以有...
-
24
简介 权限管理在几乎每个系统中都是必备的模块。如果项目开发每次都要实现一次权限管理,无疑会浪费开发时间,增加开发成本。因此, casbin 库出现了。 casbin 是一个强大、高效的访问控制库。支持...
-
37
原文 ,省略了一些无关的内容。 如果你搜到这篇文章,那么什么是gin以及casbin应该不用过多解释了。 项目结构
-
17
集成gin和casbin9月 28, 2020 发布在 Golang, 菜鸟翻译...
-
60
Casdoor A UI-first centralized authenticatio...
-
16
Authentication and authorization in Gin application with JWT and CasbinIntroductionJWT ConceptsJSON Web Token (JWT) is an open standard (RFC 7519) that de...
-
10
Casbin开源社区推出开源身份认证、单点登录框架Casdoor! douke0320 · 7天之前 · 385 次点击 · 预...
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK