Arch/utils/security.py

144 lines
3.5 KiB
Python

import os
import hashlib
import re
from flask import current_app, request, abort
from functools import wraps
from flask_login import current_user
from werkzeug.utils import secure_filename
# 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
def check_file_type(file_stream, allowed_mime_types):
"""
Verificar el tipo MIME real de un archivo.
Args:
file_stream: Stream del archivo a verificar
allowed_mime_types (list): Lista de tipos MIME permitidos
Returns:
bool: True si el tipo es permitido, False en caso contrario
"""
# Guardar posición actual en el stream
current_position = file_stream.tell()
# Leer los primeros bytes para detectar el tipo
file_head = file_stream.read(2048)
file_stream.seek(current_position) # Restaurar posición
# Detectar tipo MIME
mime = magic.Magic(mime=True)
file_type = mime.from_buffer(file_head)
return file_type in allowed_mime_types
def calculate_checksum(file_path):
"""
Calcular el hash SHA-256 de un archivo.
Args:
file_path (str): Ruta al archivo
Returns:
str: Hash SHA-256 en formato hexadecimal
"""
sha256_hash = hashlib.sha256()
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)
return sha256_hash.hexdigest()
def sanitize_filename(filename):
"""
Sanitizar nombre de archivo.
Args:
filename (str): Nombre de archivo original
Returns:
str: Nombre de archivo sanitizado
"""
# Primero usar secure_filename de Werkzeug
safe_name = secure_filename(filename)
# Eliminar caracteres problemáticos adicionales
safe_name = re.sub(r"[^\w\s.-]", "", safe_name)
# Reemplazar espacios por guiones bajos
safe_name = safe_name.replace(" ", "_")
return safe_name
def generate_unique_filename(base_dir, filename_pattern):
"""
Generar nombre de archivo único basado en un patrón.
Args:
base_dir (str): Directorio base
filename_pattern (str): Patrón de nombre (puede contener {counter})
Returns:
str: Nombre de archivo único
"""
counter = 1
filename = filename_pattern.format(counter=counter)
while os.path.exists(os.path.join(base_dir, filename)):
counter += 1
filename = filename_pattern.format(counter=counter)
return filename
def permission_required(min_level):
"""
Decorador para verificar nivel de permisos.
Args:
min_level (int): Nivel mínimo requerido
Returns:
function: Decorador configurado
"""
def decorator(f):
@wraps(f)
def decorated_function(*args, **kwargs):
if not current_user.is_authenticated:
return current_app.login_manager.unauthorized()
if not current_user.has_permission(min_level):
return forbidden_error()
return f(*args, **kwargs)
return decorated_function
return decorator
def forbidden_error():
"""Respuesta para error 403 (acceso denegado)."""
from flask import render_template
return (
render_template("error.html", error_code=403, error_message="Acceso denegado"),
403,
)