Skip to main content

Flask: Role-Based Access Control (RBAC)

Flask: Role-Based Access Control (RBAC)

Role-Based Access Control (RBAC) in Flask enables fine-grained permission management, restricting access to resources based on user roles. Building on Flask Authentication, Flask User Registration, Flask User Login and Logout, Flask Password Hashing with Flask-Bcrypt, Flask Querying the Database, and Flask Relationships in Models, Flask integrates with extensions like Flask-Login and Flask-SQLAlchemy to implement RBAC. This tutorial explores Flask Role-Based Access Control, covering role assignment, permission checks, and route protection, with practical applications in secure web development.


01. Why Implement Role-Based Access Control?

RBAC ensures users can only access resources their roles permit, enhancing security and user experience in applications like content management systems, e-commerce platforms, or admin dashboards. Flask’s flexible framework, combined with SQLAlchemy and NumPy Array Operations for efficient database queries, supports scalable RBAC systems to manage permissions dynamically.

Example: Basic RBAC Implementation

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required, current_user
from functools import wraps

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///rbac.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    role = db.Column(db.String(20), nullable=False)  # e.g., 'admin', 'user'

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

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:
                return "Access denied"
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/admin')
@login_required
@role_required('admin')
def admin_panel():
    return "Welcome to the admin panel"

@app.route('/user')
@login_required
@role_required('user')
def user_dashboard():
    return "Welcome to the user dashboard"

with app.app_context():
    db.create_all()
    db.session.add_all([
        User(username='alice', role='admin'),
        User(username='bob', role='user')
    ])
    db.session.commit()

Output:

Welcome to the admin panel (for user 'alice' with role 'admin' accessing /admin)
Welcome to the user dashboard (for user 'bob' with role 'user' accessing /user)
Access denied (for unauthorized role access)

Explanation:

  • role_required - Custom decorator to enforce role-based access.
  • current_user.role - Checks the user’s role from the database.
  • Integrates with Flask Authentication via Flask-Login.

02. Key RBAC Techniques

Flask enables RBAC through role assignments, permission checks, and route protection. Below is a summary of key techniques and their applications in web applications:

Technique Description Use Case
Role Assignment Assign roles to users Define admins, editors, or users
Permission Checks Verify user roles for access Restrict admin panels
Route Protection Secure endpoints with decorators Protect sensitive routes
Role-Based Models Store roles in database Manage permissions dynamically
Error Handling Handle unauthorized access Redirect or display errors


2.1 Role Assignment

Example: Assigning Roles During Registration

from flask import Flask, request
from flask_sqlalchemy import SQLAlchemy
from flask_bcrypt import Bcrypt

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///roles.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)

class User(db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(120), nullable=False)
    role = db.Column(db.String(20), nullable=False, default='user')

@app.route('/register', methods=['POST'])
def register():
    username = request.form['username']
    password = request.form['password']
    role = request.form.get('role', 'user')  # Default to 'user'
    if User.query.filter_by(username=username).first():
        return "Username taken"
    hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
    user = User(username=username, password_hash=hashed_password, role=role)
    db.session.add(user)
    db.session.commit()
    return f"Registered as {role}"

with app.app_context():
    db.create_all()

Output:

Registered as user (on valid POST request with default role)
Registered as admin (on valid POST request with role='admin')

Explanation:

  • role - Stores the user’s role in the database.
  • Defaults to 'user' but allows admin assignment during registration.

2.2 Permission Checks

Example: Checking Roles for Access

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required, current_user
from functools import wraps

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///permissions.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
login_manager = LoginManager(app)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    role = db.Column(db.String(20), nullable=False)

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

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:
                return "Access denied: Insufficient permissions"
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/editor')
@login_required
@role_required('editor')
def editor_panel():
    return "Editor panel access granted"

with app.app_context():
    db.create_all()
    db.session.add(User(username='charlie', role='editor'))
    db.session.commit()

Output:

Editor panel access granted (for user 'charlie' with role 'editor' accessing /editor)

Explanation:

  • role_required - Verifies the user’s role before granting access.
  • Combines with login_required for authentication.

2.3 Route Protection

Example: Protecting Routes with Multiple Roles

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required, current_user
from functools import wraps

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///multi_roles.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
login_manager = LoginManager(app)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    role = db.Column(db.String(20), nullable=False)

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

def roles_required(*roles):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.is_authenticated or current_user.role not in roles:
                return "Access denied: Insufficient permissions"
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/management')
@login_required
@roles_required('admin', 'manager')
def management_panel():
    return "Management panel access granted"

with app.app_context():
    db.create_all()
    db.session.add_all([
        User(username='dave', role='admin'),
        User(username='eve', role='manager')
    ])
    db.session.commit()

Output:

Management panel access granted (for users 'dave' or 'eve' with roles 'admin' or 'manager')

Explanation:

  • roles_required - Allows multiple roles to access a route.
  • Enhances flexibility for complex permission structures.

2.4 Role-Based Models

Example: Managing Roles with Relationships

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required, current_user
from functools import wraps

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///role_models.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
login_manager = LoginManager(app)

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

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    role_id = db.Column(db.Integer, db.ForeignKey('role.id'), nullable=False)
    role = db.relationship('Role', backref='users')

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

def role_required(role_name):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.is_authenticated or current_user.role.name != role_name:
                return "Access denied"
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/admin')
@login_required
@role_required('admin')
def admin_panel():
    return "Admin panel access granted"

with app.app_context():
    db.create_all()
    admin_role = Role(name='admin')
    user_role = Role(name='user')
    db.session.add_all([admin_role, user_role])
    db.session.add(User(username='frank', role=admin_role))
    db.session.commit()

Output:

Admin panel access granted (for user 'frank' with role 'admin')

Explanation:

  • Role - Separate model for roles, linked to users via relationships.
  • Enables dynamic role management and scalability.

2.5 Incorrect RBAC Setup

Example: Missing Role Check

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///insecure.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
login_manager = LoginManager(app)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    role = db.Column(db.String(20), nullable=False)

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

@app.route('/admin')
@login_required
def admin_panel():
    return "Admin panel access granted"  # No role check

with app.app_context():
    db.create_all()

Output:

Admin panel access granted (for any authenticated user, regardless of role)

Explanation:

  • Lacks role-based permission checks, allowing unauthorized access.
  • Solution: Add role_required decorator.

03. Effective Usage

3.1 Recommended Practices

  • Use custom decorators for role checks and integrate with Flask-Login.

Example: Comprehensive RBAC System

from flask import Flask, request, redirect, url_for
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_user, login_required, current_user
from flask_bcrypt import Bcrypt
from functools import wraps

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///complete.db'
app.config['SECRET_KEY'] = 'secure-secret-key'
db = SQLAlchemy(app)
bcrypt = Bcrypt(app)
login_manager = LoginManager(app)
login_manager.login_view = 'login'

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

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    password_hash = db.Column(db.String(120), nullable=False)
    role_id = db.Column(db.Integer, db.ForeignKey('role.id'), nullable=False)
    role = db.relationship('Role', backref='users')

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

def role_required(role_name):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.is_authenticated or current_user.role.name != role_name:
                return redirect(url_for('unauthorized'))
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/register', methods=['POST'])
def register():
    username = request.form['username']
    password = request.form['password']
    role_name = request.form.get('role', 'user')
    role = Role.query.filter_by(name=role_name).first()
    if not role:
        return "Invalid role"
    if User.query.filter_by(username=username).first():
        return "Username taken"
    hashed_password = bcrypt.generate_password_hash(password).decode('utf-8')
    user = User(username=username, password_hash=hashed_password, role=role)
    db.session.add(user)
    db.session.commit()
    return "Registered"

@app.route('/login', methods=['POST'])
def login():
    username = request.form['username']
    password = request.form['password']
    user = User.query.filter_by(username=username).first()
    if user and bcrypt.check_password_hash(user.password_hash, password):
        login_user(user)
        return redirect(url_for('dashboard'))
    return "Invalid credentials"

@app.route('/dashboard')
@login_required
def dashboard():
    return f"Welcome, {current_user.username} ({current_user.role.name})"

@app.route('/admin')
@login_required
@role_required('admin')
def admin_panel():
    return "Admin panel access granted"

@app.route('/unauthorized')
def unauthorized():
    return "Access denied: Insufficient permissions"

with app.app_context():
    db.create_all()
    db.session.add_all([Role(name='admin'), Role(name='user')])
    db.session.commit()

Output:

Registered (on valid registration)
Welcome, username (role) (on valid login)
Admin panel access granted (for admin role)
Access denied: Insufficient permissions (for unauthorized access)
  • Uses a separate Role model for scalability.
  • Redirects unauthorized users to a dedicated page.
  • Integrates with Flask Password Hashing with Flask-Bcrypt for secure authentication.

3.2 Practices to Avoid

  • Avoid hardcoding roles or skipping permission checks.

Example: Hardcoded Role Check

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required, current_user

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///hardcoded.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
login_manager = LoginManager(app)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)

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

@app.route('/admin')
@login_required
def admin_panel():
    if current_user.username == 'admin':  # Hardcoded check
        return "Admin panel access granted"
    return "Access denied"

with app.app_context():
    db.create_all()

Output:

Admin panel access granted (for user 'admin' only)
  • Hardcoding roles is inflexible and error-prone.
  • Solution: Use a role-based model and decorators.

04. Common Use Cases in Web Development

4.1 Admin Dashboard Access

Restrict admin dashboards to users with admin roles.

Example: Admin Dashboard RBAC

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required, current_user
from functools import wraps

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///admin.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
login_manager = LoginManager(app)

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    role = db.Column(db.String(20), nullable=False)

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

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:
                return "Access denied"
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/admin/dashboard')
@login_required
@role_required('admin')
def admin_dashboard():
    return "Admin dashboard access granted"

with app.app_context():
    db.create_all()
    db.session.add(User(username='grace', role='admin'))
    db.session.commit()

Output:

Admin dashboard access granted (for user 'grace' with role 'admin')

Explanation:

  • Secures admin dashboards with role-based restrictions.
  • Integrates with Flask User Login and Logout for authentication.

4.2 Content Management System (CMS)

Control access to content editing based on roles like editor or admin.

Example: CMS RBAC

from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from flask_login import LoginManager, UserMixin, login_required, current_user
from functools import wraps

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///cms.db'
app.config['SECRET_KEY'] = 'your-secret-key'
db = SQLAlchemy(app)
login_manager = LoginManager(app)

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

class User(UserMixin, db.Model):
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(80), unique=True, nullable=False)
    role_id = db.Column(db.Integer, db.ForeignKey('role.id'), nullable=False)
    role = db.relationship('Role', backref='users')

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

def roles_required(*role_names):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            if not current_user.is_authenticated or current_user.role.name not in role_names:
                return "Access denied"
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/cms/edit')
@login_required
@roles_required('admin', 'editor')
def cms_edit():
    return "Content editing access granted"

with app.app_context():
    db.create_all()
    db.session.add_all([Role(name='admin'), Role(name='editor')])
    db.session.add(User(username='helen', role=Role.query.filter_by(name='editor').first()))
    db.session.commit()

Output:

Content editing access granted (for user 'helen' with role 'editor')

Explanation:

  • Allows editors and admins to edit content.
  • Uses a scalable role model for CMS permissions.

Conclusion

Flask Role-Based Access Control, powered by Flask-Login, Flask-SQLAlchemy, and integration with Flask Authentication and NumPy Array Operations, provides a secure and scalable way to manage permissions. Key takeaways:

  • Use custom decorators like role_required for flexible permission checks.
  • Store roles in a separate model for dynamic management.
  • Apply RBAC in admin dashboards, CMS, or e-commerce systems.
  • Avoid hardcoding roles or skipping permission checks.

With these techniques, you can build Flask applications that enforce secure, role-based access and protect sensitive resources effectively!

Comments