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 usersRole
– Contains a set of permissionsSession
– Active user session with activated rolesPermission
– Authorization to perform operations
The activity diagram below provides an overview of how these components interact:
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.