Repository: encode/django-rest-framework

Issue:
`permissions.DjangoModelPermissionsOrAnonReadOnly` doesn't actually enable anonymous read-only access in 3.15
### Discussed in https://github.com/encode/django-rest-framework/discussions/9298

<div type='discussions-op-text'>

<sup>Originally posted by **lpomfrey** March 18, 2024</sup>
As the title states, it seems from DRF 3.15 the `permissions.DjangoModelPermissionsOrAnonReadOnly` doesn't actually allow anonymous read only access as it inherits the check for the view permission on the model from `permissions.DjangoModelPermissions` class. 

It would seem to replicate the older behaviour `DjangoModelPermissionsOrAnonReadOnly` should set `'GET'` and `'HEAD'` in the `perms_map` to `[]` (along with setting `authenticated_users_only = False`).

I'm not sure if this is by design and the recommended solution is to compose a set of permissions like `permissions.DjangoModelPermissions | ReadOnly` (providing a custom `ReadOnly` class), but the documentation still suggests it should work as it did in 3.14 and before.</div>


Discussion:
Confirmed. I thought I was going crazy.
So... I think we should reassess and probably revert #8009, at least for a 3.15.1 release. (?)

Requiring "view" permissions is a decent idea, but it unavoidably changes an existing behaviour in a way that's going to break some existing installations.

Given that our permissions system was introduced before Django included the model "view" permission (I think?) it's not obvious that there's really a good way forward other than documenting what our built-in permissions do, and describing how to change them if needed. (Eg. how to require "view" permissions.)

Is this the most sensible course of action on this one?
If we were ignoring existing installations, it kind of feels like the plain `DjangoModelPermissions` _should_ handle the "view" permission.

Although that then raises the question of what `DjangoModelPermissionsOrAnonReadOnly` should do in the case of a logged in user that _doesn't_ have the "view" permission, I'm not sure there's a common use case where you'd want an anonymous user to have more privileges than an authenticated one, but it could at first glance be expected to work either way.

So, yes, ignoring the "view" permission and documenting that is probably the most sensible route, at least in the short-term.

Resolving patch:
diff --git a/rest_framework/permissions.py b/rest_framework/permissions.py
--- a/rest_framework/permissions.py
+++ b/rest_framework/permissions.py
@@ -186,9 +186,9 @@ class DjangoModelPermissions(BasePermission):
     # Override this if you need to also provide 'view' permissions,
     # or if you want to provide custom permission codes.
     perms_map = {
-        'GET': ['%(app_label)s.view_%(model_name)s'],
+        'GET': [],
         'OPTIONS': [],
-        'HEAD': ['%(app_label)s.view_%(model_name)s'],
+        'HEAD': [],
         'POST': ['%(app_label)s.add_%(model_name)s'],
         'PUT': ['%(app_label)s.change_%(model_name)s'],
         'PATCH': ['%(app_label)s.change_%(model_name)s'],
@@ -239,13 +239,8 @@ def has_permission(self, request, view):
 
         queryset = self._queryset(view)
         perms = self.get_required_permissions(request.method, queryset.model)
-        change_perm = self.get_required_permissions('PUT', queryset.model)
-
-        user = request.user
-        if request.method == 'GET':
-            return user.has_perms(perms) or user.has_perms(change_perm)
 
-        return user.has_perms(perms)
+        return request.user.has_perms(perms)
 
 
 class DjangoModelPermissionsOrAnonReadOnly(DjangoModelPermissions):


Test patch:
diff --git a/tests/test_permissions.py b/tests/test_permissions.py
--- a/tests/test_permissions.py
+++ b/tests/test_permissions.py
@@ -80,8 +80,7 @@ def setUp(self):
         user.user_permissions.set([
             Permission.objects.get(codename='add_basicmodel'),
             Permission.objects.get(codename='change_basicmodel'),
-            Permission.objects.get(codename='delete_basicmodel'),
-            Permission.objects.get(codename='view_basicmodel')
+            Permission.objects.get(codename='delete_basicmodel')
         ])
 
         user = User.objects.create_user('updateonly', 'updateonly@example.com', 'password')
@@ -140,15 +139,6 @@ def test_get_queryset_has_create_permissions(self):
         response = get_queryset_list_view(request, pk=1)
         self.assertEqual(response.status_code, status.HTTP_201_CREATED)
 
-    def test_has_get_permissions(self):
-        request = factory.get('/', HTTP_AUTHORIZATION=self.permitted_credentials)
-        response = root_view(request)
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-
-        request = factory.get('/1', HTTP_AUTHORIZATION=self.updateonly_credentials)
-        response = root_view(request, pk=1)
-        self.assertEqual(response.status_code, status.HTTP_200_OK)
-
     def test_has_put_permissions(self):
         request = factory.put('/1', {'text': 'foobar'}, format='json',
                               HTTP_AUTHORIZATION=self.permitted_credentials)
@@ -166,15 +156,6 @@ def test_does_not_have_create_permissions(self):
         response = root_view(request, pk=1)
         self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
 
-    def test_does_not_have_get_permissions(self):
-        request = factory.get('/', HTTP_AUTHORIZATION=self.disallowed_credentials)
-        response = root_view(request)
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
-        request = factory.get('/1', HTTP_AUTHORIZATION=self.disallowed_credentials)
-        response = root_view(request, pk=1)
-        self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN)
-
     def test_does_not_have_put_permissions(self):
         request = factory.put('/1', {'text': 'foobar'}, format='json',
                               HTTP_AUTHORIZATION=self.disallowed_credentials)