Introduction
Role-based access control is a method of assigning permissions to users based on their role. Users with the same set of permissions are grouped under the same role, simplifying the authorization process. This gives us a clear and structured way to manage user authorization and access to secured resources using simple bit operations.
Bit Field Role-Based Access Control is a method of assigning a bit field to each permission to represent a role. Each bit corresponds to a specific permission. If a user is authorized, it can then be quickly determined by the bit field associated with their role. This enables us to manage our access control easily without having to store arrays of strings for managing roles and their permissions.
How It Works
The permissions are represented using bit values making them easier to manage efficiently without requiring much space. Here's the breakdown:
Binary Representation
Let's consider we have a system of four permissions. We can assign a bit to each permission like below:
- CREATE - Represented by the first bit
0001
- READ - Represented by the second bit
0010
- UPDATE - Represented by the third bit
0100
- DELETE - Represented by the fourth bit
1000
Creating Roles
We can now combine permissions to create roles using bitwise OR
operation like shown below:
- Editor Role:
READ: 0010
UPDATE: 0100
Combined: 0010 | 0100 = 0110
Editor Role Representation in Binary Form
- Admin Role:
CREATE: 0001
READ: 0010
UPDATE: 0100
DELETE: 1000
Combined: 0001 | 0010 | 0100 | 1000 = 1111
Admin Role Representation in Binary Form
Checking Permissions
We can now check if a user has access to the role using bitwise AND
operation between the user's role and the role in question. To check if a person with the Editor (0110
) role has access to UPDATE (0100
) permissions by performing bitwise AND
operation between the user's role and permission in a question like this:
EDITOR ROLE: 0110
UPDATE ROLE : 0100
Combined : 0110 & 0100 = 0100 // (0100 = 0100) Has Access
Checking Permission using AND
operation
If the result matches the permission, the user has access to the resource thus, simplifying the process of authorizing users based on their permission.
Implementation of Bit Field RBAC
Here's an example of the implementation of Bit Field RBAC demonstrated in Express.js and Prisma, the core concept remaining the same, for you to implement it in your application.
Define Schema
model Role {
id Int @id @default(autoincrement())
name String @unique
permissions BigInt
users User[]
}
model User {
id Int @id @default(autoincrement())
username String @unique
password String
role Role @relation(fields: [roleId], references: [id])
roleId Int
}
Prisma Schema for User and User Roles
Here, we define a simple schema where there is an one-to-many relationship between role and users. This lets use create multiple roles in our application effectively. We use Bigint
to store our permission field enabling us to handle more permissions in the future if required.
Insert Roles
Now, we can define permissions as bit fields:
//permissions.ts
const PERMISSIONS = {
BLOG: {
CREATE: 1 << 0,
READ: 1 << 1,
UPDATE: 1 << 2,
DELETE: 1 << 3
},
COMMENT:{
CREATE: 1 << 4,
READ: 1 << 5,
UPDATE: 1 << 6,
DELETE: 1 << 7
},
...OTHER:{
....
}
}
Definition of Permission using Bitfield
Then, we can create roles like this:
//permissions-db.ts
const VIEWER_ROLE_PERMISSIONS = PERMISSIONS.BLOG.READ | PERMISSIONS.COMMENT.READ;
//Combination of all the permissions
const ADMIN_ROLE_PERMISSION = Object.values(PERMISSIONS).reduce(
(acc, permission) => {
return acc | Object.values(permission).reduce((acc, p) => acc | p, 0n);
},
0n
);
async function createRole(name: string, permissions:bigint){
await prisma.role.create({
data:{
name,
permissions
}
})
}
await createRole("viewer", VIEWER_ROLE_PERMISSIONS);
Creating Role Function
This way, we can combine multiple bits to create role with multiple permissions and store it efficiently in our database.
Check Permissions
Now, we can create a utility function for checking if a user's role has specific permissions.
//user-db.ts
async function getUserAndRole(userId: string){
return await prisma.user.findUnique({
where:{
id: userId,
include:{
role: true
}
}
})
}
Retrieving user and user role from the database
const hasPermission = async (userId: string, permission: bigint) => {
// Fetch user and their role
const user = await getUserAndRole(userId);
if (!user) {
throw new Error('User not found');
}
// Check if user role has the permission
return (user.role.permissions & permission) === permission;
};
Validating permission
// src/app.ts
import express, { Request, Response, NextFunction } from 'express';
import { hasPermission, PERMISSIONS } from './permissions';
const app = express();
const findUserById = (req: Request, res: Response, next: NextFunction) => {
req.userId = parseInt(req.params.userId, 10);
if (!req.userId) return res.status(400).send('User ID is required');
next();
};
const checkPermission = (permission: number) => async (req: Request, res: Response, next: NextFunction) => {
try {
const hasPerm = await hasPermission(req.userId, permission);
if (hasPerm) {
next();
} else {
res.status(403).send('Forbidden');
}
} catch (err) {
res.status(500).send(err.message);
}
};
app.get('/blog/:userId', findUserById, checkPermission(PERMISSIONS.BLOG.READ), (req: Request, res: Response) => {
res.send('Access granted to read blog');
});
app.post('/blog/:userId', findUserById, checkPermission(PERMISSIONS.BLOG.WRITE), (req: Request, res: Response) => {
res.send('Blog post created');
});
app.delete('/blog/:userId', findUserById, checkPermission(PERMISSIONS.BLOG.DELETE), (req: Request, res: Response) => {
res.send('Blog post deleted');
});
app.listen(3000, () => {
console.log('Server is running on port 3000');
});
Express app with Bit Field RBAC Middleware
Summary
- Define Permission: Set Bit Field for each permission in our config file.
- Insert Roles: Assign permission to Roles using bitwise
OR
operation. - Update Utility functions: Check if a user has permission using bitwise
AND
operation. - Add Middleware: Use middleware to enforce permissions on routes.
Conclusion
In this way, a Bit Field Role-Based Access Control System provides us with an efficient way to manage permissions within an application. This helps to handle complex permission structures with minimal overhead. It simplifies the logic for handling permission and roles efficiently. It can be very effective when dealing with a vast number of permissions and roles. It also allows us to include user's permission in our JWT payload avoiding the need to call our database for retrieving a user's permission. However, we need to be careful with the management of bit positions of the bit field which could break the whole permission system if not handled correctly.
Thank you for reading this article. See you in the next one! Please consider subscribing for more insightful articles like this.