ATALAIA
  1. Inicio
  2. Recursos
  3. Blog
  4. Tests de autorización que tu equipo de QA puede asumir
Pruebas · Guía

Tests de autorización que tu equipo de QA puede asumir

El control de acceso roto es el bug que QA está mejor situado para detectar. Una matriz de rol por endpoint y unas pocas líneas de pytest lo vuelven rutina.

3 min de lecturaEquipo Atalaia

QAAutorizaciónpytestPruebas de API

El control de acceso roto está arriba del OWASP Top 10 por algo. Además, es la clase de bug que un equipo de QA está mejor situado para encontrar. No hace falta saber explotar. Hace falta una declaración clara de quién debería poder hacer qué, y tests que comprueben que el sistema está de acuerdo.

La mayoría de las suites de QA ya comprueban que el usuario correcto puede hacer lo correcto. El hueco es el caso negativo: que el usuario equivocado no puede. Esta guía muestra cómo hacer esos casos sistemáticos.

Empieza con una matriz

Anota todos los roles que tiene tu producto y todos los endpoints o acciones que importan. Rellena cada celda con el resultado esperado. Es un ejercicio de requisitos tanto como de pruebas, y a menudo saca a la luz desacuerdos que nadie conocía.

EndpointAnónimoViewerEditorAdminAdmin de otro tenant
GET /projects/{id}401200200200404
PATCH /projects/{id}401403200200404
DELETE /projects/{id}401403403204404
POST /projects/{id}/members401403403201404
GET /admin/audit-log401403403200403

La última columna es la que los equipos olvidan. "Admin de otro tenant" es un usuario con todos los privilegios en otra organización. No debería obtener nada de tus objetos. Muchos fallos de control de acceso solo aparecen en esa columna, porque todas las comprobaciones de rol pasan y solo falta la de propiedad.

Define los actores

El entorno de pruebas necesita un usuario por columna, en al menos dos tenants, más un objeto que pertenezca al primer tenant. Créalos en un fixture para que cada test parta del mismo estado. Usa cuentas de prueba desechables que solo existan en el entorno de pruebas.

# conftest.py
import pytest
from api_client import Client, create_tenant, create_user, create_project

@pytest.fixture(scope="session")
def world():
    a = create_tenant("tenant-a")
    b = create_tenant("tenant-b")
    users = {
        "anonymous": None,
        "viewer": create_user(a, role="viewer"),
        "editor": create_user(a, role="editor"),
        "admin": create_user(a, role="admin"),
        "other_admin": create_user(b, role="admin"),
    }
    project = create_project(a, name="qa-fixture")
    return {"users": users, "project": project}

def client_for(world, actor):
    user = world["users"][actor]
    return Client(token=user.token if user else None)

Convierte la matriz en tests

Codifica la matriz como datos y deja que pytest genere un test por celda. Cuando cambia un requisito, cambias una línea de la tabla, no una función de test.

# test_authz_projects.py
import pytest
from conftest import client_for

ACTORS = ["anonymous", "viewer", "editor", "admin", "other_admin"]

MATRIX = {
    ("GET",    "/projects/{id}"):         [401, 200, 200, 200, 404],
    ("PATCH",  "/projects/{id}"):         [401, 403, 200, 200, 404],
    ("DELETE", "/projects/{id}"):         [401, 403, 403, 204, 404],
    ("POST",   "/projects/{id}/members"): [401, 403, 403, 201, 404],
}

CASES = [
    (method, path, actor, expected)
    for (method, path), row in MATRIX.items()
    for actor, expected in zip(ACTORS, row)
]

@pytest.mark.parametrize("method,path,actor,expected", CASES)
def test_project_authz(world, method, path, actor, expected):
    client = client_for(world, actor)
    url = path.format(id=world["project"].id)
    resp = client.request(method, url, json={"name": "x"})
    assert resp.status_code == expected, (
        f"{actor} {method} {url}: got {resp.status_code}, want {expected}"
    )

Ejecuta las celdas destructivas, como DELETE, contra un objeto nuevo por test, o ponlas al final, para que un borrado que pasa no rompa las celdas siguientes.

Más allá de los códigos de estado

Un código de estado correcto es necesario, pero no suficiente. Añade unas pocas comprobaciones que miren lo que se devuelve:

  • Endpoints de listado. Un viewer que lista proyectos ve solo los de su tenant. Haz la aserción sobre los ids, no sobre el número.
  • Exposición a nivel de campo. La respuesta para un viewer no incluye campos como datos de facturación o emails de miembros si la matriz dice que no deben verse.
  • Mass assignment. Un editor que envía "role": "admin" o "tenant_id" en el cuerpo de un PATCH no cambia esos campos.
  • Referencias indirectas. Los objetos hijos como /projects/{id}/files/{file_id} comprueban que el fichero pertenece a ese proyecto, no solo que el proyecto es accesible.

Qué hacer el lunes

  1. Elige un recurso, como proyectos o facturas, y escribe su matriz de roles por endpoint junto con producto.
  2. Añade la columna "otro tenant" y acordad la regla de 403 o 404.
  3. Crea el fixture con un usuario por columna en dos tenants.
  4. Codifica la matriz como datos y ejecútala en CI en cada build.
  5. Añade una fila nueva cada vez que una historia añada un endpoint. Enlázala con el caso de abuso de la historia, para que el requisito y el test vayan juntos.

Dónde encaja en la línea

Habladlo

Una llamada de 30 minutos. Sin diapositivas, sin lista de precios y con un siguiente paso en cualquier caso.