├── src ├── lib.rs └── middleware.rs ├── examples ├── rbac_policy.csv ├── rbac_with_domains_policy.csv ├── rbac_with_pattern_policy.csv ├── rbac_model.conf ├── rbac_with_pattern_model.conf └── rbac_with_domains_model.conf ├── .gitignore ├── .github └── workflows │ ├── coverage.yml │ ├── release.yml │ └── ci.yml ├── Cargo.toml ├── tests ├── test_middleware_domain.rs ├── test_middleware.rs ├── test_set_enforcer.rs └── test_custom_error_handlers.rs └── README.md /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use casbin; 2 | pub mod middleware; 3 | pub use middleware::{CasbinMiddleware, CasbinService, CasbinVals}; 4 | -------------------------------------------------------------------------------- /examples/rbac_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, data1, read 2 | p, bob, data2, write 3 | p, data2_admin, data2, read 4 | p, data2_admin, data2, write 5 | g, alice, data2_admin 6 | -------------------------------------------------------------------------------- /examples/rbac_with_domains_policy.csv: -------------------------------------------------------------------------------- 1 | p, admin, domain1, /pen/1, GET 2 | p, admin, domain1, /pen/2, GET 3 | p, admin, domain2, /book/1, GET 4 | p, admin, domain2, /book/2, GET 5 | g, alice, admin, domain1 6 | g, bob, admin, domain2 7 | -------------------------------------------------------------------------------- /examples/rbac_with_pattern_policy.csv: -------------------------------------------------------------------------------- 1 | p, alice, /pen/1, GET 2 | p, alice, /pen2/1, GET 3 | p, book_admin, book_group, GET 4 | p, pen_admin, pen_group, GET 5 | 6 | g, alice, book_admin 7 | g, bob, pen_admin 8 | g2, /book/:id, book_group 9 | g2, /pen/:id, pen_group -------------------------------------------------------------------------------- /examples/rbac_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub) && r.obj == p.obj && r.act == p.act -------------------------------------------------------------------------------- /examples/rbac_with_pattern_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, obj, act 3 | 4 | [policy_definition] 5 | p = sub, obj, act 6 | 7 | [role_definition] 8 | g = _, _ 9 | g2 = _, _ 10 | 11 | [policy_effect] 12 | e = some(where (p.eft == allow)) 13 | 14 | [matchers] 15 | m = g(r.sub, p.sub) && g2(r.obj, p.obj) && regexMatch(r.act, p.act) 16 | -------------------------------------------------------------------------------- /examples/rbac_with_domains_model.conf: -------------------------------------------------------------------------------- 1 | [request_definition] 2 | r = sub, dom, obj, act 3 | 4 | [policy_definition] 5 | p = sub, dom, obj, act 6 | 7 | [role_definition] 8 | g = _, _, _ 9 | 10 | [policy_effect] 11 | e = some(where (p.eft == allow)) 12 | 13 | [matchers] 14 | m = g(r.sub, p.sub, r.dom) && r.dom == p.dom && r.obj == p.obj && regexMatch(r.act, p.act) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated by Cargo 2 | # will have compiled files and executables 3 | /target 4 | 5 | # Remove Cargo.lock from gitignore if creating an executable, leave it for libraries 6 | # More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html 7 | Cargo.lock 8 | 9 | # These are backup files generated by rustfmt 10 | **/*.rs.bk 11 | 12 | # Ignore IDE files 13 | .vscode/ 14 | .idea/ 15 | .DS_Store 16 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Code Coverage 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | cover: 13 | name: Auto Codecov Coverage 14 | runs-on: ubuntu-latest 15 | 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Rust toolchain 21 | run: | 22 | rustup set profile minimal 23 | rustup update --no-self-update stable 24 | rustup default stable 25 | 26 | - name: Run cargo-tarpaulin 27 | run: | 28 | cargo install cargo-tarpaulin 29 | cargo tarpaulin --out xml 30 | 31 | - name: Upload to codecov.io 32 | uses: codecov/codecov-action@v4 33 | with: 34 | token: ${{secrets.CODECOV_TOKEN}} -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Auto Release 2 | 3 | on: 4 | push: 5 | # Sequence of patterns matched against refs/tags 6 | tags: 7 | - "v*" # Push events to matching v*, i.e. v1.0, v20.15.10 8 | 9 | jobs: 10 | release: 11 | name: Auto Release by Tags 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - name: Checkout Repository 16 | uses: actions/checkout@v4 17 | 18 | - name: Install Rust toolchain 19 | run: | 20 | rustup set profile minimal 21 | rustup update --no-self-update stable 22 | rustup default stable 23 | 24 | - name: Cargo Login 25 | run: cargo login ${{ secrets.CARGO_TOKEN }} 26 | 27 | - name: Cargo Publish 28 | run: cargo publish 29 | 30 | - name: GitHub Release 31 | id: create_release 32 | uses: actions/create-release@v1 33 | env: 34 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # This token is provided by Actions, you do not need to create your own token 35 | with: 36 | tag_name: ${{ github.ref }} 37 | release_name: Release ${{ github.ref }} 38 | draft: false 39 | prerelease: false 40 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Auto Build CI 14 | runs-on: ${{ matrix.os }} 15 | strategy: 16 | matrix: 17 | os: [ubuntu-latest, windows-latest, macOS-latest] 18 | rust: [nightly, beta, stable] 19 | 20 | steps: 21 | - name: Checkout Repository 22 | uses: actions/checkout@v4 23 | 24 | - name: Install Rust toolchain 25 | run: | 26 | rustup set profile minimal 27 | rustup update --no-self-update ${{ matrix.rust }} 28 | rustup component add --toolchain ${{ matrix.rust }} rustfmt clippy 29 | rustup default ${{ matrix.rust }} 30 | 31 | - name: Install Dependencies (for ubuntu) 32 | if: matrix.os == 'ubuntu-latest' 33 | run: | 34 | sudo apt-get install libssl-dev 35 | 36 | - name: Cargo Build 37 | run: cargo build 38 | 39 | - name: Cargo Test For tokio 40 | run: cargo test --no-default-features --features runtime-tokio rt 41 | 42 | - name: Cargo Test For async-std 43 | run: cargo test --no-default-features --features runtime-async-std 44 | 45 | - name: Cargo Clippy 46 | run: cargo clippy -- -D warnings 47 | 48 | - name: Cargo Fmt Check 49 | run: cargo fmt --all -- --check 50 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "actix-casbin-auth" 3 | version = "1.1.0" 4 | authors = ["Eason Chai ","Cheng JIANG "] 5 | edition = "2018" 6 | license = "Apache-2.0" 7 | description = "Casbin actix-web access control middleware" 8 | repository = "https://github.com/casbin-rs/actix-casbin-auth" 9 | readme= "README.md" 10 | 11 | [lib] 12 | name = "actix_casbin_auth" 13 | path = "src/lib.rs" 14 | 15 | [dependencies] 16 | casbin = { version = "2.0.9", default-features = false, features = ["incremental", "cached"] } 17 | tokio = { version = "1.17.0", default-features = false, optional = true } 18 | async-std = { version = "1.10.0", default-features = false, optional = true } 19 | actix-web = { version = "4.0.1", default-features = false } 20 | actix-service = "2.0.0" 21 | futures = "0.3" 22 | 23 | [features] 24 | default = ["runtime-tokio"] 25 | explain = ["casbin/explain"] 26 | logging = ["casbin/logging"] 27 | 28 | runtime-tokio = ["casbin/runtime-tokio", "tokio/sync"] 29 | runtime-async-std = ["casbin/runtime-async-std", "async-std/std"] 30 | 31 | [dev-dependencies] 32 | tokio = { version = "1.17.0", features = [ "full" ] } 33 | async-std = { version = "1.10.0", features = [ "attributes" ] } 34 | actix-rt = "2.7.0" 35 | serde_json = "1.0" 36 | 37 | [profile.release] 38 | codegen-units = 1 39 | lto = true 40 | opt-level = 3 41 | 42 | [profile.dev] 43 | split-debuginfo = "unpacked" 44 | -------------------------------------------------------------------------------- /tests/test_middleware_domain.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::pin::Pin; 3 | use std::rc::Rc; 4 | use std::task::{Context, Poll}; 5 | 6 | use actix_service::{Service, Transform}; 7 | use actix_web::{ 8 | body::MessageBody, dev::ServiceRequest, dev::ServiceResponse, Error, HttpMessage, HttpResponse, 9 | }; 10 | use futures::future::{ok, Future, Ready}; 11 | 12 | use actix_casbin_auth::{CasbinService, CasbinVals}; 13 | 14 | use actix_web::{test, web, App}; 15 | use casbin::{DefaultModel, FileAdapter}; 16 | 17 | pub struct FakeAuth; 18 | 19 | impl Transform for FakeAuth 20 | where 21 | S: Service, Error = Error> + 'static, 22 | B: MessageBody, 23 | { 24 | type Response = ServiceResponse; 25 | type Error = Error; 26 | type InitError = (); 27 | type Transform = FakeAuthMiddleware; 28 | type Future = Ready>; 29 | 30 | fn new_transform(&self, service: S) -> Self::Future { 31 | ok(FakeAuthMiddleware { 32 | service: Rc::new(RefCell::new(service)), 33 | }) 34 | } 35 | } 36 | 37 | pub struct FakeAuthMiddleware { 38 | service: Rc>, 39 | } 40 | 41 | impl Service for FakeAuthMiddleware 42 | where 43 | S: Service, Error = Error> + 'static, 44 | B: MessageBody, 45 | { 46 | type Response = ServiceResponse; 47 | type Error = Error; 48 | type Future = Pin>>>; 49 | 50 | fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { 51 | self.service.poll_ready(cx) 52 | } 53 | 54 | fn call(&self, req: ServiceRequest) -> Self::Future { 55 | let svc = self.service.clone(); 56 | 57 | Box::pin(async move { 58 | let vals = CasbinVals { 59 | subject: String::from("alice"), 60 | domain: Option::from(String::from("domain1")), 61 | }; 62 | req.extensions_mut().insert(vals); 63 | svc.call(req).await 64 | }) 65 | } 66 | } 67 | 68 | #[actix_rt::test] 69 | async fn test_middleware() { 70 | let m = DefaultModel::from_file("examples/rbac_with_domains_model.conf") 71 | .await 72 | .unwrap(); 73 | let a = FileAdapter::new("examples/rbac_with_domains_policy.csv"); 74 | 75 | let casbin_middleware = CasbinService::new(m, a).await.unwrap(); 76 | 77 | let mut app = test::init_service( 78 | App::new() 79 | .wrap(casbin_middleware.clone()) 80 | .wrap(FakeAuth) 81 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())) 82 | .route("/book/1", web::get().to(|| HttpResponse::Ok())), 83 | ) 84 | .await; 85 | 86 | let req_pen = test::TestRequest::get().uri("/pen/1").to_request(); 87 | let resp_pen = test::call_service(&mut app, req_pen).await; 88 | assert!(resp_pen.status().is_success()); 89 | 90 | let req_book = test::TestRequest::get().uri("/book/1").to_request(); 91 | let resp_book = test::call_service(&mut app, req_book).await; 92 | assert!(!resp_book.status().is_success()); 93 | } 94 | -------------------------------------------------------------------------------- /tests/test_middleware.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::pin::Pin; 3 | use std::rc::Rc; 4 | use std::task::{Context, Poll}; 5 | 6 | use actix_service::{Service, Transform}; 7 | use actix_web::{ 8 | body::MessageBody, dev::ServiceRequest, dev::ServiceResponse, Error, HttpMessage, HttpResponse, 9 | }; 10 | use futures::future::{ok, Future, Ready}; 11 | 12 | use actix_casbin_auth::{CasbinService, CasbinVals}; 13 | 14 | use actix_web::{test, web, App}; 15 | use casbin::function_map::key_match2; 16 | use casbin::{CoreApi, DefaultModel, FileAdapter}; 17 | 18 | pub struct FakeAuth; 19 | 20 | impl Transform for FakeAuth 21 | where 22 | S: Service, Error = Error> + 'static, 23 | B: MessageBody, 24 | { 25 | type Response = ServiceResponse; 26 | type Error = Error; 27 | type InitError = (); 28 | type Transform = FakeAuthMiddleware; 29 | type Future = Ready>; 30 | 31 | fn new_transform(&self, service: S) -> Self::Future { 32 | ok(FakeAuthMiddleware { 33 | service: Rc::new(RefCell::new(service)), 34 | }) 35 | } 36 | } 37 | 38 | pub struct FakeAuthMiddleware { 39 | service: Rc>, 40 | } 41 | 42 | impl Service for FakeAuthMiddleware 43 | where 44 | S: Service, Error = Error> + 'static, 45 | B: MessageBody, 46 | { 47 | type Response = ServiceResponse; 48 | type Error = Error; 49 | type Future = Pin>>>; 50 | 51 | fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { 52 | self.service.poll_ready(cx) 53 | } 54 | 55 | fn call(&self, req: ServiceRequest) -> Self::Future { 56 | let svc = self.service.clone(); 57 | 58 | Box::pin(async move { 59 | let vals = CasbinVals { 60 | subject: String::from("alice"), 61 | domain: None, 62 | }; 63 | req.extensions_mut().insert(vals); 64 | svc.call(req).await 65 | }) 66 | } 67 | } 68 | 69 | #[actix_rt::test] 70 | async fn test_middleware() { 71 | let m = DefaultModel::from_file("examples/rbac_with_pattern_model.conf") 72 | .await 73 | .unwrap(); 74 | let a = FileAdapter::new("examples/rbac_with_pattern_policy.csv"); 75 | 76 | let casbin_middleware = CasbinService::new(m, a).await.unwrap(); 77 | 78 | casbin_middleware 79 | .write() 80 | .await 81 | .get_role_manager() 82 | .write() 83 | .matching_fn(Some(key_match2), None); 84 | 85 | let mut app = test::init_service( 86 | App::new() 87 | .wrap(casbin_middleware.clone()) 88 | .wrap(FakeAuth) 89 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())) 90 | .route("/book/{id}", web::get().to(|| HttpResponse::Ok())), 91 | ) 92 | .await; 93 | 94 | let req_pen = test::TestRequest::get().uri("/pen/1").to_request(); 95 | let resp_pen = test::call_service(&mut app, req_pen).await; 96 | assert!(resp_pen.status().is_success()); 97 | 98 | let req_book = test::TestRequest::get().uri("/book/2").to_request(); 99 | let resp_book = test::call_service(&mut app, req_book).await; 100 | assert!(resp_book.status().is_success()); 101 | } 102 | -------------------------------------------------------------------------------- /tests/test_set_enforcer.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::pin::Pin; 3 | use std::rc::Rc; 4 | use std::task::{Context, Poll}; 5 | 6 | use actix_service::{Service, Transform}; 7 | use actix_web::{ 8 | body::MessageBody, dev::ServiceRequest, dev::ServiceResponse, Error, HttpMessage, HttpResponse, 9 | }; 10 | use futures::future::{ok, Future, Ready}; 11 | 12 | use actix_casbin_auth::{CasbinService, CasbinVals}; 13 | 14 | use actix_web::{test, web, App}; 15 | use casbin::function_map::key_match2; 16 | use casbin::{CachedEnforcer, CoreApi, DefaultModel, FileAdapter}; 17 | 18 | use std::sync::Arc; 19 | 20 | #[cfg(feature = "runtime-tokio")] 21 | use tokio::sync::RwLock; 22 | 23 | #[cfg(feature = "runtime-async-std")] 24 | use async_std::sync::RwLock; 25 | 26 | pub struct FakeAuth; 27 | 28 | impl Transform for FakeAuth 29 | where 30 | S: Service, Error = Error> + 'static, 31 | B: MessageBody, 32 | { 33 | type Response = ServiceResponse; 34 | type Error = Error; 35 | type InitError = (); 36 | type Transform = FakeAuthMiddleware; 37 | type Future = Ready>; 38 | 39 | fn new_transform(&self, service: S) -> Self::Future { 40 | ok(FakeAuthMiddleware { 41 | service: Rc::new(RefCell::new(service)), 42 | }) 43 | } 44 | } 45 | 46 | pub struct FakeAuthMiddleware { 47 | service: Rc>, 48 | } 49 | 50 | impl Service for FakeAuthMiddleware 51 | where 52 | S: Service, Error = Error> + 'static, 53 | B: MessageBody, 54 | { 55 | type Response = ServiceResponse; 56 | type Error = Error; 57 | type Future = Pin>>>; 58 | 59 | fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { 60 | self.service.poll_ready(cx) 61 | } 62 | 63 | fn call(&self, req: ServiceRequest) -> Self::Future { 64 | let svc = self.service.clone(); 65 | 66 | Box::pin(async move { 67 | let vals = CasbinVals { 68 | subject: String::from("alice"), 69 | domain: None, 70 | }; 71 | req.extensions_mut().insert(vals); 72 | svc.call(req).await 73 | }) 74 | } 75 | } 76 | 77 | #[actix_rt::test] 78 | async fn test_set_enforcer() { 79 | let m = DefaultModel::from_file("examples/rbac_with_pattern_model.conf") 80 | .await 81 | .unwrap(); 82 | let a = FileAdapter::new("examples/rbac_with_pattern_policy.csv"); 83 | 84 | let enforcer = Arc::new(RwLock::new(CachedEnforcer::new(m, a).await.unwrap())); 85 | 86 | let casbin_middleware = CasbinService::set_enforcer(enforcer); 87 | 88 | casbin_middleware 89 | .write() 90 | .await 91 | .get_role_manager() 92 | .write() 93 | .matching_fn(Some(key_match2), None); 94 | 95 | let mut app = test::init_service( 96 | App::new() 97 | .wrap(casbin_middleware.clone()) 98 | .wrap(FakeAuth) 99 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())) 100 | .route("/book/{id}", web::get().to(|| HttpResponse::Ok())), 101 | ) 102 | .await; 103 | 104 | let req_pen = test::TestRequest::get().uri("/pen/1").to_request(); 105 | let resp_pen = test::call_service(&mut app, req_pen).await; 106 | assert!(resp_pen.status().is_success()); 107 | 108 | let req_book = test::TestRequest::get().uri("/book/2").to_request(); 109 | let resp_book = test::call_service(&mut app, req_book).await; 110 | assert!(resp_book.status().is_success()); 111 | } 112 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Actix Casbin Middleware 2 | 3 | [![Crates.io](https://img.shields.io/crates/d/actix-casbin-auth)](https://crates.io/crates/actix-casbin-auth) 4 | [![Docs](https://docs.rs/actix-casbin-auth/badge.svg)](https://docs.rs/actix-casbin-auth) 5 | [![CI](https://github.com/casbin-rs/actix-casbin-auth/actions/workflows/ci.yml/badge.svg)](https://github.com/casbin-rs/actix-casbin-auth/actions/workflows/ci.yml) 6 | [![codecov](https://codecov.io/gh/casbin-rs/actix-casbin-auth/branch/master/graph/badge.svg)](https://codecov.io/gh/casbin-rs/actix-casbin-auth) 7 | 8 | [Casbin](https://github.com/casbin/casbin-rs) access control middleware for [actix-web](https://github.com/actix/actix-web) framework 9 | 10 | ## Install 11 | 12 | Add dependencies 13 | 14 | ```bash 15 | cargo add actix-rt 16 | cargo add actix-web 17 | cargo add actix-casbin --no-default-features --features runtime-async-std 18 | cargo add actix-casbin-auth --no-default-features --features runtime-async-std 19 | ``` 20 | 21 | ## Requirement 22 | 23 | **Casbin only takes charge of permission control**, so you need to implement an `Authentication Middleware` to identify user. 24 | 25 | You should put `actix_casbin_auth::CasbinVals` which contains `subject`(username) and `domain`(optional) into [Extension](https://docs.rs/actix-web/2.0.0/actix_web/dev/struct.Extensions.html). 26 | 27 | For example: 28 | 29 | ```rust 30 | use std::cell::RefCell; 31 | use std::pin::Pin; 32 | use std::rc::Rc; 33 | use std::task::{Context, Poll}; 34 | 35 | use actix_service::{Service, Transform}; 36 | use actix_web::{dev::ServiceRequest, dev::ServiceResponse, Error, HttpMessage}; 37 | use futures::future::{ok, Future, Ready}; 38 | 39 | use actix_casbin_auth::CasbinVals; 40 | 41 | 42 | pub struct FakeAuth; 43 | 44 | impl Transform for FakeAuth 45 | where 46 | S: Service, Error = Error>, 47 | S::Future: 'static, 48 | B: 'static, 49 | { 50 | type Response = ServiceResponse; 51 | type Error = Error; 52 | type InitError = (); 53 | type Transform = FakeAuthMiddleware; 54 | type Future = Ready>; 55 | 56 | fn new_transform(&self, service: S) -> Self::Future { 57 | ok(FakeAuthMiddleware { 58 | service: Rc::new(RefCell::new(service)), 59 | }) 60 | } 61 | } 62 | 63 | pub struct FakeAuthMiddleware { 64 | service: Rc>, 65 | } 66 | 67 | impl Service for FakeAuthMiddleware 68 | where 69 | S: Service, Error = Error> + 'static, 70 | S::Future: 'static, 71 | B: 'static, 72 | { 73 | type Response = ServiceResponse; 74 | type Error = Error; 75 | type Future = Pin>>>; 76 | 77 | fn poll_ready(&mut self, cx: &mut Context) -> Poll> { 78 | self.service.poll_ready(cx) 79 | } 80 | 81 | fn call(&mut self, req: ServiceRequest) -> Self::Future { 82 | let mut svc = self.service.clone(); 83 | 84 | Box::pin(async move { 85 | let vals = CasbinVals { 86 | subject: String::from("alice"), 87 | domain: None, 88 | }; 89 | req.extensions_mut().insert(vals); 90 | svc.call(req).await 91 | }) 92 | } 93 | } 94 | ```` 95 | 96 | 97 | ## Example 98 | 99 | ```rust 100 | use actix_casbin_auth::casbin::{DefaultModel, FileAdapter, Result}; 101 | use actix_casbin_auth::CasbinService; 102 | use actix_web::{web, App, HttpResponse, HttpServer}; 103 | use actix_casbin_auth::casbin::function_map::key_match2; 104 | 105 | #[allow(dead_code)] 106 | mod fake_auth; 107 | 108 | #[actix_rt::main] 109 | async fn main() -> Result<()> { 110 | let m = DefaultModel::from_file("examples/rbac_with_pattern_model.conf") 111 | .await 112 | .unwrap(); 113 | let a = FileAdapter::new("examples/rbac_with_pattern_policy.csv"); //You can also use diesel-adapter or sqlx-adapter 114 | 115 | let casbin_middleware = CasbinService::new(m, a).await?; 116 | 117 | casbin_middleware 118 | .write() 119 | .await 120 | .get_role_manager() 121 | .write() 122 | .unwrap() 123 | .matching_fn(Some(key_match2), None); 124 | 125 | HttpServer::new(move || { 126 | App::new() 127 | .wrap(casbin_middleware.clone()) 128 | .wrap(FakeAuth) 129 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())) 130 | .route("/book/{id}", web::get().to(|| HttpResponse::Ok())) 131 | }) 132 | .bind("127.0.0.1:8080")? 133 | .run() 134 | .await?; 135 | 136 | Ok(()) 137 | } 138 | ``` 139 | 140 | ## Customizing Error Responses 141 | 142 | By default, the middleware returns the following HTTP status codes: 143 | - `401 Unauthorized` - when no `CasbinVals` are found in request extensions 144 | - `403 Forbidden` - when access is denied by Casbin enforcer 145 | - `502 Bad Gateway` - when the Casbin enforcer returns an error 146 | 147 | You can customize these error responses to return structured JSON or any other format: 148 | 149 | ```rust 150 | use actix_casbin_auth::casbin::{DefaultModel, FileAdapter, Result}; 151 | use actix_casbin_auth::CasbinService; 152 | use actix_web::{web, App, HttpResponse, HttpServer}; 153 | use serde_json::json; 154 | 155 | #[actix_rt::main] 156 | async fn main() -> Result<()> { 157 | let m = DefaultModel::from_file("examples/rbac_with_pattern_model.conf") 158 | .await 159 | .unwrap(); 160 | let a = FileAdapter::new("examples/rbac_with_pattern_policy.csv"); 161 | 162 | let casbin_middleware = CasbinService::new(m, a) 163 | .await? 164 | .set_unauthorized_handler(|| { 165 | HttpResponse::Unauthorized() 166 | .json(json!({ 167 | "error": "Authentication required", 168 | "code": 401 169 | })) 170 | }) 171 | .set_forbidden_handler(|| { 172 | HttpResponse::Forbidden() 173 | .json(json!({ 174 | "error": "Access forbidden", 175 | "code": 403 176 | })) 177 | }) 178 | .set_error_handler(|| { 179 | HttpResponse::InternalServerError() 180 | .json(json!({ 181 | "error": "Internal server error", 182 | "code": 500 183 | })) 184 | }); 185 | 186 | HttpServer::new(move || { 187 | App::new() 188 | .wrap(casbin_middleware.clone()) 189 | .wrap(FakeAuth) 190 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())) 191 | .route("/book/{id}", web::get().to(|| HttpResponse::Ok())) 192 | }) 193 | .bind("127.0.0.1:8080")? 194 | .run() 195 | .await?; 196 | 197 | Ok(()) 198 | } 199 | ``` 200 | 201 | ## License 202 | 203 | This project is licensed under 204 | 205 | * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) 206 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | #![allow(clippy::type_complexity)] 2 | 3 | use std::cell::RefCell; 4 | use std::ops::{Deref, DerefMut}; 5 | use std::rc::Rc; 6 | use std::sync::Arc; 7 | use std::task::{Context, Poll}; 8 | 9 | use futures::future::{ok, LocalBoxFuture, Ready}; 10 | use futures::FutureExt; 11 | 12 | use actix_service::{Service, Transform}; 13 | use actix_web::{ 14 | body::{EitherBody, MessageBody}, 15 | dev::{ServiceRequest, ServiceResponse}, 16 | Error, HttpMessage, HttpResponse, Result, 17 | }; 18 | 19 | use casbin::prelude::{TryIntoAdapter, TryIntoModel}; 20 | use casbin::{CachedEnforcer, CoreApi, Result as CasbinResult}; 21 | 22 | #[cfg(feature = "runtime-tokio")] 23 | use tokio::sync::RwLock; 24 | 25 | #[cfg(feature = "runtime-async-std")] 26 | use async_std::sync::RwLock; 27 | 28 | #[derive(Clone)] 29 | pub struct CasbinVals { 30 | pub subject: String, 31 | pub domain: Option, 32 | } 33 | 34 | type ErrorHandler = Arc HttpResponse + Send + Sync>; 35 | 36 | #[derive(Clone)] 37 | pub struct CasbinService { 38 | enforcer: Arc>, 39 | unauthorized_handler: Option, 40 | forbidden_handler: Option, 41 | error_handler: Option, 42 | } 43 | 44 | impl CasbinService { 45 | pub async fn new(m: M, a: A) -> CasbinResult { 46 | let enforcer: CachedEnforcer = CachedEnforcer::new(m, a).await?; 47 | Ok(CasbinService { 48 | enforcer: Arc::new(RwLock::new(enforcer)), 49 | unauthorized_handler: None, 50 | forbidden_handler: None, 51 | error_handler: None, 52 | }) 53 | } 54 | 55 | pub fn get_enforcer(&mut self) -> Arc> { 56 | self.enforcer.clone() 57 | } 58 | 59 | pub fn set_enforcer(e: Arc>) -> CasbinService { 60 | CasbinService { 61 | enforcer: e, 62 | unauthorized_handler: None, 63 | forbidden_handler: None, 64 | error_handler: None, 65 | } 66 | } 67 | 68 | pub fn set_unauthorized_handler(mut self, handler: F) -> Self 69 | where 70 | F: Fn() -> HttpResponse + Send + Sync + 'static, 71 | { 72 | self.unauthorized_handler = Some(Arc::new(handler)); 73 | self 74 | } 75 | 76 | pub fn set_forbidden_handler(mut self, handler: F) -> Self 77 | where 78 | F: Fn() -> HttpResponse + Send + Sync + 'static, 79 | { 80 | self.forbidden_handler = Some(Arc::new(handler)); 81 | self 82 | } 83 | 84 | pub fn set_error_handler(mut self, handler: F) -> Self 85 | where 86 | F: Fn() -> HttpResponse + Send + Sync + 'static, 87 | { 88 | self.error_handler = Some(Arc::new(handler)); 89 | self 90 | } 91 | } 92 | 93 | impl Transform for CasbinService 94 | where 95 | S: Service, Error = Error> + 'static, 96 | B: MessageBody, 97 | { 98 | type Response = ServiceResponse>; 99 | type Error = Error; 100 | type InitError = (); 101 | type Transform = CasbinMiddleware; 102 | type Future = Ready>; 103 | 104 | fn new_transform(&self, service: S) -> Self::Future { 105 | ok(CasbinMiddleware { 106 | enforcer: self.enforcer.clone(), 107 | service: Rc::new(RefCell::new(service)), 108 | unauthorized_handler: self.unauthorized_handler.clone(), 109 | forbidden_handler: self.forbidden_handler.clone(), 110 | error_handler: self.error_handler.clone(), 111 | }) 112 | } 113 | } 114 | 115 | impl Deref for CasbinService { 116 | type Target = Arc>; 117 | 118 | fn deref(&self) -> &Self::Target { 119 | &self.enforcer 120 | } 121 | } 122 | 123 | impl DerefMut for CasbinService { 124 | fn deref_mut(&mut self) -> &mut Self::Target { 125 | &mut self.enforcer 126 | } 127 | } 128 | 129 | pub struct CasbinMiddleware { 130 | service: Rc>, 131 | enforcer: Arc>, 132 | unauthorized_handler: Option, 133 | forbidden_handler: Option, 134 | error_handler: Option, 135 | } 136 | 137 | impl Service for CasbinMiddleware 138 | where 139 | S: Service, Error = Error> + 'static, 140 | B: MessageBody, 141 | { 142 | type Response = ServiceResponse>; 143 | type Error = S::Error; 144 | type Future = LocalBoxFuture<'static, Result>; 145 | 146 | fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { 147 | self.service.poll_ready(cx) 148 | } 149 | 150 | fn call(&self, req: ServiceRequest) -> Self::Future { 151 | let cloned_enforcer = self.enforcer.clone(); 152 | let srv = self.service.clone(); 153 | let unauthorized_handler = self.unauthorized_handler.clone(); 154 | let forbidden_handler = self.forbidden_handler.clone(); 155 | let error_handler = self.error_handler.clone(); 156 | 157 | async move { 158 | let path = req.path().to_string(); 159 | let action = req.method().as_str().to_string(); 160 | let option_vals = req.extensions().get::().map(|x| x.to_owned()); 161 | let vals = match option_vals { 162 | Some(value) => value, 163 | None => { 164 | let response = unauthorized_handler 165 | .map(|h| h()) 166 | .unwrap_or_else(|| HttpResponse::Unauthorized().finish()); 167 | return Ok(req.into_response(response.map_into_right_body())); 168 | } 169 | }; 170 | let subject = vals.subject.clone(); 171 | 172 | if !vals.subject.is_empty() { 173 | if let Some(domain) = vals.domain { 174 | let mut lock = cloned_enforcer.write().await; 175 | match lock.enforce_mut(vec![subject, domain, path, action]) { 176 | Ok(true) => { 177 | drop(lock); 178 | srv.call(req).await.map(|res| res.map_into_left_body()) 179 | } 180 | Ok(false) => { 181 | drop(lock); 182 | let response = forbidden_handler 183 | .map(|h| h()) 184 | .unwrap_or_else(|| HttpResponse::Forbidden().finish()); 185 | Ok(req.into_response(response.map_into_right_body())) 186 | } 187 | Err(_) => { 188 | drop(lock); 189 | let response = error_handler 190 | .map(|h| h()) 191 | .unwrap_or_else(|| HttpResponse::BadGateway().finish()); 192 | Ok(req.into_response(response.map_into_right_body())) 193 | } 194 | } 195 | } else { 196 | let mut lock = cloned_enforcer.write().await; 197 | match lock.enforce_mut(vec![subject, path, action]) { 198 | Ok(true) => { 199 | drop(lock); 200 | srv.call(req).await.map(|res| res.map_into_left_body()) 201 | } 202 | Ok(false) => { 203 | drop(lock); 204 | let response = forbidden_handler 205 | .map(|h| h()) 206 | .unwrap_or_else(|| HttpResponse::Forbidden().finish()); 207 | Ok(req.into_response(response.map_into_right_body())) 208 | } 209 | Err(_) => { 210 | drop(lock); 211 | let response = error_handler 212 | .map(|h| h()) 213 | .unwrap_or_else(|| HttpResponse::BadGateway().finish()); 214 | Ok(req.into_response(response.map_into_right_body())) 215 | } 216 | } 217 | } 218 | } else { 219 | let response = unauthorized_handler 220 | .map(|h| h()) 221 | .unwrap_or_else(|| HttpResponse::Unauthorized().finish()); 222 | Ok(req.into_response(response.map_into_right_body())) 223 | } 224 | } 225 | .boxed_local() 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /tests/test_custom_error_handlers.rs: -------------------------------------------------------------------------------- 1 | use std::cell::RefCell; 2 | use std::pin::Pin; 3 | use std::rc::Rc; 4 | use std::task::{Context, Poll}; 5 | 6 | use actix_service::{Service, Transform}; 7 | use actix_web::{ 8 | body::MessageBody, dev::ServiceRequest, dev::ServiceResponse, Error, HttpMessage, HttpResponse, 9 | }; 10 | use futures::future::{ok, Future, Ready}; 11 | use serde_json::json; 12 | 13 | use actix_casbin_auth::{CasbinService, CasbinVals}; 14 | 15 | use actix_web::{test, web, App}; 16 | use casbin::{CoreApi, DefaultModel, FileAdapter}; 17 | 18 | pub struct FakeAuth; 19 | 20 | impl Transform for FakeAuth 21 | where 22 | S: Service, Error = Error> + 'static, 23 | B: MessageBody, 24 | { 25 | type Response = ServiceResponse; 26 | type Error = Error; 27 | type InitError = (); 28 | type Transform = FakeAuthMiddleware; 29 | type Future = Ready>; 30 | 31 | fn new_transform(&self, service: S) -> Self::Future { 32 | ok(FakeAuthMiddleware { 33 | service: Rc::new(RefCell::new(service)), 34 | }) 35 | } 36 | } 37 | 38 | pub struct FakeAuthMiddleware { 39 | service: Rc>, 40 | } 41 | 42 | impl Service for FakeAuthMiddleware 43 | where 44 | S: Service, Error = Error> + 'static, 45 | B: MessageBody, 46 | { 47 | type Response = ServiceResponse; 48 | type Error = Error; 49 | type Future = Pin>>>; 50 | 51 | fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { 52 | self.service.poll_ready(cx) 53 | } 54 | 55 | fn call(&self, req: ServiceRequest) -> Self::Future { 56 | let svc = self.service.clone(); 57 | 58 | Box::pin(async move { 59 | let vals = CasbinVals { 60 | subject: String::from("alice"), 61 | domain: None, 62 | }; 63 | req.extensions_mut().insert(vals); 64 | svc.call(req).await 65 | }) 66 | } 67 | } 68 | 69 | pub struct NoAuth; 70 | 71 | impl Transform for NoAuth 72 | where 73 | S: Service, Error = Error> + 'static, 74 | B: MessageBody, 75 | { 76 | type Response = ServiceResponse; 77 | type Error = Error; 78 | type InitError = (); 79 | type Transform = NoAuthMiddleware; 80 | type Future = Ready>; 81 | 82 | fn new_transform(&self, service: S) -> Self::Future { 83 | ok(NoAuthMiddleware { 84 | service: Rc::new(RefCell::new(service)), 85 | }) 86 | } 87 | } 88 | 89 | pub struct NoAuthMiddleware { 90 | service: Rc>, 91 | } 92 | 93 | impl Service for NoAuthMiddleware 94 | where 95 | S: Service, Error = Error> + 'static, 96 | B: MessageBody, 97 | { 98 | type Response = ServiceResponse; 99 | type Error = Error; 100 | type Future = Pin>>>; 101 | 102 | fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { 103 | self.service.poll_ready(cx) 104 | } 105 | 106 | fn call(&self, req: ServiceRequest) -> Self::Future { 107 | let svc = self.service.clone(); 108 | 109 | Box::pin(async move { 110 | // Don't insert CasbinVals - this will trigger unauthorized error 111 | svc.call(req).await 112 | }) 113 | } 114 | } 115 | 116 | #[actix_rt::test] 117 | async fn test_custom_forbidden_handler() { 118 | let m = DefaultModel::from_file("examples/rbac_with_pattern_model.conf") 119 | .await 120 | .unwrap(); 121 | let a = FileAdapter::new("examples/rbac_with_pattern_policy.csv"); 122 | 123 | let casbin_middleware = CasbinService::new(m, a) 124 | .await 125 | .unwrap() 126 | .set_forbidden_handler(|| { 127 | HttpResponse::Forbidden().json(json!({ 128 | "error": "Access forbidden", 129 | "code": 403 130 | })) 131 | }); 132 | 133 | casbin_middleware 134 | .write() 135 | .await 136 | .get_role_manager() 137 | .write() 138 | .matching_fn(Some(casbin::function_map::key_match2), None); 139 | 140 | let mut app = test::init_service( 141 | App::new() 142 | .wrap(casbin_middleware.clone()) 143 | .wrap(FakeAuth) 144 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())) 145 | .route("/data/1", web::get().to(|| HttpResponse::Ok())), 146 | ) 147 | .await; 148 | 149 | // alice can access /pen/1 - should succeed 150 | let req_pen = test::TestRequest::get().uri("/pen/1").to_request(); 151 | let resp_pen = test::call_service(&mut app, req_pen).await; 152 | assert!(resp_pen.status().is_success()); 153 | 154 | // alice cannot access /data/1 (no permission) - should trigger custom forbidden handler 155 | let req_data = test::TestRequest::get().uri("/data/1").to_request(); 156 | let resp_data = test::call_service(&mut app, req_data).await; 157 | assert_eq!(resp_data.status().as_u16(), 403); 158 | } 159 | 160 | #[actix_rt::test] 161 | async fn test_custom_unauthorized_handler() { 162 | let m = DefaultModel::from_file("examples/rbac_with_pattern_model.conf") 163 | .await 164 | .unwrap(); 165 | let a = FileAdapter::new("examples/rbac_with_pattern_policy.csv"); 166 | 167 | let casbin_middleware = CasbinService::new(m, a) 168 | .await 169 | .unwrap() 170 | .set_unauthorized_handler(|| { 171 | HttpResponse::Unauthorized().json(json!({ 172 | "error": "Authentication required", 173 | "code": 401 174 | })) 175 | }); 176 | 177 | casbin_middleware 178 | .write() 179 | .await 180 | .get_role_manager() 181 | .write() 182 | .matching_fn(Some(casbin::function_map::key_match2), None); 183 | 184 | let mut app = test::init_service( 185 | App::new() 186 | .wrap(casbin_middleware.clone()) 187 | .wrap(NoAuth) 188 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())), 189 | ) 190 | .await; 191 | 192 | // No CasbinVals provided - should trigger custom unauthorized handler 193 | let req_pen = test::TestRequest::get().uri("/pen/1").to_request(); 194 | let resp_pen = test::call_service(&mut app, req_pen).await; 195 | assert_eq!(resp_pen.status().as_u16(), 401); 196 | } 197 | 198 | #[actix_rt::test] 199 | async fn test_all_custom_handlers() { 200 | let m = DefaultModel::from_file("examples/rbac_with_pattern_model.conf") 201 | .await 202 | .unwrap(); 203 | let a = FileAdapter::new("examples/rbac_with_pattern_policy.csv"); 204 | 205 | let casbin_middleware = CasbinService::new(m, a) 206 | .await 207 | .unwrap() 208 | .set_unauthorized_handler(|| { 209 | HttpResponse::Unauthorized().json(json!({ 210 | "error": "Authentication required", 211 | "code": 401 212 | })) 213 | }) 214 | .set_forbidden_handler(|| { 215 | HttpResponse::Forbidden().json(json!({ 216 | "error": "Access forbidden", 217 | "code": 403 218 | })) 219 | }) 220 | .set_error_handler(|| { 221 | HttpResponse::InternalServerError().json(json!({ 222 | "error": "Internal server error", 223 | "code": 500 224 | })) 225 | }); 226 | 227 | casbin_middleware 228 | .write() 229 | .await 230 | .get_role_manager() 231 | .write() 232 | .matching_fn(Some(casbin::function_map::key_match2), None); 233 | 234 | let mut app = test::init_service( 235 | App::new() 236 | .wrap(casbin_middleware.clone()) 237 | .wrap(FakeAuth) 238 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())) 239 | .route("/data/1", web::get().to(|| HttpResponse::Ok())), 240 | ) 241 | .await; 242 | 243 | // alice can access /pen/1 - should succeed 244 | let req_pen = test::TestRequest::get().uri("/pen/1").to_request(); 245 | let resp_pen = test::call_service(&mut app, req_pen).await; 246 | assert!(resp_pen.status().is_success()); 247 | 248 | // alice cannot access /data/1 - should trigger custom forbidden handler 249 | let req_data = test::TestRequest::get().uri("/data/1").to_request(); 250 | let resp_data = test::call_service(&mut app, req_data).await; 251 | assert_eq!(resp_data.status().as_u16(), 403); 252 | } 253 | 254 | #[actix_rt::test] 255 | async fn test_default_handlers_still_work() { 256 | // Test that when no custom handlers are set, the default behavior is preserved 257 | let m = DefaultModel::from_file("examples/rbac_with_pattern_model.conf") 258 | .await 259 | .unwrap(); 260 | let a = FileAdapter::new("examples/rbac_with_pattern_policy.csv"); 261 | 262 | let casbin_middleware = CasbinService::new(m, a).await.unwrap(); 263 | 264 | casbin_middleware 265 | .write() 266 | .await 267 | .get_role_manager() 268 | .write() 269 | .matching_fn(Some(casbin::function_map::key_match2), None); 270 | 271 | let mut app = test::init_service( 272 | App::new() 273 | .wrap(casbin_middleware.clone()) 274 | .wrap(FakeAuth) 275 | .route("/pen/1", web::get().to(|| HttpResponse::Ok())) 276 | .route("/data/1", web::get().to(|| HttpResponse::Ok())), 277 | ) 278 | .await; 279 | 280 | // alice can access /pen/1 - should succeed 281 | let req_pen = test::TestRequest::get().uri("/pen/1").to_request(); 282 | let resp_pen = test::call_service(&mut app, req_pen).await; 283 | assert!(resp_pen.status().is_success()); 284 | 285 | // alice cannot access /data/1 - should return default 403 Forbidden 286 | let req_data = test::TestRequest::get().uri("/data/1").to_request(); 287 | let resp_data = test::call_service(&mut app, req_data).await; 288 | assert_eq!(resp_data.status().as_u16(), 403); 289 | } 290 | --------------------------------------------------------------------------------