Authorization using Casbin in GO

Authorization using Casbin in GO

What is Authorization and why do we need it?

Imagine that you are the owner of a big store chain. Now to manage your company efficiently you have divided your resources into stores, shelves, and products. So now you can easily create, remove or update your products. All you need to point out is which store, which shelf, and which product. Now you have employed various store managers, shelf managers, and product managers to do these operations and you have given a key card to all your employees. So whenever one of your employees walks into one of your stores the guard sitting at the gate might do these two very technical things.

  1. He will authenticate whether you are a valid employee by looking at your key card.
  2. Then he will check in his database whether you are authorized to do the operation that you are here for. For example: Adding a new product X, on shelf K, inside store C.

By now you must have guessed what is the difference between authentication and authorization but let me clarify it once more for you...

Authentication verifies the identity of a user or service, and authorization determines their access rights. Although the two terms sound alike, they play separate but equally essential roles in securing applications and data. Understanding the difference is crucial. Combined, they determine the security of a system. So irrespective of the key card that we have provided to all our employees the guards also need a database to check the access rights of each employee and when you have a system where multiple users can come to the same account or store in this example and perform some CRUD operation on either the shelves or the products residing on these shelves our authorization system changes dynamically and poses a tricky problem.

No alt text provided for this image
Tricky Problem

Imagine this case now. One of your product managers walks into one of your stores and adds a new product on the shelf. What your database system does is that it adds this product manager as the owner of that product and thinks that the work is done here but tomorrow your shelf manager walks in and tells the guard sitting outside that he/she wants to remove this product from this store as it is not working for your region. Now the guard checks the database but your database shows that the shelf manager is not authorized to remove this product so now the shelf manager argues that I am the owner of the entire shelf then I should be authorized but the guard just throws the manager out.

No alt text provided for this image

So essentially you have a hierarchy of resources here and you should keep in mind that a particular store manager is the owner of all the shelves inside that store and all the products on those shelves and a shelf manager is the owner of all the products on his shelf but he cannot go to shelf adjacent to it and say the same thing and so on...

No alt text provided for this image

To solve this problem and many other cases related to authorization issues. You can take the help of a package called Casbin. Casbin is nothing but an authorization library that supports access control models like ACL, RBAC, and ABAC and supports multiple programming languages with some of the key features as listed below:

  • Hybrid Access Control Models
  • Flexible Policy Storage
  • Policy Enforcement at Scale
  • Role Management

How CASBIN works?

Before jumping onto how it works. We should have a look at all things it does and all things it is not meant to do.

What Casbin does:

  1. Enforce the policy in the classic { subject, object, action } form or a customized form as you defined. Both allow and deny authorizations are supported.
  2. Handle the storage of the access control model and its policy.
  3. Manage the role-user mappings and role-role mappings (aka role hierarchy in RBAC).
  4. Support built-in superusers like root or administrator. A superuser can do anything without explicit permission.
  5. Multiple built-in operators to support the rule matching. For example, keyMatch can map a resource key /foo/bar to the pattern /foo*.

What Casbin does NOT do:

  1. Authentication (aka verify username and password when a user logs in)
  2. Manage the list of users or roles.

It's more convenient for the project to manage its list of users, roles, or passwords. Users usually have their passwords, and Casbin is not designed as a password container. However, Casbin stores the user-role mapping for the RBAC scenario.

So now we are all set to see how casbin works. It basically uses configuration files to set the access control model. It has two configuration files, model.conf and policy.csv. Among them, model.conf stores our access model, and policy.csv stores our specific user permission configuration. We just need one main structure: enforcer. When constructing this structure, model.conf and policy.csv will be loaded.

In another word, for a new Casbin enforcer, you must provide a Model and an Adapter.


import 
??? "log"

??? "github.com/casbin/casbin/v2"
??? "github.com/casbin/casbin/v2/model"
??? xormadapter "github.com/casbin/xorm-adapter/v2"
??? _ "github.com/go-sql-driver/mysql"
)

// Initialize a Xorm adapter with MySQL database.
a, err := xormadapter.NewAdapter("mysql", "mysql_username:mysql_password@tcp(127.0.0.1:3306)/casbin")
if err != nil {
??? log.Fatalf("error: adapter: %s", err)
}

m, err := model.NewModelFromString(`
[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
`)
if err != nil {
??? log.Fatalf("error: model: %s", err)
}

e, err := casbin.NewEnforcer(m, a)
if err != nil {
??? log.Fatalf("error: enforcer: %s", err)
}        

Check permissions

Just add an enforcement hook into your code right before the access happens:

sub := "alice" // the user that wants to access a resource
obj := "data1" // the resource that is going to be accessed.
act := "read" // the operation that the user performs on the resource.

ok, err := e.Enforce(sub, obj, act)

if err != nil {
??? // handle err
}

if ok == true {
??? // permit alice to read data1
} else {
??? // deny the request, show an error
}

// You could use BatchEnforce() to enforce some requests in batches.
// This method returns a bool slice, and this slice's index corresponds to the row index of the two-dimensional array.
// e.g. results[0] is the result of {"alice", "data1", "read"}
results, err := e.BatchEnforce([][]interface{}{
        

Important terms inside a casbin model:

A model CONF should have at least four sections: [request_definition], [policy_definition], [policy_effect], [matchers]. If a model uses RBAC, it should also add the [role_definition] section.

Request Definition

[request_definition] is the definition for the access request. It defines the arguments in e.Enforce(...) function.

[request_definition]
r = sub, obj, act        

sub, obj, act represents the classic triple: accessing entity (Subject), accessed resource (Object) and the access method (Action). However, you can customize your own request form, like sub, act if you don't need to specify a particular resource, or sub, sub2, obj, act if you somehow have two accessing entities.

Policy definition

[policy_definition] is the definition of the policy. It defines the meaning of the policy. For example, we have the following model:

[policy_definition]
p = sub, obj, act
p2 = sub, act        

And we have the following policy (if in a policy file)

p, alice, data1, read
p2, bob, write-all-objects        

Each line in a policy is called a policy rule. Each policy rule starts with a policy type, e.g., p, p2. It is used to match the policy definition if there are multiple definitions. The above policy shows the following binding. The binding can be used in the matcher.

(alice, data1, read) -> (p.sub, p.obj, p.act)
(bob, write-all-objects) -> (p2.sub, p2.act)        

Policy effect

[policy_effect] is the definition of the policy effect. It defines whether the access request should be approved if multiple policy rules match the request. For example, one rule permits and the other denies.

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

The above policy effect means if there's any matched policy rule of allow, the final effect is allow (aka allow-override). p.eft is the effect for a policy, it can be allow or deny. It's optional and the default value is allow. So as we didn't specify it above, it uses the default value.

Another example of policy effect is:

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

It means if there are no matched policy rules of deny, the final effect is allow (aka deny-override). some means: if there exists one matched policy rule. any means all matched policy rules (not used here). The policy effect can even be connected with logic expressions:

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

It means at least one matched policy rule of allow, and there is no matched policy rule of deny. So in this way, both the allow and deny authorizations are supported, and the deny overrides.

Matchers

[matchers] is the definition for policy matchers. The matchers are expressions. It defines how the policy rules are evaluated against the request.

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

The above matcher is the simplest, it means that the subject, object, and action in a request should match the ones in a policy rule.

You can use arithmetic like +, -, *, / and logical operators like &&, ||, ! in matchers.


So this is good! But a normal casbin model and policy file won't work in our store management case ??. We'll need to use a model called Restful KeyMatch which looks something like this:

[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 && keyMatch(r.obj, p.obj) && regexMatch(r.act, p.act)]        

This model basically uses a function inside the matcher. The keyMatch function is used to match a URL path or a star pattern like /alice_data/*

Whereas the regexMatch is used to match any regex string pattern.

Now to solve our store case. We can add a few policy lines like this in our policy file:

# Roshan is a store manager of store 
p, Roshan, /store/A, (GET)|(UPDATE)|(DELETE)
p, Roshan, /store/A/*, (GET)|(POST)|(UPDATE)|(DELETE)

# Pawan is a shelf manager of a shelf x in store A
p, Pawan, /store/A/shelf/x, (GET)|(UPDATE)|(DELETE)
p, Pawan, /store/A/shelf/x/*, (GET)|(POST)|(UPDATE)|(DELETE)

# Chait is a product manager of product K on a shelf x in the store A
p, Chait, /store/A/shelf/x/product/k, (GET)|(UPDATE)|(DELETE)
p, Chait, /store/A/shelf/x/product/k/*, (GET)|(POST)|(UPDATE)|(DELETE)A        

What these policies simply mean Roshan who is a store manager can do certain actions at the store level and he will be authorized to do certain actions for all the shelves and products inside that store. Similarly, Pawan who is a shelf manager will be able to do the added actions for all the products on the shelves even if they are added in the future.

You can test your model and policy using the casbin online editor or using your favorite programming language. For this case, my test cases look something like this:

# Roshan can update store 
Roshan, /store/A, UPDATE                                          true
# Roshan cannot update store B
Roshan, /store/B, UPDATE                                          false
# Roshan can update shelf y in store A
Roshan, /store/A/shelf/y, POST                                    true
# I guess you guys can figure it out now :)
Roshan, /store/A/shelf/z, UPDATE                                  true
Chait, /store/A, GET                                              false  
Pawan, /store/A, GET                                              false
Pawan, /store/A/shelf/x, UPDATE                                   true
Pawan, /store/A/shelf/y, UPDATE                                   false
Pawan, /store/A/shelf/x/product/B, UPDATE                         true
Chait, /store/A/shelf/x/product/k, UPDATE                         true
Chait, /store/A/shelf/x/product/B, UPDATE                         false        

HAPPY CODING ??!

Akshay T.

Senior Manager, Technology & Transformation

2 年

Fantastic way to write a technical piece! Loved it.

要查看或添加评论,请登录

Chaitanya Tyagi的更多文章

  • External Authorization using Istio, OPA and OpenFGA

    External Authorization using Istio, OPA and OpenFGA

    I have been looking into multiple solutions around authentication and authorization lately and today we are going to…

    3 条评论
  • Pluggable Authentication Modules (PAM)

    Pluggable Authentication Modules (PAM)

    This is a very introductory article about PAM but I hope that this will kindle a bit of curiosity among the readers on…

  • Using Video and Image Assets Inside a Flutter App

    Using Video and Image Assets Inside a Flutter App

    This article is simply about how to display images and play videos on your flutter app using network or local assets…

  • Creating a Public and Private Subnet in AWS

    Creating a Public and Private Subnet in AWS

    AWS offer highly secure and available network solutions with consistently high performance and global coverage. Today…

  • Deploying Infrastructure Using Terraform

    Deploying Infrastructure Using Terraform

    Terraform enables users to define and provision a datacenter infrastructure using a high-level configuration language…

  • Automate the learning using Docker and Jenkins.

    Automate the learning using Docker and Jenkins.

    Let's automate the learning process using Docker and Jenkins! THE TASK AT HAND: Let's say you have to train a model on…

    3 条评论

社区洞察

其他会员也浏览了