🛠 Unidad ISS-21 · HasResourceAccess — capa 🛠 CONSTRUIR
Capa Página Para qué 🧠 Aprender — no está en la fuente comprender, explicar y relacionar 🛠 Construir esta página ejecutar, programar y verificar ✅ GATE Condiciones de cierre condición para pasar al bloque siguiente Esta es la guía ejecutable. El cuerpo de abajo es el ISS técnico verbatim: comandos, rutas, versiones, verificaciones y criterios, sin simplificar.
Guía de estudio. Esta unidad no tiene cuaderno en
material/django/iss/ISS-21/aprendizaje/. La página publica solo el ISS técnico.
ISS-21 — HasResourceAccess
Objetivo
Autorizar por método y patrón de ruta, con negación por defecto. Esta clase es la mitad de autorización del acceso JWT con token + RBAC. Sola no autentica: el JWT ya tuvo que identificar al usuario. Sin esta clase, el perfil seguiría siendo solo JWT con token.
Requisitos
- ISS-20 superado.
- Los modelos del ISS-16 existen.
Construcción
apps/security/permissions.py es nuevo. El archivo completo es la clase HasResourceAccess que compara request.method con request.resolver_match.route y exige activos al usuario, al RoleUser, al Role, al ResourceRole y al Resource. HEAD se evalúa como GET. OPTIONS, si ya hay usuario, no busca recurso.
cat > apps/security/permissions.py <<'EOF'
from rest_framework.permissions import BasePermission
from apps.common.status import RecordStatus
from apps.security.models import ResourceRole
class HasResourceAccess(BasePermission):
message = "No tiene autorización para ejecutar esta operación."
def has_permission(self, request, view):
user = request.user
if not user or not user.is_authenticated:
return False
if getattr(user, "status", None) != RecordStatus.ACTIVE:
return False
if request.method.upper() == "OPTIONS":
return True
match = request.resolver_match
if match is None:
return False
method = request.method.upper()
if method == "HEAD":
method = "GET"
return ResourceRole.objects.filter(
status=RecordStatus.ACTIVE,
resource__status=RecordStatus.ACTIVE,
resource__method=method,
resource__route=match.route,
role__status=RecordStatus.ACTIVE,
role__assignments__user=user,
role__assignments__status=RecordStatus.ACTIVE,
).exists()
EOF
HEAD se evalúa como GET. OPTIONS, con usuario ya autenticado, no busca recurso: es el preflight, no una operación de negocio.
Explicación
DRF convierte el fallo así:
hay authenticators y ninguno autenticó → 401 NotAuthenticated
usuario autenticado y el permiso falla → 403 PermissionDenied
Por eso la misma clase produce 401 o 403 según haya identidad. No se compara la URL /api/clients/15/ con un texto guardado: se compara resolver_match.route, que en este proyecto es api/clients/<int:pk>/.
Si el filtro no encuentra una cadena completa y activa, exists() es falso. No hay un permiso implícito.
Request
↓
JWT
↓
User active?
↓
method + route
↓
¿todos los eslabones active?
├── no → 403
└── sí → endpoint
Criterios de aceptación
- AC-21-01: usuario autenticado sin concesión recibe 403 en
GET /api/clients/. - AC-21-02: el mismo request sin usuario recibe 401.
- AC-21-03: con la concesión, la lista responde 200.
- AC-21-04: apagar el rol, el
RoleUser, elResourceRoleo elResourcevuelve a 403.
Verificación
RbacTests en PostgreSQL, incluyendo el caso del recurso inactivo reejecutado al final. MySQL y SQL Server pasaron la suite cuando el test ya cubría rol, asignación y concesión; el caso del Resource inactivo se reconfirmó en PostgreSQL.
Evidencias
- EVI-21-01:
RbacTestsPASS.
GATE
| AC | Verificación | Evidencia | Resultado |
|---|---|---|---|
| AC-21-01 | 403 | EVI-21-01 | PASS |
| AC-21-02 | 401 | EVI-21-01 | PASS |
| AC-21-03 | concesión activa | EVI-21-01 | PASS |
| AC-21-04 | cada eslabón inactivo | EVI-21-01 | PASS |
✅ GATE de la unidad ISS-21 — este bloque no añade ningún criterio nuevo.
Las condiciones de cierre son las de esta misma página:
- 📋 Criterios de aceptación → Criterios de aceptación
- ✅ GATE → GATE
- 🔎 Verificación → Verificación
Con el GATE en verde queda habilitado el bloque siguiente de la ruta.
Navegación de la ruta: ← ISS-20 · 🛠 Construir · ↑ Ruta Django · → ISS-22 · 🛠 Construir