Cómo implementar el control de acceso basado en roles en Flask

Video thumbnail

En los sistemas típicos que se requiere proteger los recursos de una aplicación, usualmente se utilizan los roles para manejar el acceso controlado a cada uno de los recursos de nuestra aplicación, en el caso de nuestra aplicación sería el módulo de dashboard; los roles son un mecanismo para controlar y restringir el acceso a diferentes partes de una aplicación web.

En Flask, podemos gestionar roles utilizando relaciones entre usuarios y roles en la base de datos. Esto permite asignar uno o varios roles a cada usuario y controlar su acceso a diferentes rutas.

1. Definición de modelos

Implementemos los siguientes modelos y relación foránea en la tabla de usuarios:

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. Migraciones de base de datos

Y aplicamos los cambios a la base de datos con:

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

3. Crear roles y usuarios de prueba

Para la aplicación de tareas, vamos a tener los siguientes roles

  1. Lector de tareas
  2. Gestión de tareas
  3. Lector de categorías
  4. Gestión de categorías
  5. Administrador, acceso a sus recursos y el de los demás
  6. Editor, acceso solamente a sus recursos

En resumen:

  • ADMIN → acceso completo
  • EDITOR → acceso a sus propios recursos
  • LECTOR → solo lectura

Creamos un usuario de prueba:

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

Creamos los 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');

Creamos usuarios de prueba:

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

Asignamos 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. Decorador para proteger rutas según roles

Creamos un decorador que revisa si el usuario en sesión tiene el rol requerido:

Creamos un decorador para verificar los roles, el cual verifica si el token suministrado desde la vista/controlador existe en el String de tokens en cargado en la sesión:

# 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

Uso del decorador en rutas:

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 "Bienvenido al dashboard"

En ambos casos, necesitaremos un método que verifique si el usuario tiene el acceso a los roles creados anteriormente; primero, asignamos los roles en la sesión:

my_app\auth\models.py

class User(db.Model):
   ***
    @property
    def serialize(self):

        ***
        roles = ''
        if len(self.roles) > 0:
            for r in self.roles:
                roles += r.name+','

        return {
            ***
            'roles' : roles
        }

Ya que los roles son un array:

[SAVE_TASK,SAVE_CATEGORY,ADMIN,READ_TASK,READ_CATEGORY]

Para que puedan ser serializables y con esto, establecerlo en la sesión, lo convertimos a un String de roles separados por comas como hicimos anteriormente:

SAVE_TASK,SAVE_CATEGORY,ADMIN,READ_TASK,READ_CATEGORY

Conclusión

Como puedes deducir de los modelos anteriores, un usuario puede tener múltiples roles, por lo tanto, dependiendo de la complejidad de la aplicación, puede tener más de un rol asignado; por ejemplo, supongamos que tenemos una aplicación tipo blog con un módulo de gestión o dashboard y otro módulo de cara al usuario final, la aplicación tipo blog consiste en posts y categorías asignadas a estos posts.

Para los posts/tareas en un rol administrador:

  • Crear post.
  • Actualizar post.
  • Eliminar post.
  • Detalle/Listado post.

Para las categorías en un rol administrador:

  • Crear categoría.
  • Actualizar categoría.
  • Eliminar categoría.
  • Detalle/Listado categoría.

Para los posts en un rol editor:

  • Crear post/tareas.
  • Actualizar post/tareas (solamente las suyas).
  • Eliminar post/tareas (solamente las suyas).
  • Detalle/Listado post/tareas.

Para los posts en un rol lector:

  • Detalle/Listado post.

Para las categorías en un rol lector:

  • Detalle/Listado categoría.

En nuestro caso sería para administrar tareas en vez de posts.

Como puedes apreciar en el ejemplo anterior, tendríamos tres roles:

  1. Administrador, que puede ingresar a todos los módulos de la aplicación, específicamente los CRUDs para los posts y categorías.
  2. Editor, que solamente puede gestionar post y categorías que fueran creadas por su usuario y de lectura.
  3. Lector, solo lectura.

Para la aplicación que tenemos actualmente que es de solo tareas, no sería necesario implementar dicha lógica empleando múltiples roles.

Este es solamente un posible esquema que pudieras implementar pero puedes personalizar a gusto, por ejemplo, crear un rol super administrador o solo un rol de administrador u otros tipos de roles.

Puedes obtener más información en:

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

Acepto recibir anuncios de interes sobre este Blog.

Los roles son un mecanismo para controlar y restringir el acceso a diferentes partes de una aplicación web, en este post crearemos la estructura básica para manejarlos en Flask.

| 👤 Andrés Cruz

🇺🇸 In english