How to implement role-based access control in Flask

Video thumbnail

In typical systems that require protecting application resources, roles are usually used to manage controlled access to each of our application's resources, in the case of our application this would be the dashboard module; roles are a mechanism to control and restrict access to different parts of a web application.

In Flask, we can manage roles by using relationships between users and roles in the database. This allows one or more roles to be assigned to each user and their access to different routes to be controlled.

1. Model Definition

Let's implement the following models and foreign relationship in the users table:

my_app\auth\models.py

from flask_sqlalchemy import SQLAlchemy

db = SQLAlchemy()

class User(db.Model):
    __tablename__ = 'users'
    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(50), unique=True, nullable=False)
    email = db.Column(db.String(120), unique=True, nullable=False)
    pwdhash = db.Column(db.String(255), nullable=False)

    # Relación muchos a muchos con roles
    roles = db.relationship('Role', secondary='user_roles', backref='users')

    @property
    def serialize(self):
        roles_str = ','.join([r.name for r in self.roles])
        return {
            'id': self.id,
            'username': self.username,
            'email': self.email,
            'roles': roles_str
        }

class Role(db.Model):
    __tablename__ = 'roles'
    id = db.Column(db.Integer(), primary_key=True)
    name = db.Column(db.String(50), unique=True)

class UserRoles(db.Model):
    __tablename__ = 'user_roles'
    id = db.Column(db.Integer(), primary_key=True)
    user_id = db.Column(db.Integer(), db.ForeignKey('users.id', ondelete='CASCADE'))
    role_id = db.Column(db.Integer(), db.ForeignKey('roles.id', ondelete='CASCADE'))

2. Database Migrations

And we apply the changes to the database with:

$ python -m flask db migrate -m "User roles" 
$ python -m flask db upgrade

3. Create roles and test users

For the task application, we will have the following roles:

  1. Task Reader
  2. Task Management
  3. Category Reader
  4. Category Management
  5. Administrator, access to their resources and those of others
  6. Editor, access only to their own resources

In summary:

  • ADMIN → full access
  • EDITOR → access to their own resources
  • READER → read-only

We create a test user:

INSERT INTO `users` (`id`, `username`, `pwdhash`, `email`, `first_name`, `last_name`, `avatar_id`, `address_id`, `lang`) VALUES (2, 'editor', 'scrypt:32768:8:1$sLDyIDXY0csuow8Y$cada365071c5b0c6ece9c3e684f37e781092acfcf391ecda21914ee759e3cab5c275124e67463a177543ea888792b75995fad1d4b978a85236c5367616e0c34f', 'editor@admin.com', 'Editor', 'Cruz', 32, 4, 'EN');

We create the roles:

INSERT INTO roles (name) VALUES ('READ_TASK');
INSERT INTO roles (name) VALUES ('SAVE_TASK');
INSERT INTO roles (name) VALUES ('ADMIN');
INSERT INTO roles (name) VALUES ('EDITOR');
INSERT INTO roles (name) VALUES ('READ_CATEGORY');
INSERT INTO roles (name) VALUES ('SAVE_CATEGORY');

We create test users:

INSERT INTO users (id, username, pwdhash, email) VALUES 
(1, 'admin', 'hashed_password', 'admin@domain.com'),
(2, 'editor', 'hashed_password', 'editor@domain.com');

We assign roles:

-- Admin tiene todos los permisos
INSERT INTO user_roles (user_id, role_id) VALUES (1, 1); -- READ_TASK
INSERT INTO user_roles (user_id, role_id) VALUES (1, 2); -- SAVE_TASK
INSERT INTO user_roles (user_id, role_id) VALUES (1, 3); -- ADMIN
-- Editor tiene permisos limitados
INSERT INTO user_roles (user_id, role_id) VALUES (2, 2); -- SAVE_TASK
INSERT INTO user_roles (user_id, role_id) VALUES (2, 6); -- EDITOR

4. Decorator to protect routes based on roles

We create a decorator that checks if the logged-in user has the required role:

We create a decorator to verify roles, which checks if the token provided from the view/controller exists in the String of tokens loaded in the session:

# my_app/__init__.py
from functools import wraps
from flask import session
def roles_required(*required_roles):
   def wrapper(f):
       @wraps(f)
       def decorated_function(*args, **kwargs):
           user_roles = session.get('user', {}).get('roles', '')
           for role in required_roles:
               if role not in user_roles.split(','):
                   return "No tienes permiso para realizar esta operación", 401
           return f(*args, **kwargs)
       return decorated_function
   return wrapper

Usage of the decorator on routes:

from flask import Flask, session from my_app.__init__ import roles_required app = Flask(__name__) @app.route('/dashboard') @roles_required('ADMIN', 'EDITOR') def dashboard(): return "Welcome to the dashboard"

In both cases, we will need a method that verifies if the user has access to the roles created previously; first, we assign the roles to the session:

my_app\auth\models.py

from flask import Flask, session
from my_app.__init__ import roles_required
app = Flask(__name__)
@app.route('/dashboard')
@roles_required('ADMIN', 'EDITOR')
def dashboard():
   return "Welcome dashboard"

Since roles are an array:

[SAVE_TASK,SAVE_CATEGORY,ADMIN,READ_TASK,READ_CATEGORY]

So that they can be serializable and thus set in the session, we convert it to a comma-separated String of roles as we did previously:

SAVE_TASK,SAVE_CATEGORY,ADMIN,READ_TASK,READ_CATEGORY

Conclusion

As you can deduce from the previous models, a user can have multiple roles, therefore, depending on the complexity of the application, they may have more than one role assigned; for example, suppose we have a blog-type application with a management module or dashboard and another module facing the end user, the blog-type application consists of posts and categories assigned to these posts.

For posts/tasks in an administrator role:

  • Create post.
  • Update post.
  • Delete post.
  • Post Detail/List.

For categories in an administrator role:

  • Create category.
  • Update category.
  • Delete category.
  • Category Detail/List.

For posts in an editor role:

  • Create post/tasks.
  • Update post/tasks (only their own).
  • Delete post/tasks (only their own).
  • Post/task Detail/List.

For posts in a reader role:

  • Post Detail/List.

For categories in a reader role:

  • Category Detail/List.

In our case, it would be to manage tasks instead of posts.

As you can see in the previous example, we would have three roles:

  1. Administrator, who can access all application modules, specifically the CRUDs for posts and categories.
  2. Editor, who can only manage posts and categories that were created by their user and has read access.
  3. Reader, read-only.

For the application we currently have, which is only for tasks, it would not be necessary to implement such logic using multiple roles.

This is only one possible scheme you could implement, but you can customize it as you like, for example, creating a super administrator role or just one administrator role or other types of roles.

You can get more information at:

https://flask-user.readthedocs.io/en/latest/basic_app.html

I agree to receive announcements of interest about this Blog.

Roles are a mechanism to control and restrict access to different parts of a web application. In this post we will create the basic structure to manage them in Flask.

| 👤 Andrés Cruz

🇪🇸 En español