Honor explicit allauth opt-in

This commit is contained in:
Nick Sweeting 2026-09-02 04:14:43 -07:00
parent a32145b251
commit c680aa58b2
No known key found for this signature in database
8 changed files with 106 additions and 46 deletions

View File

@ -20,7 +20,13 @@ def main() -> None:
"name": f"main/{path.stem}",
"paths": [test_path],
"paths_arg": test_path,
"extra": "ldap" if test_path.endswith("/test_auth_ldap.py") else "",
"extra": (
"ldap"
if test_path.endswith("/test_auth_ldap.py")
else "allauth"
if test_path.endswith("/test_allauth_integration.py")
else ""
),
"count": 1,
},
)

View File

@ -11,8 +11,10 @@ from admin_data_views.admin import (
from admin_data_views.admin import (
get_app_list as adv_get_app_list,
)
from django.conf import settings
from django.contrib import admin
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model, login as auth_login
from django.contrib.auth import REDIRECT_FIELD_NAME, get_user_model
from django.contrib.auth import login as auth_login
from django.contrib.auth.decorators import login_not_required
from django.contrib.auth.forms import UserCreationForm
from django.contrib.auth.views import LoginView
@ -31,7 +33,6 @@ from archivebox.core.routes_util import is_allowed_archivebox_redirect_url
if TYPE_CHECKING:
from admin_data_views.typing import AppDict
from django.http import HttpRequest
from django.template.response import TemplateResponse
from django.urls import URLPattern, URLResolver
@ -114,6 +115,7 @@ class ArchiveBoxAdmin(admin.AdminSite):
"app_path": request.get_full_path(),
"username": request.user.get_username(),
"first_admin_setup": first_admin_setup,
"allauth_enabled": settings.ALLAUTH_ENABLED,
}
if REDIRECT_FIELD_NAME not in request.GET and REDIRECT_FIELD_NAME not in request.POST:
context[REDIRECT_FIELD_NAME] = reverse("admin:index", current_app=self.name)

View File

@ -43,6 +43,7 @@ WSGI_APPLICATION = "archivebox.core.wsgi.application"
ASGI_APPLICATION = "archivebox.core.asgi.application"
ROOT_URLCONF = "archivebox.core.urls"
LOGIN_URL = "/accounts/login/"
LOGOUT_REDIRECT_URL = CONFIG.LOGOUT_REDIRECT_URL
PASSWORD_RESET_URL = "/accounts/password_reset/"
@ -151,16 +152,9 @@ try:
"email": CONFIG.LDAP_EMAIL_ATTR,
}
# Use custom LDAP backend that supports LDAP_CREATE_SUPERUSER
# Include allauth backend first if allauth is installed
try:
import allauth as _allauth_check # noqa: F401
_allauth_backend = ["allauth.account.auth_backends.AuthenticationBackend"]
except ImportError:
_allauth_backend = []
AUTHENTICATION_BACKENDS = _allauth_backend + [
# Use custom LDAP backend that supports LDAP_CREATE_SUPERUSER.
# The allauth block below prepends its backend when explicitly enabled.
AUTHENTICATION_BACKENDS = [
"archivebox.ldap.auth.ArchiveBoxLDAPBackend",
"django.contrib.auth.backends.RemoteUserBackend",
"django.contrib.auth.backends.ModelBackend",
@ -184,10 +178,16 @@ except ImportError:
################################################################################
### django-allauth Configuration
# Conditionally loaded if django-allauth is installed
# Installing an optional dependency must not silently change authentication.
# ALLAUTH_ENABLED is the single switch for apps, middleware, backend, and routes.
################################################################################
try:
import allauth # noqa: F401
ALLAUTH_ENABLED = CONFIG.ALLAUTH_ENABLED
if ALLAUTH_ENABLED:
try:
import allauth # noqa: F401
except ImportError as err:
raise ImportError("ALLAUTH_ENABLED=True requires the archivebox[allauth] optional dependency") from err
INSTALLED_APPS += [
"archivebox.auth",
@ -213,7 +213,7 @@ try:
"allauth.account.middleware.AccountMiddleware",
]
# Prepend allauth backend to the list (only if not already present, e.g., from LDAP block)
# Prepend allauth so its email authentication runs before Django's username backend.
_allauth_auth_backend = "allauth.account.auth_backends.AuthenticationBackend"
if _allauth_auth_backend not in AUTHENTICATION_BACKENDS:
AUTHENTICATION_BACKENDS = [_allauth_auth_backend] + AUTHENTICATION_BACKENDS
@ -239,13 +239,9 @@ try:
# e.g. SOCIALACCOUNT_PROVIDERS='{"google": {"APP": {"client_id": "...", "secret": "..."}}}'
SOCIALACCOUNT_PROVIDERS = CONFIG.SOCIALACCOUNT_PROVIDERS
LOGIN_URL = "/accounts/login/"
LOGIN_REDIRECT_URL = "/admin/"
ACCOUNT_LOGOUT_REDIRECT_URL = LOGOUT_REDIRECT_URL or "/"
except ImportError:
pass
################################################################################
### Staticfile and Template Settings
################################################################################

View File

@ -27,6 +27,17 @@ from archivebox.opencode.views import opencode_proxy_view
from archivebox.progressmonitor.views import live_progress_view
from archivebox.search.views import public_snapshot_search_stream_view
if settings.ALLAUTH_ENABLED:
account_urlpatterns = [path("accounts/", include("allauth.urls"))]
else:
# Preserve ArchiveBox's built-in auth surface unless allauth was explicitly
# enabled. Optional package installation alone must not change login behavior.
account_urlpatterns = [
path("accounts/login/", RedirectView.as_view(url="/admin/login/", query_string=True)),
path("accounts/logout/", RedirectView.as_view(url="/admin/logout/", query_string=True)),
path("accounts/", include("django.contrib.auth.urls")),
]
urlpatterns = [
re_path(r"^static/(?P<path>.*)$", serve_static),
path("robots.txt", static.serve, {"document_root": CONSTANTS.STATIC_DIR, "path": "robots.txt"}),
@ -61,12 +72,8 @@ urlpatterns = [
path("admin/core/snapshot/add/", RedirectView.as_view(url="/add/")),
path("admin/core/snapshot/replay-auth/", SnapshotReplayAuthView.as_view(), name="snapshot-replay-auth"),
path("add/", AddView.as_view(), name="add"),
# ``query_string=True`` preserves the ``?next=…`` param that Django's
# auth/login mixins append, so e.g. ``UserPassesTestMixin`` redirecting
# an unauthenticated ``/add`` visitor to ``/accounts/login/?next=/add/``
# carries the ``next`` through to ``/admin/login/`` and lands them at
# ``/add/`` after login instead of the admin homepage.
path("accounts/", include("allauth.urls")),
# The disabled-mode login redirect preserves Django's ``?next=…`` query.
*account_urlpatterns,
path("progress.json", live_progress_view, name="live_progress"),
path("admin/", archivebox_admin.urls),
path("api/", include("archivebox.api.urls"), name="api"),

View File

@ -56,9 +56,11 @@
<label>&nbsp;</label><input type="submit" value="{% trans 'Create admin and continue' %}">
</div>
</form>
{% if allauth_enabled %}
<p><a href="/accounts/login/?next={{ next|default:'/admin/'|urlencode }}">Use a configured identity provider instead</a></p>
{% endif %}
</div>
{% else %}
{% elif allauth_enabled %}
<script>
// Keep Django's destination while handing authentication to allauth.
var next = "{{ next|default:"/admin/"|escapejs }}";
@ -67,5 +69,43 @@
<noscript>
<p><a href="/accounts/login/?next={{ next|default:'/admin/'|urlencode }}">Click here to log in</a></p>
</noscript>
{% else %}
{% if form.errors and not form.non_field_errors %}
<p class="errornote">
{% if form.errors.items|length == 1 %}{% trans "Please correct the error below." %}{% else %}{% trans "Please correct the errors below." %}{% endif %}
</p>
{% endif %}
{% if form.non_field_errors %}
{% for error in form.non_field_errors %}
<p class="errornote">{{ error }}</p>
{% endfor %}
{% endif %}
<div id="content-main">
{% if user.is_authenticated %}
<p class="errornote">
{% blocktrans trimmed %}
You are authenticated as {{ username }}, but are not authorized to
access this page. Would you like to login to a different account?
{% endblocktrans %}
</p>
{% endif %}
<form action="{{ app_path }}" method="post" id="login-form">{% csrf_token %}
<div class="form-row" style="gap: 0;">
{{ form.username.errors }}
{{ form.username.label_tag }} {{ form.username }}
</div>
<div class="form-row" style="gap: 0;">
{{ form.password.errors }}
{{ form.password.label_tag }} {{ form.password }}
<input type="hidden" name="next" value="{{ next }}">
</div>
<div class="submit-row" style="border: none;">
<label>&nbsp;</label><input type="submit" value="{% trans 'Log in' %}">
</div>
</form>
</div>
{% endif %}
{% endblock %}

View File

@ -1,26 +1,27 @@
"""archivebox/tests/conftest.py - Pytest fixtures for CLI tests."""
import os
import ctypes
import json
import os
import re
import secrets
import signal
import select
import shutil
import signal
import socket
import subprocess
import sys
import tempfile
import textwrap
import time
import shutil
import ctypes
from collections.abc import Callable
from datetime import timedelta
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
from importlib.util import find_spec
from pathlib import Path
from threading import Event, Thread
from types import SimpleNamespace
from typing import Any
from collections.abc import Callable
import psutil
import pytest
@ -35,6 +36,11 @@ SESSION_DATA_DIR = Path(
os.environ["ARCHIVEBOX_PYTEST_SESSION_DATA_DIR"] = str(SESSION_DATA_DIR)
os.environ["DATA_DIR"] = str(SESSION_DATA_DIR)
# When the optional dependency is present, exercise the opt-in auth stack in
# the normal test process. The disabled-by-default contract is verified in a
# clean subprocess by test_allauth_config.py.
if find_spec("allauth") is not None:
os.environ.setdefault("ALLAUTH_ENABLED", "true")
(SESSION_DATA_DIR / "tests").mkdir(parents=True, exist_ok=True)
os.chdir(SESSION_DATA_DIR)
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "archivebox.core.settings")
@ -1153,6 +1159,7 @@ def run_python_cwd(
input=script,
capture_output=True,
text=True,
check=False,
cwd=cwd,
env=base_env,
timeout=timeout,
@ -1355,9 +1362,10 @@ def get_http_response(
def make_latest_schedule_due(cwd: Path) -> None:
from django.utils import timezone
from archivebox.crawls.models import Crawl, CrawlSchedule
from archivebox.tests.test_orm_helpers import use_archivebox_db
from django.utils import timezone
with use_archivebox_db(cwd):
schedule = CrawlSchedule.objects.order_by("-created_at").select_related("template").first()
@ -1653,6 +1661,7 @@ def resolve_abxpkg_binary_env(
command,
capture_output=True,
text=True,
check=False,
env=command_env,
)
assert result.returncode == 0, result.stderr or result.stdout

View File

@ -34,18 +34,15 @@ import json
django.setup()
from django.conf import settings
from django.urls import Resolver404, resolve
from django.urls import resolve
try:
resolve('/accounts/login/')
accounts_route_enabled = True
except Resolver404:
accounts_route_enabled = False
login_match = resolve('/accounts/login/')
route_uses_allauth = login_match.url_name == 'account_login'
print(json.dumps({
'app_enabled': 'allauth.account' in settings.INSTALLED_APPS,
'backend_enabled': 'allauth.account.auth_backends.AuthenticationBackend' in settings.AUTHENTICATION_BACKENDS,
'route_enabled': accounts_route_enabled,
'route_uses_allauth': route_uses_allauth,
}))
""",
],
@ -56,7 +53,7 @@ print(json.dumps({
text=True,
)
state = json.loads(result.stdout.splitlines()[-1])
assert state == {"app_enabled": False, "backend_enabled": False, "route_enabled": False}
assert state == {"app_enabled": False, "backend_enabled": False, "route_uses_allauth": False}
def test_allauth_config_from_env(monkeypatch):

View File

@ -43,13 +43,11 @@ def test_admin_login_preserves_first_admin_setup(client):
@pytest.mark.django_db
def test_email_password_login_works(client, django_user_model, monkeypatch):
# Disable subdomain routing so AdminCookieIsolationMiddleware does not
# strip the session cookie from responses issued to the test client's
# default "testserver" host.
from archivebox.config.common import get_config
from archivebox.core.routes_util import get_admin_host
config = get_config()
monkeypatch.setattr(type(config), "USES_SUBDOMAIN_ROUTING", property(lambda self: False))
admin_host = get_admin_host(config=config)
user = django_user_model.objects.create_user(
username="logintest",
@ -70,6 +68,7 @@ def test_email_password_login_works(client, django_user_model, monkeypatch):
"login": "logintest@example.com",
"password": "testpassword123",
},
HTTP_HOST=admin_host,
follow=True,
)
assert response.status_code == 200
@ -78,6 +77,9 @@ def test_email_password_login_works(client, django_user_model, monkeypatch):
@pytest.mark.django_db
def test_registration_mode_approval_creates_inactive_user(client, settings, monkeypatch):
from archivebox.config.common import get_config
from archivebox.core.routes_util import get_admin_host
settings.ACCOUNT_ADAPTER = "archivebox.auth.adapters.ArchiveBoxAccountAdapter"
import archivebox.auth.adapters as m
@ -90,6 +92,7 @@ def test_registration_mode_approval_creates_inactive_user(client, settings, monk
"password1": "ComplexPass999!",
"password2": "ComplexPass999!",
},
HTTP_HOST=get_admin_host(config=get_config()),
)
from django.contrib.auth import get_user_model