Saltar a contenido
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.

Leer la versión original en inglés

Autenticación

Usted protege la interfaz de administración implementando un único método:

async def authenticate(request) -> AdminUser | None

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:

request.state.admin_user

En caso de fallo, la solicitud se marca como anónima:

request.state.is_anonymous = True

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 devuelva None para que el framework redirija a next o al índice del panel, o devuelva una Response para redirigir a otro lugar.
  • En caso de fallo: Lance LoginFailed("message") para mostrar un error encima del formulario, o lance FormValidationError({"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.session durante login().
  • Busque el usuario en su base de datos.
  • Devuelva una instancia de AdminUser cuando el usuario exista y sea válido.
  • Devuelva None cuando 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:

  1. Redirija al usuario hacia el proveedor de identidad.
  2. El proveedor autentica al usuario.
  3. El proveedor redirige de vuelta a su aplicación.
  4. Su aplicación intercambia el callback por la identidad del usuario.
  5. 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

https://your-domain.com/admin/oauth/callback

Desarrollo local

http://localhost:8000/admin/oauth/callback

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:

/admin/oauth/callback

Una vez desplegado, se convierte en:

https://your-domain.com/admin/oauth/callback

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:

  1. Herede de AdminUser, una dataclass sencilla, para añadir una lista roles.
  2. Devuelva esa subclase desde el método authenticate() de su proveedor, poblada desde su almacén de usuarios.
  3. Lea request.state.admin_user.roles en 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:

provider = MyAuthProvider(allow_routes=["register"])

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?

  • Security: CSRF, claves secretas y qué protege el framework automáticamente.
  • Views: can_create, can_edit, can_delete y la lista completa de hooks de permisos.
  • Actions: is_action_allowed e is_row_action_allowed para acciones masivas y por fila.