Developing a secure system requires robust authentication and authorization implementation. While authenticating users might not take much effort, the authorization aspect can get complicated. This is especially true if the application has various kinds of users. In this article, we will look into RBAC and CBAC access control mechanisms. CBAC – an awesome access control mechanism means Claim-based access control whereas RBAC means Role-based access control. The examples in the post use GraphQL, Nexus, Prisma, and React. We will also see the pros and cons with an explanation in the article as we go on.
Table of Contents
What is access control?
Access control is a security mechanism that administers the accessibility of resources in computing systems. A piece of software consists of various components targeted at different types of users. For instance, HRIS (Human Resource Information System) software can have different types of employees like admin, HRs, salesmen, accountants, interns, regular employees, supervisors, etc. As a regular employee, you can apply for leaves while your supervisor can approve or reject your leaves. Such restrictions on certain kinds of users call forth the need for access control mechanisms.
Types of access control – RBAC and CBAC
There are two basic types of access control mechanisms, RBAC (Role-based AC) and CBAC (Claim-based AC).
To understand both of the methods, let’s take an example of the entry system in airport terminals.
In the RBAC system, the authorization is thought of as follows.
- If you are an employee of the airport, you can enter the terminal.
- If you are a passenger for an upcoming flight, you can enter the terminal.
- If you are a pilot for an upcoming flight, you can enter the terminal.
- If you are a VIP, you can enter the terminal.
As we can see, this system of authorization determines access based on who is trying to access the resource. Now, let’s see how is this different from the CBAC system.
- To enter the terminal, you must have a valid employee ID card from the airport.
- Or, you must have a ticket for the upcoming flight.
- Or, you must have a pilot’s ID for the upcoming flight.
- Or, you must have a VIP pass
So, in this mechanism, it doesn’t check whether you are a pilot, passenger, or employee. As long as you have a claim to enter the terminal, you get access to it.
RBAC Access Control Example
To summarize, in a role-based system, we don’t explicitly write down the rules to perform a certain task. Rather, we create conditions in our code to satisfy the need of different roles.
enum Role {
Admin = "ADMIN",
Normal = "NORMAL",
Guest = "GUEST"
}
interface User {
id: string;
role: Role;
}
// Contains logged in user
interface Context {
user: User
}
interface Post {
id: string;
title?: string;
body?: string;
createdBy: User['id'];
}
// Create different users
const adminUser: User = {
id: "admin_user",
role: Role.Admin
}
const normalUser: User = {
id: "normal_user",
role: Role.Normal
}
const guestUser: User = {
id: "guest_user",
role: Role.Guest
}
const users: User[] = [adminUser, normalUser, guestUser];
// A mock posts list
let posts: Post[] = [
{id: "post_1", title: "how to be a software engineer?", createdBy:"admin_user"},
{id: "post_2", title: "doubling money in 25 days", createdBy:"normal_user"},
]
interface PostDeleteInput {
id: string;
}
// Service to delete posts
export function deletePost(context: Context, body: PostDeleteInput) {
if(context.user.role === Role.Guest) throw new Error("Cannot delete any posts.")
const post = posts.find(item => item.id === body.id);
if(context.user.role === Role.Normal) {
if(post?.createdBy !== context.user.id) throw new Error("Cannot delete posts of others.")
}
const updatedPosts = posts.filter(item => item.id !== body.id);
posts = updatedPosts;
return posts;
}
In the example above, we have created three types of users, “admin”, “normal” and “guest”. There are posts created by the admin user and the normal user.
The access control is that,
- Guest users cannot delete anyone’s posts.
- Normal users can delete their own posts.
- Admins can delete anyone’s posts.
If we look into the code, it’s quite simple and intuitive. However, when there are a lot of roles, it’s very difficult to maintain them.
Extension to RBAC – CBAC
In CBAC, instead of checking roles, we check if the request has a claim to access the resource.
Check the CASL website: https://casl.js.org/v6/en/
type RawRule = {
action: string | string[];
subject: string;
fields?: string[];
conditions?: Record<string, unknown>;
inverted?: boolean;
}
interface User {
id: string;
permissions: RawRule[];
}
// Contains logged in user
interface Context {
user: User
ability: any; // This is created from the permissions for the user (by CASL)
}
interface Post {
id: string;
title?: string;
body?: string;
createdBy: User['id'];
}
// Create different users
const adminUser: User = {
id: "admin_user",
permissions: [{action:"delete", subject:"Post"}]
}
const normalUser: User = {
id: "normal_user",
permissions: [{action:"delete", subject:"Post", conditions:{createdBy: "normal_user"}}]
}
const guestUser: User = {
id: "guest_user",
permissions: []
}
const users: User[] = [adminUser, normalUser, guestUser];
// A mock posts list
let posts: Post[] = [
{id: "post_1", title: "how to be a software engineer?", createdBy:"admin_user"},
{id: "post_2", title: "doubling money in 25 days", createdBy:"normal_user"},
]
interface PostDeleteInput {
id: string;
}
// Service to delete posts
export function deletePost(context: Context, body: PostDeleteInput) {
const post = posts.find(item => item.id === body.id);
if (!post) throw new Error("No posts found with the supplied ID");
// ability is an object that is created from the permissions rules of a user.
// the following code checks if there is an ability that has access to delete the post
if (context.ability.cannot("delete", post)) throw new Error("You don't have access to delete the post.")
const updatedPosts = posts.filter(item => item.id !== body.id);
posts = updatedPosts;
return posts;
}
As we see above, instead of roles, we set permission rules for users. However, we can set permission rules for users through roles. That is, we first assign permissions to a role, and then assign the role to a user. Then, a library like CASL utilizes this set of permissions to give us their abstraction.
A permission set can contain the following properties (in CASL).
- Action: It decides the operation that a user can perform on a subject. For example, “read”, “create”, “update”, “delete”, etc. can be some actions.
- Subject: It decides where to apply the restrictions. It can be a database model, a class, or an object. In a blogging website, “Post”, “Comment”, “User”, and “Review” can be some of the subjects.
- Fields (Optional): A field is a property of a subject. For example, in a Post subject, “title”, “body”, “createdBy”, etc. can be some of the fields.
- Conditions (Optional): A user can have conditional access to a subject. For example, we can let users delete their posts only.
- Inverted (Optional): We can invert the logic with this property. For example, if a user cannot delete read-only posts, we can write the permission as {action: “delete”, subject: “Post”, conditions: {readonly: true}, inverted: true}.
The first two, “action” and “subject” are mandatory.
The main advantage of having a permission set is that we can reuse these sets in different services. Therefore, we don’t need to explicitly write conditions for a different role.
For example, in the frontend React code, we can wrap the delete post button as follows.
<Can I="delete" this={post} >
<DeleteJobButton id={post.id} />
</Can>
Like in the backend, we don’t have to write conditional code in the frontend code too.
However, people might not like the idea since it exposes some DB models, and fields. But I don’t mind this at all because sharing the DB schema and sharing the DB are two completely different things. It’s just my opinion though. Nevertheless, we can create a different permission set for the client side or manipulate the original rules for it.
Cons of CBAC Access Control Mechanism
Although the code doesn’t need to change once written, it’s a challenge to maintain the permission rules for all roles or users. This is more complicated if the permissions are dynamic and need to be changed from a dashboard. If we don’t have to allow fields and conditional access, then it becomes easier. This is because we can maintain a set of actions and subjects easily. However, maintaining fields and conditions is troublesome.
Another problem with this approach is, if we write or check permissions wrong, this can create a lot of loopholes. For example, let’s check the following code that checks if the user has access to delete a post.
// Throws an error if no access to delete a post
ForbiddenError.from(ability).throwUnlessCan("delete", "Post");
How is this problematic? As we see in the earlier examples, a normal user can only delete his posts whereas an admin can delete everyone’s posts. So, technically, both of the users have access to the Post subject.
The above code only throws an error if the user completely doesn’t have access to delete a job like in the case of a guest user. Thus, we need to check the object not the subject in this case. The code should be as follows.
// Throws an error if no access to delete a post
ForbiddenError.from(ability).throwUnlessCan("delete", subject("Post", post));
Similarly, if we write incorrect or loose rules, it can cause security issues such as in the following example. For example, let’s say that the developers added a feature to protect a post by not allowing deletion from users other than admin. But, they forgot to update the existing permissions.
// Old permissions (bug)
[{action:"delete", subject:"Post", conditions:{createdBy: "{{userId}}"}}]
// New permissions (expected)
[{action:"delete", subject:"Post", conditions:{createdBy: "{{userId}}", isProtected: false}}]
If the access control has loopholes like these, and the permission set is exposed to the front, then, it’s an easy piece of cake for hackers to exploit.
As a final note, CBAC gives cleaner code and an extensible solution to manage access. However, maintaining the solution is pretty challenging.
Check this post for feature flag and remote config management using Flagsmith