Skip to content

RBAC in Action: A Step-by-Step Guide to Securing Web Applications Like a Pro

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.

Let’s start by reviewing the entity-relationship model. We’ll define five core tables:

  • tbl_users: User accounts
  • tbl_roles: Role definitions
  • tbl_permissions: Permission definitions
  • tbl_user_roles: Many-to-many relationship between users and roles
  • tbl_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.

from datetime import datetime
from portfolio_app import db
from 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}')"
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.

from functools import wraps
from flask import jsonify
from flask_jwt_extended import get_jwt_identity, verify_jwt_in_request
from 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 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 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 False

Now let’s see how to use these decorators in real API endpoints:

from flask import Blueprint, jsonify, request
from flask_jwt_extended import jwt_required
from 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"}), 200

Let’s initialize our system with predefined roles and permissions:

from flask import current_app
from portfolio_app import db
from 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!")

Here’s how to integrate RBAC with JWT authentication:

from flask import jsonify
from flask_jwt_extended import create_access_token, create_refresh_token
from 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()
}
}), 200

Now let’s integrate RBAC with our React frontend using Redux for state management:

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>
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>
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 components
function 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>
);
}

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>
);
}

Here’s how to test your implementation:

import pytest
from portfolio_app import create_app, db
from portfolio_app.models.tbl_users import User, Roles, UserRoles
@pytest.fixture
def 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 permissions

Best Practices and Security Considerations

Section titled “Best Practices and Security Considerations”
  1. Always Validate on Backend: Never trust frontend permission checks alone
  2. Use HTTPS: Always use HTTPS in production to protect tokens
  3. Token Rotation: Implement refresh token rotation for better security
  4. Audit Logging: Log all permission checks and denials
  5. Principle of Least Privilege: Grant minimum permissions necessary
  6. Regular Review: Periodically review and audit role assignments

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!