Diferencia entre revisiones de «Flask: creando roles de usuarios»

De nuxpy
Ir a la navegación Ir a la búsqueda
 
(No se muestra una edición intermedia del mismo usuario)
Línea 142: Línea 142:
 
                 is_role = Role.get_or_none(Role.code == r)
 
                 is_role = Role.get_or_none(Role.code == r)
 
                 if is_role is not None:
 
                 if is_role is not None:
                     is_user = UserInRole.get_or_none(
+
                     is_user = UserInRole.get_or_none(UserInRole.role_id == is_role.id,  
                        UserInRole.role_id == is_role.id,  
 
 
                         UserInRole.user_id == current_user.id)
 
                         UserInRole.user_id == current_user.id)
 
                     if is_user is not None:
 
                     if is_user is not None:
Línea 246: Línea 245:
 
* [[Flask: creación de un proyecto]]
 
* [[Flask: creación de un proyecto]]
 
* [[Peewee ORM para Python]]
 
* [[Peewee ORM para Python]]
[[Categoría:Python]]
+
[[Categoría:Flask]]

Revisión actual del 19:15 13 mar 2023

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:

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.

Descargar este ejemplo

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

Temas relacionados