CVE-2024-35228: Insufficient Information (NVD-CWE-noinfo)
value: Wagtail is an open source content management system built on Django. Due to an improperly applied permission check in the `wagtail.contrib.settings` module, a user with access to the Wagtail admin and knowledge of the URL of the edit view for a settings model can access and update that setting, even when they have not been granted permission over the model. The vulnerability is not exploitable by an ordinary site visitor without access to the Wagtail admin. Patched versions have been released as Wagtail 6.0.5 and 6.1.2. Wagtail releases prior to 6.0 are unaffected. Users are advised to upgrade. Site owners who are unable to upgrade to a patched version can avoid the vulnerability in `ModelViewSet` by registering the model as a snippet instead. No workaround is available for `wagtail.contrib.settings`.
CWE description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
Published: 2024-05-30T19:15Z
Repository: https://github.com/wagtail/wagtail
Language: Python
Files changed: wagtail/contrib/settings/tests/generic/test_admin.py, wagtail/contrib/settings/tests/site_specific/test_admin.py, wagtail/contrib/settings/views.py
Commit date: 2024-05-30 14:32:36 +0100
Commit message: Restore permission check on settings EditView

Diff: diff --git a/wagtail/contrib/settings/tests/generic/test_admin.py b/wagtail/contrib/settings/tests/generic/test_admin.py
index 0eaee41e3c9..7052fcc4676 100644
--- a/wagtail/contrib/settings/tests/generic/test_admin.py
+++ b/wagtail/contrib/settings/tests/generic/test_admin.py
@@ -75,15 +75,10 @@ class BaseTestGenericSettingView(WagtailTestUtils, TestCase):
 
 
 class TestGenericSettingCreateView(BaseTestGenericSettingView):
     def setUp(self):
         self.user = self.login()
-        self.user.user_permissions.add(
-            Permission.objects.get(
-                content_type__app_label="wagtailadmin", codename="access_admin"
-            )
-        )
 
     def test_get_edit(self):
         response = self.get()
         self.assertEqual(response.status_code, 200)
 
@@ -111,15 +106,66 @@ class TestGenericSettingCreateView(BaseTestGenericSettingView):
     def test_file_upload_multipart(self):
         response = self.get(setting=FileGenericSetting)
         # Ensure the form supports file uploads
         self.assertContains(response, 'enctype="multipart/form-data"')
 
-    def test_create_restricted_field_without_permission(self):
+    def test_create_restricted_field_without_any_permission(self):
+        # User has no permissions over the setting model, only access to the admin
         self.user.is_superuser = False
         self.user.save()
+        self.user.user_permissions.add(
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+        )
 
         self.assertFalse(TestPermissionedGenericSetting.objects.exists())
+        # GET should redirect away with permission denied
+        response = self.get(setting=TestPermissionedGenericSetting)
+        self.assertRedirects(response, status_code=302, expected_url="/admin/")
+
+        # the GET might create a setting object, depending on when the permission check is done,
+        # so remove any created objects prior to testing the POST
+        TestPermissionedGenericSetting.objects.all().delete()
+
+        # POST should redirect away with permission denied
+        response = self.post(
+            post_data={"sensitive_email": "test@example.com", "title": "test"},
+            setting=TestPermissionedGenericSetting,
+        )
+        self.assertRedirects(response, status_code=302, expected_url="/admin/")
+
+        # The retrieved setting should contain none of the submitted data
+        setting = TestPermissionedGenericSetting.load()
+        self.assertEqual(setting.title, "")
+        self.assertEqual(setting.sensitive_email, "")
+
+    def test_create_restricted_field_without_field_permission(self):
+        # User has edit permission over the setting model, but not the sensitive_email field
+        self.user.is_superuser = False
+        self.user.save()
+        self.user.user_permissions.add(
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+            Permission.objects.get(
+                content_type__app_label="tests",
+                codename="change_testpermissionedgenericsetting",
+            ),
+        )
+
+        self.assertFalse(TestPermissionedGenericSetting.objects.exists())
+        # GET should provide a form with title but not sensitive_email
+        response = self.get(setting=TestPermissionedGenericSetting)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("title", list(response.context["form"].fields))
+        self.assertNotIn("sensitive_email", list(response.context["form"].fields))
+
+        # the GET creates a setting object, so remove any created objects prior to testing the POST
+        TestPermissionedGenericSetting.objects.all().delete()
+
+        # POST should allow the title to be set, but not the sensitive_email
         response = self.post(
             post_data={"sensitive_email": "test@example.com", "title": "test"},
             setting=TestPermissionedGenericSetting,
         )
         self.assertEqual(response.status_code, 302)
@@ -127,15 +173,35 @@ class TestGenericSettingCreateView(BaseTestGenericSettingView):
         settings = TestPermissionedGenericSetting.objects.get()
         self.assertEqual(settings.title, "test")
         self.assertEqual(settings.sensitive_email, "")
 
     def test_create_restricted_field(self):
+        # User has edit permission over the setting model, including the sensitive_email field
         self.user.is_superuser = False
         self.user.save()
         self.user.user_permissions.add(
-            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+            Permission.objects.get(
+                content_type__app_label="tests",
+                codename="change_testpermissionedgenericsetting",
+            ),
+            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting"),
         )
+
+        self.assertFalse(TestPermissionedGenericSetting.objects.exists())
+        # GET should provide a form with title and sensitive_email
+        response = self.get(setting=TestPermissionedGenericSetting)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("title", list(response.context["form"].fields))
+        self.assertIn("sensitive_email", list(response.context["form"].fields))
+
+        # the GET creates a setting object, so remove any created objects prior to testing the POST
+        TestPermissionedGenericSetting.objects.all().delete()
+
+        # POST should allow both title and sensitive_email to be set
         self.assertFalse(TestPermissionedGenericSetting.objects.exists())
         response = self.post(
             post_data={"sensitive_email": "test@example.com", "title": "test"},
             setting=TestPermissionedGenericSetting,
         )
@@ -151,15 +217,10 @@ class TestGenericSettingEditView(BaseTestGenericSettingView):
         self.test_setting = TestGenericSetting()
         self.test_setting.title = "Setting title"
         self.test_setting.save()
 
         self.user = self.login()
-        self.user.user_permissions.add(
-            Permission.objects.get(
-                content_type__app_label="wagtailadmin", codename="access_admin"
-            )
-        )
 
     def test_get_edit(self):
         response = self.get()
         self.assertEqual(response.status_code, 200)
 
@@ -204,52 +265,119 @@ class TestGenericSettingEditView(BaseTestGenericSettingView):
             status_code=302,
             expected_url=f"{url}{TestGenericSetting.objects.first().pk}/",
         )
 
     def test_edit_restricted_field(self):
+        # User has edit permission over the setting model, including the sensitive_email field
         test_setting = TestPermissionedGenericSetting()
         test_setting.sensitive_email = "test@example.com"
+        test_setting.title = "Old title"
         test_setting.save()
         self.user.is_superuser = False
         self.user.save()
 
         self.user.user_permissions.add(
-            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+            Permission.objects.get(
+                content_type__app_label="tests",
+                codename="change_testpermissionedgenericsetting",
+            ),
+            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting"),
         )
 
+        # GET should provide a form with title and sensitive_email
         response = self.get(setting=TestPermissionedGenericSetting)
         self.assertEqual(response.status_code, 200)
+        self.assertIn("title", list(response.context["form"].fields))
         self.assertIn("sensitive_email", list(response.context["form"].fields))
 
+        # POST should allow both title and sensitive_email to be set
         response = self.post(
             setting=TestPermissionedGenericSetting,
-            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
+            post_data={
+                "sensitive_email": "test-updated@example.com",
+                "title": "New title",
+            },
         )
         self.assertEqual(response.status_code, 302)
 
         test_setting.refresh_from_db()
         self.assertEqual(test_setting.sensitive_email, "test-updated@example.com")
+        self.assertEqual(test_setting.title, "New title")
 
-    def test_edit_restricted_field_without_permission(self):
+    def test_edit_restricted_field_without_field_permission(self):
+        # User has edit permission over the setting model, but not the sensitive_email field
         test_setting = TestPermissionedGenericSetting()
         test_setting.sensitive_email = "test@example.com"
+        test_setting.title = "Old title"
         test_setting.save()
         self.user.is_superuser = False
         self.user.save()
+        self.user.user_permissions.add(
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+            Permission.objects.get(
+                content_type__app_label="tests",
+                codename="change_testpermissionedgenericsetting",
+            ),
+        )
 
+        # GET should provide a form with title but not sensitive_email
         response = self.get(setting=TestPermissionedGenericSetting)
         self.assertEqual(response.status_code, 200)
+        self.assertIn("title", list(response.context["form"].fields))
         self.assertNotIn("sensitive_email", list(response.context["form"].fields))
 
+        # POST should allow the title to be set, but not the sensitive_email
         response = self.post(
             setting=TestPermissionedGenericSetting,
-            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
+            post_data={
+                "sensitive_email": "test-updated@example.com",
+                "title": "New title",
+            },
         )
         self.assertEqual(response.status_code, 302)
 
         test_setting.refresh_from_db()
         self.assertEqual(test_setting.sensitive_email, "test@example.com")
+        self.assertEqual(test_setting.title, "New title")
+
+    def test_edit_restricted_field_without_any_permission(self):
+        # User has no permissions over the setting model, only access to the admin
+        test_setting = TestPermissionedGenericSetting()
+        test_setting.sensitive_email = "test@example.com"
+        test_setting.title = "Old title"
+        test_setting.save()
+        self.user.is_superuser = False
+        self.user.save()
+        self.user.user_permissions.add(
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+        )
+
+        # GET should redirect away with permission denied
+        response = self.get(setting=TestPermissionedGenericSetting)
+        self.assertRedirects(response, status_code=302, expected_url="/admin/")
+
+        # POST should redirect away with permission denied
+        response = self.post(
+            setting=TestPermissionedGenericSetting,
+            post_data={
+                "sensitive_email": "test-updated@example.com",
+                "title": "new title",
+            },
+        )
+        self.assertRedirects(response, status_code=302, expected_url="/admin/")
+
+        # The retrieved setting should be unchanged
+        test_setting.refresh_from_db()
+        self.assertEqual(test_setting.sensitive_email, "test@example.com")
+        self.assertEqual(test_setting.title, "Old title")
 
 
 class TestAdminPermission(WagtailTestUtils, TestCase):
     def test_registered_permission(self):
         permission = Permission.objects.get_by_natural_key(
diff --git a/wagtail/contrib/settings/tests/site_specific/test_admin.py b/wagtail/contrib/settings/tests/site_specific/test_admin.py
index 8400799ddca..c3b98eac53d 100644
--- a/wagtail/contrib/settings/tests/site_specific/test_admin.py
+++ b/wagtail/contrib/settings/tests/site_specific/test_admin.py
@@ -71,15 +71,10 @@ class BaseTestSiteSettingView(WagtailTestUtils, TestCase):
 
 
 class TestSiteSettingCreateView(BaseTestSiteSettingView):
     def setUp(self):
         self.user = self.login()
-        self.user.user_permissions.add(
-            Permission.objects.get(
-                content_type__app_label="wagtailadmin", codename="access_admin"
-            )
-        )
 
     def test_get_edit(self):
         response = self.get()
         self.assertEqual(response.status_code, 200)
 
@@ -107,15 +102,65 @@ class TestSiteSettingCreateView(BaseTestSiteSettingView):
     def test_file_upload_multipart(self):
         response = self.get(setting=FileSiteSetting)
         # Ensure the form supports file uploads
         self.assertContains(response, 'enctype="multipart/form-data"')
 
-    def test_create_restricted_field_without_permission(self):
+    def test_create_restricted_field_without_any_permission(self):
+        # User has no permissions over the setting model, only access to the admin
+        self.user.is_superuser = False
+        self.user.save()
+        self.user.user_permissions.add(
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+        )
+
+        self.assertFalse(TestPermissionedSiteSetting.objects.exists())
+        # GET should redirect away with permission denied
+        response = self.get(setting=TestPermissionedSiteSetting)
+        self.assertRedirects(response, status_code=302, expected_url="/admin/")
+
+        # the GET might create a setting object, depending on when the permission check is done,
+        # so remove any created objects prior to testing the POST
+
+        # POST should redirect away with permission denied
+        response = self.post(
+            post_data={"sensitive_email": "test@example.com", "title": "test"},
+            setting=TestPermissionedSiteSetting,
+        )
+        self.assertRedirects(response, status_code=302, expected_url="/admin/")
+
+        # The retrieved setting should contain none of the submitted data
+        settings = TestPermissionedSiteSetting.for_site(Site.objects.get(pk=1))
+        self.assertEqual(settings.title, "")
+        self.assertEqual(settings.sensitive_email, "")
+
+    def test_create_restricted_field_without_field_permission(self):
+        # User has edit permission over the setting model, but not the sensitive_email field
         self.user.is_superuser = False
         self.user.save()
+        self.user.user_permissions.add(
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+            Permission.objects.get(
+                content_type__app_label="tests",
+                codename="change_testpermissionedsitesetting",
+            ),
+        )
 
         self.assertFalse(TestPermissionedSiteSetting.objects.exists())
+        # GET should provide a form with title but not sensitive_email
+        response = self.get(setting=TestPermissionedSiteSetting)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("title", list(response.context["form"].fields))
+        self.assertNotIn("sensitive_email", list(response.context["form"].fields))
+
+        # the GET creates a setting object, so remove any created objects prior to testing the POST
+        TestPermissionedSiteSetting.objects.all().delete()
+
+        # POST should allow the title to be set, but not the sensitive_email
         response = self.post(
             post_data={"sensitive_email": "test@example.com", "title": "test"},
             setting=TestPermissionedSiteSetting,
         )
         self.assertEqual(response.status_code, 302)
@@ -123,16 +168,34 @@ class TestSiteSettingCreateView(BaseTestSiteSettingView):
         settings = TestPermissionedSiteSetting.objects.get()
         self.assertEqual(settings.title, "test")
         self.assertEqual(settings.sensitive_email, "")
 
     def test_create_restricted_field(self):
+        # User has edit permission over the setting model, including the sensitive_email field
         self.user.is_superuser = False
         self.user.save()
         self.user.user_permissions.add(
-            Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+            Permission.objects.get(
+                content_type__app_label="tests",
+                codename="change_testpermissionedsitesetting",
+            ),
+            Permission.objects.get(codename="can_edit_sensitive_email_site_setting"),
         )
         self.assertFalse(TestPermissionedSiteSetting.objects.exists())
+        # GET should provide a form with title and sensitive_email
+        response = self.get(setting=TestPermissionedSiteSetting)
+        self.assertEqual(response.status_code, 200)
+        self.assertIn("title", list(response.context["form"].fields))
+        self.assertIn("sensitive_email", list(response.context["form"].fields))
+
+        # the GET creates a setting object, so remove any created objects prior to testing the POST
+        TestPermissionedSiteSetting.objects.all().delete()
+
+        # POST should allow both title and sensitive_email to be set
         response = self.post(
             post_data={"sensitive_email": "test@example.com", "title": "test"},
             setting=TestPermissionedSiteSetting,
         )
         self.assertEqual(response.status_code, 302)
@@ -151,15 +214,10 @@ class TestSiteSettingEditView(BaseTestSiteSettingView):
         self.test_setting.email = "initial@example.com"
         self.test_setting.site = self.default_site
         self.test_setting.save()
 
         self.user = self.login()
-        self.user.user_permissions.add(
-            Permission.objects.get(
-                content_type__app_label="wagtailadmin", codename="access_admin"
-            )
-        )
 
     def test_get_edit(self):
         response = self.get()
         self.assertEqual(response.status_code, 200)
 
@@ -209,54 +267,121 @@ class TestSiteSettingEditView(BaseTestSiteSettingView):
         url = reverse("wagtailsettings:edit", args=("tests", "testsitesetting"))
         response = self.client.get(url)
         self.assertRedirects(response, status_code=302, expected_url="/admin/")
 
     def test_edit_restricted_field(self):
+        # User has edit permission over the setting model, including the sensitive_email field
         test_setting = TestPermissionedSiteSetting()
+        test_setting.title = "Old title"
         test_setting.sensitive_email = "test@example.com"
         test_setting.site = self.default_site
         test_setting.save()
         self.user.is_superuser = False
         self.user.save()
-
         self.user.user_permissions.add(
-            Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+            Permission.objects.get(
+                content_type__app_label="tests",
+                codename="change_testpermissionedsitesetting",
+            ),
+            Permission.objects.get(codename="can_edit_sensitive_email_site_setting"),
         )
 
+        # GET should provide a form with title and sensitive_email
         response = self.get(setting=TestPermissionedSiteSetting)
         self.assertEqual(response.status_code, 200)
+        self.assertIn("title", list(response.context["form"].fields))
         self.assertIn("sensitive_email", list(response.context["form"].fields))
 
+        # POST should allow both title and sensitive_email to be set
         response = self.post(
             setting=TestPermissionedSiteSetting,
-            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
+            post_data={
+                "sensitive_email": "test-updated@example.com",
+                "title": "New title",
+            },
         )
         self.assertEqual(response.status_code, 302)
 
         test_setting.refresh_from_db()
         self.assertEqual(test_setting.sensitive_email, "test-updated@example.com")
+        self.assertEqual(test_setting.title, "New title")
 
-    def test_edit_restricted_field_without_permission(self):
+    def test_edit_restricted_field_without_field_permission(self):
+        # User has edit permission over the setting model, but not the sensitive_email field
         test_setting = TestPermissionedSiteSetting()
+        test_setting.title = "Old title"
         test_setting.sensitive_email = "test@example.com"
         test_setting.site = self.default_site
         test_setting.save()
         self.user.is_superuser = False
         self.user.save()
+        self.user.user_permissions.add(
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+            Permission.objects.get(
+                content_type__app_label="tests",
+                codename="change_testpermissionedsitesetting",
+            ),
+        )
 
+        # GET should provide a form with title but not sensitive_email
         response = self.get(setting=TestPermissionedSiteSetting)
         self.assertEqual(response.status_code, 200)
+        self.assertIn("title", list(response.context["form"].fields))
         self.assertNotIn("sensitive_email", list(response.context["form"].fields))
 
+        # POST should allow the title to be set, but not the sensitive_email
         response = self.post(
             setting=TestPermissionedSiteSetting,
-            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
+            post_data={
+                "sensitive_email": "test-updated@example.com",
+                "title": "New title",
+            },
         )
         self.assertEqual(response.status_code, 302)
 
         test_setting.refresh_from_db()
         self.assertEqual(test_setting.sensitive_email, "test@example.com")
+        self.assertEqual(test_setting.title, "New title")
+
+    def test_edit_restricted_field_without_any_permission(self):
+        # User has no permissions over the setting model, only access to the admin
+        test_setting = TestPermissionedSiteSetting()
+        test_setting.title = "Old title"
+        test_setting.sensitive_email = "test@example.com"
+        test_setting.site = self.default_site
+        test_setting.save()
+        self.user.is_superuser = False
+        self.user.save()
+        self.user.user_permissions.add(
+            Permission.objects.get(
+                content_type__app_label="wagtailadmin", codename="access_admin"
+            ),
+        )
+
+        # GET should redirect away with permission denied
+        response = self.get(setting=TestPermissionedSiteSetting)
+        self.assertRedirects(response, status_code=302, expected_url="/admin/")
+
+        # POST should redirect away with permission denied
+        response = self.post(
+            setting=TestPermissionedSiteSetting,
+            post_data={
+                "sensitive_email": "test-updated@example.com",
+                "title": "New title",
+            },
+        )
+        self.assertRedirects(response, status_code=302, expected_url="/admin/")
+
+        # The retrieved setting should be unchanged
+        test_setting.refresh_from_db()
+        self.assertEqual(test_setting.sensitive_email, "test@example.com")
+        self.assertEqual(test_setting.title, "Old title")
 
 
 @override_settings(
     ALLOWED_HOSTS=["testserver", "example.com", "noneoftheabove.example.com"]
 )
diff --git a/wagtail/contrib/settings/views.py b/wagtail/contrib/settings/views.py
index 869985d8512..78ede20362d 100644
--- a/wagtail/contrib/settings/views.py
+++ b/wagtail/contrib/settings/views.py
@@ -12,10 +12,11 @@ from wagtail.admin.panels import (
     ObjectList,
     extract_panel_definitions_from_model_class,
 )
 from wagtail.admin.views import generic
 from wagtail.models import Site
+from wagtail.permission_policies import ModelPermissionPolicy
 
 from .forms import SiteSwitchForm
 from .models import BaseGenericSetting, BaseSiteSetting
 from .registry import registry
 
@@ -79,15 +80,17 @@ def redirect_to_relevant_instance(request, app_name, model_name):
 
 
 class EditView(generic.EditView):
     template_name = "wagtailsettings/edit.html"
     error_message = gettext_lazy("The setting could not be saved due to errors.")
+    permission_required = "change"
 
     def setup(self, request, app_name, model_name, *args, **kwargs):
         self.app_name = app_name
         self.model_name = model_name
         self.model = get_model_from_url_params(app_name, model_name)
+        self.permission_policy = ModelPermissionPolicy(self.model)
         self.pk = kwargs.get(self.pk_url_kwarg)
         super().setup(request, app_name, model_name, *args, **kwargs)
 
     def get_object(self, queryset=None):
         self.site = None

Vulnerable code:         self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            )
        )
    def test_create_restricted_field_without_permission(self):
            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            )
        )
            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting")
            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
    def test_edit_restricted_field_without_permission(self):
            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            )
        )
    def test_create_restricted_field_without_permission(self):
            Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            )
        )

            Permission.objects.get(codename="can_edit_sensitive_email_site_setting")
            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
    def test_edit_restricted_field_without_permission(self):
            post_data={"sensitive_email": "test-updated@example.com", "title": "title"},
Fixed code:     def test_create_restricted_field_without_any_permission(self):
        # User has no permissions over the setting model, only access to the admin
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
        )
        # GET should redirect away with permission denied
        response = self.get(setting=TestPermissionedGenericSetting)
        self.assertRedirects(response, status_code=302, expected_url="/admin/")

        # the GET might create a setting object, depending on when the permission check is done,
        # so remove any created objects prior to testing the POST
        TestPermissionedGenericSetting.objects.all().delete()

        # POST should redirect away with permission denied
        response = self.post(
            post_data={"sensitive_email": "test@example.com", "title": "test"},
            setting=TestPermissionedGenericSetting,
        )
        self.assertRedirects(response, status_code=302, expected_url="/admin/")

        # The retrieved setting should contain none of the submitted data
        setting = TestPermissionedGenericSetting.load()
        self.assertEqual(setting.title, "")
        self.assertEqual(setting.sensitive_email, "")

    def test_create_restricted_field_without_field_permission(self):
        # User has edit permission over the setting model, but not the sensitive_email field
        self.user.is_superuser = False
        self.user.save()
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
            Permission.objects.get(
                content_type__app_label="tests",
                codename="change_testpermissionedgenericsetting",
            ),
        )

        self.assertFalse(TestPermissionedGenericSetting.objects.exists())
        # GET should provide a form with title but not sensitive_email
        response = self.get(setting=TestPermissionedGenericSetting)
        self.assertEqual(response.status_code, 200)
        self.assertIn("title", list(response.context["form"].fields))
        self.assertNotIn("sensitive_email", list(response.context["form"].fields))

        # the GET creates a setting object, so remove any created objects prior to testing the POST
        TestPermissionedGenericSetting.objects.all().delete()

        # POST should allow the title to be set, but not the sensitive_email
        # User has edit permission over the setting model, including the sensitive_email field
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
            Permission.objects.get(
                content_type__app_label="tests",
                codename="change_testpermissionedgenericsetting",
            ),
            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting"),

        self.assertFalse(TestPermissionedGenericSetting.objects.exists())
        # GET should provide a form with title and sensitive_email
        response = self.get(setting=TestPermissionedGenericSetting)
        self.assertEqual(response.status_code, 200)
        self.assertIn("title", list(response.context["form"].fields))
        self.assertIn("sensitive_email", list(response.context["form"].fields))

        # the GET creates a setting object, so remove any created objects prior to testing the POST
        TestPermissionedGenericSetting.objects.all().delete()

        # POST should allow both title and sensitive_email to be set
        # User has edit permission over the setting model, including the sensitive_email field
        test_setting.title = "Old title"
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
            Permission.objects.get(
                content_type__app_label="tests",
                codename="change_testpermissionedgenericsetting",
            ),
            Permission.objects.get(codename="can_edit_sensitive_email_generic_setting"),
        # GET should provide a form with title and sensitive_email
        self.assertIn("title", list(response.context["form"].fields))
        # POST should allow both title and sensitive_email to be set
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        self.assertEqual(test_setting.title, "New title")
    def test_edit_restricted_field_without_field_permission(self):
        # User has edit permission over the setting model, but not the sensitive_email field
        test_setting.title = "Old title"
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
            Permission.objects.get(
                content_type__app_label="tests",
                codename="change_testpermissionedgenericsetting",
            ),
        )
        # GET should provide a form with title but not sensitive_email
        self.assertIn("title", list(response.context["form"].fields))
        # POST should allow the title to be set, but not the sensitive_email
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        self.assertEqual(test_setting.title, "New title")

    def test_edit_restricted_field_without_any_permission(self):
        # User has no permissions over the setting model, only access to the admin
        test_setting = TestPermissionedGenericSetting()
        test_setting.sensitive_email = "test@example.com"
        test_setting.title = "Old title"
        test_setting.save()
        self.user.is_superuser = False
        self.user.save()
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
        )

        # GET should redirect away with permission denied
        response = self.get(setting=TestPermissionedGenericSetting)
        self.assertRedirects(response, status_code=302, expected_url="/admin/")

        # POST should redirect away with permission denied
        response = self.post(
            setting=TestPermissionedGenericSetting,
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "new title",
            },
        )
        self.assertRedirects(response, status_code=302, expected_url="/admin/")

        # The retrieved setting should be unchanged
        test_setting.refresh_from_db()
        self.assertEqual(test_setting.sensitive_email, "test@example.com")
        self.assertEqual(test_setting.title, "Old title")
    def test_create_restricted_field_without_any_permission(self):
        # User has no permissions over the setting model, only access to the admin
        self.user.is_superuser = False
        self.user.save()
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
        )

        self.assertFalse(TestPermissionedSiteSetting.objects.exists())
        # GET should redirect away with permission denied
        response = self.get(setting=TestPermissionedSiteSetting)
        self.assertRedirects(response, status_code=302, expected_url="/admin/")

        # the GET might create a setting object, depending on when the permission check is done,
        # so remove any created objects prior to testing the POST

        # POST should redirect away with permission denied
        response = self.post(
            post_data={"sensitive_email": "test@example.com", "title": "test"},
            setting=TestPermissionedSiteSetting,
        )
        self.assertRedirects(response, status_code=302, expected_url="/admin/")

        # The retrieved setting should contain none of the submitted data
        settings = TestPermissionedSiteSetting.for_site(Site.objects.get(pk=1))
        self.assertEqual(settings.title, "")
        self.assertEqual(settings.sensitive_email, "")

    def test_create_restricted_field_without_field_permission(self):
        # User has edit permission over the setting model, but not the sensitive_email field
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
            Permission.objects.get(
                content_type__app_label="tests",
                codename="change_testpermissionedsitesetting",
            ),
        )
        # GET should provide a form with title but not sensitive_email
        response = self.get(setting=TestPermissionedSiteSetting)
        self.assertEqual(response.status_code, 200)
        self.assertIn("title", list(response.context["form"].fields))
        self.assertNotIn("sensitive_email", list(response.context["form"].fields))

        # the GET creates a setting object, so remove any created objects prior to testing the POST
        TestPermissionedSiteSetting.objects.all().delete()

        # POST should allow the title to be set, but not the sensitive_email
        # User has edit permission over the setting model, including the sensitive_email field
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
            Permission.objects.get(
                content_type__app_label="tests",
                codename="change_testpermissionedsitesetting",
            ),
            Permission.objects.get(codename="can_edit_sensitive_email_site_setting"),
        # GET should provide a form with title and sensitive_email
        response = self.get(setting=TestPermissionedSiteSetting)
        self.assertEqual(response.status_code, 200)
        self.assertIn("title", list(response.context["form"].fields))
        self.assertIn("sensitive_email", list(response.context["form"].fields))

        # the GET creates a setting object, so remove any created objects prior to testing the POST
        TestPermissionedSiteSetting.objects.all().delete()

        # POST should allow both title and sensitive_email to be set
        # User has edit permission over the setting model, including the sensitive_email field
        test_setting.title = "Old title"
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
            Permission.objects.get(
                content_type__app_label="tests",
                codename="change_testpermissionedsitesetting",
            ),
            Permission.objects.get(codename="can_edit_sensitive_email_site_setting"),
        # GET should provide a form with title and sensitive_email
        self.assertIn("title", list(response.context["form"].fields))
        # POST should allow both title and sensitive_email to be set
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        self.assertEqual(test_setting.title, "New title")
    def test_edit_restricted_field_without_field_permission(self):
        # User has edit permission over the setting model, but not the sensitive_email field
        test_setting.title = "Old title"
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
            Permission.objects.get(
                content_type__app_label="tests",
                codename="change_testpermissionedsitesetting",
            ),
        )
        # GET should provide a form with title but not sensitive_email
        self.assertIn("title", list(response.context["form"].fields))
        # POST should allow the title to be set, but not the sensitive_email
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        self.assertEqual(test_setting.title, "New title")

    def test_edit_restricted_field_without_any_permission(self):
        # User has no permissions over the setting model, only access to the admin
        test_setting = TestPermissionedSiteSetting()
        test_setting.title = "Old title"
        test_setting.sensitive_email = "test@example.com"
        test_setting.site = self.default_site
        test_setting.save()
        self.user.is_superuser = False
        self.user.save()
        self.user.user_permissions.add(
            Permission.objects.get(
                content_type__app_label="wagtailadmin", codename="access_admin"
            ),
        )

        # GET should redirect away with permission denied
        response = self.get(setting=TestPermissionedSiteSetting)
        self.assertRedirects(response, status_code=302, expected_url="/admin/")

        # POST should redirect away with permission denied
        response = self.post(
            setting=TestPermissionedSiteSetting,
            post_data={
                "sensitive_email": "test-updated@example.com",
                "title": "New title",
            },
        )
        self.assertRedirects(response, status_code=302, expected_url="/admin/")

        # The retrieved setting should be unchanged
        test_setting.refresh_from_db()
        self.assertEqual(test_setting.sensitive_email, "test@example.com")
        self.assertEqual(test_setting.title, "Old title")
from wagtail.permission_policies import ModelPermissionPolicy
    permission_required = "change"
        self.permission_policy = ModelPermissionPolicy(self.model)