🛠 Unidad ISS-16 · Role, RoleUser, Resource y ResourceRole — 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-16/aprendizaje/. La página publica solo el ISS técnico.
ISS-16 — Role, RoleUser, Resource y ResourceRole
Objetivo
Modelar la matriz de autorización sin usar Groups ni Permissions como regla de la API.
Requisitos
- ISS-15 superado.
Construcción
apps/security/models.py ya tiene User y RefreshToken. Estas cuatro clases se agregan al final. No se reescribe lo anterior.
ARCHIVO: apps/security/models.py
AL FINAL DEL ARCHIVO, AGREGAR HttpMethod, Role, RoleUser, Resource y ResourceRole.
Tablas roles, role_users, resources y resource_roles. Todos con status default inactive. Las parejas (role, user), (resource, role) y (method, route) son únicas. Las FK usan RESTRICT. Resource.route guarda el patrón (api/clients/<int:pk>/), no el path con el id ya sustituido.
class HttpMethod(models.TextChoices):
GET = "GET", "GET"
POST = "POST", "POST"
PUT = "PUT", "PUT"
PATCH = "PATCH", "PATCH"
DELETE = "DELETE", "DELETE"
class Role(models.Model):
name = models.CharField(max_length=100, unique=True)
description = models.CharField(max_length=255, null=True, blank=True)
status = models.CharField(
max_length=8, choices=RecordStatus.choices, default=RecordStatus.INACTIVE
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "roles"
constraints = [
models.CheckConstraint(
condition=models.Q(status__in=RecordStatus.values),
name="roles_status_valid",
)
]
def __str__(self):
return self.name
class RoleUser(models.Model):
role = models.ForeignKey(Role, on_delete=models.RESTRICT, related_name="assignments")
user = models.ForeignKey(
settings.AUTH_USER_MODEL, on_delete=models.RESTRICT, related_name="role_links"
)
status = models.CharField(
max_length=8, choices=RecordStatus.choices, default=RecordStatus.INACTIVE
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "role_users"
constraints = [
models.UniqueConstraint(fields=["role", "user"], name="role_user_unique"),
models.CheckConstraint(
condition=models.Q(status__in=RecordStatus.values),
name="role_users_status_valid",
),
]
class Resource(models.Model):
name = models.CharField(max_length=150)
method = models.CharField(max_length=10, choices=HttpMethod.choices)
route = models.CharField(max_length=255)
status = models.CharField(
max_length=8, choices=RecordStatus.choices, default=RecordStatus.INACTIVE
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "resources"
constraints = [
models.UniqueConstraint(fields=["method", "route"], name="resource_method_route_uniq"),
models.CheckConstraint(
condition=models.Q(status__in=RecordStatus.values),
name="resources_status_valid",
),
]
def __str__(self):
return f"{self.method} {self.route}"
class ResourceRole(models.Model):
resource = models.ForeignKey(Resource, on_delete=models.RESTRICT, related_name="grants")
role = models.ForeignKey(Role, on_delete=models.RESTRICT, related_name="grants")
status = models.CharField(
max_length=8, choices=RecordStatus.choices, default=RecordStatus.INACTIVE
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
db_table = "resource_roles"
constraints = [
models.UniqueConstraint(fields=["resource", "role"], name="resource_role_unique"),
models.CheckConstraint(
condition=models.Q(status__in=RecordStatus.values),
name="resource_roles_status_valid",
),
]
Resource.method es un TextChoices HTTP. Resource.route guarda el patrón, no el path con el id sustituido. El comando seed_rbac lo obtiene con resolve(path).route. En este proyecto el patrón real quedó, por ejemplo, api/clients/<int:pk>/.
Explicación
Django ya tiene grupos. StoreLab pide otra pregunta: dado un método y un patrón de ruta, ¿la cadena usuario → rol → recurso está toda activa? Esa cadena no se expresa con auth_permission.
Un eslabón que nace inactive no autoriza. Hay que activarlo a propósito. Esa es la forma de negar por defecto también en los datos, no solo en el código.
Criterios de aceptación
- AC-16-01: las cuatro tablas existen.
- AC-16-02: no se puede repetir la pareja rol-usuario ni recurso-rol.
- AC-16-03:
seed_rbacgraba la ruta queresolve()devuelve, no el ejemplo/api/clients/1/.
Verificación
python manage.py seed_rbac contra storelab_django insertó 53 recursos. La consulta SQL mostró api/clients/<int:pk>/, no el path de ejemplo.
Evidencias
- EVI-16-01: 53 filas en
resourcesdespués de la semilla. - EVI-16-02: tests RBAC que dependen de estas tablas PASS.
GATE
| AC | Verificación | Evidencia | Resultado |
|---|---|---|---|
| AC-16-01 | migración | security.0001 en el repo final |
PASS |
| AC-16-02 | UniqueConstraint |
modelos | PASS |
| AC-16-03 | rutas con <int:pk> |
consulta a resources |
PASS |
✅ GATE de la unidad ISS-16 — 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-15 · 🛠 Construir · ↑ Ruta Django · → ISS-17 · 🛠 Construir