Skip to main content

Flask Role Management

Introduction

Role management is a critical aspect of web application security that governs what actions different users can perform within your application. After authenticating users (confirming who they are), you need to control what resources they can access and what operations they can perform. This concept is called authorization, and role-based access control (RBAC) is one of the most common implementation approaches.

In this tutorial, you'll learn how to implement role management in Flask applications. We'll explore different ways to define roles, assign them to users, and restrict access to certain routes based on user roles. This knowledge will help you build more secure and organized Flask applications with proper permission structures.

Understanding Role-Based Access Control

Before diving into code, let's understand the core concepts of role-based access control:

  1. Roles: Categories that define what actions users can perform (e.g., admin, editor, viewer)
  2. Permissions: Specific actions that can be performed (e.g., create_post, delete_user)
  3. Users: Individuals who are assigned one or more roles
  4. Resources: Parts of your application that need protection (routes, views, data)

A typical RBAC implementation might look like:

Users → Roles → Permissions → Resources

For example, a user with the "Admin" role might have permissions to create, read, update, and delete all posts, while a user with the "Editor" role might only be able to create and update their own posts.

Setting Up a Basic Flask Application with User Authentication

Let's start by creating a basic Flask application with authentication. We'll use Flask-Login for authentication and SQLAlchemy for database operations.

First, install the necessary packages:

bash
pip install flask flask-login flask-sqlalchemy

Now, let's set up a basic Flask application with a user model:

python
from flask import Flask, render_template, redirect, url_for, request, flash
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, logout_user, current_user
from werkzeug.security import generate_password_hash, check_password_hash

app = Flask(__name__)
app.config['SECRET_KEY'] = 'your-secret-key'
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///users.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

db = SQLAlchemy(app)
login_manager = LoginManager()
login_manager.init_app(app)
login_manager.login_view = 'login'

class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), unique=True)
password = db.Column(db.String(200))
# We'll add role information here later

@login_manager.user_loader
def load_user(user_id):
return User.query.get(int(user_id))

# Basic routes for register, login, logout
@app.route('/register', methods=['GET', 'POST'])
def register():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')

user = User.query.filter_by(username=username).first()
if user:
flash('Username already exists')
return redirect(url_for('register'))

new_user = User(username=username,
password=generate_password_hash(password, method='sha256'))
db.session.add(new_user)
db.session.commit()

return redirect(url_for('login'))

return render_template('register.html')

@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')

user = User.query.filter_by(username=username).first()

if not user or not check_password_hash(user.password, password):
flash('Please check your login details and try again.')
return redirect(url_for('login'))

login_user(user)
return redirect(url_for('profile'))

return render_template('login.html')

@app.route('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('login'))

@app.route('/profile')
@login_required
def profile():
return render_template('profile.html', name=current_user.username)

if __name__ == '__main__':
with app.app_context():
db.create_all()
app.run(debug=True)

Adding Role Management

Now, let's enhance our application to support roles. We'll implement role management in three different ways, from simple to more complex:

Method 1: Simple Role Field

This is the most straightforward approach, suitable for applications with a small number of predefined roles.

Let's modify our User model to include a role field:

python
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), unique=True)
password = db.Column(db.String(200))
role = db.Column(db.String(20), default='user') # 'user', 'admin', 'editor', etc.

Now let's create a decorator to restrict access based on roles:

python
from functools import wraps
from flask import abort

def role_required(role):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or current_user.role != role:
abort(403) # Forbidden access
return f(*args, **kwargs)
return decorated_function
return decorator

We can use this decorator to protect routes:

python
@app.route('/admin')
@login_required
@role_required('admin')
def admin():
return render_template('admin.html')

@app.route('/editor')
@login_required
@role_required('editor')
def editor():
return render_template('editor.html')

Method 2: Multiple Roles with a Many-to-Many Relationship

For more complex applications where users can have multiple roles, we can use a many-to-many relationship:

python
# Define the association table
user_roles = db.Table('user_roles',
db.Column('user_id', db.Integer, db.ForeignKey('user.id')),
db.Column('role_id', db.Integer, db.ForeignKey('role.id'))
)

class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), unique=True)
description = db.Column(db.String(200))

class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), unique=True)
password = db.Column(db.String(200))
roles = db.relationship('Role', secondary=user_roles,
backref=db.backref('users', lazy='dynamic'))

def has_role(self, role_name):
return any(role.name == role_name for role in self.roles)

Now, we need to update our decorator to check for multiple roles:

python
def role_required(role_name):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.has_role(role_name):
abort(403) # Forbidden access
return f(*args, **kwargs)
return decorated_function
return decorator

Method 3: Roles with Permissions

For the most flexible approach, we can implement roles with granular permissions:

python
role_permissions = db.Table('role_permissions',
db.Column('role_id', db.Integer, db.ForeignKey('role.id')),
db.Column('permission_id', db.Integer, db.ForeignKey('permission.id'))
)

class Permission(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), unique=True)
description = db.Column(db.String(200))

class Role(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(20), unique=True)
description = db.Column(db.String(200))
permissions = db.relationship('Permission', secondary=role_permissions,
backref=db.backref('roles', lazy='dynamic'))

def has_permission(self, permission_name):
return any(permission.name == permission_name for permission in self.permissions)

class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(100), unique=True)
password = db.Column(db.String(200))
roles = db.relationship('Role', secondary=user_roles,
backref=db.backref('users', lazy='dynamic'))

def has_permission(self, permission_name):
for role in self.roles:
if role.has_permission(permission_name):
return True
return False

With this setup, we can create a decorator to check for specific permissions:

python
def permission_required(permission_name):
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated or not current_user.has_permission(permission_name):
abort(403) # Forbidden access
return f(*args, **kwargs)
return decorated_function
return decorator

Now we can protect routes based on specific permissions:

python
@app.route('/create-post')
@login_required
@permission_required('create_post')
def create_post():
return render_template('create_post.html')

@app.route('/delete-post/<int:post_id>')
@login_required
@permission_required('delete_post')
def delete_post(post_id):
# Delete post logic here
return redirect(url_for('posts'))

Practical Example: Blog Application with Roles

Let's implement a simple blog application with different roles:

  1. Admin: Can create, edit, and delete any post
  2. Editor: Can create posts and edit their own posts
  3. User: Can view posts and comment
python
# Database models
class Post(db.Model):
id = db.Column(db.Integer, primary_key=True)
title = db.Column(db.String(100))
content = db.Column(db.Text)
author_id = db.Column(db.Integer, db.ForeignKey('user.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)

class Comment(db.Model):
id = db.Column(db.Integer, primary_key=True)
content = db.Column(db.Text)
post_id = db.Column(db.Integer, db.ForeignKey('post.id'))
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
created_at = db.Column(db.DateTime, default=datetime.utcnow)

# Create utility functions for role checking
def is_admin():
return current_user.is_authenticated and current_user.has_role('admin')

def is_editor():
return current_user.is_authenticated and current_user.has_role('editor')

def is_author_of(post):
return current_user.is_authenticated and post.author_id == current_user.id

# Route for viewing all posts
@app.route('/')
def index():
posts = Post.query.order_by(Post.created_at.desc()).all()
return render_template('index.html', posts=posts,
is_admin=is_admin,
is_editor=is_editor)

# Route for viewing a specific post
@app.route('/post/<int:post_id>')
def view_post(post_id):
post = Post.query.get_or_404(post_id)
return render_template('post.html', post=post,
is_admin=is_admin,
is_editor=is_editor,
is_author=is_author_of(post))

# Route for creating a new post
@app.route('/post/new', methods=['GET', 'POST'])
@login_required
@permission_required('create_post') # Both admin and editor have this permission
def create_post():
if request.method == 'POST':
title = request.form.get('title')
content = request.form.get('content')

post = Post(title=title, content=content, author_id=current_user.id)
db.session.add(post)
db.session.commit()

return redirect(url_for('view_post', post_id=post.id))

return render_template('create_post.html')

# Route for editing a post
@app.route('/post/<int:post_id>/edit', methods=['GET', 'POST'])
@login_required
def edit_post(post_id):
post = Post.query.get_or_404(post_id)

# Check if user is admin or the author of the post
if not is_admin() and not is_author_of(post):
abort(403)

if request.method == 'POST':
post.title = request.form.get('title')
post.content = request.form.get('content')
db.session.commit()

return redirect(url_for('view_post', post_id=post.id))

return render_template('edit_post.html', post=post)

# Route for deleting a post
@app.route('/post/<int:post_id>/delete')
@login_required
def delete_post(post_id):
post = Post.query.get_or_404(post_id)

# Only admin can delete any post
if not is_admin():
abort(403)

db.session.delete(post)
db.session.commit()

return redirect(url_for('index'))

Using Flask-Principal for Advanced Role Management

For more advanced role management, you might want to use the Flask-Principal extension. Flask-Principal provides a way to handle identity management, which includes user authentication and authorization.

First, install Flask-Principal:

bash
pip install flask-principal

Here's how to integrate it with our application:

python
from flask_principal import Principal, Permission, RoleNeed, identity_loaded, UserNeed, Identity, AnonymousIdentity

# Initialize Principal
principals = Principal(app)

# Define permissions
admin_permission = Permission(RoleNeed('admin'))
editor_permission = Permission(RoleNeed('editor'))
user_permission = Permission(RoleNeed('user'))

# Set up the identity loading function
@identity_loaded.connect_via(app)
def on_identity_loaded(sender, identity):
# Set the identity user object
identity.user = current_user

# Add the UserNeed to the identity
if hasattr(current_user, 'id'):
identity.provides.add(UserNeed(current_user.id))

# Add roles to the identity
if hasattr(current_user, 'roles'):
for role in current_user.roles:
identity.provides.add(RoleNeed(role.name))

# Update login to handle the identity
@app.route('/login', methods=['GET', 'POST'])
def login():
if request.method == 'POST':
username = request.form.get('username')
password = request.form.get('password')

user = User.query.filter_by(username=username).first()

if not user or not check_password_hash(user.password, password):
flash('Please check your login details and try again.')
return redirect(url_for('login'))

login_user(user)

# Tell Flask-Principal the identity changed
identity = Identity(user.id)
identity_changed.send(current_app._get_current_object(), identity=identity)

return redirect(url_for('profile'))

return render_template('login.html')

# Update logout to handle the identity
@app.route('/logout')
@login_required
def logout():
logout_user()

# Remove session keys set by Flask-Principal
for key in ('identity.name', 'identity.auth_type'):
session.pop(key, None)

# Tell Flask-Principal the user is anonymous
identity_changed.send(current_app._get_current_object(), identity=AnonymousIdentity())

return redirect(url_for('login'))

# Using permissions in routes
@app.route('/admin-dashboard')
@admin_permission.require(http_exception=403)
def admin_dashboard():
return render_template('admin_dashboard.html')

@app.route('/editor-dashboard')
@editor_permission.require(http_exception=403)
def editor_dashboard():
return render_template('editor_dashboard.html')

Best Practices for Role Management

  1. Keep roles simple: Start with a minimal set of roles and add more as needed.
  2. Use the principle of least privilege: Assign users the minimum permissions they need.
  3. Don't hardcode roles: Store roles in the database to make your application more flexible.
  4. Multiple layers of protection: Don't rely just on frontend UI hiding; ensure server-side checks are in place.
  5. Audit and logging: Keep track of role changes and permission usage.
  6. Regular review: Periodically review and clean up user roles.
  7. Test thoroughly: Create tests for your permission system to ensure it works correctly.

Summary

In this tutorial, we've learned about implementing role management in Flask applications:

  1. We started with a simple role field in the user model
  2. We advanced to many-to-many relationships for multiple roles
  3. We implemented granular permissions for maximum flexibility
  4. We created a practical blog application with different roles
  5. We explored Flask-Principal for advanced role management

Role management is essential for building secure web applications. By implementing proper role-based access control, you can ensure that users can only access the resources they are authorized to use, protecting your application from unauthorized access and potential security threats.

Additional Resources

Exercises

  1. Extend the blog application to include a "Moderator" role that can edit comments but not posts.
  2. Implement a user management page for administrators to assign roles to users.
  3. Create a hierarchical role system where higher roles inherit permissions from lower roles.
  4. Add a time-based permission system where certain roles can only access specific resources during business hours.
  5. Implement API endpoint protection using the same role-based system.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)