mirror of
https://github.com/ArchiveBox/ArchiveBox.git
synced 2026-09-12 19:50:57 +05:00
Honor explicit allauth opt-in
This commit is contained in:
parent
a32145b251
commit
c680aa58b2
8
.github/scripts/discover_test_matrix.py
vendored
8
.github/scripts/discover_test_matrix.py
vendored
@ -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,
|
||||
},
|
||||
)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
################################################################################
|
||||
|
||||
@ -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"),
|
||||
|
||||
@ -56,9 +56,11 @@
|
||||
<label> </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> </label><input type="submit" value="{% trans 'Log in' %}">
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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):
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user