How to Implement Role-Based Access Control RBAC in Python: A Comprehensive Guide

How to Implement Role-Based Access Control RBAC in Python A Comprehensive Guide

Introduction

Role-based access control (RBAC) is an authorization strategy used to restrict system access to authorized users. With RBAC, access rights are grouped by job role, and each user is assigned roles that dictate their level of access.

RBAC simplifies access management as permissions are not directly assigned to specific users. Instead, users acquire permissions through their assigned roles. This makes it easy to manage access at scale just by updating a user’s roles.

This in-depth tutorial explains how to implement RBAC in Python, allowing you to add robust access control to your Python applications and APIs.

RBAC Key Concepts

Before diving into the implementation, it’s important to understand the core concepts of RBAC:

  • Roles – A collection of permissions. Roles are assigned to users. Example roles: Administrator, Editor, Guest etc.
  • Permissions – Authorization to perform certain operations like view, edit, delete etc. Permissions are grouped under roles.
  • Users – Individual users in the system. Users can be assigned multiple roles.
  • Sessions – Mapping between user and their activated role(s) for a login session.

With these entities, RBAC regulates access by only allowing users to do what their active roles permit during a session.

For instance:

  • User “Bob” is assigned “Editor” and “Guest” roles
  • In one session, “Editor” role is activated granting editing permissions
  • In another session, only “Guest” role is activated for read-only access

This makes RBAC very flexible and scalable vs hardcoding user permissions.

Implementing RBAC in Python

We will implement a simple RBAC system in Python with the following components:

  • User – Represents system users
  • Role – Contains a set of permissions
  • Session – Active user session with activated roles
  • Permission – Authorization to perform operations

The activity diagram below provides an overview of how these components interact:

RBAC pattern

Let’s model each component as Python classes/objects.

1. Modeling Permissions

We start with the base Permission class that represents a single privilege:

class Permission:
  def __init__(self, name):
    self.name = name
  
  def __str__(self):
    return self.name

# Permissions examples
p1 = Permission("create")  
p2 = Permission("delete")

The name captures what this permission authorizes someone to do, like create, edit, view etc.

We implement the __str__() method to return a human-readable string representation of the permission. This will be helpful later for debugging.

2. Modeling Roles

A role contains a collection of permissions. We can model it as:

class Role:
  
  def __init__(self, name, permissions):
    self.name = name
    self.permissions = permissions

  def add_permission(self, permission):
    self.permissions.append(permission)
  
  def remove_permission(self, permission):
    self.permissions.remove(permission)
    
  def __str__(self):
    return f"{self.name} : {self.permissions}"

# Role example
permissions = [p1, p2]
role = Role("admin", permissions)

The Role constructor takes a name and initialized permissions list. We also implement methods to add/remove permissions from the role.

The __str__() method returns the string representation of the role including its permissions for debugging.

3. Modeling Users

A user can be assigned multiple roles. We can model this as:

class User:

  def __init__(self, name, roles):
    self.name = name
    self.roles = roles

  def add_role(self, role):
    self.roles.append(role)

  def remove_role(self, role):
    self.roles.remove(role)

  def __str__(self):
    return f"{self.name} : {self.roles}"

# User example 
roles = [role1, role2]
user = User("Alice", roles)

The User constructor takes the user’s name and initialized roles. We also implement add_role() and remove_role() methods.

The string representation includes the name and assigned roles.

4. Modeling Sessions

A session represents the mapping between a user and their activated subset of roles for a login session.

class Session:

  def __init__(self, user):
    self.user = user
    self.active_roles = {}

  def add_role(self, role):
    self.active_roles[role.name] = role

  def drop_role(self, role):
    del self.active_roles[role.name]

  def __str__(self):
    return f"{self.user.name} : {self.active_roles.values()}"

# Session example
user = # instantiated User object
session = Session(user)
session.add_role(role1)

The session is initialized with a user object. We store the currently activated roles in a active_roles dictionary.

add_role() activates a role for the session while drop_role() removes an active role.

The string representation displays the user and their active roles for debugging.

This completes our basic RBAC models! Now we can bring them together to implement access control logic.

5. Check Access

The core RBAC logic is checking if a user’s active session roles grant them permission to perform a certain operation.

We can implement a check_permission() method on Session to encapsulate this logic:

def check_permission(self, permission):
  
  # Iterate through active roles
  for role in self.active_roles.values():  

    # Check if required permission belongs to role
    if permission in role.permissions:
      return True
  
  # Permission not found    
  return False

This iterates through the user’s currently active roles during the session. If the required permission is found in any role’s permissions, access is granted and True returned.

If not found across any roles, False is returned to deny access.

For example:

# User has active 'admin' and 'writer' roles
session.active_roles = {
  "admin": admin_role,
  "writer": writer_role
}

# Check if user can delete 
if session.check_permission(delete_permission):
  print("Delete allowed")
else:
  print("Delete not allowed")

This encapsulates the access logic neatly inside Session. The rest of the app simply has to call check_permission() to authorize operations.

Using the RBAC Classes

Let’s write a simple example to demonstrate using the RBAC classes together:

# Permissions
p1 = Permission("create")
p2 = Permission("delete")

# Roles 
writer_permissions = [p1]
writer_role = Role("writer", writer_permissions)

all_permissions = [p1, p2]
admin_role = Role("admin", all_permissions)

# Users
writer_user = User("Alice", [writer_role])  
admin_user = User("Bob", [admin_role, writer_role])

# Sessions
writer_session = Session(writer_user)
writer_session.add_role(writer_role)

admin_session = Session(admin_user)
admin_session.add_role(admin_role)
admin_session.add_role(writer_role)

# Permission checks
writer_can_delete = writer_session.check_permission(p2) # False
admin_can_delete = admin_session.check_permission(p2) # True

We create some sample Permission, Role, User and Session objects.

The writer_user Alice only has the writer_role assigned. The admin_user Bob has both admin_role and writer_role.

In the sessions, only the writer_role is activated for writer_session. But both roles are activated in admin_session.

Finally, we check permissions by calling check_permission() on the sessions. This correctly determines that only Bob’s admin session has delete permission, not Alice’s writer session.

This demonstrates how we can use the RBAC classes together to authorize different operations.

Enforcing Authorization

Now that we can check permissions via roles, the next step is enforcing authorization in our application.

There are two main approaches for this:

1. Decorator

We can create a @role_required decorator to check if user has required permission before executing a function:

from functools import wraps

def role_required(permission):
  
  def decorator(f):
    
    @wraps(f)
    def wrap(session, *args, **kwargs):
      
      if session.check_permission(permission):
        return f(session, *args, **kwargs)
      else:
        raise UnauthorizedError
    
    return wrap
  
  return decorator

This follows a typical decorator pattern. We check if session has required permission via check_permission().

If authorized, the original function f executes. If not, we raise an UnauthorizedError.

To use:

@role_required(delete_permission)
def delete_record(session, record_id):
  # delete logic
  
# Will only execute if session has 
# delete permission
delete_record(admin_session, 123)

2. Class method decorator

We can also decorate methods inside a class using method_decorator:

from functools import method_decorator

class Record:

  @method_decorator(role_required(delete_permission))
  def delete(session, self, record_id):
   # delete logic
   
record = Record()

# Delete will raise error if session lacks permission  
record.delete(unauthorized_session, 123)

This way specific class methods can be restricted based on permissions.

Decorators are just one approach. There are other ways like middleware to enforce authorization too. Pick the approach that best fits your application architecture.

Key Benefits of RBAC

Implementing RBAC as above provides some great advantages:

  • Separation of duties – Different roles can be defined with distinct permissions avoiding conflicts
  • Least privilege – Users get minimum required permissions through specific roles
  • Ease of management – Instead of per-user access control, just manage roles
  • Reusability – Roles can be reused across applications and organizations
  • Auditability – Logging roles provides an audit trail of permissions granted

As applications grow to have more users and resources, RBAC becomes vital for manageable access control.

Conclusion

This covers a simple but complete approach to implement role-based access control in Python. The key concepts are:

  • Permissions are individual privileges like create or delete
  • Roles group relevant permissions like writer or admin
  • Users are assigned roles based on their job duties
  • Active sessions determine what roles are enabled for a user
  • Checking if a role has required permission enables authorization

With this RBAC foundation, you can build access control for diverse scenarios like:

  • REST APIs and Microservices
  • User-based systems and dashboards
  • Administrative interfaces
  • Multi-tenant applications
  • Resource restriction in large organizations

RBAC prevents unauthorized access and enables managing permissions at scale. Implement it using the classes and patterns above to authorize users in your Python projects securely.

Leave a Reply

Your email address will not be published. Required fields are marked *