Casbin, an alternative to validate user permissions
Sometimes we need to ensure access level in our applications, the most known is when we identify a user and according to the role the application decide what resources can be accessed for that user and what actions can do with that resources, if you think a moment these permissions are globals, I mean admin role has access to users administration module, the admin can add, edit or eliminate user information, but a customer role can’t do that actions, is fine, because you control the access and actions in your application. Now, imagine this scenario you have an application where your users upload files and they want to share their files only with specific users, and one user has the permission to edit and another only to see the file. How do you achieve that? You need specific permissions for each resource and user, It means to create an association with a resource, an action, and a user.
If you are working with a relational database, maybe you think well I just need to add a new table where the association is created. This solution is fine, but think the work that implies, you need to create functions to add permissions, validate those permissions, delete the permissions if the resources are deleted, etc. Here is where Casbin can help you.
First, What is Casbin? Well, it’s an authorization library, that allows validating permissions associated with users, roles, or resources. It means that Casbin can do the hard work for us, create permissions, delete all permissions associated with specific resources or users, update permissions, etc; and we don’t need to implement queries o something like that to manage the data.
Casbin can work with access control models like:
- ACL (Access Control List, list of permissions associated with a system resource)
- RBAC (Role-Based Access Control, permissions based on roles like for example admin, customer, investor, etc)
- ABAC (Attribute-based access control): Access decisions based on the attributes of the subject, resource, action. For example environment or owner.
- REST (Representational state transfer) HTTP paths (/test/*) or methods (POST, PUT, GET, etc.)
These models represent the associations that you can do in the permissions, for example, you can add permission for specific resources like files, or add global permissions for an admin group, lock an HTTP path only for specifics users, even create one permission to validate that the owner is the only one authorized to see that resource. Here I want to say something, Casbin isn’t the only one to achieve this kind of stuff. For example, you can create policies in AWS to lock resources, and the only one to access the resource is the owner, but if you don’t know anything about AWS policies or other kinds of cloud technologies, maybe Casbin is the best for you.
Additional supported models:
- ACL with superuserACL without users: especially useful for systems that don’t have authentication or user log-ins.
- ACL without resources: some scenarios may target for a type of resources instead of an individual resource by using permissions like write-article, read-log. It doesn’t control access to a specific article or log.
- RBAC with resource roles: both users and resources can have roles (or groups) at the same time.
- RBAC with domains/tenants: users can have different role sets for different domains/tenants. the subject, resource, action, and for example environment or owner.
- RESTful, supports paths like /res/*, /res/:id and HTTP methods like GET, POST, PUT, DELETE.
- Deny-override: both allow and deny authorizations are supported, deny overrides the allow.
- Priority: the policy rules can be prioritized like firewall rules.
Originally Casbin was written in Golang, but is compatible with a lot of languages and frameworks:
- Java
- C/C++
- Node.js
- Javascript
- PHP
- Laravel
- Python
- .NET (C#)
- Delphi
- Rust
- Ruby
- Swift (Objective-C)
- Lua (OpenResty)
- Dart (Flutter)
- Elixir
In my case, I used Casbin with python (pycasbin), but the functions are the same, so if you have the fear about changes in each language, basically is the same implementation in each one.
Casbin uses an access control model that is abstracted into a CONF file based on the PERM metamodel (Policy, Effect, Request, Matchers). This means that you need a file with all definitions and rules that you want to apply with each policy(rule) that you store. So if you need to change the logic in the validations you just need to upgrade your configuration file.
Here are some configuration definitions in Casbin:
· Policy: In Casbin’s terminology, “policy” is equivalent to our concept of a ‘permission’. In fact, it defines the name and order of the fields in the policy rule.
For instance: p={sub, obj, act} or p={sub, obj, act, eft}
Where sub
is equivalent to a subject, obj
is the object, and act
is an action. You can store more attributes for example you can see an attribute called eft
that represents a custom effect in the policy. In your policy isn’t necessary to define the three parameters you can define a group(role like admin) for example and one action to apply in that group.
· Effect: Defines what action is taken if a request is matched according to the matcher in the configuration.
For example: e = some(where(p.eft == allow))
This means what is the action applied if our request satisfies our match(main rule), you can see in the example that if our request matches with our rule then return the authorization or rejection.
· Request: We define our authorization request to interact with Casbin. A basic request is a tuple object, requiring at least a subject (accessed entity), object (accessed resource), and action (access method)
For instance, a request definition may look like this: r={sub,obj,act}
Here we define what parameters are incoming in each validation. These parameters are used in the matching rule.
· Matchers: Defines the boolean expression needed to satisfy an authorization request with existing policies.
For example: m = r.sub == p.sub && r.act == p.act && r.obj == p.obj.
This simple and common matching rule means that if the requested parameters (subjects, resources, and methods) are equal to one policy stored in our policies register, a positive authorization is returned. The result of the strategy will be saved in p.eft.
How does it work?
Maybe the definitions sound wear, but here I’m going to explain how Casbin uses those definitions and applies the rules. First, the model definition needs to be added, you need to create a .conf file and here you need to add at least four sections: [request_definition]
, [policy_definition]
, [policy_effect]
, and [matchers]
. Additionally, if the model is following RBAC it should contain a [role_definition] section. In these sections, we are going to define the structure of our permissions, how is the structure of the requests, the effect to apply if the request matches with our rule, and finally our matcher(rule) that define the conditions to allow or reject a request.
Now, how exactly Casbin takes these configurations and then it applies the result? You define your rules about policy and request, Casbin searches into the file or database the information about your user or role and then apply the rule(matchers) using the information that you provided(request), if your request match the rule of policy, the effect is applied (allow or reject the request).
The configuration models are not strict, you can customize your own access control model by combining the available models. The most basic and simplest model in Casbin is ACL. ACL’s model CONF is:
[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
In this model, we define a request that requires three parameters: sub, obj, and act. This means that we need in each request a user identifier (id, name, or any kind of identifier), the resource that is the target, and the action that we need to validate (read, write, delete, etc).
Now, where are stored the policies? Well, you have two options, the first one is to store each policy in a local file, which isn’t no the best option but when you are testing and learning is the right choice. The second one is to store your policies in the cloud, I’m going to talk about this method later.
Here, an example, we stored an ACL model in a local file, and looks like this:
p, Alice, data1, readp, Bob, data2, write
It means:
· Alice can read data1
· Bob can write data2
If our request has a user(sub) Alice, resource(obj) data1, and an action(act) read, the response going to be true because in our policies we have a rule that allows that action in that resource with the user Alice, but if we send in our request Alice, data2, write, the response going to be negative because in our policies we don’t have any policy to allow at Alice write-in resource data 2.
Policy persistence
I talked about cloud storing to your policies, well, Casbin policy can be stored in lots of places. Currently, dozens of databases are supported, from MySQL, Postgres, Oracle to MongoDB, Redis, Cassandra, AWS S3.
In Casbin, the policy storage is implemented as an adapter. For details of adapters, please refer to the documentation.
Implementation
Now, I want to show some examples with Casbin, lets start with a simple example with basic operations. If you remember, I said that you can use a local file or an adapter to store your policies, in this case, I’m going to show you both ways.
First, the simple way, to do this you need to add the configuration file (the same that I used in the explanation about configuration) and add one file called policy.csv (here the policies are going to be stored). My policy looks like this:
p, b8b1294c-04b2-4245-9d4d-0ac60a7804b7, 3f70632a-2168-11ec-9621-0242ac130002, read
p, b8b1294c-04b2-4245-9d4d-0ac60a7804b8, 3f70632a-2168-11ec-9621-0242ac130005, write
The letter p is important because is the way to recognize a policy. When the files are ready, you need to configure Casbin to use the configuration and set the location of the policies.
import casbin
e: Any = casbin.Enforcer("./model.conf", "./policy.csv")
Now, you need to establish your request parameters I used static fields but you can modify the code and use custom inputs.
sub = "b8b1294c-04b2-4245-9d4d-0ac60a7804b7" # the user that wants to access a resource.obj = "3f70632a-2168-11ec-9621-0242ac130002" # the resource that is going to be accessed.act = "read" # the operation that the user performs on the resource.
If you don’t remember sub is our user or subject, obj is the resource or object, and action is the permission. In this case, I want to compare, if the user b8b1294c-04b2–4245–9d4d-0ac60a7804b7
has permission to read
the resource with id 3f70632a-2168–11ec-9621–0242ac130002
. Is a simple request we have a user x, we want to validate an action with x resource.
The validation is simple, we need to call the enforce
function of Casbin and set the parameters sub, obj, and act. If you set the section request_definition
with more parameters you need to pass those parameters to make the validation.
if e.enforce(sub, obj, act):
print(f'Hi, {sub} is authorized to {act} resource {obj}')
pass
else:
print(f'Hi, {sub} is not authorized to {act} resource {obj}')
pass
And the result is some like this:
If we test with another user:
Casbin prints a warning with the request parameters to show what is the problem with the request. If the sub or obj doesn’t exist in the policy.csv, casbin doesn’t show an error, it does the validation and throws the result.
The validation is really easy, but I’m going to share some methods that I think are interesting. There are many functions and you can find information in this link.
First, to add a new policy, you need to call the function add_policy
and set the user, resource, and action. If you defined more request parameters in your config you need to set them in the function.
e.add_policy(sub, obj, action)e.add_policy("bob", "data2", "write")
To update a policy, you pass the old policy and the new policy.
e.update_policy(["bob", "data2", "delete"],
["bob", "data2", "read"])
To remove a policy, you need to pass an array with the user, resource, and action.
e.remove_policy([sub, obj, action])e.remove_policy(["bob", "data2", "delete"])
If you want to remove all policies associated with one resource, you need to call remove_filtered_policy
, and set the first parameter as zero because when this function filters the policy return a kind of matrix, so you need the first position with policy results. The second parameter could be and specific user but you can leave an empty string and finally the resource ID. In this case, I want to remove all policies associated with the resource 3f70632a-2168–11ec-9621–0242ac130002
:
e.remove_filtered_policy(0, subj, obj)e.remove_filtered_policy(0, "", "3f70632a-2168-11ec-9621-0242ac130002")
If you need to get all permissions for one user you can use this:
e.get_permissions_for_user(sub)e.get_permissions_for_user("b8b1294c-04b2-4245-9d4d-0ac60a7804b7")
Finally, if you want to get all permissions associated with a user and specific resource you must use:
e.get_filtered_policy(0, sub, obj)e.get_filtered_policy(0, "b8b1294c-04b2-4245-9d4d-0ac60a7804b7", "3f70632a-2168-11ec-9621-0242ac130002")
When you’re working with local files you need to call the function save_policy()
at the end of each function that modifies the policy. For example:
e.add_policy("bob", "data2", "write")
e.save_policy()
Now, what happens if you want to use a database to store and retrieve my policies, well in that case you need to use an adapter, the only thing that changes is the configuration in Casbin, you need to replace the CSV file with your adapter.
Is important that you know that each storage method has its own adapter, but everyone does the same thing, the adapter has two responsibilities, the first one is to create the table in your database, this table is called casbin_rule
you can’t change the table’s name, and when the adapter creates the table adds 8 columns, well that was my case with this adapter SQLAlchemy Adapter, when I added the adapter it created the table and the 8 columns automatically.
Maybe other adapters have different behavior, here you can see all adapters to each language supported.
Here is a view of one policy stored in my database:
As I said, the implementation with policies stored in databases is easy, in my case, I used PostgreSQL and python into an AWS lambda. And my connection was really easy to implement.
import os
import casbin_sqlalchemy_adapter
import casbin# Get environment variables
username = os.getenv("DB_USER")
password = os.environ.get("DB_PASSWORD")
host = os.environ.get("DB_HOST")
db_name = os.environ.get("DB_NAME")adapter = casbin_sqlalchemy_adapter.Adapter( f"postgresql://{username}:{password}@{host}/{db_name}")casbin_sqlalchemy_adapter.CasbinRule#Load Casbin configuration
e = casbin.Enforcer("./model.conf", adapter)def handler(event, context): # Fake data to simulate request params
sub = "b8b1294c-04b2-4245-9d4d-0ac60a7804b7" # the user that wants to access a resource. obj = "3f70632a-2168-11ec-9621-0242ac130002" # the resource that is going to be accessed. act = "read" # the operation that the user performs on the resource. # Make a request to validate the permission
validation = e.enforce(sub, obj, act) # Object to show validation result
object = {"auth": validation, "sub": sub, "obj": obj, "act": act}
return {
"statusCode": 202,
"body": json.dumps(object),
"headers": {"Content-Type": "application/json"},
}
In this example, you can see my adapter, it replaces my policy file and the functions don’t change. The difference is when I call functions that modify the policy information I don’t need to call the function save_policy
, because the adapter makes requests to the database and modifies the information. Basically, the adapter behind has implementations to make requests and modify your database, so you don’t need to add queries or something like that.
You can see the code of the project here: https://github.com/ridouku/pycasbin
More information about:
Questions? Comments? Contact me at ridouku@gmail.com