Traducción automática supervisada
Este contenido se traduce mediante generación automática guiada por glosarios y guías de estilo revisados por personas. Dado que el texto no se revisa manualmente línea por línea, pueden producirse errores ocasionales o expresiones poco naturales.
En caso de cualquier discrepancia, la versión en inglés constituye la autoridad y la fuente de referencia.
Autenticación
Usted protege la interfaz de administración implementando un único método:
Cada solicitud protegida del panel pasa por este método. Cuando devuelve un AdminUser, la solicitud queda autenticada. Cuando devuelve None, la solicitud no está autenticada y el flujo de inicio de sesión o de OAuth que usted configuró toma el control.
En caso de éxito, el AdminUser devuelto se almacena en:
En caso de fallo, la solicitud se marca como anónima:
Las rutas públicas y parcialmente protegidas pueden entonces distinguir entre solicitudes autenticadas y no autenticadas sin iniciar un flujo de inicio de sesión.
Elija un proveedor de autenticación
| Proveedor | Cuándo usarlo | Qué debe implementar |
|---|---|---|
AuthProvider |
Usted quiere la página de inicio de sesión integrada y verifica las credenciales por su cuenta. | login(), logout(), authenticate() |
OAuthProvider |
Usted quiere un flujo de redirección OAuth2 u OIDC, como Auth0, Okta o Google. | redirect_to_provider(), handle_callback(), authenticate() |
Ambos heredan de BaseAuthProvider y comparten el mismo contrato. authenticate() se ejecuta en cada solicitud. Lo que devuelve se convierte en request.state.admin_user, y cuando devuelve None, el framework establece request.state.is_anonymous = True.
AuthProvider: página de inicio de sesión integrada
Use este proveedor cuando quiera que el framework renderice y gestione el formulario de inicio de sesión mientras usted verifica las credenciales. El framework se encarga de la plantilla, del manejo del POST y de la redirección.
Aquí tiene un ejemplo completo:
from sqlalchemy import create_engine
from starlette.applications import Starlette
from starlette.middleware import Middleware
from starlette.middleware.sessions import SessionMiddleware
from starlette.requests import Request
from starlette_admin.auth import AdminUser, AuthProvider, LoginFailed
from starlette_admin.contrib.sqla import Admin
SECRET = "change-me-in-production"
# Demo user store: replace with a real lookup against your database
USERS = {"admin": {"name": "Administrator", "password": "password"}}
class MyAuthProvider(AuthProvider):
async def login(
self, username: str, password: str, remember_me: bool, request: Request
) -> None:
user = USERS.get(username)
if user and password == user["password"]:
request.session["username"] = username
return
raise LoginFailed("Invalid username or password")
async def authenticate(self, request: Request) -> AdminUser | None:
username = request.session.get("username")
user = USERS.get(username)
if user:
return AdminUser(username=user["name"])
return None
async def logout(self, request: Request) -> None:
request.session.clear()
engine = create_engine("sqlite:///admin.sqlite")
app = Starlette(middleware=[Middleware(SessionMiddleware, secret_key=SECRET)])
admin = Admin(
engine, title="My Admin", auth_provider=MyAuthProvider(), secret_key=SECRET
)
admin.mount_to(app)
Métodos obligatorios
Un AuthProvider implementa tres métodos. Aquí es necesario SessionMiddleware, ya que persiste el estado de sesión iniciada del usuario entre solicitudes.
login()
Este método gestiona el envío del formulario. Recibe el username, el password, un booleano remember_me y el request actual.
- En caso de éxito: Escriba un identificador, como un ID de usuario o un nombre de usuario, en
request.session. Luego devuelvaNonepara que el framework redirija anexto al índice del panel, o devuelva unaResponsepara redirigir a otro lugar. - En caso de fallo: Lance
LoginFailed("message")para mostrar un error encima del formulario, o lanceFormValidationError({"username": "..."})para marcar un campo específico como inválido.
authenticate()
Este método se ejecuta en cada solicitud a una ruta protegida del panel. Recibe el objeto request.
- Lea el identificador que guardó en
request.sessiondurantelogin(). - Busque el usuario en su base de datos.
- Devuelva una instancia de
AdminUsercuando el usuario exista y sea válido. - Devuelva
Nonecuando el usuario no exista o no haya iniciado sesión.
logout()
Este método gestiona el cierre de sesión. Recibe el objeto request, y usted debe borrar los datos del usuario de request.session para revocar el acceso. Devuelva None para la redirección predeterminada al índice del panel, o devuelva una Response para redirigir a otro lugar.
OAuthProvider: flujo de redirección OAuth2/OIDC
Use OAuthProvider cuando delegue la autenticación en un proveedor de identidad externo como Auth0, Okta, Google o Microsoft Entra ID.
AuthProvider gestiona un formulario de usuario y contraseña dentro del panel. OAuthProvider, en cambio, utiliza un flujo basado en redirecciones:
- Redirija al usuario hacia el proveedor de identidad.
- El proveedor autentica al usuario.
- El proveedor redirige de vuelta a su aplicación.
- Su aplicación intercambia el callback por la identidad del usuario.
authenticate()restaura al usuario desde la sesión.
Configuración de la URL de callback (obligatoria)
Antes de implementar OAuthProvider, registre la URL de callback de su aplicación en el panel de su proveedor de identidad. Por seguridad, los proveedores de OAuth solo redirigen a URLs previamente aprobadas.
Ejemplo de URL de callback
Desarrollo local
Important
Su URL de callback exacta depende de su configuración. Se construye a partir del route_name que usted usa al montar Admin, más el callback_path del proveedor.
La configuración predeterminada usa:
route_name="admin"callback_path="oauth/callback"
lo cual produce esta URL de callback:
Una vez desplegado, se convierte en:
Si cambia el prefijo de montaje o la ruta de callback del proveedor, la URL cambia con ellos, y usted deberá actualizarla en la configuración de su proveedor de OAuth.
Métodos obligatorios
Un OAuthProvider usa el mismo patrón basado en sesión que AuthProvider, pero divide el inicio de sesión en una redirección y un callback.
redirect_to_provider()
Este método inicia el flujo de OAuth. Recibe el request y un callback_url generado, y debe devolver una Response que redirija el navegador del usuario hacia su proveedor de identidad.
handle_callback()
Este método se ejecuta cuando el navegador regresa del proveedor con un código de autorización. Recibe el request. Intercambie el código por un token de acceso, obtenga el perfil del usuario y almacene su identidad en request.session.
authenticate()
Al igual que con AuthProvider, este método lee lo que handle_callback() almacenó en la sesión. Devuelva un AdminUser cuando la sesión contenga datos válidos del usuario, o None cuando no los contenga.
logout()
Borre los datos de la sesión. Para cerrar también la sesión del usuario en el proveedor de identidad (cierre de sesión RP-initiated de OIDC), sobrescriba este método y devuelva una Response de redirección que apunte al endpoint de fin de sesión del proveedor en lugar de devolver None.
Aquí tiene un ejemplo completo:
import os
from authlib.integrations.starlette_client import OAuth
from starlette.datastructures import URL
from starlette.requests import Request
from starlette.responses import RedirectResponse, Response
from starlette.status import HTTP_303_SEE_OTHER
from starlette_admin.auth import AdminUser, OAuthProvider
AUTH0_CLIENT_ID = os.getenv("AUTH0_CLIENT_ID", "your-auth0-client-id")
AUTH0_CLIENT_SECRET = os.getenv("AUTH0_CLIENT_SECRET", "your-auth0-client-secret")
AUTH0_DOMAIN = os.getenv("AUTH0_DOMAIN", "your-auth0-domain")
oauth = OAuth()
oauth.register(
"auth0",
client_id=AUTH0_CLIENT_ID,
client_secret=AUTH0_CLIENT_SECRET,
client_kwargs={
"scope": "openid profile email",
},
server_metadata_url=f"https://{AUTH0_DOMAIN}/.well-known/openid-configuration",
)
class Auth0Provider(OAuthProvider):
async def redirect_to_provider(
self, request: Request, callback_url: str
) -> Response:
client = oauth.create_client("auth0")
return await client.authorize_redirect(request, callback_url)
async def handle_callback(self, request: Request) -> None:
client = oauth.create_client("auth0")
token = await client.authorize_access_token(request)
request.session["user"] = dict(token["userinfo"])
async def authenticate(self, request: Request) -> AdminUser | None:
user = request.session.get("user")
if user:
return AdminUser(username=user["name"], photo_url=user.get("picture"))
return None
async def logout(self, request: Request) -> None:
request.session.clear()
client = oauth.create_client("auth0")
metadata = await client.load_server_metadata()
end_session_endpoint = metadata.get("end_session_endpoint")
if end_session_endpoint:
logout_url = str(
URL(end_session_endpoint).include_query_params(
post_logout_redirect_uri="https://www.google.com/",
client_id=AUTH0_CLIENT_ID,
)
)
return RedirectResponse(logout_url, status_code=HTTP_303_SEE_OTHER)
return None
SECRET_KEY = "change-me"
engine = create_engine("sqlite:///admin.sqlite")
app = Starlette()
# required because Auth0Provider's handle_callback/authenticate methods use request.session
app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY)
admin = Admin(
engine, title="My Admin", auth_provider=Auth0Provider(), secret_key=SECRET_KEY
)
admin.mount_to(app)
Registre el proveedor
Después de implementar un proveedor de autenticación, adjúntelo a la instancia de Admin.
admin = Admin(
engine, title="My Admin", auth_provider=MyAuthProvider(), secret_key="..."
)
admin.mount_to(app)
Esa única línea constituye toda la integración. Admin monta el AuthMiddleware del proveedor antes de cada ruta del panel y añade las rutas del proveedor (inicio de sesión, cierre de sesión y el callback de OAuthProvider) dentro del prefijo del panel.
Comprobaciones de permisos
La autenticación responde a «¿Quién es?». Los permisos responden a «¿Qué puede hacer?». Los permisos pertenecen a la vista. El control de acceso basado en roles consta de tres pasos:
- Herede de
AdminUser, una dataclass sencilla, para añadir una listaroles. - Devuelva esa subclase desde el método
authenticate()de su proveedor, poblada desde su almacén de usuarios. - Lea
request.state.admin_user.rolesen los hooks de permisos de la vista.
from dataclasses import dataclass, field
from typing import Any
from starlette.requests import Request
from starlette_admin import action, row_action
from starlette_admin.auth import AdminUser, AuthProvider, LoginFailed
from starlette_admin.contrib.sqla import ModelView
from starlette_admin.exceptions import ActionFailed
from starlette_admin.fields import BaseField
from starlette_admin.types import RequestAction
# Demo user store: replace with a real lookup against your database
USERS = {
"admin": {
"name": "Administrator",
"password": "password",
"roles": ["read", "create", "edit", "delete", "read_body", "publish"],
},
"editor": {
"name": "Editor",
"password": "password",
"roles": ["read", "create", "edit", "read_body", "publish"],
},
"viewer": {"name": "Viewer", "password": "password", "roles": ["read"]},
}
# Step 1: AdminUser is a plain dataclass, so subclassing it to add `roles` is
# the intended pattern rather than a workaround.
@dataclass
class MyAdminUser(AdminUser):
roles: list[str] = field(default_factory=list)
class MyAuthProvider(AuthProvider):
async def login(
self, username: str, password: str, remember_me: bool, request: Request
) -> None:
user = USERS.get(username)
if user and password == user["password"]:
request.session["username"] = username
return
raise LoginFailed("Invalid username or password")
# Step 2: authenticate() looks up the roles for the signed-in user and
# returns them on a MyAdminUser instead of a plain AdminUser.
async def authenticate(self, request: Request) -> AdminUser | None:
username = request.session.get("username")
user = USERS.get(username)
if user:
return MyAdminUser(username=user["name"], roles=user["roles"])
return None
async def logout(self, request: Request) -> None:
request.session.clear()
# Step 3: Every hook below reads request.state.admin_user.roles, which is
# populated only because authenticate() returned a MyAdminUser.
class ArticleView(ModelView):
def is_accessible(self, request: Request) -> bool:
return "read" in request.state.admin_user.roles # Hides the view entirely
def can_create(self, request: Request) -> bool:
return "create" in request.state.admin_user.roles
def can_edit(self, request: Request) -> bool:
return "edit" in request.state.admin_user.roles
def can_delete(self, request: Request) -> bool:
return "delete" in request.state.admin_user.roles
def can_access_field(
self, request: Request, field: BaseField, action: RequestAction | None = None
) -> bool:
if field.name == "body":
return "read_body" in request.state.admin_user.roles
return super().can_access_field(request, field, action)
async def is_action_allowed(self, request: Request, name: str) -> bool:
if name == "publish":
return "publish" in request.state.admin_user.roles
return await super().is_action_allowed(request, name)
Registre MyAuthProvider como cualquier otro proveedor, con admin = Admin(engine, auth_provider=MyAuthProvider(), secret_key=SECRET). Cada hook anterior tendrá entonces acceso a request.state.admin_user.roles.
is_accessible(), disponible en todas las BaseView, oculta toda la vista, incluida su entrada en la barra lateral. can_create, can_edit, can_delete, can_export, can_import y can_view_detail controlan operaciones individuales en una ModelView. can_access_field oculta campos específicos, e is_action_allowed e is_row_action_allowed restringen las acciones masivas y por fila. Cuando una acción por fila depende del registro en lugar del usuario, como ocultar publish en un artículo que ya está publicado, sobrescriba is_row_action_allowed_for_obj(request, name, obj) en su lugar. Este método recibe el objeto subyacente de la fila y recurre a is_row_action_allowed.
Para la referencia completa de la API, consulte Views y Actions. Una versión funcional completa con can_export, can_import y una acción por fila está disponible en examples/03-auth.
@login_not_required
Algunas rutas permanecen públicas incluso en un panel de administración bloqueado, como un formulario de autorregistro o un health check. Decore el endpoint, y AuthMiddleware dejará pasar la solicitud sin comprobar si hay un resultado válido de authenticate():
from starlette.requests import Request
from starlette.responses import RedirectResponse, Response
from starlette_admin import CustomView, route
from starlette_admin.auth import login_not_required
class AccountsView(CustomView):
menu_label = "Accounts"
path = "/accounts"
@route("/register", methods=["GET", "POST"], name="register")
@login_not_required
async def register(self, request: Request) -> Response:
if request.method == "GET":
return self.templates.TemplateResponse(
request=request, name="register.html", context={}
)
form = await request.form()
# Create the user account before granting access to the panel
await create_user(email=form["email"], password=form["password"])
return RedirectResponse(request.url_for("admin:login"), status_code=302)
Tanto @route como @login_not_required etiquetan la función con un atributo y la devuelven sin cambios, por lo que el orden en que los apile no importa.
allow_routes
allow_routes le ofrece la misma exención a nivel de nombre de ruta en lugar de a nivel de función. Úselo cuando no controle la definición del endpoint, o cuando quiera tener la lista de exenciones en un solo lugar:
La cadena es el nombre de la ruta: ya sea el nombre del método, o el valor que usted pasó al parámetro name= en @route, como name="register" en el ejemplo anterior. AuthMiddleware siempre permite "login" y "static", además de las rutas personalizadas que usted liste.
AdminUser
Lo que authenticate() devuelve puebla request.state.admin_user. La barra superior lee dos campos de él:
| Atributo | Tipo | Valor predeterminado | Descripción |
|---|---|---|---|
username |
str |
"Administrator" (traducible) |
El nombre mostrado en el menú de usuario de la barra superior. |
photo_url |
str | None |
None |
La URL de la imagen de avatar. Muestra un icono de marcador de posición cuando no está definida. |
AdminUser es una @dataclass sencilla, por lo que heredar de ella para transportar roles, un ID de inquilino o cualquier otra cosa que sus hooks de permisos necesiten es el patrón previsto. El ejemplo de MyAdminUser anterior lo muestra en la práctica.
¿Qué sigue?