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.
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.
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.
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...
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:
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:
What Casbin does NOT do:
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.
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 ??!
Senior Manager, Technology & Transformation
2 年Fantastic way to write a technical piece! Loved it.