diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index e5a856e1327c..1772638748c3 100644 --- a/django/contrib/admin/options.py +++ b/django/contrib/admin/options.py @@ -2060,13 +2060,8 @@ def _changeform_view(self, request, object_id, form_url, extra_context): else: obj = self.get_object(request, unquote(object_id), to_field) - - if request.method == "POST": - if not self.has_change_permission(request, obj): - raise PermissionDenied - else: - if not self.has_view_or_change_permission(request, obj): - raise PermissionDenied + if not self.has_view_or_change_permission(request, obj): + raise PermissionDenied if obj is None: return self._get_obj_does_not_exist_redirect( @@ -2105,6 +2100,9 @@ def _changeform_view(self, request, object_id, form_url, extra_context): and "_continue" not in request.POST and "_addanother" not in request.POST ): + selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) + if len(selected) != 1 or selected[0] != str(obj.pk): + raise BadRequest queryset = self.model._default_manager.get_queryset() if response := self.response_action( request, queryset, action_location=ActionLocation.CHANGE_FORM @@ -2112,6 +2110,9 @@ def _changeform_view(self, request, object_id, form_url, extra_context): return response return HttpResponseRedirect(request.get_full_path()) + if not add and not self.has_change_permission(request, obj): + raise PermissionDenied + form = ModelForm(request.POST, request.FILES, instance=obj) formsets, inline_instances = self._create_formsets( request, diff --git a/django/contrib/admin/templates/admin/change_form.html b/django/contrib/admin/templates/admin/change_form.html index f7dac55e0d9f..988e92e163b8 100644 --- a/django/contrib/admin/templates/admin/change_form.html +++ b/django/contrib/admin/templates/admin/change_form.html @@ -43,7 +43,7 @@
{% if action_form and not is_popup %} {% change_form_admin_actions %} - + {% endif %} {% if is_popup %}{% endif %} {% if to_field %}{% endif %} diff --git a/tests/admin_views/test_actions.py b/tests/admin_views/test_actions.py index 32bd1266dcb1..dbed3a33b6f0 100644 --- a/tests/admin_views/test_actions.py +++ b/tests/admin_views/test_actions.py @@ -514,7 +514,10 @@ def test_popup_template_escaping(self): self.assertIn(r""obj\\"", output) -@override_settings(ROOT_URLCONF="admin_views.urls") +@override_settings( + ROOT_URLCONF="admin_views.urls", + MAILERS={"default": {"BACKEND": "django.core.mail.backends.locmem.EmailBackend"}}, +) class AdminActionsPermissionTests(TestCase): @classmethod def setUpTestData(cls): @@ -568,6 +571,32 @@ def test_model_admin_no_delete_permission_externalsubscriber(self): ) self.assertEqual(response.status_code, 403) + def test_minimum_permission(self): + permission = Permission.objects.get(codename="view_externalsubscriber") + self.user.user_permissions.add(permission) + es = ExternalSubscriber.objects.create( + name="John Doe", email="john@example.org" + ) + + cl_url = reverse("admin:admin_views_externalsubscriber_changelist") + cl_post = {ACTION_CHECKBOX_NAME: [es.pk], "action": "external_mail"} + cf_url = reverse("admin:admin_views_externalsubscriber_change", args=[es.pk]) + cf_post = {ACTION_CHECKBOX_NAME: [es.pk], "CHANGE_FORM-action": "external_mail"} + + for url, post_data in [(cl_url, cl_post), (cf_url, cf_post)]: + mail.outbox = [] + with self.subTest(url=url): + response = self.client.get(url) + self.assertContains( + response, + '", + html=True, + ) + response = self.client.post(url, post_data) + self.assertEqual(len(mail.outbox), 1) + self.assertEqual(response.status_code, 302) + @override_settings( ROOT_URLCONF="admin_views.urls", @@ -673,6 +702,30 @@ def test_run_action_permissions(self): self.assertEqual(response.status_code, 200) self.assertEqual(response.content, b"OK") + def test_action_changeform_cannot_target_different_objects(self): + changeform_url = reverse( + "admin:admin_views_externalsubscriber_change", args=[self.s1.pk] + ) + external_subscriber = ExternalSubscriber.objects.create( + name="Jane Austen", email="jane@example.org" + ) + for invalid_checkbox_value in [ + [external_subscriber.pk], + [self.s1.pk, external_subscriber.pk], + [], + ]: + with self.subTest(invalid_checkbox_value=invalid_checkbox_value): + response = self.client.post( + changeform_url, + { + "CHANGE_FORM-action": "external_mail", + ACTION_CHECKBOX_NAME: invalid_checkbox_value, + "index": 0, + }, + ) + self.assertEqual(len(mail.outbox), 0) + self.assertEqual(response.status_code, 400) + def test_select_across_ignored(self): ExternalSubscriber.objects.create(name="Bob Bobson", email="bob@example.org") change_url = reverse(