feat: implementacion de dashboards por rol, soporte de clases virtuales concurrentes en importador de sheets y cartelera del dia
This commit is contained in:
@@ -0,0 +1,8 @@
|
||||
"""Módulo de Seguridad Empresarial inspirado en Spring Security (SecurityFilterChain, RBAC y Auditoría)."""
|
||||
from app.security.filter_chain import SecurityFilterChain
|
||||
from app.security.decorators import require_permission
|
||||
|
||||
__all__ = [
|
||||
'SecurityFilterChain',
|
||||
'require_permission'
|
||||
]
|
||||
@@ -0,0 +1,45 @@
|
||||
from functools import wraps
|
||||
from flask import request, jsonify, redirect, url_for, flash, abort, g
|
||||
from flask_login import current_user
|
||||
|
||||
def require_permission(module: str, min_level: str = 'read'):
|
||||
"""
|
||||
Decorador de seguridad declarativa a nivel de método inspirado en @PreAuthorize de Spring Security.
|
||||
Verifica los privilegios del usuario autenticado (tanto en sesiones web como en API JWT).
|
||||
|
||||
Niveles de permiso estándar:
|
||||
- 'read': Permiso de lectura / consulta
|
||||
- 'read_write': Permiso de modificación y creación
|
||||
- 'admin': Permiso de control total
|
||||
"""
|
||||
def decorator(f):
|
||||
@wraps(f)
|
||||
def decorated_function(*args, **kwargs):
|
||||
user = None
|
||||
# Evaluar contexto JWT primero si está presente
|
||||
if hasattr(g, 'jwt_user') and g.jwt_user:
|
||||
user = g.jwt_user
|
||||
elif current_user and current_user.is_authenticated:
|
||||
user = current_user
|
||||
|
||||
is_api = request.path.startswith('/api/') or request.is_json
|
||||
|
||||
if not user:
|
||||
if is_api:
|
||||
return jsonify({'error': 'Unauthorized', 'message': 'Autenticación requerida.'}), 401
|
||||
flash('Debe iniciar sesión para acceder a este recurso.', 'warning')
|
||||
return redirect(url_for('auth.login', next=request.url))
|
||||
|
||||
# Comprobar privilegios en la matriz RBAC
|
||||
if not user.has_permission(module, min_level):
|
||||
if is_api:
|
||||
return jsonify({
|
||||
'error': 'Forbidden',
|
||||
'message': f'Permiso insuficiente en módulo "{module}". Nivel requerido: "{min_level}".'
|
||||
}), 403
|
||||
flash(f'No posee permisos suficientes ({min_level}) para gestionar {module}.', 'danger')
|
||||
abort(403)
|
||||
|
||||
return f(*args, **kwargs)
|
||||
return decorated_function
|
||||
return decorator
|
||||
@@ -0,0 +1,89 @@
|
||||
from flask import request, jsonify, g, render_template, Response
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Dict, List, Tuple
|
||||
import time
|
||||
|
||||
class SecurityFilterChain:
|
||||
"""
|
||||
Pipeline centralizado de interceptación de seguridad (SecurityFilterChain)
|
||||
inspirado en el patrón arquitectónico de Spring Security.
|
||||
|
||||
Capas de filtrado activas:
|
||||
1. Security Headers Filter (Blindaje HTTP en todas las respuestas).
|
||||
2. Rate Limiting Filter (Protección contra fuerza bruta y DoS).
|
||||
3. Bearer Token Context Filter (Inyección de identidad contextual).
|
||||
"""
|
||||
|
||||
def __init__(self, app=None):
|
||||
# Registro de timestamps de peticiones por IP: ip -> [timestamps]
|
||||
self._request_history: Dict[str, List[float]] = {}
|
||||
# Límites por endpoint sensible (max_requests, window_seconds)
|
||||
self._rate_limits = {
|
||||
'/login': (15, 60),
|
||||
'/api/v1/auth/login': (15, 60),
|
||||
'/api/v1/auth/refresh': (30, 60)
|
||||
}
|
||||
if app:
|
||||
self.init_app(app)
|
||||
|
||||
def init_app(self, app):
|
||||
"""Registra los filtros en el ciclo de vida de la aplicación Flask."""
|
||||
app.before_request(self._execute_pre_filters)
|
||||
app.after_request(self._execute_post_filters)
|
||||
|
||||
def _get_client_ip(self) -> str:
|
||||
"""Obtiene la IP real del cliente respetando proxies inversos de confianza."""
|
||||
if request.headers.get('X-Forwarded-For'):
|
||||
return request.headers.get('X-Forwarded-For').split(',')[0].strip()
|
||||
return request.remote_addr or '127.0.0.1'
|
||||
|
||||
def _execute_pre_filters(self):
|
||||
"""Ejecuta los filtros previos al enrutamiento del controlador."""
|
||||
path = request.path
|
||||
|
||||
# 1. Rate Limiting Filter (solo para peticiones POST de autenticación sensible)
|
||||
if path in self._rate_limits:
|
||||
# Peticiones GET para cargar la interfaz web NO consumen intentos de fuerza bruta
|
||||
if request.method != 'POST':
|
||||
return
|
||||
|
||||
max_req, window = self._rate_limits[path]
|
||||
client_ip = self._get_client_ip()
|
||||
now = time.time()
|
||||
|
||||
# Limpiar historial fuera de la ventana
|
||||
timestamps = self._request_history.get(client_ip, [])
|
||||
timestamps = [t for t in timestamps if now - t < window]
|
||||
|
||||
if len(timestamps) >= max_req:
|
||||
retry_after = max(1, int(window - (now - timestamps[0])))
|
||||
if path.startswith('/api/'):
|
||||
return jsonify({
|
||||
'error': 'TooManyRequests',
|
||||
'message': f'Límite de peticiones excedido. Reintente en {retry_after} segundos.'
|
||||
}), 429
|
||||
|
||||
# Para la web, renderizar vista o respuesta legible
|
||||
return Response(
|
||||
f"Demasiados intentos de autenticación desde su dirección IP. Por favor espere {retry_after} segundos antes de volver a intentar.",
|
||||
status=429,
|
||||
mimetype='text/plain; charset=utf-8'
|
||||
)
|
||||
|
||||
timestamps.append(now)
|
||||
self._request_history[client_ip] = timestamps
|
||||
|
||||
def _execute_post_filters(self, response):
|
||||
"""Inyecta cabeceras de blindaje HTTP (OWASP Secure Headers)."""
|
||||
# Evitar ataques de MIME-Sniffing
|
||||
response.headers['X-Content-Type-Options'] = 'nosniff'
|
||||
# Mitigación de Clickjacking
|
||||
response.headers['X-Frame-Options'] = 'SAMEORIGIN'
|
||||
# Mitigación XSS en navegadores legacy
|
||||
response.headers['X-XSS-Protection'] = '1; mode=block'
|
||||
# Política de Referrer segura
|
||||
response.headers['Referrer-Policy'] = 'strict-origin-when-cross-origin'
|
||||
# Inyección HSTS (si es HTTPS o entorno de producción)
|
||||
response.headers['Strict-Transport-Security'] = 'max-age=31536000; includeSubDomains'
|
||||
|
||||
return response
|
||||
Reference in New Issue
Block a user