Saltar al contenido
🐍 Curso de Python
/

Módulo 5 · Lección 18 de 30

Permisos

El permiso de objeto que no se llama en el listado, y la acción propia por la que un desconocido borra la reseña de otro.

⏱ 17 min de lectura

La lección anterior resolvió quién eres. Esta resuelve qué puedes hacer, y es donde están los agujeros de verdad, porque un permiso mal puesto no da error: da acceso.

Y hay dos casos concretos que la gente cree cubiertos y no lo están. Los dos se pueden medir.

Los que trae#

python
class LibroViewSet(viewsets.ModelViewSet):
        permission_classes = [permissions.IsAuthenticated]
PermisoQué hace
AllowAnyTodos. Es el de fábrica
IsAuthenticatedSolo identificados
IsAdminUserSolo con is_staff
IsAuthenticatedOrReadOnlyTodos leen, los identificados escriben
DjangoModelPermissionsLos permisos de Django por modelo
DjangoObjectPermissionsLos mismos, por objeto

Que AllowAny sea el de fábrica es lo de la lección 12: cerrado por defecto en los ajustes, y se abre donde haga falta.

DjangoModelPermissions enlaza con la lección 33 del módulo 4 y tiene dos pegas que conviene saber: exige estar identificado aunque el permiso de ver sea público, y no comprueba nada en GET, así que leer queda abierto a cualquier usuario identificado. Si quieres que leer también se controle, hay que subclasificarlo.

Los propios#

Dos métodos, y la diferencia entre ellos es todo:

python
from rest_framework import permissions


class SoloLecturaOAutor(permissions.BasePermission):
        """Cualquiera lee; solo el autor modifica."""

    def has_object_permission(self, peticion, vista, objeto):
                if peticion.method in permissions.SAFE_METHODS:
                        return True
        return objeto.autor_id == peticion.user.id

Funcionando, con un usuario que no es el autor:

Salida en pantalla
GET   /api/resenas/1/                       -> 200
PATCH /api/resenas/1/  {"texto": "..."}     -> 403
texto: 'Muy bueno'   (sin tocar)

Tres detalles del código:

SAFE_METHODS son GET, HEAD y OPTIONS. Usar la constante y no escribirlos a mano evita olvidarse de alguno.

objeto.autor_id y no objeto.autor.id. La primera lee la columna que ya está cargada; la segunda hace una consulta. En un permiso que se ejecuta en cada petición, eso importa.

peticion.user.id y no peticion.user. Comparar objetos funciona y falla con un usuario anónimo, que no tiene identificador pero sí se puede comparar. Con identificadores, un anónimo da None y nunca coincide.

Cuándo se llama a cada método#

Aquí está lo que hay que saber de memoria. Espiando las llamadas:

Salida en pantalla
GET    /api/espia/      (list)      -> ['has_permission']
GET    /api/espia/1/    (retrieve)  -> ['has_permission', 'has_object_permission']
POST   /api/espia/      (create)    -> ['has_permission']
PATCH  /api/espia/1/    (update)    -> ['has_permission', 'has_object_permission']
DELETE /api/espia/1/    (destroy)   -> ['has_permission', 'has_object_permission']

has_permission se llama siempre. has_object_permission solo cuando hay un objeto.

Y de ahí salen los dos agujeros.

Componer permisos#

Se combinan con operadores, cosa que casi nadie sabe:

python
permission_classes = [IsAuthenticated & (EsAutor | IsAdminUser)]
permission_classes = [~EsBaneado]

Y una lista son varios and: se cumplen todos. Esto:

python
permission_classes = [IsAuthenticated, EsAutor]

es lo mismo que IsAuthenticated & EsAutor. El error clásico es escribir la lista esperando un «o».

Y cuando dependen de la acción, es lo de la lección 16:

python
def get_permissions(self):
        if self.action in ('list', 'retrieve'):
                return [permissions.AllowAny()]
    if self.action == 'publicar':
                return [EsEditor()]
    return [permissions.IsAdminUser()]

Con los paréntesis, que devuelve instancias, y acabando en lo restrictivo.