Arch/utils/security.py

144 lines
3.5 KiB
Python
Raw Permalink Normal View History

2025-03-03 15:35:24 -03:00
import os
import hashlib
import re
2025-03-03 17:50:11 -03:00
from flask import current_app, request, abort
2025-03-03 15:35:24 -03:00
from functools import wraps
from flask_login import current_user
from werkzeug.utils import secure_filename
2025-03-03 17:50:11 -03:00
# Try to import magic with a fallback for Windows
try:
import magic
HAS_MAGIC = True
except ImportError:
HAS_MAGIC = False
current_app.logger.warning("libmagic not found, using basic file type detection")
from utils.file_utils import detect_file_type
2025-03-03 15:35:24 -03:00
def check_file_type(file_stream, allowed_mime_types):
"""
Verificar el tipo MIME real de un archivo.
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Args:
file_stream: Stream del archivo a verificar
allowed_mime_types (list): Lista de tipos MIME permitidos
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Returns:
bool: True si el tipo es permitido, False en caso contrario
"""
# Guardar posición actual en el stream
current_position = file_stream.tell()
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
# Leer los primeros bytes para detectar el tipo
file_head = file_stream.read(2048)
file_stream.seek(current_position) # Restaurar posición
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
# Detectar tipo MIME
mime = magic.Magic(mime=True)
file_type = mime.from_buffer(file_head)
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
return file_type in allowed_mime_types
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
def calculate_checksum(file_path):
"""
Calcular el hash SHA-256 de un archivo.
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Args:
file_path (str): Ruta al archivo
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Returns:
str: Hash SHA-256 en formato hexadecimal
"""
sha256_hash = hashlib.sha256()
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
with open(file_path, "rb") as f:
# Leer por bloques para archivos grandes
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
return sha256_hash.hexdigest()
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
def sanitize_filename(filename):
"""
Sanitizar nombre de archivo.
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Args:
filename (str): Nombre de archivo original
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Returns:
str: Nombre de archivo sanitizado
"""
# Primero usar secure_filename de Werkzeug
safe_name = secure_filename(filename)
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
# Eliminar caracteres problemáticos adicionales
2025-03-03 17:50:11 -03:00
safe_name = re.sub(r"[^\w\s.-]", "", safe_name)
2025-03-03 15:35:24 -03:00
# Reemplazar espacios por guiones bajos
2025-03-03 17:50:11 -03:00
safe_name = safe_name.replace(" ", "_")
2025-03-03 15:35:24 -03:00
return safe_name
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
def generate_unique_filename(base_dir, filename_pattern):
"""
Generar nombre de archivo único basado en un patrón.
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Args:
base_dir (str): Directorio base
filename_pattern (str): Patrón de nombre (puede contener {counter})
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Returns:
str: Nombre de archivo único
"""
counter = 1
filename = filename_pattern.format(counter=counter)
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
while os.path.exists(os.path.join(base_dir, filename)):
counter += 1
filename = filename_pattern.format(counter=counter)
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
return filename
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
def permission_required(min_level):
"""
Decorador para verificar nivel de permisos.
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Args:
min_level (int): Nivel mínimo requerido
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
Returns:
function: Decorador configurado
"""
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
if not current_user.has_permission(min_level):
return forbidden_error()
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
return f(*args, **kwargs)
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
return decorated_function
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
return decorator
2025-03-03 17:50:11 -03:00
2025-03-03 15:35:24 -03:00
def forbidden_error():
"""Respuesta para error 403 (acceso denegado)."""
from flask import render_template
2025-03-03 17:50:11 -03:00
return (
render_template("error.html", error_code=403, error_message="Acceso denegado"),
403,
)