diff --git a/django/contrib/admin/widgets.py b/django/contrib/admin/widgets.py index 67eac083e793..7a0ccf42de32 100644 --- a/django/contrib/admin/widgets.py +++ b/django/contrib/admin/widgets.py @@ -28,6 +28,8 @@ class FilteredSelectMultiple(forms.SelectMultiple): catalog has been loaded in the page """ + use_fieldset = True + class Media: js = [ "admin/js/core.js", @@ -300,7 +302,7 @@ def __init__( self.can_view_related = supported and can_view_related # To check if the related object is registered with this AdminSite. self.admin_site = admin_site - self.use_fieldset = True + self.use_fieldset = widget.use_fieldset def __deepcopy__(self, memo): obj = copy.copy(self) diff --git a/django/contrib/auth/__init__.py b/django/contrib/auth/__init__.py index 2702c38aa4d3..21e6dc43d633 100644 --- a/django/contrib/auth/__init__.py +++ b/django/contrib/auth/__init__.py @@ -205,6 +205,8 @@ async def alogin(request, user, backend=None): await request.session.aset(SESSION_KEY, user._meta.pk.value_to_string(user)) await request.session.aset(BACKEND_SESSION_KEY, backend) await request.session.aset(HASH_SESSION_KEY, session_auth_hash) + if hasattr(request, "user"): + request.user = user if hasattr(request, "auser"): async def auser(): @@ -244,13 +246,21 @@ async def alogout(request): user = None await user_logged_out.asend(sender=user.__class__, request=request, user=user) await request.session.aflush() - if hasattr(request, "auser"): + + has_user = hasattr(request, "user") + has_auser = hasattr(request, "auser") + if has_user or has_auser: from django.contrib.auth.models import AnonymousUser - async def auser(): - return AnonymousUser() + anon = AnonymousUser() + if has_user: + request.user = anon + if has_auser: - request.auser = auser + async def auser(): + return anon + + request.auser = auser def get_user_model(): diff --git a/django/db/models/sql/query.py b/django/db/models/sql/query.py index 7a4cf843c1b1..8be560856b06 100644 --- a/django/db/models/sql/query.py +++ b/django/db/models/sql/query.py @@ -13,7 +13,7 @@ import sys import warnings from collections import Counter, namedtuple -from collections.abc import Iterator, Mapping +from collections.abc import Iterable, Iterator, Mapping from itertools import chain, count, product from string import ascii_uppercase @@ -1638,7 +1638,17 @@ def build_filter( ): lookup_class = targets[0].get_lookup("isnull") col = self._get_col(targets[0], join_info.targets[0], alias) - clause.add(lookup_class(col, False), AND) + # Use OR + IS NULL when RHS `in` values include None. + if ( + lookup_type == "in" + # Check containers (not strings or bytes). + and isinstance(condition.rhs, Iterable) + and not isinstance(condition.rhs, (str, bytes)) + and any(v is None for v in condition.rhs) + ): + clause.add(lookup_class(col, True), OR) + else: + clause.add(lookup_class(col, False), AND) # If someval is a nullable column, someval IS NOT NULL is # added. if isinstance(value, Col) and self.is_nullable(value.target): diff --git a/docs/howto/auth-remote-user.txt b/docs/howto/auth-remote-user.txt index 533f460e4ac4..fe48bb3dc86a 100644 --- a/docs/howto/auth-remote-user.txt +++ b/docs/howto/auth-remote-user.txt @@ -99,19 +99,26 @@ instead of :class:`django.contrib.auth.middleware.RemoteUserMiddleware`:: .. warning:: - Be very careful if using a ``RemoteUserMiddleware`` subclass with a custom - HTTP header. You must be sure that your front-end web server always sets or - strips that header based on the appropriate authentication checks, never - permitting an end-user to submit a fake (or "spoofed") header value. Since - the HTTP headers ``X-Auth-User`` and ``X-Auth_User`` (for example) both - normalize to the ``HTTP_X_AUTH_USER`` key in ``request.META``, you must - also check that your web server doesn't allow a spoofed header using + ``RemoteUserMiddleware`` must not be deployed in configurations where a + client can supply the header. You must be sure that your web server or + reverse proxy always sets or strips that header based on the appropriate + authentication checks, never permitting an end user to submit a fake (or + "spoofed") header value. In particular, ASGI deployments cannot be exposed + directly to the internet (that is, without a reverse proxy) when using this + middleware. + + Since the HTTP headers ``X-Auth-User`` and ``X-Auth_User`` (for example) + both normalize to the ``HTTP_X_AUTH_USER`` key in ``request.META``, you + must also check that your web server doesn't allow a spoofed header using underscores in place of dashes. - This warning doesn't apply to ``RemoteUserMiddleware`` in its default - configuration with ``header = 'REMOTE_USER'``, since a key that doesn't - start with ``HTTP_`` in ``request.META`` can only be set by your WSGI - or ASGI server, not directly from an HTTP request header. + Under WSGI, this warning doesn't apply to ``RemoteUserMiddleware`` in its + default configuration with ``header = "REMOTE_USER"``, since a key that + doesn't start with ``HTTP_`` in ``request.META`` can only be set by your + WSGI server, not directly from an HTTP request header. This warning *does* + apply by default on ASGI, because in the async path, the middleware + prepends ``HTTP_`` to the defined header name before looking it up in + ``request.META``. If you need more control, you can create your own authentication backend that inherits from :class:`~django.contrib.auth.backends.RemoteUserBackend` and diff --git a/docs/internals/security.txt b/docs/internals/security.txt index 5214bf0704a4..3dd811fcd0ee 100644 --- a/docs/internals/security.txt +++ b/docs/internals/security.txt @@ -43,6 +43,39 @@ the industry-standard 90 days. Confirmed vulnerabilities with a .. _our public Trac instance: https://code.djangoproject.com/query +.. _respecting-maintainer-time: + +Respecting maintainer time +-------------------------- + +Django's security team are volunteers. Please be mindful and respectful of +their time when submitting reports. Your initial report should give the team +enough to make a triage decision, no more. It should include: + +* A brief description of the issue and where in Django it occurs. + +* A minimal, working proof of concept (code snippet or reproduction steps). + +* The versions of Django and Python you tested against. + +* Optionally, a minimal patch with the mitigation for the issue. + +Please do not include severity scores (CVSS or otherwise), lengthy background +sections, multiple headers, or a determination of whether the issue constitutes +a vulnerability. The security team will make those assessments. Extensive +upfront analysis makes triage slower, not faster. If the team confirms the +issue is a valid vulnerability, they will follow up and welcome further detail +at that stage. + +If you have identified multiple potential issues, please wait for a triage +result on your initial report before submitting further ones. Exceptions can be +made for issues that are clearly and directly related to an already reported +finding. Feedback on an initial report is often relevant to subsequent ones, +and taking the time to read and incorporate it leads to better reports overall. + +The security team is not able to process large volumes of reports submitted in +a short period of time, and reports submitted in bulk may be put on hold. + Reporting guidelines -------------------- @@ -131,6 +164,12 @@ not been sanitized:: q = MyModel.objects.extra(select={"id": query}) return HttpResponse(q.values()) +Some HTTP headers must also be sanitized by a web server or fronting proxy +before they can be used, such as ``Remote-User`` and ``X-Forwarded-*``. For +instance, under ASGI, it is a deployment misconfiguration (rather than any flaw +in Django) for Django to be the direct HTTP endpoint when +:class:`~django.contrib.auth.middleware.RemoteUserMiddleware` is used. + Request headers and URLs must be under 8K bytes ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/ref/middleware.txt b/docs/ref/middleware.txt index a9638cf36e5c..e53c51039354 100644 --- a/docs/ref/middleware.txt +++ b/docs/ref/middleware.txt @@ -576,13 +576,14 @@ Customize the login URL or field name for authenticated views with the .. class:: RemoteUserMiddleware Middleware for utilizing web server provided authentication. See - :doc:`/howto/auth-remote-user` for usage details. + :doc:`/howto/auth-remote-user` for usage details, including security + considerations. .. class:: PersistentRemoteUserMiddleware Middleware for utilizing web server provided authentication when enabled only on the login page. See :ref:`persistent-remote-user-middleware-howto` - for usage details. + for usage details, including security considerations. CSRF protection middleware -------------------------- diff --git a/docs/releases/6.0.4.txt b/docs/releases/6.0.4.txt index a32a49dc93e0..1967edacc840 100644 --- a/docs/releases/6.0.4.txt +++ b/docs/releases/6.0.4.txt @@ -10,4 +10,11 @@ issues with severity "low", and several bugs in 6.0.3. Bugfixes ======== -* ... +* Fixed a regression in Django 6.0 where :func:`~django.contrib.auth.alogin` + and :func:`~django.contrib.auth.alogout` did not respectively set or clear + ``request.user`` if it had already been materialized (e.g., by sync + middleware) (:ticket:`37017`). + +* Fixed a regression in Django 6.0 in admin forms where + ``RelatedFieldWidgetWrapper`` incorrectly wrapped all widgets in a + ``
`` (:ticket:`36949`). diff --git a/tests/admin_views/tests.py b/tests/admin_views/tests.py index e01ab3b538aa..4359a3113501 100644 --- a/tests/admin_views/tests.py +++ b/tests/admin_views/tests.py @@ -7296,17 +7296,28 @@ def test_use_fieldset_fields_render(self): "Difficulty:", "Materials:", "Start datetime:", - "Categories:", ] url = reverse("admin:admin_views_course_change", args=(course.pk,)) self.selenium.get(self.live_server_url + url) fieldsets = self.selenium.find_elements( By.CSS_SELECTOR, "fieldset.aligned fieldset" ) + self.assertEqual(len(fieldsets), len(expected_legend_tags_text)) for index, fieldset in enumerate(fieldsets): legend = fieldset.find_element(By.TAG_NAME, "legend") self.assertEqual(legend.text, expected_legend_tags_text[index]) + # FilteredSelectMultiple uses
. + url = reverse("admin:admin_views_camelcaserelatedmodel_add") + self.selenium.get(self.live_server_url + url) + fieldsets = self.selenium.find_elements( + By.CSS_SELECTOR, "fieldset.aligned fieldset" + ) + self.assertEqual(len(fieldsets), 1) + for index, fieldset in enumerate(fieldsets): + legend = fieldset.find_element(By.TAG_NAME, "legend") + self.assertEqual(legend.text, "M2m:") + @screenshot_cases(["desktop_size", "mobile_size", "rtl", "dark", "high_contrast"]) def test_use_fieldset_with_grouped_fields(self): from selenium.webdriver.common.by import By diff --git a/tests/auth_tests/test_middleware.py b/tests/auth_tests/test_middleware.py index 5e106d40f731..894b49548b27 100644 --- a/tests/auth_tests/test_middleware.py +++ b/tests/auth_tests/test_middleware.py @@ -4,7 +4,7 @@ AuthenticationMiddleware, LoginRequiredMiddleware, ) -from django.contrib.auth.models import User +from django.contrib.auth.models import AnonymousUser, User from django.core.exceptions import ImproperlyConfigured from django.http import HttpRequest, HttpResponse from django.test import TestCase, modify_settings, override_settings @@ -77,6 +77,35 @@ async def test_auser_after_alogout(self): self.assertTrue(auser_second.is_anonymous) +class TestAsyncLoginLogoutAfterSyncMiddleware(TestCase): + @classmethod + def setUpTestData(cls): + cls.user = User.objects.create_user( + "test_user", "test@example.com", "test_password" + ) + cls.user2 = User.objects.create_user( + "test_user2", "test2@example.com", "test_password2" + ) + + def setUp(self): + self.middleware = AuthenticationMiddleware(lambda req: HttpResponse()) + self.client.force_login(self.user) + self.request = HttpRequest() + self.request.session = self.client.session + # Populate self.request.user. + self.middleware(self.request) + # .user is lazy, so materialize it by accessing an attribute. + self.request.user.is_authenticated + + async def test_user_after_alogin(self): + await alogin(self.request, self.user2) + self.assertEqual(self.request.user, self.user2) + + async def test_user_after_alogout(self): + await alogout(self.request) + self.assertEqual(self.request.user, AnonymousUser()) + + @override_settings(ROOT_URLCONF="auth_tests.urls") @modify_settings( MIDDLEWARE={"append": "django.contrib.auth.middleware.LoginRequiredMiddleware"} diff --git a/tests/composite_pk/test_filter.py b/tests/composite_pk/test_filter.py index c4b393d6ea84..fdaa323fd872 100644 --- a/tests/composite_pk/test_filter.py +++ b/tests/composite_pk/test_filter.py @@ -607,3 +607,30 @@ def setUp(self): ) self.enterContext(feature_patch_1) self.enterContext(feature_patch_2) + + +class CompositePKExcludeNoneTests(TestCase): + @classmethod + def setUpTestData(cls): + cls.tenant = Tenant.objects.create() + cls.user = User.objects.create( + tenant=cls.tenant, id=1, email="exclude@example.com" + ) + + def test_filter_pk_in_partial_none(self): + self.assertQuerySetEqual( + User.objects.filter(pk__in=[(self.user.pk[0], None)]), [] + ) + + def test_filter_pk_in_full_none(self): + self.assertQuerySetEqual(User.objects.filter(pk__in=[(None, None)]), []) + + def test_exclude_pk_in_partial_none(self): + self.assertQuerySetEqual( + User.objects.exclude(pk__in=[(self.user.pk[0], None)]), [self.user] + ) + + def test_exclude_pk_in_full_none(self): + self.assertQuerySetEqual( + User.objects.exclude(pk__in=[(None, None)]), [self.user] + ) diff --git a/tests/modeladmin/tests.py b/tests/modeladmin/tests.py index f50a557d0241..2516ca32071f 100644 --- a/tests/modeladmin/tests.py +++ b/tests/modeladmin/tests.py @@ -830,6 +830,55 @@ class BandAdmin(ModelAdmin): ["extra", "transport", "id", "DELETE", "main_band"], ) + def test_foreign_key_as_custom_widget(self): + class CustomSelectMultiple(forms.SelectMultiple): + def build_attrs(self, base_attrs, extra_attrs=None): + attrs = super().build_attrs(base_attrs, extra_attrs) + attrs["data-custom-widget"] = "true" + return attrs + + class ConcertAdmin(ModelAdmin): + formfield_overrides = { + models.ForeignKey: {"widget": CustomSelectMultiple}, + } + + cma = ConcertAdmin(Concert, self.site) + cmafa = cma.get_form(request) + expected = ( + '
" + ) + self.assertInHTML(expected, cmafa().render()) + + def test_foreign_key_as_custom_widget_with_fieldset(self): + class CustomSelectMultipleFieldset(forms.RadioSelect): + use_fieldset = True + + def build_attrs(self, base_attrs, extra_attrs=None): + attrs = super().build_attrs(base_attrs, extra_attrs) + attrs["use_fieldset"] = "true" + return attrs + + class ConcertAdmin(ModelAdmin): + formfield_overrides = { + models.ForeignKey: {"widget": CustomSelectMultipleFieldset}, + } + + cma = ConcertAdmin(Concert, self.site) + cmafa = cma.get_form(request) + expected = ( + '
Main band:
' + ) + self.assertInHTML(expected, cmafa().render()) + def test_log_actions(self): ma = ModelAdmin(Band, self.site) mock_request = MockRequest() diff --git a/tests/queries/tests.py b/tests/queries/tests.py index f2136df2433e..c00a78e2edfd 100644 --- a/tests/queries/tests.py +++ b/tests/queries/tests.py @@ -3536,13 +3536,7 @@ def test_null_in_exclude_qs(self): # into subquery above self.assertIs(inner_qs._result_cache, None) - @unittest.expectedFailure def test_col_not_in_list_containing_null(self): - """ - The following case is not handled properly because - SQL's COL NOT IN (list containing null) handling is too weird to - abstract away. - """ self.assertQuerySetEqual( NullableName.objects.exclude(name__in=[None]), ["i1"], attrgetter("name") )