Web authorization with Casbin

16 August 2019

Casbin is an authorization library that supports ACL, RBAC, ABAC permissions on resources. If you are not familiar with those terms, we will be running through examples in another post. Here we will be starting off with what Casbin is and how we can start using it to secure our web applications.

I wrote an article on Open Policy Agent, which is another popular permission system that you might want to check out.

What will go through:

  • Casbin syntax/DSL
  • Integrating into a Golang application

At a high level Casbin provides a language around permissions, as well as a toolset for using that in a web environment. The authors refer to it as the PERM Modeling Language (PML), and have a published paper on it. The building blocks we will be looking at are a ‘casbin model’, ‘casbin policy’, and ‘casbin enforcer’.

The way the block fit together is by instantiating an enforcer with a model and policies, then evaluate with arguments.

enforcer := casbin.NewEnforcer(“model.conf", "policy.csv")
role, path, method := “admin”, “/admin/route”, “POST”
isAllowed := enforcer.Enforce(role, path, method)

Casbin will provide the facilities to evaluate policies, models, and arguments, but how do we configure it?

Starting with the policy.csv

Example policy.csv

p, guest, /foo, GET
p, admin, /bar, POST

This role will be modified whenever a rule changes for example adding or removing a resource. The default format is a csv where each line is a rule, but we can store that in any format we want so long as casbin receives the format it expects.

Each row is a rule starting with a letter p for policy, for example: p, account_view, /accounts/:id, GET. What follows the p are a set of strings that we will be using for evaluation in the casbin model. More on the model coming up, but as an example the model will bind policies with something like this p = sub, obj, act. In the model evaluation the 1st string is the subject, the 2nd string is the object, and the 3rd string is the action. The order and meaning can be changed by us, but the important aspect is to define a new rule, we insert another row following the convention we have set up. This is a full example:

Model with model.conf

The more complicated piece is that model.conf, this file will likely be written once and only needs to be modified when the architecture of your permissions change not when we need to add a new rule, an example of our model:

[request_definition]
r = sub, obj, act

[policy_definition]
p = sub, obj, act

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

[matchers]
m = r.sub == p.sub && r.obj == p.obj && r.act == p.act

It might look a little overwhelming at first, but the DSL is not that complicated when we go through it. Casbin uses dynamic binding to assign values on the left to tuples on the right

Request definition:

r is for request, we assign it to the arguments passed into the .Enforce(...) function. sub, obj, act are conventions in this case for accessing entity (Subject), accessed resource (Object) and the access method (Action), but we could change the order or number of them if we wanted.

Policy definition:

p is for policy, it binds our policy rules from the policy.csv file. A rule in that file might look like this: p, guest, /foo, GET. In our model we translate that to p = “guest”, “ /foo”, “GET”

Policy matches:

m is the result of checking a policy rule. This is the part that I mostly associate the authorization part of our rule evaluations happening. We compare r and p from the previous sections using logical operators. For matching sub/obj/act: m = r.sub == p.sub && r.obj == p.obj && r.act == p.act, we using dot-accessors to say the match is true if the request and policy have equal subjects, objects, and actions.

Policy effect:

e is what result we want based off the matching our our policies. In a simple case we could use something like this e = some(where (p.eft == allow)), which says if any of the rules we evaluated allow then the overall result is to allow the request. A more advanced option would look like this e = some(where (p.eft == allow)) && !some(where (p.eft == deny)), if at least one rule allowed and no rules denied then the overall result is to allow the request.

Syntax recap

With those 4 sections we map r from the arguments to .Evaluate(...), p from our policy file, evaluate the two for a match m and then return the overall effect e from the matched policies.

Integrating this example into a golang project (using echo):

package main

import (
  "github.com/casbin/casbin"
  "github.com/labstack/echo"
)

type Enforcer struct {
  enforcer *casbin.Enforcer
}

func (e *Enforcer) Enforce(next echo.HandlerFunc) echo.HandlerFunc {
  return func(c echo.Context) error {
    user, _, _ := c.Request().BasicAuth()
    method := c.Request().Method
    path := c.Request().URL.Path

    result := e.enforcer.Enforce(user, path, method)

    if result {
      return next(c)
    }
    return echo.ErrForbidden
  }
}

func main() {
  e := echo.New()
  enforcer := Enforcer{enforcer: casbin.NewEnforcer("model.conf", "policy.csv")}
  e.Use(enforcer.Enforce)
  e.Logger.Fatal(e.Start("0.0.0.0:3000"))
}

Walking through what this looks like we extract 3 parameters from an incoming request, send them as arguments for enforce and if casbin allows the request we let it through.

# allow
curl http://guest:@0.0.0.0:3000/foo

# deny
curl http://guest:@0.0.0.0:3000/bar

# allow
curl -X POST http://admin:@0.0.0.0:3000/bar

What we have achieved with this is an extremely performant authorization framework in our web application that requires exactly 0 calls to persistent storage in the request path. By using something like a JWT for more user information we can start supporting uses cases that involve RBAC/ABAC/ACL, but for this example we just have basic auth to keep it simple.

Our example we are loading hardcoded model and policy files at application start, which is alright to get started with. Casbin supports persistent storage adapters for models and policies, allowing us to have an updated policy be applied to running applications without a code change or deploy. Another exciting extension of this would be running casbin using the sidecar pattern for non-golang applications that want to be supported by the same authorization framework.