RBAC in Action: A Step-by-Step Guide to Securing Web Applications Like a Pro
Introduction
Section titled “Introduction”When I first thought about implementing a Role-Based Access Control (RBAC) system, complexity was the first thing that came to mind. How do you design a system that is secure, scalable, and easy to maintain? How do you ensure users only have access to the features they’re authorized for? How do you prevent users from accessing protected routes through URL manipulation? How do you guarantee that authenticated users can’t access protected routes without the necessary permissions?
These questions led me to build a comprehensive RBAC system for my portfolio application. In this article, I’ll walk you through the complete implementation, from database design to frontend integration.
The Database Design: Foundation of RBAC
Section titled “The Database Design: Foundation of RBAC”Let’s start by reviewing the entity-relationship model. We’ll define five core tables:
tbl_users: User accountstbl_roles: Role definitionstbl_permissions: Permission definitionstbl_user_roles: Many-to-many relationship between users and rolestbl_role_permissions: Many-to-many relationship between roles and permissions
This design allows maximum flexibility: users can have multiple roles, and roles can have multiple permissions.
User Model
Section titled “User Model”from datetime import datetimefrom portfolio_app import dbfrom werkzeug.security import generate_password_hash, check_password_hash
class User(db.Model): __tablename__ = "tbl_users"
ccn_user = db.Column(db.Integer, primary_key=True) first_name = db.Column(db.String(50), nullable=False) middle_name = db.Column(db.String(50), nullable=True) last_name = db.Column(db.String(50), nullable=False) email = db.Column(db.String(100), unique=True, nullable=False, index=True) password = db.Column(db.String(300), nullable=False) account_id = db.Column(db.String(300), nullable=False, unique=True) is_active = db.Column(db.Boolean, default=True) created_at = db.Column(db.DateTime, nullable=False, default=datetime.now)
# Relationships roles = db.relationship("UserRoles", back_populates="user") posts = db.relationship("Post", back_populates="author")
def __init__(self, first_name, middle_name, last_name, email, password): self.first_name = first_name self.middle_name = middle_name self.last_name = last_name self.email = email self.password = generate_password_hash(password) self.account_id = self._generate_account_id()
def _generate_account_id(self): """Generate unique account ID using hash""" data = f"{self.email}{self.first_name}{self.last_name}{datetime.now()}" return generate_password_hash(data)
def has_permission(self, resource: str, action: str) -> bool: """Check if user has a specific permission""" permission_name = f"{resource}_{action}" user_permissions = self.get_permissions() return any(p['permission_name'] == permission_name for p in user_permissions)
def has_role(self, role_name: str) -> bool: """Check if user has a specific role""" return any(ur.role.role_name == role_name for ur in self.roles)
def get_roles(self): """Get all user roles""" return [ur.role for ur in self.roles]
def get_permissions(self): """Get all permissions from all user roles""" permissions = [] for user_role in self.roles: for role_perm in user_role.role.permissions: perm = role_perm.permission permissions.append({ 'ccn_permission': perm.ccn_permission, 'permission_name': perm.permission_name, 'resource': perm.resource, 'action': perm.action, 'description': perm.description }) return permissions
def check_password(self, password: str) -> bool: """Verify password""" return check_password_hash(self.password, password)
def __repr__(self): return f"User('{self.first_name} {self.last_name}', Email: '{self.email}')"Roles and Permissions Models
Section titled “Roles and Permissions Models”class Roles(db.Model): __tablename__ = "tbl_roles"
ccn_role = db.Column(db.Integer, primary_key=True) role_name = db.Column(db.String(50), unique=True, nullable=False) description = db.Column(db.String(200)) created_at = db.Column(db.DateTime, default=datetime.now)
# Relationships users = db.relationship("UserRoles", back_populates="role") permissions = db.relationship("RolePermissions", back_populates="role")
def __repr__(self): return f"Role('{self.role_name}')"
class Permissions(db.Model): __tablename__ = "tbl_permissions"
ccn_permission = db.Column(db.Integer, primary_key=True) permission_name = db.Column(db.String(100), unique=True, nullable=False) resource = db.Column(db.String(50), nullable=False) action = db.Column(db.String(20), nullable=False) description = db.Column(db.String(200)) created_at = db.Column(db.DateTime, default=datetime.now)
# Relationships roles = db.relationship("RolePermissions", back_populates="permission")
def __repr__(self): return f"Permission('{self.permission_name}')"
class UserRoles(db.Model): __tablename__ = "tbl_user_roles"
ccn_user_role = db.Column(db.Integer, primary_key=True) ccn_user = db.Column(db.Integer, db.ForeignKey("tbl_users.ccn_user"), nullable=False) ccn_role = db.Column(db.Integer, db.ForeignKey("tbl_roles.ccn_role"), nullable=False) assigned_at = db.Column(db.DateTime, default=datetime.now)
# Relationships user = db.relationship("User", back_populates="roles") role = db.relationship("Roles", back_populates="users")
class RolePermissions(db.Model): __tablename__ = "tbl_role_permissions"
ccn_role_permission = db.Column(db.Integer, primary_key=True) ccn_role = db.Column(db.Integer, db.ForeignKey("tbl_roles.ccn_role"), nullable=False) ccn_permission = db.Column(db.Integer, db.ForeignKey("tbl_permissions.ccn_permission"), nullable=False) assigned_at = db.Column(db.DateTime, default=datetime.now)
# Relationships role = db.relationship("Roles", back_populates="permissions") permission = db.relationship("Permissions", back_populates="roles")Authorization Decorators: The Security Guards
Section titled “Authorization Decorators: The Security Guards”Now comes the interesting part: creating reusable decorators to protect our API endpoints. These decorators will check permissions before allowing access to routes.
Base Permission Decorator
Section titled “Base Permission Decorator”from functools import wrapsfrom flask import jsonifyfrom flask_jwt_extended import get_jwt_identity, verify_jwt_in_requestfrom portfolio_app.models.tbl_users import User
def require_permission(resource: str, action: str): """ Decorator to require a specific permission for accessing an endpoint.
Usage: @require_permission("posts", "create") def create_post(): # Only users with posts_create permission can access pass """ def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): verify_jwt_in_request() current_user_email = get_jwt_identity() user = User.query.filter_by(email=current_user_email).first()
if not user: return jsonify({"error": "User not found"}), 404
if not user.has_permission(resource, action): return jsonify({ "error": "Permission denied", "required_permission": f"{resource}_{action}" }), 403
return fn(*args, **kwargs) return wrapper return decoratorRole-Based Decorator
Section titled “Role-Based Decorator”def require_role(role_name: str): """ Decorator to require a specific role for accessing an endpoint.
Usage: @require_role("admin") def admin_only_function(): # Only users with admin role can access pass """ def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): verify_jwt_in_request() current_user_email = get_jwt_identity() user = User.query.filter_by(email=current_user_email).first()
if not user: return jsonify({"error": "User not found"}), 404
if not user.has_role(role_name): return jsonify({ "error": "Insufficient privileges", "required_role": role_name }), 403
return fn(*args, **kwargs) return wrapper return decoratorOwnership or Permission Decorator
Section titled “Ownership or Permission Decorator”def require_ownership_or_permission(resource: str, action: str): """ Allows access if user owns the resource OR has the general permission.
Usage: @require_ownership_or_permission("posts", "update") def update_post(id): # Allows if user is post owner OR has posts_update permission pass """ def decorator(fn): @wraps(fn) def wrapper(*args, **kwargs): verify_jwt_in_request() current_user_email = get_jwt_identity() user = User.query.filter_by(email=current_user_email).first()
if not user: return jsonify({"error": "User not found"}), 404
# Check if user has general permission if user.has_permission(resource, action): return fn(*args, **kwargs)
# Check ownership (implement your ownership logic here) # For example, check if the resource belongs to the user resource_id = kwargs.get('id') or args[0] if args else None if resource_id and _check_ownership(user, resource, resource_id): return fn(*args, **kwargs)
return jsonify({ "error": "Permission denied", "message": "You must own this resource or have the appropriate permission" }), 403
return wrapper return decorator
def _check_ownership(user, resource, resource_id): """Helper function to check resource ownership""" if resource == "posts": from portfolio_app.models.tbl_posts import Post post = Post.query.get(resource_id) return post and post.ccn_author == user.ccn_user # Add more resource types as needed return FalseProtecting API Endpoints
Section titled “Protecting API Endpoints”Now let’s see how to use these decorators in real API endpoints:
from flask import Blueprint, jsonify, requestfrom flask_jwt_extended import jwt_requiredfrom portfolio_app.decorators.auth_decorators import ( require_permission, require_role, require_ownership_or_permission)
blueprint_api_post = Blueprint("api_post", __name__)
@blueprint_api_post.route("/api/v1/posts", methods=["POST"])@jwt_required()@require_permission("posts", "create")def create_post(): """Only users with posts_create permission can create posts""" request_data = request.get_json() # Create post logic here return jsonify({"message": "Post created successfully"}), 201
@blueprint_api_post.route("/api/v1/posts/<int:post_id>", methods=["PUT"])@jwt_required()@require_ownership_or_permission("posts", "update")def update_post(post_id): """Post owner OR users with posts_update permission can update""" request_data = request.get_json() # Update post logic here return jsonify({"message": "Post updated successfully"}), 200
@blueprint_api_post.route("/api/v1/posts/<int:post_id>", methods=["DELETE"])@jwt_required()@require_permission("posts", "delete")def delete_post(post_id): """Only users with posts_delete permission can delete posts""" # Delete post logic here return jsonify({"message": "Post deleted successfully"}), 200
@blueprint_api_post.route("/api/v1/admin/dashboard", methods=["GET"])@jwt_required()@require_role("admin")def admin_dashboard(): """Only admin users can access this endpoint""" # Admin dashboard logic here return jsonify({"data": "Admin dashboard data"}), 200Default Roles Setup
Section titled “Default Roles Setup”Let’s initialize our system with predefined roles and permissions:
from flask import current_appfrom portfolio_app import dbfrom portfolio_app.models.tbl_users import User, Roles, Permissions, RolePermissions
def initialize_roles_and_permissions(): """Initialize default roles and permissions"""
# Define resources and actions resources = ["posts", "users", "pumps", "categories", "comments", "roles"] actions = ["create", "read", "update", "delete"]
# Create all permissions permissions = {} for resource in resources: for action in actions: perm_name = f"{resource}_{action}" perm = Permissions.query.filter_by(permission_name=perm_name).first() if not perm: perm = Permissions( permission_name=perm_name, resource=resource, action=action, description=f"{action.capitalize()} {resource}" ) db.session.add(perm) permissions[perm_name] = perm
db.session.commit()
# Define role configurations role_configs = { "admin": { "description": "Full system access", "permissions": [p for p in permissions.keys()] # All permissions }, "moderator": { "description": "Content moderation access", "permissions": [ "posts_create", "posts_read", "posts_update", "users_read", "pumps_create", "pumps_read", "pumps_update", "categories_read", "comments_create", "comments_read", "comments_update" ] }, "user": { "description": "Standard user access", "permissions": [ "posts_create", "posts_read", "posts_update", "pumps_read", "comments_create", "comments_read", "comments_update" ] }, "guest": { "description": "Read-only access", "permissions": [ "posts_read", "pumps_read", "comments_read" ] } }
# Create roles and assign permissions for role_name, config in role_configs.items(): role = Roles.query.filter_by(role_name=role_name).first() if not role: role = Roles(role_name=role_name, description=config["description"]) db.session.add(role) db.session.commit()
# Clear existing permissions RolePermissions.query.filter_by(ccn_role=role.ccn_role).delete()
# Assign permissions for perm_name in config["permissions"]: perm = permissions.get(perm_name) if perm: role_perm = RolePermissions( ccn_role=role.ccn_role, ccn_permission=perm.ccn_permission ) db.session.add(role_perm)
db.session.commit() print("✅ Roles and permissions initialized successfully!")JWT Authentication Integration
Section titled “JWT Authentication Integration”Here’s how to integrate RBAC with JWT authentication:
from flask import jsonifyfrom flask_jwt_extended import create_access_token, create_refresh_tokenfrom datetime import timedelta
@blueprint_auth.route("/api/v1/token", methods=["POST"])def login(): """User login endpoint""" data = request.get_json() email = data.get("email") password = data.get("password")
user = User.query.filter_by(email=email).first()
if not user or not user.check_password(password): return jsonify({"error": "Invalid credentials"}), 401
# Create tokens access_token = create_access_token( identity=email, expires_delta=timedelta(hours=1) ) refresh_token = create_refresh_token( identity=email, expires_delta=timedelta(days=30) )
# Return user data with roles and permissions return jsonify({ "current_user": { "user_info": { "ccn_user": user.ccn_user, "email": user.email, "first_name": user.first_name, "last_name": user.last_name }, "token": access_token, "refresh_token": refresh_token, "account_id": user.account_id, "roles": [ {"ccn_role": r.ccn_role, "role_name": r.role_name} for r in user.get_roles() ], "permissions": user.get_permissions() } }), 200Frontend Integration with React
Section titled “Frontend Integration with React”Now let’s integrate RBAC with our React frontend using Redux for state management:
Permission Guard Component
Section titled “Permission Guard Component”import { useSelector } from 'react-redux';import { Navigate } from 'react-router-dom';
export const PermissionGuard = ({ children, resource, action, fallback = null}) => { const { permissions } = useSelector((state) => state.auth);
const hasPermission = permissions?.some( (perm) => perm.resource === resource && perm.action === action );
if (!hasPermission) { return fallback || <Navigate to="/unauthorized" replace />; }
return children;};
// Usage example<PermissionGuard resource="posts" action="create"> <button onClick={handleCreatePost}>Create Post</button></PermissionGuard>Role Guard Component
Section titled “Role Guard Component”export const RoleGuard = ({ children, roles, fallback = null}) => { const { userRoles } = useSelector((state) => state.auth);
const hasRole = userRoles?.some((userRole) => roles.includes(userRole.role_name) );
if (!hasRole) { return fallback || <Navigate to="/unauthorized" replace />; }
return children;};
// Usage example<RoleGuard roles={['admin', 'moderator']}> <AdminPanel /></RoleGuard>Custom Permission Hook
Section titled “Custom Permission Hook”import { useSelector } from 'react-redux';
export const usePermissions = () => { const { permissions, roles } = useSelector((state) => state.auth);
const hasPermission = (resource, action) => { return permissions?.some( (perm) => perm.resource === resource && perm.action === action ); };
const hasRole = (roleName) => { return roles?.some((role) => role.role_name === roleName); };
const hasAnyPermission = (permissionsList) => { return permissionsList.some(({ resource, action }) => hasPermission(resource, action) ); };
const hasAllPermissions = (permissionsList) => { return permissionsList.every(({ resource, action }) => hasPermission(resource, action) ); };
return { hasPermission, hasRole, hasAnyPermission, hasAllPermissions, permissions, roles };};
// Usage in componentsfunction PostActions({ postId }) { const { hasPermission } = usePermissions();
return ( <div> {hasPermission('posts', 'update') && ( <button onClick={() => handleEdit(postId)}>Edit</button> )} {hasPermission('posts', 'delete') && ( <button onClick={() => handleDelete(postId)}>Delete</button> )} </div> );}Admin Interface for Role Management
Section titled “Admin Interface for Role Management”Here’s a complete admin interface for managing roles and permissions:
import { useState, useEffect } from 'react';import { useForm } from 'react-hook-form';
function RolesManagement() { const [roles, setRoles] = useState([]); const [permissions, setPermissions] = useState([]); const [selectedRole, setSelectedRole] = useState(null); const { register, handleSubmit, reset } = useForm();
useEffect(() => { fetchRoles(); fetchPermissions(); }, []);
const fetchRoles = async () => { const response = await fetch('/api/v1/roles', { headers: { 'Authorization': `Bearer ${localStorage.getItem('access_token')}` } }); const data = await response.json(); setRoles(data.roles); };
const fetchPermissions = async () => { const response = await fetch('/api/v1/permissions', { headers: { 'Authorization': `Bearer ${localStorage.getItem('access_token')}` } }); const data = await response.json(); setPermissions(data.permissions); };
const onSubmit = async (data) => { const response = await fetch('/api/v1/roles', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${localStorage.getItem('access_token')}` }, body: JSON.stringify(data) });
if (response.ok) { fetchRoles(); reset(); } };
return ( <div className="max-w-6xl mx-auto p-6"> <h1 className="text-3xl font-bold mb-6">Role Management</h1>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6"> {/* Create Role Form */} <div className="bg-white p-6 rounded-lg shadow"> <h2 className="text-xl font-semibold mb-4">Create New Role</h2> <form onSubmit={handleSubmit(onSubmit)}> <input {...register('role_name', { required: true })} className="w-full p-2 border rounded mb-3" placeholder="Role Name" /> <textarea {...register('description')} className="w-full p-2 border rounded mb-3" placeholder="Description" rows="3" /> <button type="submit" className="w-full bg-blue-500 text-white p-2 rounded hover:bg-blue-600" > Create Role </button> </form> </div>
{/* Roles List */} <div className="bg-white p-6 rounded-lg shadow"> <h2 className="text-xl font-semibold mb-4">Existing Roles</h2> <div className="space-y-2"> {roles.map((role) => ( <div key={role.ccn_role} className="p-3 border rounded cursor-pointer hover:bg-gray-50" onClick={() => setSelectedRole(role)} > <div className="font-semibold">{role.role_name}</div> <div className="text-sm text-gray-600">{role.description}</div> </div> ))} </div> </div> </div>
{/* Permission Assignment */} {selectedRole && ( <div className="mt-6 bg-white p-6 rounded-lg shadow"> <h2 className="text-xl font-semibold mb-4"> Permissions for {selectedRole.role_name} </h2> <div className="grid grid-cols-2 md:grid-cols-4 gap-4"> {permissions.map((perm) => ( <label key={perm.ccn_permission} className="flex items-center space-x-2"> <input type="checkbox" className="form-checkbox" // Add checked state and onChange handler /> <span className="text-sm">{perm.permission_name}</span> </label> ))} </div> </div> )} </div> );}Testing Your RBAC System
Section titled “Testing Your RBAC System”Here’s how to test your implementation:
import pytestfrom portfolio_app import create_app, dbfrom portfolio_app.models.tbl_users import User, Roles, UserRoles
@pytest.fixturedef client(): app = create_app('testing') with app.test_client() as client: with app.app_context(): db.create_all() yield client db.drop_all()
def test_user_has_permission(client): """Test user permission checking""" # Create test user with admin role user = User( first_name="Test", middle_name="", last_name="User", email="test@example.com", password="password123" ) db.session.add(user)
admin_role = Roles.query.filter_by(role_name="admin").first() user_role = UserRoles(ccn_user=user.ccn_user, ccn_role=admin_role.ccn_role) db.session.add(user_role) db.session.commit()
# Test permission check assert user.has_permission("posts", "create") == True assert user.has_permission("users", "delete") == True assert user.has_role("admin") == True
def test_protected_endpoint(client): """Test endpoint protection""" # Try to access protected endpoint without token response = client.post('/api/v1/posts') assert response.status_code == 401
# Login and get token response = client.post('/api/v1/token', json={ 'email': 'test@example.com', 'password': 'password123' }) token = response.json['current_user']['token']
# Access with token but insufficient permissions response = client.post('/api/v1/posts', headers={'Authorization': f'Bearer {token}'}, json={'title': 'Test', 'content': 'Content'} ) # Assert based on user's actual permissionsBest Practices and Security Considerations
Section titled “Best Practices and Security Considerations”- Always Validate on Backend: Never trust frontend permission checks alone
- Use HTTPS: Always use HTTPS in production to protect tokens
- Token Rotation: Implement refresh token rotation for better security
- Audit Logging: Log all permission checks and denials
- Principle of Least Privilege: Grant minimum permissions necessary
- Regular Review: Periodically review and audit role assignments
Conclusion
Section titled “Conclusion”You now have a complete, production-ready RBAC system with:
- ✅ Flexible database design supporting multiple roles per user
- ✅ Reusable authorization decorators
- ✅ JWT authentication integration
- ✅ React frontend with permission guards
- ✅ Admin interface for role management
This implementation provides enterprise-grade security while remaining maintainable and scalable. You can extend it further by adding features like:
- Permission hierarchies
- Time-based permissions
- Resource-level permissions
- Permission inheritance
The complete code for this implementation is available in my GitHub repository.
What’s Next? Check out my other articles on building a full-stack portfolio application, including deployment strategies and database optimization techniques.
Have questions or suggestions? Feel free to reach out on LinkedIn or open an issue on GitHub!