Flask: creando roles de usuarios
En este artículo se presenta un ejemplo de cómo crear roles de usuarios para el sistema usando un decorador personalizado, Flask-Login y el ORM Peewee.
Existen varias librerías o herramientas orientadas a Flask que permiten gestionar usuarios, sesiones, roles de usuarios, permisos; sin embargo, algunas de estas librerías están obsoletas o no tienen más soporte, por otro lado hay otras de estas herramientas que gestionan los roles y permisos de manera cruda en el código.
Entre las librerías o herramientas que están disponibles de manera pública pudieran ser:
- Flask-Login, ya revisada en otro artículo.
- Flask-Perm.
- Flask-RBAC.
- Flask-Admin.
- Flask-User.
- Flask-Principal.
El ejemplo a continuación hace referencia a la base de datos y los permisos sobre la función o método específico del sistema, permitiendo acceder o denegar la entrada a una determinada sección del sistema.
Árbol del sistema de ejemplo
El árbol del ejemplo se vería de la siguiente manera:
.
├── data
│ ├── roles.csv
│ ├── userinrole.csv
│ └── users.csv
├── roles_usuarios.py
└── templates
├── index.html
├── login.html
├── system.html
├── web_sys_1.html
└── web_sys_2.html
Es parecido al ejemplo del artículo sesiones de usuarios con la diferencia que se agregan dos tablas más, la tabla Role y la tabla UserInRole al modelo de la base de datos.
Igualmente se agregan otras plantillas HTML para dar usabilidad a los roles de usuarios en el sistema según su nivel de acceso.
Fichero roles_usuarios.py
A diferencia del fichero roles_usuarios.py en el ejemplo de sesiones de usuarios, en este ejemplo se amplía un poco más, agregando las tablas al modelo de la base de datos y el decorador check_role que hará la discriminación del nivel de acceso según el usuario.
El contenido sería algo parecido al siguiente:
# -*- coding: utf-8 -*-
import os
import csv
import re
from functools import wraps
from flask import Flask, render_template, request, redirect, url_for, abort
from flask_login import (
current_user, login_user, logout_user,
login_required, LoginManager, UserMixin
)
from peewee import *
app = Flask(__name__)
app.secret_key = b'Zlnkkmv37di0aV3f'
login_manager = LoginManager()
login_manager.init_app(app)
#--------- BASE DE DATOS ---------#
db = PostgresqlDatabase('roles_db', user='mibase', password='mibase',
host='localhost', port=5432, autorollback=True)
class BaseModel(Model):
class Meta:
database = db
class Users(UserMixin, BaseModel):
id = AutoField()
username = CharField(max_length=1024)
password = CharField(max_length=1024)
email = CharField(max_length=1024)
@login_manager.user_loader
def load_user(user_id):
for user in Users:
if user.id == int(user_id):
return user
return None
def load_data():
path_data_file = os.path.join('data', 'users.csv')
data = []
with open(path_data_file) as csv_file:
dict_file = csv.DictReader(csv_file, delimiter=';')
for l in dict_file:
data.append(dict(l))
for d in data:
rec = Users.get_or_none(Users.username == d['username'])
if rec is None:
Users.create(**d)
class Role(BaseModel):
id = AutoField()
name = CharField(max_length=1024)
code = CharField(max_length=1024)
def load_data():
path_data_file = os.path.join('data', 'roles.csv')
data = []
with open(path_data_file) as csv_file:
dict_file = csv.DictReader(csv_file, delimiter=';')
for l in dict_file:
data.append(dict(l))
for d in data:
rec = Role.get_or_none(Role.code == d['code'])
if rec is None:
Role.create(**d)
class UserInRole(BaseModel):
user_id = ForeignKeyField(Users, null=True)
role_id = ForeignKeyField(Role, null=True)
def load_data():
path_data_file = os.path.join('data', 'userinrole.csv')
data = []
with open(path_data_file) as csv_file:
dict_file = csv.DictReader(csv_file, delimiter=';')
for l in dict_file:
data.append(dict(l))
for d in data:
rec = UserInRole.get_or_none(UserInRole.user_id == d['user_id'], UserInRole.role_id == d['role_id'])
if rec is None:
UserInRole.create(**d)
class Meta:
indexes = (
(('user_id', 'role_id'), True),
)
#--------- Carga de datos en tablas ---------#
db.create_tables([Users, Role, UserInRole], safe=True)
Users.load_data()
Role.load_data()
UserInRole.load_data()
#--------- CHECK ROLES ---------#
def check_role(roles): # Declaración del decorador
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for r in roles:
is_role = Role.get_or_none(Role.code == r)
if is_role is not None:
is_user = UserInRole.get_or_none(UserInRole.role_id == is_role.id,
UserInRole.user_id == current_user.id)
if is_user is not None:
return func(*args, **kwargs)
abort(403)
return wrapper
return decorator
#--------- SISTEMA ---------#
@app.route('/')
@app.route('/index')
def index():
return render_template('index.html')
@app.route('/login', methods=['GET','POST'])
def login():
if request.method == 'POST':
username = request.form['username']
password = request.form['password']
user = Users.get_or_none(Users.username == username)
if user != None and user.password == password:
login_user(user, user.email)
return redirect(url_for('system'))
return render_template('login.html')
@app.route('/system')
@login_required
@check_role(['user']) # Llamada del decorador
def system():
return render_template('system.html')
@app.route('/seccion_1')
@login_required
@check_role(['admin'])
def seccion_1():
return render_template('web_sys_1.html')
@app.route('/seccion_2')
@login_required
@check_role(['user','admin','manager'])
def seccion_2():
return render_template('web_sys_2.html')
@app.get('/logout')
@login_required
def logout():
logout_user()
return redirect(url_for('index'))
#--------- Arranque del sistema ---------#
app.run(host='0.0.0.0', port='8080', debug=True)
Decorador check_role
Este decorador es personalizado y simplemente revisa en la tabal de Role si existe el role asociado a la función o método que se llama, si existe dicho role verifica si el usuario que está autenticado pertenece a ese grupo de role.
El decorador se apoya con la variable diccionario current_user de Flask-Login. Así, se puede verificar directamente el nivel de acceso del usuario consultando a la base de datos.
Código del decorador:
def check_role(roles):
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
for r in roles:
is_role = Role.get_or_none(Role.code == r)
if is_role != None:
is_user = UserInRole.get_or_none(UserInRole.role_id == is_role.id,
UserInRole.user_id == current_user.id)
if is_user != None:
return func(*args, **kwargs)
abort(403)
return wrapper
return decorator
Si el usuario autenticado no está dentro del grupo asignado a la sección del sistema, entonces se regresa un mensaje de acceso prohibido.
Llamada del decorador
Como se ve resaltado en el fichero roles_usuarios.py, la llamada del decorador check_role se realiza de la siguiente manera:
@check_role(['user','admin','manager'])
Antes de la declaración de la función o método de la sección del sistema y después del decorador @login_required que permite el acceso a la misma función solo a usuarios con sesión iniciada.
El llamado del decorador se podrá realizar para uno o más grupos o niveles de roles, tal como se muestra en el ejemplo.
Ejecución del ejemplo
Para ejecutar o correr este ejemplo, se puede hacer yendo a la raíz del directorio del árbol del ejemplo y ejecutar:
python3 roles_usuarios.py
Y desde el navegador web en la barra de URL abrir con:
http://localhost:8080