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.