feat(roadmap): implementar Sprints 1, 2 y 3 (MVC, Stateless JWT API y Spring Security)

This commit is contained in:
Carlos Tello
2026-09-05 01:46:57 -03:00
parent fa878cb0d5
commit 69b8b7ccef
34 changed files with 1750 additions and 21 deletions
+8
View File
@@ -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'
]
+45
View File
@@ -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
+83
View File
@@ -0,0 +1,83 @@
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
if path in self._rate_limits:
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 = 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
return Response(
f"Demasiados intentos. Por favor espere {retry_after} segundos antes de volver a intentar.",
status=429,
mimetype='text/plain'
)
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