Concepto clave
La autorización basada en roles y permisos es un mecanismo de control de acceso que define qué puede hacer un usuario autenticado en tu API. A diferencia de la autenticación (que verifica quién es el usuario), la autorización determina qué acciones puede realizar ese usuario dentro del sistema.
Imagina un edificio corporativo: la autenticación es tu tarjeta de identificación que te permite entrar al edificio, mientras que la autorización son los permisos específicos que tienes dentro—puedes acceder al piso 10 (tu departamento) pero no al piso 15 (dirección ejecutiva). En APIs, esto se traduce en roles como admin, editor, o lector, cada uno con permisos específicos sobre recursos como usuarios, artículos o configuraciones.
FastAPI no incluye un sistema de autorización incorporado, lo que te da flexibilidad para implementar soluciones personalizadas. El enfoque más común combina dependencias para verificar permisos y decoradores para aplicar restricciones a endpoints específicos. Esta separación clara entre autenticación y autorización es crucial para mantener APIs seguras y mantenibles.
Cómo funciona en la práctica
Implementar autorización en FastAPI sigue un flujo estructurado que comienza después de que el usuario se autentica. Primero, defines roles y permisos en tu base de datos o configuración. Luego, creas funciones de verificación que se ejecutan antes de que el endpoint procese la solicitud.
Paso 1: Define una estructura de permisos. Por ejemplo, permisos como "users:read", "users:write", "articles:delete". Los roles agrupan estos permisos: un rol admin podría tener todos los permisos, mientras que un editor solo tiene permisos relacionados con artículos.
Paso 2: Crea una dependencia de FastAPI que verifique los permisos. Esta dependencia recibe el usuario autenticado (por ejemplo, desde un token JWT) y el permiso requerido para el endpoint. Si el usuario tiene el permiso, la dependencia permite continuar; de lo contrario, lanza una excepción HTTP 403.
Paso 3: Aplica la dependencia a tus endpoints usando el parámetro dependencies en los decoradores de ruta. Esto centraliza la lógica de autorización y hace que tu código sea más limpio y testeable.
Código en acción
Aquí tienes un ejemplo completo de implementación de autorización basada en roles en FastAPI. Comenzamos con una versión básica y luego la mejoramos.
Antes: Sin autorización, cualquier usuario autenticado puede acceder a todos los endpoints.
from fastapi import FastAPI, Depends, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
app = FastAPI()
security = HTTPBearer()
class User(BaseModel):
id: int
username: str
role: str
def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
# Simulación: decodificar token JWT y obtener usuario
# En producción, usarías una librería como PyJWT
token = credentials.credentials
if token == "admin_token":
return User(id=1, username="admin", role="admin")
elif token == "editor_token":
return User(id=2, username="editor", role="editor")
else:
raise HTTPException(status_code=401, detail="Invalid token")
@app.get("/users")
async def get_users(user: User = Depends(get_current_user)):
return {"message": "Lista de usuarios"}
@app.delete("/users/{user_id}")
async def delete_user(user_id: int, user: User = Depends(get_current_user)):
return {"message": f"Usuario {user_id} eliminado"}Después: Con autorización basada en roles, solo usuarios con permisos específicos pueden realizar acciones.
from fastapi import FastAPI, Depends, HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel
from enum import Enum
app = FastAPI()
security = HTTPBearer()
class Role(str, Enum):
ADMIN = "admin"
EDITOR = "editor"
VIEWER = "viewer"
class Permission(str, Enum):
USERS_READ = "users:read"
USERS_WRITE = "users:write"
ARTICLES_READ = "articles:read"
ARTICLES_WRITE = "articles:write"
# Mapeo de roles a permisos (en producción, almacenar en BD)
ROLE_PERMISSIONS = {
Role.ADMIN: [Permission.USERS_READ, Permission.USERS_WRITE, Permission.ARTICLES_READ, Permission.ARTICLES_WRITE],
Role.EDITOR: [Permission.ARTICLES_READ, Permission.ARTICLES_WRITE],
Role.VIEWER: [Permission.ARTICLES_READ]
}
class User(BaseModel):
id: int
username: str
role: Role
def get_current_user(credentials: HTTPAuthorizationCredentials = Security(security)):
token = credentials.credentials
if token == "admin_token":
return User(id=1, username="admin", role=Role.ADMIN)
elif token == "editor_token":
return User(id=2, username="editor", role=Role.EDITOR)
elif token == "viewer_token":
return User(id=3, username="viewer", role=Role.VIEWER)
else:
raise HTTPException(status_code=401, detail="Invalid token")
def require_permission(required_permission: Permission):
def permission_dependency(current_user: User = Depends(get_current_user)):
user_permissions = ROLE_PERMISSIONS.get(current_user.role, [])
if required_permission not in user_permissions:
raise HTTPException(status_code=403, detail="Insufficient permissions")
return current_user
return permission_dependency
@app.get("/users", dependencies=[Depends(require_permission(Permission.USERS_READ))])
async def get_users():
return {"message": "Lista de usuarios"}
@app.delete("/users/{user_id}", dependencies=[Depends(require_permission(Permission.USERS_WRITE))])
async def delete_user(user_id: int):
return {"message": f"Usuario {user_id} eliminado"}
@app.get("/articles", dependencies=[Depends(require_permission(Permission.ARTICLES_READ))])
async def get_articles():
return {"message": "Lista de articulos"}Errores comunes
- Mezclar autenticación y autorización en una sola función: Esto dificulta el mantenimiento y las pruebas. Siempre separa la lógica: una dependencia para obtener el usuario y otra para verificar permisos.
- Almacenar permisos en el cliente (como en tokens JWT): Los permisos pueden cambiar, y si están en el token, necesitas reemitirlo. Es mejor almacenar roles en el token y permisos en el servidor.
- No usar enums para roles y permisos: Usar strings directamente (como
"admin") causa errores tipográficos. Los enums de Python proporcionan seguridad de tipos y autocompletado. - Falta de logging en fallos de autorización: Registra los intentos de acceso no autorizados para detectar ataques o configuraciones erróneas. Usa
logging.warningen tu dependencia de permisos. - Autorización solo a nivel de endpoint: En APIs complejas, necesitas autorización a nivel de recurso (por ejemplo, un usuario solo puede editar sus propios artículos). Combina permisos con verificación de propiedad en la lógica de negocio.
Checklist de dominio
- He definido roles y permisos usando enums de Python para evitar errores.
- He creado una dependencia reutilizable que verifica permisos basados en el rol del usuario.
- He aplicado la dependencia a endpoints específicos usando el parámetro
dependenciesen FastAPI. - He separado claramente la autenticación (obtener usuario) de la autorización (verificar permisos).
- He implementado logging para registrar intentos de acceso no autorizados.
- He probado la autorización con diferentes roles y permisos en un entorno de desarrollo.
- He considerado la escalabilidad: los permisos se almacenan en el servidor, no en tokens del cliente.
Implementa un sistema de autorización con permisos granulares y middleware de logging
En este ejercicio, extenderás el ejemplo de código proporcionado para crear un sistema de autorización más robusto que incluya permisos granulares y middleware para logging de seguridad.
- Configura el proyecto: Crea un nuevo archivo Python (por ejemplo,
authorization_exercise.py) y copia el código del ejemplo "Después" de la lección. Asegúrate de tener FastAPI instalado (pip install fastapi). - Añade permisos granulares: Extiende el enum
Permissionpara incluir permisos más específicos, comoUSERS_DELETE,ARTICLES_PUBLISH, ySETTINGS_UPDATE. Actualiza el mapeoROLE_PERMISSIONSpara asignar estos nuevos permisos a los roles existentes (por ejemplo, soloADMINpuede tenerUSERS_DELETE). - Crea un endpoint protegido con nuevo permiso: Añade un nuevo endpoint
PUT /settingsque requiera el permisoSETTINGS_UPDATE. El endpoint debe aceptar un cuerpo JSON con una configuración y devolver un mensaje de confirmación. Aplica la dependenciarequire_permissioncon el permiso adecuado. - Implementa middleware de logging: Usa el sistema de middleware de FastAPI para registrar todas las solicitudes que fallen por autorización (código 403). Crea una función middleware que verifique el código de estado de la respuesta y, si es 403, registre un mensaje de advertencia con detalles como la ruta, el método HTTP, y el usuario (si está disponible). Usa el módulo
loggingde Python para esto. - Prueba la implementación: Ejecuta la aplicación con
uvicorn authorization_exercise:app --reload. Usa herramientas como curl o Postman para enviar solicitudes con diferentes tokens (admin_token, editor_token, viewer_token) a los endpoints y verifica que los permisos funcionen correctamente. Comprueba que el logging se active en intentos no autorizados.
- Usa el decorador @app.middleware("http") en FastAPI para crear middleware. La función middleware debe recibir request y call_next, y manejar la respuesta después de llamar a call_next.
- Para el logging, configura el módulo logging básico con logging.basicConfig(level=logging.INFO) y usa logging.warning() en el middleware cuando detectes un 403.
- En el endpoint de settings, define un modelo Pydantic para el cuerpo de la solicitud, como class SettingsUpdate(BaseModel): setting_name: str; value: str, y úsalo en el parámetro del endpoint.
Evalua tu comprension
Completa el quiz interactivo de arriba para ganar XP.