Icono del sitio IT&ライフハックブログ|学びと実践のためのアイデア集

Introducción a la gestión de autorización RBAC/ABAC en FastAPI: una guía práctica para diseñar autorizaciones seguras con roles, atributos y políticas

green snake

Photo by Pixabay on Pexels.com

Introducción a la gestión de autorización RBAC/ABAC en FastAPI: una guía práctica para diseñar autorizaciones seguras con roles, atributos y políticas


Resumen

  • La autenticación es el mecanismo para confirmar quién es una persona, mientras que la autorización es el mecanismo para decidir qué puede hacer esa persona. En el desarrollo real con FastAPI, el diseño de autorización tiene un gran impacto tanto en la seguridad de la aplicación como en su mantenibilidad.
  • En aplicaciones pequeñas, es práctico comenzar con RBAC (Role-Based Access Control). Agrupar permisos en roles como admin, editor y viewer hace que tanto la implementación como la operación sean más fáciles de entender.
  • Sin embargo, en entornos SaaS y multi-tenant, la lógica basada solo en roles suele volverse insuficiente. Ahí es donde ABAC (Attribute-Based Access Control) se vuelve importante, al combinar condiciones como pertenencia al tenant, propiedad, estado de publicación y nivel del plan.
  • En FastAPI, un enfoque limpio es usar Depends y Security para resolver el usuario autenticado, el tenant y el recurso objetivo mediante funciones de dependencia, y luego centralizar las decisiones de autorización en funciones de política.
  • En este artículo recorreremos, paso a paso y con cuidado, las diferencias entre RBAC y ABAC, los patrones de implementación en FastAPI, cómo combinarlos con multi-tenancy, cómo conectarlos con logs de auditoría y cómo probarlos.

Quién se beneficiará de leer esto

Desarrolladores individuales y personas que están aprendiendo

Esto es para quienes ya implementaron funcionalidad de login, pero no saben bien cómo escribir reglas de autorización como “solo los administradores pueden editar” o “solo el propio usuario puede eliminar su cuenta”.
Al principio, algo como if current_user.role == "admin" puede parecer suficiente, pero a medida que las funciones crecen, ese estilo tiende a volverse desordenado. Este artículo te ayudará a pasar de esa etapa hacia una estructura más organizada.

Ingenieros backend en equipos pequeños

Esto es para quienes están construyendo paneles de administración, herramientas internas o APIs SaaS con FastAPI y sienten que “las comprobaciones de roles están dispersas por todas partes y parecen arriesgadas” o que “es difícil ver el impacto de los cambios de permisos”.
La estructura aquí está pensada para responder preguntas prácticas como hasta dónde puede llevarte RBAC por sí solo, cuándo conviene introducir ABAC y cómo dividir responsabilidades entre capas de servicio y funciones de dependencia.

Equipos SaaS y startups

Esto es para equipos en entornos multi-tenant donde los roles a nivel de tenant, las restricciones por propiedad, los límites por plan, los logs de auditoría y preocupaciones similares han dejado claro que “una simple comprobación de admin ya no es suficiente”.
Si las reglas de autorización se añaden de forma improvisada, luego se vuelven muy difíciles de corregir. Organizar desde temprano dónde deben vivir las decisiones de autorización hace que el sistema sea más resistente a la complejidad futura.


Evaluación de accesibilidad

  • El artículo comienza presentando el panorama general y luego avanza en el orden de “fundamentos de RBAC”, “introducción a ABAC”, “dependencias en FastAPI”, “funciones de política”, “multi-tenancy” y “pruebas y auditoría”. Esto facilita entenderlo incluso si solo lees las secciones que necesitas.
  • Los términos técnicos se explican brevemente la primera vez que aparecen, y después se usa el mismo vocabulario de manera consistente. Esto busca reducir la carga cognitiva.
  • Los ejemplos de código se mantienen cortos y divididos para que cada bloque muestre solo una responsabilidad. Esto hace que sean más fáciles de seguir visualmente.
  • Los encabezados aparecen con frecuencia para que el punto clave de cada sección quede claro. El nivel objetivo es aproximadamente WCAG AA.

1. ¿Por qué es peligroso posponer la autorización?

En FastAPI, como en cualquier framework, la etapa inicial del desarrollo de una aplicación suele sentirse como un gran hito una vez que “los usuarios ya pueden iniciar sesión”.
Pero lo que realmente importa después es cómo controlas qué se les permite hacer a los usuarios que ya iniciaron sesión.

Una implementación inicial común se ve así:

if current_user.role != "admin":
    raise HTTPException(status_code=403, detail="forbidden")

Al principio esto parece suficiente, pero a medida que aumentan las funciones empiezan a aparecer problemas como estos:

  • Las comprobaciones se vuelven inconsistentes entre routers
  • Una vez que se añaden roles distintos de admin, hay demasiados lugares que actualizar
  • Empiezan a mezclarse condiciones como “solo el propietario puede editar” o “solo un admin del mismo tenant puede hacer esto”
  • Los nombres de permisos que entiende el frontend y el backend empiezan a desviarse
  • Se vuelve difícil dejar logs de auditoría claros que muestren por qué se denegó el acceso

En otras palabras, la autorización no es solo una sentencia if. Es una cuestión de diseño.
Cuando esta parte se organiza correctamente, no solo mejora la seguridad, sino también la legibilidad del código y la facilidad para cambiarlo.


2. ¿Qué es RBAC? Gestionar el acceso mediante roles

RBAC significa Role-Based Access Control, y es la idea de decidir permisos en función del rol de una persona.

Por ejemplo, en una aplicación de gestión de artículos, podrías pensarlo así:

  • admin: puede hacerlo todo
  • editor: puede crear, editar y publicar artículos
  • viewer: solo puede ver

Lo bueno de este enfoque es que es fácil de entender para las personas.
Si dices “esta persona es un editor”, eso es fácil de explicar dentro de un equipo y fácil de mostrar en la UI.

2.1 Casos en los que RBAC funciona bien

RBAC es especialmente fuerte en situaciones como estas:

  • Paneles administrativos internos
  • APIs de gestión pequeñas o medianas
  • Aplicaciones donde los patrones de permisos son relativamente estables
  • Productos SaaS donde los roles dentro de cada tenant están claramente definidos

Como primer modelo de autorización, es muy accesible.

2.2 Situaciones en las que RBAC por sí solo deja de ser suficiente

Sin embargo, en el trabajo real suelen aparecer condiciones como estas:

  • Incluso un editor solo puede editar los artículos que él mismo creó
  • Un viewer aún puede ver contenido publicado
  • Incluso un admin no puede ver datos de un tenant al que no pertenece
  • Algunas funciones no están disponibles en tenants con plan gratuito

A medida que aparecen más comprobaciones basadas en atributos distintos del rol, RBAC por sí solo se vuelve más difícil de usar.
Ahí es donde entra ABAC.


3. ¿Qué es ABAC? Tomar decisiones finas mediante atributos

ABAC significa Attribute-Based Access Control, y es la idea de decidir la autorización en función de atributos.

Aquí, los atributos pueden incluir información como:

  • Atributos del usuario
    • user_id, role, department, plan
  • Atributos del recurso
    • owner_id, tenant_id, status, visibility
  • Atributos del entorno
    • hora, dirección IP, entorno del cliente
  • Atributos del tenant
    • plan suscrito, opciones, estado de suspensión

Por ejemplo, estas son decisiones típicas de estilo ABAC:

  • “Se permite editar si el owner_id del artículo coincide con el user_id del usuario actual”
  • “Se permite exportar si el plan del tenant es pro o superior”
  • “Un recurso puede verse incluso sin login si está marcado como public

Así que ABAC no consiste solo en preguntar “¿qué rol tiene esta persona?”, sino también “¿cuál es la relación entre esta persona y el objetivo?”

3.1 RBAC y ABAC no se oponen: normalmente se combinan

En la práctica, RBAC y ABAC suelen usarse juntos en lugar de elegir solo uno.

Por ejemplo:

  • Usar RBAC para decidir el marco general
    • Un viewer no puede acceder a APIs de actualización
  • Usar ABAC para decidir las condiciones finas
    • Un editor solo puede editar proyectos de su propio tenant

Este enfoque de dos capas facilita conseguir claridad y flexibilidad al mismo tiempo.


4. Enfoque básico en FastAPI: llevar la autorización a funciones de dependencia y funciones de política

En FastAPI, la lógica de autorización suele ser más fácil de entender cuando se lleva a funciones de dependencia y funciones de política en lugar de escribirla directamente dentro de los routers.

Si separamos las responsabilidades, quedan así:

  • Dependencia de autenticación
    • obtiene el usuario actual
  • Dependencia de tenant
    • resuelve el tenant_id actual
  • Dependencia de recurso
    • obtiene el proyecto o artículo objetivo
  • Función de política
    • decide si “este usuario puede realizar esta acción”

Con esta estructura, los routers se vuelven más fáciles de leer porque puedes ver claramente qué se está usando para la autorización, y el código también resulta más fácil de probar.


5. Un modelo base para usuarios, tenants y roles

Comencemos con el modelo más pequeño que aún permita entender fácilmente los conceptos.

# app/models/auth.py
from pydantic import BaseModel

class CurrentUser(BaseModel):
    user_id: int
    email: str
    global_role: str | None = None
    tenant_ids: list[int] = []
    active_tenant_id: int | None = None

Aquí asumimos la siguiente estructura simple:

  • global_role representa autoridad a nivel de sistema, como un superadmin
  • tenant_ids es la lista de tenants a los que pertenece el usuario
  • active_tenant_id es el tenant seleccionado actualmente

Luego gestionamos por separado los roles específicos del tenant.

# app/models/membership.py
from pydantic import BaseModel
from typing import Literal

TenantRole = Literal["owner", "admin", "member", "viewer"]

class TenantMembership(BaseModel):
    user_id: int
    tenant_id: int
    role: TenantRole

Esto nos permite pensar por separado en “administradores globales” y “roles dentro de cada tenant”.


6. Resolver el usuario autenticado y el tenant mediante dependencias

6.1 Devolver el usuario actual

En la práctica, JWT es común, pero aquí simplificamos para facilitar la explicación.

# app/deps/auth.py
from fastapi import Depends
from app.models.auth import CurrentUser

def get_current_user() -> CurrentUser:
    return CurrentUser(
        user_id=1,
        email="hanako@example.com",
        global_role=None,
        tenant_ids=[10, 20],
        active_tenant_id=10,
    )

6.2 Resolver el tenant_id actual

# app/deps/tenant.py
from fastapi import Depends, Header, HTTPException, status
from app.deps.auth import get_current_user
from app.models.auth import CurrentUser

def get_current_tenant_id(
    current_user: CurrentUser = Depends(get_current_user),
    x_tenant_id: int | None = Header(default=None),
) -> int:
    tenant_id = x_tenant_id or current_user.active_tenant_id

    if tenant_id is None:
        raise HTTPException(
            status_code=status.HTTP_400_BAD_REQUEST,
            detail="tenant is not selected",
        )

    if tenant_id not in current_user.tenant_ids:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="tenant access is forbidden",
        )

    return tenant_id

El punto importante aquí es: nunca confíes en un tenant_id recibido por header o path por sí solo.
Comprueba siempre si el usuario realmente pertenece a ese tenant.


7. Implementación mínima de RBAC: convertir las comprobaciones de roles en dependencias

Empecemos con un caso simple donde RBAC por sí solo es suficiente.
Por ejemplo, supongamos que solo los administradores del tenant pueden invitar miembros.

7.1 Función para obtener el rol dentro del tenant

# app/deps/membership.py
from fastapi import Depends, HTTPException, status
from app.deps.auth import get_current_user
from app.deps.tenant import get_current_tenant_id
from app.models.auth import CurrentUser
from app.models.membership import TenantMembership

def get_current_membership(
    current_user: CurrentUser = Depends(get_current_user),
    tenant_id: int = Depends(get_current_tenant_id),
) -> TenantMembership:
    # En la práctica, obtenerlo de la BD
    if current_user.user_id == 1 and tenant_id == 10:
        return TenantMembership(user_id=1, tenant_id=10, role="admin")

    raise HTTPException(
        status_code=status.HTTP_403_FORBIDDEN,
        detail="membership not found",
    )

7.2 Dependencia que requiere un rol específico

# app/deps/permissions.py
from fastapi import Depends, HTTPException, status
from app.deps.membership import get_current_membership
from app.models.membership import TenantMembership

def require_tenant_admin(
    membership: TenantMembership = Depends(get_current_membership),
) -> TenantMembership:
    if membership.role not in {"owner", "admin"}:
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="admin role required",
        )
    return membership

7.3 Usarlo en el router

# app/api/v1/routers/members.py
from fastapi import APIRouter, Depends

from app.deps.permissions import require_tenant_admin

router = APIRouter(prefix="/members", tags=["members"])

@router.post("")
def invite_member(
    membership = Depends(require_tenant_admin),
):
    return {"status": "invited", "tenant_id": membership.tenant_id}

Con esta estructura, el router expresa claramente la intención de que “se requiere permiso de administrador”.


8. Implementación básica de ABAC: decidir según propiedad y atributos del recurso

Ahora pasemos a un caso donde RBAC por sí solo no es suficiente.
Por ejemplo: incluso dentro del mismo tenant, los miembros solo deberían poder editar los proyectos que ellos mismos crearon.

8.1 Ejemplo de modelo de recurso

# app/models/project.py
from pydantic import BaseModel
from typing import Literal

class Project(BaseModel):
    id: int
    tenant_id: int
    owner_id: int
    name: str
    visibility: Literal["private", "public"] = "private"

8.2 Dependencia para obtener el recurso objetivo

# app/deps/project.py
from fastapi import Depends, HTTPException, status
from app.deps.tenant import get_current_tenant_id
from app.models.project import Project

def get_project(project_id: int, tenant_id: int = Depends(get_current_tenant_id)) -> Project:
    # En la práctica, obtenerlo de la BD filtrando también por tenant_id
    if project_id == 1 and tenant_id == 10:
        return Project(id=1, tenant_id=10, owner_id=1, name="Project A", visibility="private")

    raise HTTPException(
        status_code=status.HTTP_404_NOT_FOUND,
        detail="project not found",
    )

El punto clave es que el recurso ya se obtiene no solo por project_id, sino junto con tenant_id.

8.3 Decidir en una función de política

# app/policies/project_policy.py
from app.models.auth import CurrentUser
from app.models.membership import TenantMembership
from app.models.project import Project

def can_edit_project(
    current_user: CurrentUser,
    membership: TenantMembership,
    project: Project,
) -> bool:
    if membership.role in {"owner", "admin"}:
        return True

    if membership.role == "member" and project.owner_id == current_user.user_id:
        return True

    return False

8.4 Envolverlo como dependencia

# app/deps/permissions.py
from fastapi import Depends, HTTPException, status
from app.deps.auth import get_current_user
from app.deps.membership import get_current_membership
from app.deps.project import get_project
from app.policies.project_policy import can_edit_project

def require_project_edit_permission(
    current_user = Depends(get_current_user),
    membership = Depends(get_current_membership),
    project = Depends(get_project),
):
    if not can_edit_project(current_user, membership, project):
        raise HTTPException(
            status_code=status.HTTP_403_FORBIDDEN,
            detail="project edit is forbidden",
        )
    return project

De esta forma, la lógica de autorización en sí permanece como una función pura fácil de probar, mientras que la parte dependiente de FastAPI sigue siendo delgada.


9. Tipos comunes de atributos usados en ABAC

Diseñar ABAC se vuelve más fácil si primero organizas los tipos de atributos que suelen aparecer en la práctica.

9.1 Atributos del usuario

  • user_id
  • global_role
  • email_verified
  • is_active
  • department
  • plan

9.2 Atributos del tenant

  • tenant_id
  • plan (free / pro / enterprise)
  • status (active / suspended)
  • feature_flags

9.3 Atributos del recurso

  • owner_id
  • tenant_id
  • status (draft / published / archived)
  • visibility (private / internal / public)

9.4 Atributos del entorno

  • hora de acceso
  • dirección IP
  • región
  • tipo de cliente

No necesitas inspeccionar todos estos desde el inicio, pero ayuda mantener esta perspectiva: además del rol, ¿qué más debe comprobarse para que el acceso sea seguro?


10. Tratar las restricciones por plan también como parte de la autorización

En SaaS, las restricciones de funciones suelen estar ligadas a planes de suscripción.
A menudo es más limpio tratar esto también como una forma de ABAC.

10.1 Decisión basada en el plan del tenant

# app/models/tenant.py
from pydantic import BaseModel
from typing import Literal

class Tenant(BaseModel):
    id: int
    name: str
    plan: Literal["free", "pro", "enterprise"]
# app/policies/export_policy.py
from app.models.tenant import Tenant
from app.models.membership import TenantMembership

def can_export_csv(tenant: Tenant, membership: TenantMembership) -> bool:
    if tenant.plan not in {"pro", "enterprise"}:
        return False
    if membership.role not in {"owner", "admin", "member"}:
        return False
    return True

De esta forma, tanto el rol como el plan se manejan explícitamente.
Más adelante, cuando se añadan reglas como “solo los tenants enterprise obtienen retención extendida de logs de auditoría”, la lógica seguirá siendo más fácil de razonar.


11. No pongas demasiado en los routers: centraliza las políticas en una capa de servicio o módulo dedicado

Una vez que empiezas a escribir autorización como if crudos en cada router, el código se vuelve muy difícil de leer unos meses después.
Una división recomendada es la siguiente:

  • Router
    • encadena funciones de dependencia
  • Funciones de política
    • expresan reglas de autorización
  • Capa de servicio
    • maneja reglas de negocio importantes junto con la autorización

Por ejemplo, “publicar un proyecto” puede requerir no solo permiso de edición, sino también una regla de negocio como “el proyecto no debe estar ya archivado”.
En un caso así, pensar la autorización junto con la capa de servicio es más natural que manejarlo todo en el router.


12. Combinarlo con logs de auditoría: registrar también las acciones denegadas

Esto también se conecta con el tema anterior del diseño de logs de auditoría, pero los intentos de autorización denegados también son evidencia importante.
Especialmente eventos como los siguientes merecen auditoría:

  • Un usuario intentó acceder a datos pertenecientes a otro tenant
  • Un miembro general intentó una acción que requería privilegios de admin
  • Se llamó a una función no disponible en el plan actual
  • Una API key intentó acceder a un endpoint prohibido

Por ejemplo, cuando la autorización falla, podrías registrar un evento de auditoría como este:

detail={
    "reason": "role_not_allowed",
    "required": ["owner", "admin"],
    "actual": "viewer",
}

Registros así facilitan mucho el soporte al cliente y el monitoreo de seguridad.


13. Patrones comunes de fallo

13.1 Decidir todo usando solo un flag is_admin

Esto parece conveniente al principio, pero tiende a desmoronarse una vez que se introducen roles por tenant y comprobaciones de propiedad.

13.2 Obtener solo por resource_id sin comprobar tenant_id

Este es uno de los errores más peligrosos en sistemas multi-tenant.
Incluye siempre tenant_id en la condición de consulta.

13.3 Lógica de autorización dispersa por routers

Esto hace muy difícil ver el impacto de los cambios.
Es mejor mover la lógica a funciones de política y dependencias.

13.4 Sentirse satisfecho con el control de visibilidad solo en frontend

Ocultar un botón no importa si la API todavía puede llamarse directamente.
El control final de autorización siempre debe ocurrir en el backend.

13.5 No documentar el diseño de autorización

Si los nombres de roles y permisos existen solo en conversaciones orales, la implementación y la operación probablemente acabarán divergiendo.
Es mucho más seguro alinearlos mediante OpenAPI, documentación interna y terminología de la UI administrativa.


14. Estrategia de pruebas: proteger la autorización asumiendo que es fácil romperla

La autorización es un área que tiende a romperse cada vez que se añaden nuevas funciones.
Por eso es importante no solo escribir pruebas de casos exitosos, sino también escribir pruebas sólidas para casos que deben ser denegados.

14.1 Las pruebas mínimas que deberías querer

  • admin puede actualizar
  • viewer no puede actualizar
  • member solo puede actualizar lo que le pertenece
  • No se puede acceder a recursos de otro tenant
  • No se permite exportar en el plan free
  • Se registran logs de auditoría cuando la autorización falla

14.2 Ejemplo de prueba unitaria para una función de política

# tests/test_project_policy.py
from app.models.auth import CurrentUser
from app.models.membership import TenantMembership
from app.models.project import Project
from app.policies.project_policy import can_edit_project

def test_admin_can_edit_any_project():
    user = CurrentUser(user_id=1, email="a@example.com", tenant_ids=[10], active_tenant_id=10)
    membership = TenantMembership(user_id=1, tenant_id=10, role="admin")
    project = Project(id=1, tenant_id=10, owner_id=2, name="P", visibility="private")

    assert can_edit_project(user, membership, project) is True

def test_member_can_edit_own_project_only():
    user = CurrentUser(user_id=1, email="a@example.com", tenant_ids=[10], active_tenant_id=10)
    membership = TenantMembership(user_id=1, tenant_id=10, role="member")
    own_project = Project(id=1, tenant_id=10, owner_id=1, name="Own", visibility="private")
    other_project = Project(id=2, tenant_id=10, owner_id=2, name="Other", visibility="private")

    assert can_edit_project(user, membership, own_project) is True
    assert can_edit_project(user, membership, other_project) is False

Cuando la lógica central de autorización se mantiene como funciones puras como esta, se vuelve muy fácil probarla.


15. Pensar también en la alineación con OpenAPI y el frontend

La autorización no es solo un asunto de backend.
El frontend también necesita saber qué puede hacer el usuario.

Dos patrones comunes son:

  • Incluir roles o una lista de permisos en la respuesta de GET /me
  • Incluir información del plan y feature flags habilitados en una API de información del tenant

Por ejemplo:

{
  "user_id": 1,
  "tenant_id": 10,
  "role": "admin",
  "permissions": [
    "project.read",
    "project.create",
    "project.update",
    "member.invite"
  ]
}

Una respuesta así facilita mucho el control de UI en el frontend.
Sin embargo, esta información debe usarse solo como guía para el control de visualización. Las decisiones finales de autorización siempre deben tomarse en el backend.


16. Hoja de ruta de adopción

Desarrolladores individuales y personas en aprendizaje

  1. Empezar con RBAC
  2. Crear funciones de dependencia como require_admin
  3. Una vez que las comprobaciones de propiedad sean necesarias, moverlas a funciones de política
  4. Añadir pruebas 403 para las APIs principales

Ingenieros en equipos pequeños

  1. Crear una tabla de roles y permisos actuales
  2. Revisar la lógica de autorización escrita directamente en los routers
  3. Moverla poco a poco a dependencias y funciones de política
  4. Registrar razones de denegación en logs de auditoría
  5. Mejorar OpenAPI y las respuestas /me visibles para frontend

Equipos SaaS y startups

  1. Separar roles globales de roles a nivel de tenant
  2. Hacer que tenant_id sea una condición obligatoria en todas las rutas principales de acceso a datos
  3. Identificar los atributos ABAC necesarios, como propiedad, plan y estado
  4. Centralizar las políticas de autorización en el código
  5. Añadir pruebas de contrato y pruebas de permisos al CI para evitar cambios rompientes

Enlaces de referencia


Conclusión

  • RBAC es fácil para empezar y es un primer paso muy práctico para el diseño de autorización en FastAPI.
  • Sin embargo, una vez que entran en juego el multi-tenancy, las reglas de propiedad y los límites por plan, ABAC se vuelve necesario.
  • En FastAPI, una estructura limpia es llevar la autenticación, la resolución del tenant y la resolución del recurso a funciones de dependencia, y centralizar las decisiones reales de autorización en funciones de política. Esto mejora la claridad y la capacidad de prueba.
  • La autorización debe diseñarse como una responsabilidad del backend, no solo como control de visualización, y los casos que deben ser denegados también deben estar protegidos por pruebas.
  • En lugar de intentar construir el sistema perfecto desde el principio, es más realista establecer primero un patrón con RBAC y luego añadir ABAC donde haga falta.

Un siguiente artículo natural en esta secuencia sería algo como “Diseño de planes, facturación y restricciones de funciones para SaaS” o “Patrones de diseño para APIs administrativas internas construidas con FastAPI”.

Salir de la versión móvil