Flask RBAC: How to Implement Role-Based Access Control in a Flask Application
- Share:
Flask is a lightweight and powerful web framework for Python, widely used for building web applications. While Flask provides essential tools for user authentication, it lacks built-in support for fine-grained access control.
This is where Role-Based Access Control (RBAC) comes in. It allows developers to enforce structured permissions and secure their applications effectively.
In this Flask RBAC tutorial, we'll build a task management application that demonstrates real-world authorization using Flask and Permit.io - making your application more secure and production-ready.
Why is Flask's Built-in Access Control Not Enough?
Flask itself does not offer a built-in authorization system, meaning developers often rely on extensions like Flask-Login for authentication. While Flask-Login efficiently handles user sessions, it does not provide a structured way to manage permissions beyond basic login protection.
This means our flask application will lack several key security features:
- Manual Role Checking – Developers often use ad-hoc role checks within routes, which leads to scattered and hard-to-maintain permission logic.
- Lack of Granular Permissions – Flask-Login only determines if a user is logged in, but it does not enforce which actions a user can or cannot perform.
- Scalability Issues – Hardcoded role logic does not scale well, making it difficult to manage user permissions as the application grows.
- Security Risks – Without a centralized, structured access control system, authorization logic becomes prone to inconsistencies, increasing the risk of unauthorized access.
As the vast majority of applications require more than just simple authentication—having at least a good RBAC implementation is a must for every application.
What Will We Build?
Through building a complete task management system, this guide will show how to implement role-based access control that handles everything from basic user permissions to complex administrative privileges. To implement this, we will be using Permit.io to:
- Design and implement a complete RBAC permission system
- Create and manage different user roles (admin, viewer)
- Secure Flask routes with proper authorization checks
- Handle user sessions and role-based access
- Deploy a production-ready RBAC system
We'll cover common scenarios you'll encounter in real applications and demonstrate how to handle them securely.
You can find the complete code in this GitHub repository or follow along as we build the application step by step.
Feature Requirements for Our Flask RBAC Application
The main requirement for this application is that an admin user has the ability to manage all tasks in the application. The admin has the ability to:
- Create tasks for any user
- View all tasks in the application
- Update any task
- Delete any task
Here is a representation in table form of the access an admin and a normal user has:
Feature | Admin | Regular User |
---|---|---|
Create Tasks | Can create tasks for any user | Can create tasks for themselves |
View Tasks | Can view all tasks | Can view only their own tasks |
Update Tasks | Can update any task | Can update only their own tasks |
Delete Tasks | Can delete any task | Can delete only their own tasks |
Manage Users | Full access | No access |
View Analytics | Full access | No access |
Prerequisites and Tech Stack
For you to be able to follow along with the tutorial, you should have:
- A basic understanding of authentication and authorization
- Experience with Python and Flask
- A Permit.io account
Here is what our admin dashboard looks like:
Here is what a normal user view looks like:
Our Tech Stack:
- Flask: A lightweight WSGI web application framework
- SQLAlchemy: SQL toolkit and ORM for Python
- PostgreSQL: Our database backend
- Flask-Login: For handling user authentication
- Flask-Migrate: For database migrations
- Jinja2: Template engine (included with Flask)
Project Setup
To get started, let's clone the project:
git clone <https://github.com/uma-victor1/Flask-RBAC-with-Permit.io>
After cloning, run these commands:
mkdir Flask-RBAC-with-Permit
cd Flask-RBAC-with-Permit
python -m venv venv
source venv/bin/activate
Now we have our project setup, let's install the necessary dependencies:
pip install -r requirements.txt
Here is what our project structure looks like:
task_manager/
├── app/
│ ├── main.py # Flask app initialization
│ ├── models/
│ │ └── models.py # SQLAlchemy models
│ ├── routes/
│ │ ├── auth.py # Authentication routes
│ │ └── tasks.py # Task management routes
│ ├── templates/
│ │ ├── auth/
│ │ └── tasks/
│ ├── static/
│ └── utils/
│ └── permit_helper.py # Permit.io integration
├── config.py
└── requirements.txt
With our starter template, authentication is set up using Flask-Login, meaning we can get the currently logged-in user. We use PostgreSQL as our database and SQLAlchemy for our ORM, giving us a solid data layer that makes accessing data easy and safe.
Database Schema for our App
For our app, we need just two tables for User and Tasks. These two tables are enough to demonstrate RBAC in our app. Here is what our SQLAlchemy Models look like:
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash
from flask_login import UserMixin
db = SQLAlchemy()
class User(UserMixin, db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
role = db.Column(db.String(20), nullable=False, default='user')
tasks = db.relationship('Task', backref='owner', lazy=True)
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
class Task(db.Model):
__tablename__ = 'tasks'
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100), nullable=False)
description = db.Column(db.Text)
due_date = db.Column(db.DateTime)
status = db.Column(db.String(20), default='pending')
created_at = db.Column(db.DateTime, default=datetime.utcnow)
updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'))
assigned_to = db.Column(db.Integer, db.ForeignKey('users.id'))
Implementing RBAC with Permit.io
Having understood the requirements for our task management app and setting up our database layout for the users
and tasks
tables, we are now ready to map out our access control policies in Permit.io.
This way, only people with the right access can perform important actions, like adding, changing, or deleting tasks.
Our next step is to create our roles, resources, and actions in the Permit dashboard and manage the permissions for resources in the policy editor.
First, go to your permit dashboard and create a new project:
After you've created a new project, your screen should look similar to the image above. Now, you can create roles and manage resources, which are all done in the policy editor screen.
Create a Resource
Resources refer to the objects or entities within your application that require permission management. In the case of our task management app, that would be the tasks.
To create the task resource, Click on the policy tab on the left sidebar, and create the task resource.
After creating the task resource, we need to create different user roles and add permissions to each role so that each user assigned a role has permission to carry out the actions determined for that role.
Create User Roles
In our application, we only need two roles:
- Admin
- Viewer
To create a role, navigate to the Policy section in the Permit dashboard, click on the Roles tab, and add the required roles.
We need to define the specific permissions associated with each role we create. For example, an Admin has permission to delete, create, or update any task in our application.
Navigate to the Policy Editor tab and adjust permissions to match our requirements.
Enforcing the Permissions in Our App
In the previous sections, we have seen how to set roles and permissions in our permit dashboard. This section will show how we can use the Permit API to test and enforce this permission in our Flask app.
In your .env
file, add these credentials:
PERMIT_API_KEY="your_permit_api_key"
PERMIT_PDP_URL="your_permit_pdp_url"
You can find your API key by going to the projects page in the permit dashboard and copying it.
In the project's app
folder, create a utils
directory and add a file called permit_helper.py
. In that file, paste the following code to initialize the SDK and connect your Flask app to the Permit.io PDP container you've set up:
from functools import wraps
from flask import current_app, abort
from flask_login import current_user
from permit import Permit
import asyncio
from functools import partial
def get_permit():
"""Initialize and return Permit.io client"""
try:
return Permit(
pdp=current_app.config["PERMIT_PDP_URL"],
token=current_app.config["PERMIT_API_KEY"],
)
except Exception as e:
current_app.logger.error(f"Failed to initialize Permit client: {str(e)}")
raise
This function initializes our connection to Permit.io using configuration values from our Flask application. The PDP_URL
points to Permit's Policy Decision Point, while the API key authenticates our requests.
The PDP is a policy engine needed for evaluating authorization queries based on defined policies. It’s very important for checking permission for roles and although permit provides a pdp URL for testing, permit advises us to deploy our own.
For now, we are using the provided PDP URL https://cloudpdp.api.permit.io.
Syncing Users
Since we have our configuration set, we can start enforcing some roles and permissions in our app. But something is missing! In our permissions dashboard, we didn't add any users, so adding roles and permissions is useless. We need a way to sync the users in our app with the users on Permit.
To achieve this, we need a unique way to identify our users. It doesn't matter what method of authentication we are using; we just need a unique ID for each user. For this project, we are using Flask-Login, so we can use the user ID to sync users to a permit.
First, let's add utility functions for creating a user on Permit and assigning them a role. In our utils/permit_helper.py
file, add the following code:
from flask import current_app
from .permit_helper import get_permit
async def assign_role(user_id: str, role: str):
permit = get_permit()
try:
await permit.api.users.assign_role(
{
"user": user_id,
"role": role,
"tenant": "default",
}
)
except Exception as e:
current_app.logger.error(f"Role assignment failed: {str(e)}")
raise
async def create_permit_user(user):
permit = get_permit()
try:
await permit.api.users.sync(
{
"key": str(user.id),
"email": user.email,
"first_name": user.username,
"last_name": user.username,
}
)
except Exception as e:
current_app.logger.error(f"User sync failed: {str(e)}")
raise
These functions handle user synchronization with Permit.io and role assignment. When a new user registers, we create their record in Permit.io and assign them an appropriate role. This ensures our authorization system always has up-to-date user information.
Then in our auth routes file app/routes/auth.py
, import the permit utility functions and sync users to Permit:
from flask import Blueprint, request, flash, redirect, url_for
from flask_login import login_user
from ..models import db, User
from ..utils.authorization import create_permit_user, assign_role
auth = Blueprint('auth', __name__)
@auth.route('/register', methods=['GET', 'POST'])
async def register():
if request.method == 'POST':
user = User(
username=request.form['username'],
email=request.form['email']
)
user.set_password(request.form['password'])
db.session.add(user)
db.session.commit()
# Sync with Permit.io
await create_permit_user(user)
await assign_role(str(user.id), "viewer")
login_user(user)
flash('Registration successful!', 'success')
return redirect(url_for('main.index'))
return render_template('auth/register.html')
Securing Task Routes with RBAC
Now that we have our permission system set up, let's implement proper authorization for our task management routes. We'll apply RBAC to control who can list, create, and delete tasks.
First, let's set up our task routes blueprint:
from flask import Blueprint, request, render_template
from flask_login import login_required, current_user
from ..models import db, Task
from ..utils.authorization import check_permission
tasks = Blueprint('tasks', __name__)
Listing Tasks with Role-Based Access
Our task listing route demonstrates a key RBAC principle: different roles see different data.
@tasks.route("/")
@login_required
@check_permission(action="readall", resource="task")
async def list_tasks():
"""List tasks based on permissions"""
try:
permit = get_permit()
# Check for readall permission instead of role
has_readall = await permit.check(
str(current_user.id), "readall", {"type": "task"}
)
if has_readall:
tasks = Task.query.all()
else:
tasks = Task.query.filter_by(user_id=current_user.id).all()
return render_template("tasks/list.html", tasks=tasks)
except Exception as e:
flash("Error loading tasks.", "danger")
return redirect(url_for("main.index"))
- The role check determines what data the user can see:
- Admins get all tasks (
Task.query.all()
) - Regular users only see their own tasks (
filter_by(user_id=current_user.id)
)
Creating Tasks
The task creation route enforces create permissions:
@tasks.route('/tasks/create', methods=['GET', 'POST'])
@login_required
@check_permission("create", "task")
def create_task():
if request.method == 'POST':
task = Task(
title=request.form['title'],
description=request.form['description'],
user_id=current_user.id # Tasks are always owned by their creator
)
db.session.add(task)
db.session.commit()
flash('Task created successfully!', 'success')
return redirect(url_for('tasks.list_tasks'))
return render_template('tasks/create.html')
Key security points:
- The
create
permission is checked before allowing task creation user_id
is set to the current user, preventing task creation on behalf of others- SQLAlchemy session handling ensures database consistency
Deleting Tasks
The delete route combines permission checking with ownership verification:
@tasks.route("/<int:id>/delete", methods=["POST"])
@login_required
@check_permission(action="delete", resource="task")
async def delete_task(id):
"""Delete a task"""
task = Task.query.get_or_404(id)
permit = get_permit()
# Check deleteany permission or ownership
has_deleteany = await permit.check(
str(current_user.id), "deleteany", {"type": "task"}
)
if not has_deleteany and task.user_id != current_user.id:
abort(403)
try:
db.session.delete(task)
db.session.commit()
flash("Task deleted successfully!", "success")
except Exception as e:
db.session.rollback()
flash("Error deleting task.", "danger")
return redirect(url_for("tasks.list_tasks"))
Permission Matrix
Here's how our permissions work across different roles:
Action | Admin | Regular User |
---|---|---|
List Tasks | All tasks | Own tasks only |
Create Task | ✓ | ✓ (self only) |
Delete Task | Any task | Own tasks only |
Our implementation follows several security best practices:
- Decorators enforce permissions consistently
- Database queries are scoped by user access level
- Ownership checks prevent unauthorized access
- All routes require authentication
- Actions are logged for audit purposes
Wrapping Up
Implementing role-based access control (RBAC) in a Flask application is essential for ensuring security, scalability, and maintainability. While Flask provides excellent authentication tools, it lacks a built-in solution for managing fine-grained access control. Relying on hardcoded role checks or manual permission management can quickly become unmanageable as an application grows.
By integrating Permit.io, we can offload authorization logic to a dedicated system, allowing for structured role management, centralized policy enforcement, and real-time permission evaluation. This approach not only simplifies development but also enhances security by preventing unauthorized access to sensitive operations.
With a production-ready RBAC implementation, your Flask application can securely handle multiple user roles, enforce clear access policies, and remain adaptable as your security needs evolve. If you're looking to enhance your Flask application's authorization model, leveraging Permit.io offers a streamlined and scalable solution.
Ready to implement Flask RBAC? Explore the full GitHub repository, or check out the Permit.io documentation for more advanced authorization features.
Got questions? Hit me up on X @umavictor
Written by
Uma Victor
Software Engineer | Typescript, Node.js, Next.js, PostgreSQL, Docker