├── .github └── workflows │ ├── ci.yml │ ├── coverage.yml │ └── release.yml ├── .gitignore ├── Cargo.toml ├── README.md ├── examples ├── rbac_model.conf ├── rbac_policy.csv ├── rbac_with_domains_model.conf ├── rbac_with_domains_policy.csv ├── rbac_with_pattern_model.conf └── rbac_with_pattern_policy.csv ├── src ├── lib.rs └── middleware.rs └── tests ├── test_middleware.rs ├── test_middleware_domain.rs └── test_set_enforcer.rs /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | 36 | [profile.release] 37 | codegen-units = 1 38 | lto = true 39 | opt-level = 3 40 | 41 | [profile.dev] 42 | split-debuginfo = "unpacked" 43 | -------------------------------------------------------------------------------- /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 | ## License 141 | 142 | This project is licensed under 143 | 144 | * 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)) 145 | -------------------------------------------------------------------------------- /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_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_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) -------------------------------------------------------------------------------- /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_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_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 -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | pub use casbin; 2 | pub mod middleware; 3 | pub use middleware::{CasbinMiddleware, CasbinService, CasbinVals}; 4 | -------------------------------------------------------------------------------- /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 | #[derive(Clone)] 35 | pub struct CasbinService { 36 | enforcer: Arc>, 37 | } 38 | 39 | impl CasbinService { 40 | pub async fn new(m: M, a: A) -> CasbinResult { 41 | let enforcer: CachedEnforcer = CachedEnforcer::new(m, a).await?; 42 | Ok(CasbinService { 43 | enforcer: Arc::new(RwLock::new(enforcer)), 44 | }) 45 | } 46 | 47 | pub fn get_enforcer(&mut self) -> Arc> { 48 | self.enforcer.clone() 49 | } 50 | 51 | pub fn set_enforcer(e: Arc>) -> CasbinService { 52 | CasbinService { enforcer: e } 53 | } 54 | } 55 | 56 | impl Transform for CasbinService 57 | where 58 | S: Service, Error = Error> + 'static, 59 | B: MessageBody, 60 | { 61 | type Response = ServiceResponse>; 62 | type Error = Error; 63 | type InitError = (); 64 | type Transform = CasbinMiddleware; 65 | type Future = Ready>; 66 | 67 | fn new_transform(&self, service: S) -> Self::Future { 68 | ok(CasbinMiddleware { 69 | enforcer: self.enforcer.clone(), 70 | service: Rc::new(RefCell::new(service)), 71 | }) 72 | } 73 | } 74 | 75 | impl Deref for CasbinService { 76 | type Target = Arc>; 77 | 78 | fn deref(&self) -> &Self::Target { 79 | &self.enforcer 80 | } 81 | } 82 | 83 | impl DerefMut for CasbinService { 84 | fn deref_mut(&mut self) -> &mut Self::Target { 85 | &mut self.enforcer 86 | } 87 | } 88 | 89 | pub struct CasbinMiddleware { 90 | service: Rc>, 91 | enforcer: Arc>, 92 | } 93 | 94 | impl Service for CasbinMiddleware 95 | where 96 | S: Service, Error = Error> + 'static, 97 | B: MessageBody, 98 | { 99 | type Response = ServiceResponse>; 100 | type Error = S::Error; 101 | type Future = LocalBoxFuture<'static, Result>; 102 | 103 | fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { 104 | self.service.poll_ready(cx) 105 | } 106 | 107 | fn call(&self, req: ServiceRequest) -> Self::Future { 108 | let cloned_enforcer = self.enforcer.clone(); 109 | let srv = self.service.clone(); 110 | 111 | async move { 112 | let path = req.path().to_string(); 113 | let action = req.method().as_str().to_string(); 114 | let option_vals = req.extensions().get::().map(|x| x.to_owned()); 115 | let vals = match option_vals { 116 | Some(value) => value, 117 | None => { 118 | return Ok(req.into_response( 119 | HttpResponse::Unauthorized().finish().map_into_right_body(), 120 | )) 121 | } 122 | }; 123 | let subject = vals.subject.clone(); 124 | 125 | if !vals.subject.is_empty() { 126 | if let Some(domain) = vals.domain { 127 | let mut lock = cloned_enforcer.write().await; 128 | match lock.enforce_mut(vec![subject, domain, path, action]) { 129 | Ok(true) => { 130 | drop(lock); 131 | srv.call(req).await.map(|res| res.map_into_left_body()) 132 | } 133 | Ok(false) => { 134 | drop(lock); 135 | Ok(req.into_response( 136 | HttpResponse::Forbidden().finish().map_into_right_body(), 137 | )) 138 | } 139 | Err(_) => { 140 | drop(lock); 141 | Ok(req.into_response( 142 | HttpResponse::BadGateway().finish().map_into_right_body(), 143 | )) 144 | } 145 | } 146 | } else { 147 | let mut lock = cloned_enforcer.write().await; 148 | match lock.enforce_mut(vec![subject, path, action]) { 149 | Ok(true) => { 150 | drop(lock); 151 | srv.call(req).await.map(|res| res.map_into_left_body()) 152 | } 153 | Ok(false) => { 154 | drop(lock); 155 | Ok(req.into_response( 156 | HttpResponse::Forbidden().finish().map_into_right_body(), 157 | )) 158 | } 159 | Err(_) => { 160 | drop(lock); 161 | Ok(req.into_response( 162 | HttpResponse::BadGateway().finish().map_into_right_body(), 163 | )) 164 | } 165 | } 166 | } 167 | } else { 168 | Ok(req.into_response(HttpResponse::Unauthorized().finish().map_into_right_body())) 169 | } 170 | } 171 | .boxed_local() 172 | } 173 | } 174 | -------------------------------------------------------------------------------- /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_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_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 | --------------------------------------------------------------------------------