0%

Introduction

Casbin-rs is an authorization library that supports access control models like ACL, RBAC, ABAC written in Rust.

Poem is a full-featured and easy-to-use web framework with the Rust programming language.

In this tutorial, we will integrate poem with casbin-rs using poem-casbin.

Write a hello-world service with poem

First, create a cargo crate, then add following dependencies in Cargo.toml:

1
2
tokio = { version = "1.20.0", features = ["rt-multi-thread", "macros"] }
poem = "1.3.35"

Add following code to main.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
use poem::{get, handler, listener::TcpListener, web::Path, Route, Server};
use std::env;

#[handler]
fn pen1() -> String {
String::from("I'm pen 1")
}

#[handler]
fn pen2() -> String {
String::from("I'm pen 2")
}

#[handler]
fn book(Path(id): Path<String>) -> String {
format!("I'm book {}", id)
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
if env::var_os("RUST_LOG").is_none() {
env::set_var("RUST_LOG", "poem=debug");
}
let app = Route::new()
.at("/pen/1", get(pen1))
.at("/pen/2", get(pen2))
.at("/book/:id", get(book));
Server::new(TcpListener::bind("127.0.0.1:3000"))
.name("poem-casbin-demo")
.run(app)
.await
}

There are 3 endpoints, /pen/1, /pen/2, and /book/:id. It’s quite simple, right? Let’s run our service, enter cargo run and our service will be available at 127.0.0.1:3000.

Let’s use curl to test our service:

Integrate with basic auth middleware

Note that casbin-poem is an authorization middleware, not an authentication middleware. Casbin only takes charge of permission control, so we need to implement an authentication middleware to identify user.

In this part, we will inegrate our service with a basic auth middleware.

To start with, add following dependency to Cargo.toml:

1
poem-casbin-auth = { git = "https://github.com/casbin-rs/poem-casbin.git" }

Then create a file named auth.rs and add following code to it:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
use poem::{
http::StatusCode,
web::{
headers,
headers::{authorization::Basic, HeaderMapExt},
},
Endpoint, Error, Middleware, Request, Result,
};
use poem_casbin_auth::CasbinVals;

pub struct BasicAuth;

impl<E: Endpoint> Middleware<E> for BasicAuth {
type Output = BasicAuthEndpoint<E>;

fn transform(&self, ep: E) -> Self::Output {
BasicAuthEndpoint { ep }
}
}

pub struct BasicAuthEndpoint<E> {
ep: E,
}

#[poem::async_trait]
impl<E: Endpoint> Endpoint for BasicAuthEndpoint<E> {
type Output = E::Output;

async fn call(&self, mut req: Request) -> Result<Self::Output> {
if let Some(auth) = req.headers().typed_get::<headers::Authorization<Basic>>() {
let vals = CasbinVals {
subject: String::from(auth.username()),
domain: None,
};
req.extensions_mut().insert(vals);
self.ep.call(req).await
} else {
Err(Error::from_status(StatusCode::UNAUTHORIZED))
}
}
}

In this mod, we implement a basic auth middleware, for simplicity, here we don’t verify username and password, instead we just insert CasbinVals with provided username into Extension,so that poem-casbin middleware can extract identity information. If the request doesn’t have basic auth, then the middleware will return 401 Unauthorized.

Then let’s integrate our service with basic auth middleware. Firstly, add following code to main.rs:

1
2
3
mod auth;

use poem_casbin_auth::CasbinVals;

Then add a new handler to confirm that our auth middleware insert identity information correctly:

1
2
3
4
#[handler]
fn user(data: Data<&CasbinVals>) -> String {
format!("Hello, {}", &data.subject)
}

Lastly, rewrite main function to add an endpoint /user and wrap all endpoints with basic auth middleware, now it looks like:

1
2
3
4
5
6
7
let app = Route::new()
.at("/pen/1", get(pen1))
.at("/pen/2", get(pen2))
.at("/book/:id", get(book))
.at("/user", get(user))
.with(casbin_middleware)
.with(auth::BasicAuth);

Now, let’s use curl again to test our service.

Now as you can see, if we don’t provide basic auth when accessing our service, we will get 401 Unauthorized. Our request is aborted by basic auth middleware. Let’s send requests with basic auth:

1
curl -u alice:123 localhost:3000/book/1

Now we can get response as normal. It seems that our basic auth middleware works well.

Integrate with poem-casbin middleware

In the last part, we will integrate our service with poem-casbin middleware.

First, we need to provide conf and policy files under the project root directory.

rbac_with_pattern_model.conf looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

[role_definition]
g = _, _
g2 = _, _

[policy_effect]
e = some(where (p.eft == allow))

[matchers]
m = g(r.sub, p.sub) && g2(r.obj, p.obj) && regexMatch(r.act, p.act)

rbac_with_pattern_policy.csv looks like:

1
2
3
4
5
6
7
8
p, alice, /pen/1, GET
p, book_admin, book_group, GET
p, pen_admin, pen_group, GET
,,,
g, alice, book_admin,
g, bob, pen_admin,
g2, /book/:id, book_group,
g2, /pen/:id, pen_group,

These policy means:

  • For alice:
    • can access /pen/1
    • is book_admin, thus can access /book/:id
  • For bob:
    • is pen_admin, thus can access /pen/:id

Now let’s focus on main.rs, first add following code to it:

1
2
3
use poem_casbin_auth::casbin::function_map::key_match2;
use poem_casbin_auth::casbin::{CoreApi, DefaultModel, FileAdapter};
use poem_casbin_auth::{CasbinService, CasbinVals};

Then rewrite main function to wrap our service with poem-casbin middleware:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
let m = DefaultModel::from_file("rbac_with_pattern_model.conf")
.await
.unwrap();
let a = FileAdapter::new("rbac_with_pattern_policy.csv");

let casbin_middleware = CasbinService::new(m, a).await.unwrap();

casbin_middleware
.write()
.await
.get_role_manager()
.write()
.matching_fn(Some(key_match2), None);

let app = Route::new()
.at("/pen/1", get(pen1))
.at("/pen/2", get(pen2))
.at("/book/:id", get(book))
.at("/user", get(user))
.with(casbin_middleware)
.with(auth::BasicAuth);

Here we first read conf and policy, then create casbin_middleware and change matching_fn to key_match to match wildcard path (like /:id). Lastly, we wrap all endpoints with casbin_middleware.

That’s all the work we have to do to integrate our service with poem-casbin middleware, quite simple, right?

Again, let’s use curl to test our service:

If alice wants to access /pen/2, she will get 403 Forbidden, because she is not allowed to access this endpoint.

Likewise, bob can’t access /book/2:

Everything is fine when both users send requests to the endpoints that they can access:

Summary

In this tutorial, we write a hello-world web service using poem, then integrate it with basic auth and casbin-poem middleware. It’s a quite simple project with only ~100 LOC, its code can be found at this repository: https://github.com/greenhandatsjtu/poem-casbin-demo

Date: June 27, 2022 → July 3, 2022

Tasks overview

  • Add CI and tests for poem-casbin
  • Take a look at casbin-grpc
  • research

Task 1 Add CI and tests for poem-casbin

First I add CI for poem-casbin, ci.yml is bases on which from actix-casbin-auth, but I made some minor changes: change branch name from master to main , and upgrade actions/checkout to v3.

The workflow will run following checks when there’s push/pull_request to main branch:

  • cargo build
  • cargo test --no-default-features --features runtime-tokio
  • cargo test --no-default-features --features runtime-async-std
  • cargo clippy -- -D warnings
  • cargo fmt --all -- --check

This is what we got:

Untitled

Then I started to add some tests and examples to poem-casbin, I also use actix-casbin-auth as reference, it has 3 test files:

  • test_middleware.rs: test basic middleware function
  • test_middleware_domain.rs: test middleware function with domain
  • test_set_enforcer.rs: test initializing middleware using set_enforcer()

In the tests, first it implements a fake authentication middleware called FakeAuth, which just simply insert a CasbinVals with subject alice and domain domain1 to request’s exetensions:

1
2
3
4
5
6
7
8
9
10
11
fn call(&self, req: ServiceRequest) -> Self::Future {
let svc = self.service.clone();

Box::pin(async move {
let vals = CasbinVals {
subject: String::from("alice"),
domain: Option::from(String::from("domain1")),
};
req.extensions_mut().insert(vals);
svc.call(req).await
})

Then it wraps its endpoints in FakeAuth and casbin middleware:

1
2
3
4
5
6
7
8
let mut app = test::init_service(
App::new()
.wrap(casbin_middleware.clone())
.wrap(FakeAuth)
.route("/pen/1", web::get().to(|| HttpResponse::Ok()))
.route("/book/{id}", web::get().to(|| HttpResponse::Ok())),
)
.await;

As for poem, it’s a little different. To use test in poem, we have to enable test feature of it, then we can test like below:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
use poem::{handler, test::TestClient, Route};

#[handler]
fn index() -> &'static str {
"hello"
}

let app = Route::new().at("/", index);
let cli = TestClient::new(app);

// send request
let resp = cli.get("/").send().await;
// check the status code
resp.assert_status_is_ok();
// check the body string
resp.assert_text("hello").await;

And poem use with to wrap endpoints with middlewares:

1
2
3
4
5
6
let app = Route::new()
.at("/pen/1", get(endpoint))
.at("/pen/2", get(endpoint))
.at("/book/:id", get(endpoint))
.with(casbin_middleware)
.with(FakeAuth);

Note that casbin_middleware must be precedent than FakeAuth.

And I learned that to switch runtime I can use cfg_attr:

1
2
3
4
5
#[cfg_attr(feature = "runtime-tokio", tokio::test)]
#[cfg_attr(feature = "runtime-async-std", async_std::test)]
async fn test_middleware() {
//...
}

I made 2 PRs which got merged:

Next week I’ll add README for this project.

Task 2 Take a look at grpc-casbin

For now, casbin-grpc build fails with bunch of errors:

Untitled

There are mainly 2 issues:

First, most functions of Cabin trait are not implemented:

Untitled

Second, new_adapter function only supports FileAdapter now:

Untitled

I think I can work on this repo to complete it in the next few weeks.

Research on real-world application

After talked with my mentor last week, I know that casdoor-rust-sdk is assigned to another student, and I need to think of a new task.

I want to do something solid, and don’t want to use Rust just to write a web application. So after I researched a while, I think I can either:

  • combine casbin-rs and yew to implement a front-end access control framework like:

    https://github.com/tower1229/Vue-Access-Control

  • dufs is a distinctive utility file server that supports accessing control, we can maintain a fork of it and use casbin-rs as access control backend

    https://github.com/sigoden/dufs

  • complete grpc-casbin and colaborate with Siddhesh Kanawade on casbin-raft, as there are actually a lot of work to do

Date: June 20, 2022 → June 26, 2022

Tasks Overview

  • implement casbin middleware for poem
  • resolve an issue of sqlx-adapter
  • fix casbin-rs benchmark workflow
  • research poem-openapi and discuss on casdoor-rust-sdk

Task 1 Poem-casbin

This week, I plan to write code for poem-casbin.

First, to get familiar with what casbin middleware do and how to implement it, I take a look at existing casbin middlewares.

For example:

https://github.com/casbin-rs/actix-casbin-auth

This repository is casbin-rs access control middleware for actix-web framwork

To figure out what does this code do, I have to first learn how to write middleware for actix. I found an awesome tutorial here:

Demystifying Actix Web Middleware

To implement a middleware, we must implement Transform and Service traits.

Actix’s Service represents anything that takes a request and returns a response, it looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pub trait Service<Req> {
/// Responses given by the service.
type Response;

/// Errors produced by the service when polling readiness or executing call.
type Error;

/// The future response value.
type Future: Future<Output = Result<Self::Response, Self::Error>>;

/// Returns `Ready` when the service is able to process requests.
fn poll_ready(&self, ctx: &mut task::Context<'_>) -> Poll<Result<(), Self::Error>>;

/// Process the request and return the response asynchronously.
fn call(&self, req: Req) -> Self::Future;
}

We’ll mainly work on call() function, and this is the main part of axtix-casbin-auth.

The Transform implementation’s only job is to create new middleware instances that wrap other services, it looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pub trait Transform<S, Req> {
/// Responses produced by the service.
type Response;

/// Errors produced by the service.
type Error;

/// The `TransformService` value created by this factory
type Transform: Service<Req, Response = Self::Response, Error = Self::Error>;

/// Errors produced while building a transform service.
type InitError;

/// The future response value.
type Future: Future<Output = Result<Self::Transform, Self::InitError>>;

/// Creates and returns a new Transform component, asynchronously
fn new_transform(&self, service: S) -> Self::Future;
}

Here we have to implement new_transform() function to wrap casbin enforcer and actix service together, like:

1
2
3
4
5
6
fn new_transform(&self, service: S) -> Self::Future {
ok(CasbinMiddleware {
enforcer: self.enforcer.clone(),
service: Rc::new(RefCell::new(service)),
})
}

Note this middleware only takes care of authorization, so user should put actix_casbin_auth::CasbinVals which contains subject(username) and domain(optional) into extension before calling this middleware.

1
2
3
4
5
let vals = CasbinVals {
subject: String::from("alice"),
domain: None,
};
req.extensions_mut().insert(vals);

Now I focus on call() function of Service trait:

First, it gets path and action (method) of request, then gets CasbinVals from extension.

1
let option_vals = req.extensions().get::<CasbinVals>().map(|x| x.to_owned());

Then he calls enforce_mut() to authorize this request and returns response depending enforce result.

Also, I take a look at tests to see how to use this middleware, I found it quite easy to use, just wrap endpoint in middlwares like:

1
2
3
4
5
6
7
let mut app = test::init_service(
App::new()
.wrap(casbin_middleware.clone())
.wrap(FakeAuth)
.route("/pen/1", web::get().to(|| HttpResponse::Ok()))
.route("/book/{id}", web::get().to(|| HttpResponse::Ok())),
)

After learning actix-casbin-auth I’m clear about what work casbin middleware does. So I turned to learn how to write poem middleware.

There are not many tutorial on implementing middleware for poem, but poem provides bunch of middleware examples in https://github.com/poem-web/poem/tree/master/examples/poem . So I took a look at these examples and figured out how to implement middleware for poem.

Just like actix, we have to implement two traits: Middleware and Endpoint.

Middleware is like Transform in actix, it wraps other services, it looks like:

1
2
3
4
5
6
7
pub traitMiddleware<E:Endpoint> {
/// New endpoint type.
type Output:Endpoint;

/// Transform the input[`Endpoint`] to another one.
fn transform(&self, ep: E) -> Self::Output;
}

Endpoint is like Service in actix, it takes a request and returns a response, it looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
pub trait Endpoint: Send + Sync {
/// Represents the response of the endpoint.
type Output: IntoResponse;

/// Get the response to the request.
async fn call(&self, req: Request) -> Result<Self::Output>;

/// Get the response to the request and return a [`Response`
async fn get_response(&self, req: Request) -> Response {
self.call(req)
.await
.map(IntoResponse::into_response)
.unwrap_or_else(|err| err.into_response())
}
}

Now I know how to write middleware in poem, so I start implementing casbin middleware for it, this is my first PR:

https://github.com/casbin-rs/poem-casbin/pull/1

Task 2 Resolve sqlx-adapter issue

https://github.com/casbin-rs/sqlx-adapter/issues/65

This issue has some update, one user complains that why DATABASE_URL must be presented instead of to be optional.

It’s because sqlx-adapter uses query! marco to statically check SQL queries.

According to docs of sqlx, to use query!:

  • The DATABASE_URL environment variable must be set at build-time to point to a database server with the schema that the query string will be checked against. All variants of query!() use dotenv so this can be in a .env file instead.
  • Or, sqlx-data.json must exist at the workspace root.

To use sqlx-data.json , you can follow the docs for offline mode.

query! can be configured to not require a live database connection for compilation, but it requires a couple extra steps:

  • Run cargo install sqlx-cli.
  • In your project with DATABASE_URL set (or in a .env file) and the database server running, run cargo sqlx prepare.
  • Check the generated sqlx-data.json file into version control.
  • Don’t have DATABASE_URL set during compilation.

Your project can now be built without a database connection (you must omit DATABASE_URL or else it will still try to connect). To update the generated file simply run cargo sqlx prepare again.

Note: As sqlx-adapter has generated sqlx-data.json for postgres, so when using postgres you don’t need provide DATABASE_URL or sqlx-data.json . But for mysql and sqlite, you must provide DATABASE_URL or sqlx-data.json

However, it would be better if we can generate sqlx-data.json for all three kinds of database in sqlx-adapter , so users won’t bother setting DATABASE_URL or generating sqlx-data.json by themselves. I’ve been tried a while but found cargo sqlx prepare would just overwrite sqlx-data.json.

I’ve been reading related issues like:

https://github.com/launchbadge/sqlx/issues/1223

https://github.com/launchbadge/sqlx/issues/121

but found them not very helpful, so I open an issue at sqlx:

https://github.com/launchbadge/sqlx/issues/1927

No one reply to me though.

Task 3 casbin-rs benchmark workflow

Last week I found benchmark workflow doesn’t work well that it fails to post comment to PR.

I’ve searching a while for this issue and reading some related discussions/issues:

https://github.com/actions/first-interaction/issues/10

GitHub actions are severely limited on PRs

Automatic token authentication - GitHub Docs

“Resource not accessible by integration” for adding a comment to a PR via action

The reason is GITHUB_TOKEN only has read permission to pull requests when access by forked repos, so when a new PR is coming, running workflows only has read-only permission (of course not allowed to post comment), that’s why we get 403 here: https://github.com/casbin/casbin-rs/runs/6409294114#step:5:459

Some users have created actions that can post comment on PR, like:

https://github.com/mshick/add-pr-comment

https://github.com/nyurik/auto_pr_comments_from_forks

But it’s difficult to combine them with boa-dev/criterion-compare-action.

GitHub has introduced a new event type: pull_request_target, which allows to run workflows from base branch and pass a token with write permission.

GitHub Actions improvements for fork and pull request workflows | The GitHub Blog

It says:

The event runs against the workflow and code from the base of the pull request. This means the workflow is running from a trusted source and is given access to a read/write token as well as secrets enabling the maintainer to safely comment on or label a pull request.

I give this a try.

I first created a PR from a fork repo:

Untitled

It fails as before.

Then I modify pull_request.yml to use pull_request_target , then create another PR.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
on:
pull_request_target:
branches: [ master ]
name: Benchmarks
jobs:
runBenchmark:
name: run benchmark
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v1
with:
ref: ${{github.event.pull_request.head.ref}}
repository: ${{github.event.pull_request.head.repo.full_name}}
# ...

Now it works well:

Untitled

I made a PR and it got merged.

https://github.com/casbin/casbin-rs/pull/298

Task 4 casdoor-rust-sdk

This week I take a look at poem as mentor suggested.

Poem has a sub crate poem-openapi

poem/poem-openapi at master · poem-web/poem

However, after I digging it a while, I found this is for writing openapi service, not client.

Here is example it provides:

1
2
3
4
5
6
7
8
9
10
#[OpenApi]
impl Api {
#[oai(path = "/hello", method = "get")]
async fn index(&self, name: Query<Option<String>>) -> PlainText<String> {
match name.0 {
Some(name) => PlainText(format!("hello, {}!", name)),
None => PlainText("hello!".to_string()),
}
}
}

Last week another guy was assigned to this project too, so this week I discussed with him, as he said he has written ~100 lines of code, I think it’s better that he made an initial PR to upload SDK framework, then we can make PRs to complete it .

https://github.com/casdoor/casdoor-rust-sdk/issues/1

However, not long after I replied under this issue, I found I’m blocked by Casdoor organization :(

I can’t post comment in issues/PRs of any repo of casdoor org, also can’t fork any repo belonging to casdoor org:
comment
fork

I don’t understand why I’m blocked and I’ll discuss with my mentor tonight.

Date: June 13, 2022 → June 19, 2022

Overview This week’s tasks

Task 1 Review PR

https://github.com/casbin/casbin-rs/pull/293

Original implementation

Original implementation use Arc<RwLock<Role>> :

1
2
3
4
5
6
7
8
pub struct DefaultRoleManager {
all_domains: HashMap<String, HashMap<String, Arc<RwLock<Role>>>>,
//...
}
pub struct Role {
name: String,
roles: Vec<Arc<RwLock<Role>>>,
}

The key of HashMap<String, HashMap<String, Arc<RwLock<Role>>>> is domain name (default value is DEFAULT), the key of HashMap<String, Arc<RwLock<Role>>> is role name, the entry is Arc<RwLock<Role>> , which stores role information: name of the role, and all roles it directly has

It is quite expensive as Arc<T> uses atomic ****operations for its reference counting, and there are many read() and write() operations for RwLock<Role> in current implmentation:

1
2
3
4
5
6
7
//L185
role1.write().delete_role(role2);

//L332
fn has_direct_role(&self, name: &str) -> bool {
self.roles.iter().any(|role| role.read().name == name)
}

Important functions:

  • add_link append role2 to role1.roles
  • delete_link delete role2 from role1.roles
  • has_link check if role name1 has role name2 (dosen’t have to be direct role), if domain_matching_fn is speicified, it will find roles in matched domains
  • get_roles get all direct roles user name has in matched domains
  • get_users get all users that directly have role name in matched domains

Novel implementation

This PR introduces 2 new crates:

This PR’s implementation is like Go Casbin (https://github.com/casbin/casbin/blob/master/rbac/default-role-manager/role_manager.go)

1
2
3
4
5
6
7
type Role struct {
name string
roles *sync.Map
users *sync.Map
matched *sync.Map
matchedBy *sync.Map
}

But instead of using map and storing related roles’ pointers in them, it use StableDiGraph to construct roles graph, and use edges EdgeVariant to represent relations:

1
2
3
4
enum EdgeVariant {
Link,
Match,
}

Edges with the EdgeVariant::Link are relations which in Go are modeled by the roles and users map.
Edges with the EdgeVariant::Match are relations which is Go are modeled by the matched and matchedBy map.

This PR gets rid of Arc<RwLock<Role>> by using StableDiGraph and NodeIndex :

1
2
3
4
5
pub struct DefaultRoleManager {
all_domains: HashMap<String, StableDiGraph<String, EdgeVariant>>,
all_domains_indices: HashMap<String, HashMap<String, NodeIndex<u32>>>,
//...
}

StableGraph<N, E, Ty, Ix> is a graph datastructure using an adjacency list representation, all_domains constructs a StableGraph with directed edges for each domain, all_domains_indices stores node identifier NodeIndex of every role node in the graph

Important functions:

  • get_or_create_role

    get or create role;

    if the role is new, graph.add_node() to add role to grapgh;

    if role_matching_fn is specified, call link_if_matches to match existing roles against new role and vice versa, if matched, call graph.add_edge() to create EdgeVariant::*Match* edge between roles.

  • add_link

    add link from role1 to role2 , call graph.add_edge() to add EdgeVariant::*Link edge*

  • delete_link

    remove edge from role1 to role2

  • has_link

    Bfs searching in graph, checking if role1 is connected to role2

  • get_roles

    Bfs searching in graph, getting all roles the user directly has

  • get_users

    Find all nodes having Direction::*Incoming* edge connected to this role, that is, getting all users that directly have this role

Why Performance impoved?

In StableGraph, nodes (roles) and edges (relations) are each numbered in an interval from 0 to some number m, so we can access nodes and edges using their indices, also, creating a role is just adding a node to graph, link roles is justing add an edge between 2 nodes, we don’t have to modify nodes and edges, so Arc and RWLock are no longer needed.

Without atomic operations and lock/unlock, role manager is much faster now.

Tests

The PR add 2 tests:

  • test_basic_role_matching test user with wildcard *
  • test_basic_role_matching2 test role with wildcard *

Migrate these tests to Go Casbin:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
func TestBasicRoleMatching(t *testing.T) {
rm := NewRoleManager(10)
rm.AddMatchingFunc("keyMatch", util.KeyMatch)

_ = rm.AddLink("bob", "book_group")
_ = rm.AddLink("*", "book_group")
_ = rm.AddLink("*", "pen_group")
_ = rm.AddLink("eve", "pen_group")

testRole(t, rm, "alice", "book_group", true)
testRole(t, rm, "eve", "book_group", true)
testRole(t, rm, "bob", "book_group", true)
testPrintRoles(t, rm, "alice", []string{"book_group", "pen_group"})
}

func TestBasicRoleMatching2(t *testing.T) {
rm := NewRoleManager(10)
rm.AddMatchingFunc("keyMatch", util.KeyMatch)

_ = rm.AddLink("alice", "book_group")
_ = rm.AddLink("alice", "*")
_ = rm.AddLink("bob", "pen_group")

testRole(t, rm, "alice", "book_group", true)
testRole(t, rm, "alice", "pen_group", true)
testRole(t, rm, "bob", "pen_group", true)
testRole(t, rm, "bob", "book_group", false)
testPrintRoles(t, rm, "alice", []string{"*", "alice", "bob", "book_group", "pen_group"})
testPrintUsers(t, rm, "*", []string{"alice"})
}

TestBasicRoleMatching passed, TestBasicRoleMatching2 failed with:

1
role_manager_test.go:371: alice: [* book_group alice book_group bob pen_group], supposed to be [* alice bob book_group pen_group]

Note that book_group appears twice, this is because in (*Role).rangeRoles :

1
2
3
4
5
6
7
8
9
10
11
12
13
func (r *Role) rangeRoles(fn func(key, value interface{}) bool) {
r.roles.Range(fn)
r.roles.Range(func(key, value interface{}) bool {
role := value.(*Role)
role.matched.Range(fn)
return true
})
r.matchedBy.Range(func(key, value interface{}) bool {
role := value.(*Role)
role.roles.Range(fn)
return true
})
}

All roles, matched, and matchedBy roles are appended to result, which is a list, whille casbin-rs uses HashSet , so there are no duplicate in casbin-rs.

Benchmark

Benchmark changes from GitHub workflow:

https://github.com/casbin/casbin-rs/runs/6409294114

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
group                                 changes                                master
----- ------- ------
b_benchmark_rbac_model_large 1.00 15.7±0.59ms ? ?/sec 1.26 19.7±0.85ms ? ?/sec
benchmark priority model 1.02 9.3±0.49µs ? ?/sec 1.00 9.1±0.34µs ? ?/sec
benchmark_abac_model 1.01 5.4±0.32µs ? ?/sec 1.00 5.3±0.55µs ? ?/sec
benchmark_basic_model 1.00 8.4±0.45µs ? ?/sec 1.00 8.4±0.49µs ? ?/sec
benchmark_key_match 1.00 30.6±2.74µs ? ?/sec 1.02 31.2±1.97µs ? ?/sec
benchmark_raw 1.01 5.0±0.18ns ? ?/sec 1.00 5.0±0.16ns ? ?/sec
benchmark_rbac_model 1.00 12.5±0.65µs ? ?/sec 1.00 12.4±0.95µs ? ?/sec
benchmark_rbac_model_medium 1.00 1380.1±59.72µs ? ?/sec 1.12 1551.8±91.11µs ? ?/sec
benchmark_rbac_model_with_domains 1.00 13.0±0.54µs ? ?/sec 1.02 13.2±0.85µs ? ?/sec
benchmark_rbac_with_deny 1.00 17.0±1.32µs ? ?/sec 1.00 17.0±1.17µs ? ?/sec
benchmark_rbac_with_resource_roles 1.00 9.9±1.04µs ? ?/sec 1.01 10.0±0.66µs ? ?/sec
benchmark_role_manager_large 1.04 8.3±0.26ms ? ?/sec 1.00 8.0±0.83ms ? ?/sec
benchmark_role_manager_medium 1.00 487.8±22.79µs ? ?/sec 1.31 640.4±41.89µs ? ?/sec
benchmark_role_manager_small 1.00 142.5±7.47µs ? ?/sec 1.09 155.4±9.60µs ? ?/sec
┌─────────┬──────────────────────────────────────┬──────────────────────┬──────────────────┬────────────┐
│ (index) │ name │ changesDuration │ masterDuration │ difference │
├─────────┼──────────────────────────────────────┼──────────────────────┼──────────────────┼────────────┤
│ 0 │ 'b_benchmark_rbac_model_large' │ '**15.7±0.59ms**' │ '19.7±0.85ms' │ '-21' │
│ 1 │ 'benchmark priority model' │ '9.3±0.49µs' │ '**9.1±0.34µs**' │ '+2.0' │
│ 2 │ 'benchmark_abac_model' │ '5.4±0.32µs' │ '**5.3±0.55µs**' │ '+1.0' │
│ 3 │ 'benchmark_basic_model' │ '8.4±0.45µs' │ '8.4±0.49µs' │ '0.0' │
│ 4 │ 'benchmark_key_match' │ '**30.6±2.74µs**' │ '31.2±1.97µs' │ '-2.0' │
│ 5 │ 'benchmark_raw' │ '5.0±0.18ns' │ '**5.0±0.16ns**' │ '+1.0' │
│ 6 │ 'benchmark_rbac_model' │ '12.5±0.65µs' │ '12.4±0.95µs' │ '0.0' │
│ 7 │ 'benchmark_rbac_model_medium' │ '**1380.1±59.72µs**' │ '1551.8±91.11µs' │ '-11' │
│ 8 │ 'benchmark_rbac_model_with_domains' │ '**13.0±0.54µs**' │ '13.2±0.85µs' │ '-2.0' │
│ 9 │ 'benchmark_rbac_with_deny' │ '17.0±1.32µs' │ '17.0±1.17µs' │ '0.0' │
│ 10 │ 'benchmark_rbac_with_resource_roles' │ '**9.9±1.04µs**' │ '10.0±0.66µs' │ '-0.99' │
│ 11 │ 'benchmark_role_manager_large' │ '8.3±0.26ms' │ '**8.0±0.83ms**' │ '+4.0' │
│ 12 │ 'benchmark_role_manager_medium' │ '**487.8±22.79µs**' │ '640.4±41.89µs' │ '-24' │
│ 13 │ 'benchmark_role_manager_small' │ '**142.5±7.47µs**' │ '155.4±9.60µs' │ '-8.3' │
│ 14 │ '' │ undefined │ undefined │ '+NaN' │
└─────────┴──────────────────────────────────────┴──────────────────────┴──────────────────┴────────────┘

Improved above 5%:

  • b_benchmark_rbac_model_large -21%
  • benchmark_rbac_model_medium -11%
  • benchmark_role_manager_medium -24%
  • benchmark_role_manager_small -8.3%

Regressed above 2%:

  • benchmark_role_manager_large +4.0%

And I found that benchmark workflow doesn’t work well:

https://github.com/casbin/casbin-rs/runs/6409294114#step:5:459

Untitled

It fails to post comment of benchmark result to current PR with status code 403

Task 2 Make 2 issue & 2 PRs

issue: benchmark workflow is unable to push benchmark result to PR comment

https://github.com/casbin/casbin-rs/issues/294

issue: As I said above, when reviewing PR, I found a bug in Go version casbin. In default-role-manager/role_manager.gofunc (r *Role) getRoles() returns result with duplicate items

https://github.com/casbin/casbin/issues/1033

PR: there are 3 errors when running cargo clippy -- -D warnings , I fixed it

https://github.com/casbin/casbin-rs/pull/296

PR: There are many duplicate code in default_role_manager, making it hard to understand and maintain code, I improved it.

https://github.com/casbin/casbin-rs/pull/295

Task 3 Swagger code generator

There are 105 enpoints in casdoor now, so when developing casdoor rust sdk, I have to write many similar and duplicate code. To avoid this, I first looked for code generator.

As Casdoor provide well annotated swagger spec (https://github.com/casdoor/casdoor/blob/master/swagger/swagger.yml), I can generate code using swagger code generator.

First, I found swagger-codegen.

https://github.com/swagger-api/swagger-codegen

It can generate rust code with 2 different implementation.

I use docker image to generate code.

I tried first implementation:

1
2
3
4
docker run -u 1000:1000 --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate \
-i /local/swagger.yml \
-l rust \
-o /local/out/rust

Code is generated very fast. The output is like:

Untitled

However, when running cargo build , I got many warnings like:

Untitled

After searching a while on the web, I found work around (https://stackoverflow.com/a/57641467): add #![allow(warnings)] at the first line in src/lib.rs

Now cargo build doesn’t produce warnings now.

Untitled

However, as the warnings are still here, this doesn’t help much, there are still many deprecated code and badly named variables.

So I tried second implementation:

1
2
3
4
docker run -u 1000:1000 --rm -v ${PWD}:/local swaggerapi/swagger-codegen-cli generate \
-i /local/swagger.yml \
-l rust-server \
-o /local/out/rust-server

Now cargo build failes with:

Untitled

Searching issues of rust-openssl for a while, I found similar issue with me:

https://github.com/sfackler/rust-openssl/issues/1436

It says rust-openssl v0.9.24 is too old and doesn’t support OpenSSL 1.1.1

My openssl version is 1.1.1

Untitled

Obviously, swagger code generator is not maintained for a while, and we should find another code generator.

Then I found openapi-generator:

https://github.com/OpenAPITools/openapi-generator

The usage is similar to swagger-codegen:

1
2
3
4
docker run -u 1000:1000 --rm -v "${PWD}:/local" openapitools/openapi-generator-cli generate 
-i /local/swagger.yml
-g rust
-o /local/out/openapi-rust

Code generator fails with:

Untitled

It says that because some endpoints in swagger.yml aren’t providing response, so the spec validation failes. For example, GetResources endpoint is not well annotated (no params, no response)

Untitled

Then I found paperclip:

https://github.com/paperclip-rs/paperclip

It’s a WIP OpenAPI tooling written in Rust and can also generate rust code.

1
paperclip --api v2 -o out/paper swagger.yml

Still, as there are several endpoints not well annotated, it fails with error:

Untitled

Also, generated code can’t have similar API with Go version Casdoor SDK:

https://github.com/casdoor/casdoor-go-sdk

After I explored these two code generators, I gave up and decide to write code on my own.

Summary:

  • generated code is difficult to maintain
  • generated code is too old and use deprecated syntax
  • generated code is badly written and has many warnings
  • generated code can’t have similar API with Go version Casdoor SDK
  • swagger.yml is not complete (some endpoint without response), some code generator will report syntax error

Next week Plan

Next week, I will:

  • start writing code for casdoor-rust-sdk
  • learn how middleware works and contribute to poem-casbin
  • maintain projects of casbin-rs

Wakatime weekly stats

Welcome to Hexo! This is your very first post. Check documentation for more info. If you get any problems when using Hexo, you can find the answer in troubleshooting or you can ask me on GitHub.

Quick Start

Create a new post

1
$ hexo new "My New Post"

More info: Writing

Run server

1
$ hexo server

More info: Server

Generate static files

1
$ hexo generate

More info: Generating

Deploy to remote sites

1
$ hexo deploy

More info: Deployment