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,editoryviewerhace 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
DependsySecuritypara 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 todoeditor: puede crear, editar y publicar artículosviewer: 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
editorsolo puede editar los artículos que él mismo creó - Un
vieweraún puede ver contenido publicado - Incluso un
adminno 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_iddel artículo coincide con eluser_iddel usuario actual” - “Se permite exportar si el
plandel tenant esproo 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
viewerno puede acceder a APIs de actualización
- Un
- Usar ABAC para decidir las condiciones finas
- Un
editorsolo puede editar proyectos de su propio tenant
- Un
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_idactual
- resuelve el
- 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_rolerepresenta autoridad a nivel de sistema, como un superadmintenant_idses la lista de tenants a los que pertenece el usuarioactive_tenant_ides 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_idglobal_roleemail_verifiedis_activedepartmentplan
9.2 Atributos del tenant
tenant_idplan(free/pro/enterprise)status(active/suspended)feature_flags
9.3 Atributos del recurso
owner_idtenant_idstatus(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
adminpuede actualizarviewerno puede actualizarmembersolo 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
- Empezar con RBAC
- Crear funciones de dependencia como
require_admin - Una vez que las comprobaciones de propiedad sean necesarias, moverlas a funciones de política
- Añadir pruebas 403 para las APIs principales
Ingenieros en equipos pequeños
- Crear una tabla de roles y permisos actuales
- Revisar la lógica de autorización escrita directamente en los routers
- Moverla poco a poco a dependencias y funciones de política
- Registrar razones de denegación en logs de auditoría
- Mejorar OpenAPI y las respuestas
/mevisibles para frontend
Equipos SaaS y startups
- Separar roles globales de roles a nivel de tenant
- Hacer que
tenant_idsea una condición obligatoria en todas las rutas principales de acceso a datos - Identificar los atributos ABAC necesarios, como propiedad, plan y estado
- Centralizar las políticas de autorización en el código
- Añadir pruebas de contrato y pruebas de permisos al CI para evitar cambios rompientes
Enlaces de referencia
- Documentación de FastAPI
- FastAPI Security
- FastAPI Dependencies
- OWASP Authorization Cheat Sheet
- Guía NIST sobre ABAC (útil para entender el concepto)
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”.
