Skip to content
4 changes: 3 additions & 1 deletion django/contrib/admin/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down
18 changes: 14 additions & 4 deletions django/contrib/auth/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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():
Expand Down Expand Up @@ -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():
Expand Down
14 changes: 12 additions & 2 deletions django/db/models/sql/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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):
Expand Down
29 changes: 18 additions & 11 deletions docs/howto/auth-remote-user.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
39 changes: 39 additions & 0 deletions docs/internals/security.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------

Expand Down Expand Up @@ -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
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

Expand Down
5 changes: 3 additions & 2 deletions docs/ref/middleware.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------------------------
Expand Down
9 changes: 8 additions & 1 deletion docs/releases/6.0.4.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
``<fieldset>`` (:ticket:`36949`).
13 changes: 12 additions & 1 deletion tests/admin_views/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 <fieldset>.
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
Expand Down
31 changes: 30 additions & 1 deletion tests/auth_tests/test_middleware.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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"}
Expand Down
27 changes: 27 additions & 0 deletions tests/composite_pk/test_filter.py
Original file line number Diff line number Diff line change
Expand Up @@ -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]
)
49 changes: 49 additions & 0 deletions tests/modeladmin/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
'<div><label for="id_main_band">Main band:</label><div '
'class="related-widget-wrapper" data-model-ref="band"><select '
'name="main_band" data-context="available-source" required '
'id="id_main_band" data-custom-widget="true" multiple>'
'<option value="">---------</option><option value="1">The Doors</option>'
"</select></div></div>"
)
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 = (
'<fieldset><legend>Main band:</legend><div class="related-widget-wrapper" '
'data-model-ref="band"><div id="id_main_band"><div><label '
'for="id_main_band_0"><input type="radio" name="main_band" value="1" '
'data-context="available-source" required id="id_main_band_0" '
'use_fieldset="true">The Doors</label></div></div></div></fieldset>'
)
self.assertInHTML(expected, cmafa().render())

def test_log_actions(self):
ma = ModelAdmin(Band, self.site)
mock_request = MockRequest()
Expand Down
6 changes: 0 additions & 6 deletions tests/queries/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
)
Expand Down
Loading