Volver a la wiki

Helper require_perm: gate de permisos por scope/level

Descripción

core.utils.require_perm(request, scope, level) es un helper de seguridad reutilizable para validar permisos en endpoints Django Ninja. Lanza HttpError(403) si el usuario no tiene el permiso requerido.

Introducido en: Auditoría Suprema s105 (fix Broken Access Control sistemático).


Firma y uso

from core.utils import require_perm

@router.post("/api/blueprints", response={201: BlueprintSchema})
def create_blueprint(request, payload: BlueprintInputSchema):
    """Create a new blueprint."""
    require_perm(request, "blueprints", "edit")  # Lanza 403 si user no tiene blueprints.edit
    ...

Parámetros:

Comportamiento:


Implementación

Archivo: core/utils/permissions.py (nueva en s105)

Pseudocódigo:

from django.http import HttpError

def require_perm(request, scope, level):
    """
    Valida que el usuario tenga permisos para scope.level.
    Lanza HttpError(403) si no.
    
    Consulta ModulePermission.objects.filter(user=request.user, module=scope, ...)
    según el level requerido.
    """
    user = getattr(request, 'user', None)
    if not user or not user.is_authenticated:
        raise HttpError(403, "Permission denied")
    
    # Lógica de consulta de BD (ModulePermission)
    # Si el usuario es readonly para el scope → solo "view" es permitido
    # Si es operator/admin → "view" + "edit" son permitidos
    
    has_perm = _check_user_perm(user, scope, level)
    if not has_perm:
        raise HttpError(403, "Permission denied")

Aplicaciones (s105)

Usado en ~25 endpoints del módulo blueprints:


Aplicaciones (23-08-2026, tasks #241/#251)

Ronda de seguridad extiende el helper al CRUD nuclear de racks (racks/api/racks.py, distinto de blueprints/api/racks.py de arriba) y a 3 escrituras de /api/settings — detalle completo en [[decision—20260823—racks-settings-broken-access-control]]:

Estos 17 puntos suman un test de contrato registry-driven (tests/api/test_racks_permissions.py, tests/api/test_settings_permissions.py) que recorre las operaciones REGISTRADAS en cada router y falla si alguna escritura no llama a require_perm — un endpoint nuevo sin gate rompe el CI solo.


Test de contrato universal (06-09-2026, task #279, v1.118.0)

tests/api/test_write_endpoints_permission_contract.py generaliza el patrón anterior (registry-driven, pero por dominio) a todos los routers Ninja montados en config.urls.api: recorre cada operación POST/PUT/PATCH/DELETE de cada router y exige un marcador de gate en el código fuente del handler — require_perm(, has_permission(, is_superuser, un return 403 explícito en el cuerpo, o delegación a una función del mismo módulo que sí gatea (alias legacy). Lista blanca razonada endpoint a endpoint para las exenciones reales (p. ej. los endpoints del Agente instalado, que se autentican con su propio JWT y no tienen “usuario” al que pedir nivel de permiso).

Este test fue el que encontró y motivó el fix de las 3 zonas de este commit, cada una un handler sin gate o con el scope equivocado:

Deuda declarada sin cerrar: explain_insight no gatea, mientras su hermano tutor_ask exige cns:view — no se tocó porque explain_insight también acepta el JWT del Agente.

Límite: solo cubre escrituras (POST/PUT/PATCH/DELETE); una lectura que filtre datos sensibles (como el propio get_scripts, que es GET y devolvía script_content a cualquier autenticado) no la ve este test y necesita el suyo propio.


Pattern para nuevas features

Cualquier endpoint nuevo en módulos auditados (o nuevos módulos) debe usar este helper:

@router.post("/{resource_id}", response={201: dict})
def create_resource(request, resource_id: int, payload: PayloadSchema):
    """Create a resource."""
    require_perm(request, "module_name", "edit")  # <-- primero, antes de lógica
    # ... resto del endpoint

Regla de oro: insertar require_perm(...) como primera línea funcional del endpoint (antes de get_current_org, queries, etc.).


Exportación

El helper está exportado en core/utils/__init__.py:

from .permissions import require_perm

__all__ = ["require_perm", "get_current_org", ...]

Para importar en cualquier módulo:

from core.utils import require_perm

Véase también

Subir