In django rest framework, whenever a permission is denied, it returns 401 to the client.
However this is very bad for items that are hidden. By sending 401 you acknowledge the user that infact, there is something there.
How can I instead return 404 in specific permissions? This one for example:
class IsVisibleOrSecretKey(permissions.BasePermission):
"""
Owner can view no matter what, everyone else must specify secret_key if private
"""
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
key = request.query_params.get('secret_key')
return (
obj.visibility != 'P'
or
request.user == obj.user
or
obj.secret_key == key
)
Going off of the DjangoObjectPermissions in the Django Rest Framework GitHub, you can raise an Http404. So instead of returning True or False, you return True if everything is good and then raise Http404 if not:
from django.http import Http404
class IsVisibleOrSecretKey(permissions.BasePermission):
"""
Owner can view no matter what, everyone else must specify secret_key if private
"""
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
key = request.query_params.get('secret_key')
if obj.visibility != 'P' or request.user == obj.user or obj.secret_key == key:
return True
else:
raise Http404
Since DjangoObjectPermissions extends BasePermission (a few steps back) this shouldn't do anything unexpected - I've just tested to make sure it returns 404 though, I haven't used it outside of this context. Just a head's up to do some testing.
Related
from rest_framework import permissions
class UserButEmailVerified(permissions.BasePermission):
def has_permission(self, request, view):
if request.user.is_authenticated:
return True
def has_object_permission(self, request, view):
if request.user.email_is_verified:
return True
return False
== Persmission class
from .permissions import UserButEmailVerified
#api_view(["POST"])
#permission_classes([UserButEmailVerified])
def sendMessage(request):
print(request.user.email_is_verified,"emai")
== also gets called even if email_is_verified returns False=?
I got the feeling that UserButEmailVerified isnt called at all, print statements are not getting executed, if I try to login unauthorized at all, I do not get access....
I know i could achive this without the permission class, but I want to learn it, so what am I doing wrong?
I am experiencing some weird behavior where djangorestframework returns a 404 when trying to browse the browsable API, but attaching a ?format=json at the end returns a normal response.
Using:
Django==4.0.3
django-guardian==2.4.0
djangorestframework==3.13.1
djangorestframework-guardian==0.3.0
A simplified version of my project setup:
#### API views
...
class UserRUDViewSet(
drf_mixins.RetrieveModelMixin,
drf_mixins.UpdateModelMixin,
drf_mixins.DestroyModelMixin,
viewsets.GenericViewSet,
):
"""Viewset combining the RUD views for the User model"""
serializer_class = serializers.UserSerializer
queryset = models.User.objects.all()
permission_classes = [permissions.RudUserModelPermissions | permissions.RudUserObjectPermissions]
...
#### app API urls
...
_api_prefix = lambda x: f"appprefix/{x}"
api_v1_router = routers.DefaultRouter()
...
api_v1_router.register(_api_prefix("user"), views.UserRUDViewSet, basename="user")
#### project urls
from app.api.urls import api_v1_router as app_api_v1_router
...
api_v1_router = routers.DefaultRouter()
api_v1_router.registry.extend(app_api_v1_router.registry)
...
urlpatterns = [
...
path("api/v1/", include((api_v1_router.urls, "project_name"), namespace="v1")),
...
]
The problem:
I am trying to add permissions in such a way that:
A user can only retrieve, update or delete its own User model instance (using per-object permissions which are assigned to his model instance on creation)
A user with model-wide retrieve, update or delete permissions (for example assigned using the admin panel), who may or may not also be a django superuser (admin) can RUD all user models.
To achieve this my logic is as follows:
Have a permissions class which only checks if a user has per-object permission:
class RudUserObjectPermissions(drf_permissions.DjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
def has_permission(self, request, view):
return True
Have a class which checks for model-wide permissions but does this in the has_object_permission method:
class RudUserModelPermissions(drf_permissions.DjangoObjectPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
...
# Same as the other permissions class
}
# has_permission() == true if we are to get anywhere - no need to override
# Originally tried like this
# def has_object_permission(self, request, view, obj):
# return super().has_permission(request, view)
# Copied from the drf_permissions. DjangoObjectPermissions class
def has_object_permission(self, request, view, obj):
# Changed the commented out lines only
queryset = self._queryset(view)
model_cls = queryset.model
user = request.user
perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
if request.method in drf_permissions.SAFE_METHODS:
raise drf_permissions.Http404
read_perms = self.get_required_object_permissions('GET', model_cls)
# if not user.has_perms(read_perms, obj):
if not user.has_perms(read_perms):
raise drf_permissions.Http404
return False
return True
The mystery:
Testing with a user who has:
PK == 3
per-object RUD permissions for User model instance with PK == 3 (its own model)
Model wide permissions for viewing users
Navigating to api/v1/appprefix/user/3: Returns HTTP 200, as expected
Navigating to api/v1/appprefix/user/2: Returns HTTP 404 (user with pk 2 exists)
Navigating to api/v1/appprefix/user/2?format=json: Returns HTTP 200, as expected
What I have tried:
Changing:
...
perms = self.get_required_object_permissions(request.method, model_cls)
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
...
To:
...
perms = ['myapp_label.view_user']
# if not user.has_perms(perms, obj):
if not user.has_perms(perms):
...
Weirdly this fixes it and api/v1/appprefix/user/2 starts returning HTTP 200
Still have not solved the weird HTTP404 error which only occurs when using the browsable API, but I found a solution to the problem I was trying to solve originally - allow users with model permissions to access all objects, while restricting the rest to only objects they have permissions for.
I have changed the permissions class to the following:
from rest_framework.permissions import DjangoObjectPermissions
from rest_framework.exceptions import PermissionDenied, NotFound
from django.http import Http404
class GlobalOrObjectPermission(DjangoObjectPermissions):
perms_map = {...}
def has_permission(self, request, view):
# Always let the request to proceed. The endpoint only serves
# individual objects so this is OK
return True
def has_object_permission(self, request, view, obj):
try:
has_perm = super().has_object_permission(request, view, obj)
except (
PermissionDenied,
# has_object_permission() raises http.Http404 instead of
# drf_exceptions.NotFound when user does not have read permissions
# but this could change so check for both exceptions
Http404,
NotFound,
) as e:
has_perm = super().has_permission(request, view)
# If user does not have model permissions, raise the original
# object permission error, which can be HTTP403 or HTTP404
if not has_perm:
if isinstance(e, Http404): # added these 2 lines
e = NotFound() # see EDIT note
raise e
return has_perm
This allows:
Users with object permissions to access the individual objects they have permissions for
Users with model permissions to access all objects of the model
EDIT:
After more testing I found that it actually suffers from the same problem as the original post. I ran it with a debugger and it seems to be a bug. I don't have time to investigate this further but for some reason when Http404 is raised from the overridden method it propagates down to Django's normal 404 page renderer. While if it is raised from the super method it propagates to the BrowsableAPI 404 renderer where it is translated to a NotFound exception and rendered. Thus, translating the Http404 exception towards the DRF's native NotFound exception in the overridden method (see code) fixes the issue, as NotFound is handled by the Browsable API renderer in both cases.
I tried raise PermissionDenied("Anonymous user") inside a custom permission function but the message I wrote is not showing in the api response. Instead, it is showing the default forbidden message that says you dont have permission to perform this action
My snippet is here:
class CustomPermission(BasePermission):
"""
returns permission based on the request method and slug
"""
def has_permission(self, request,view):
slug = request.resolver_match.kwargs["slug"]
if slug is not None and request.method == 'POST':
if slug == "abc":
user = request.user
if user.is_staff:
return True
if user.is_anonymous:
print("iam here")
raise PermissionDenied("Anonymous user")
elif slug == "mnp":
return True
else:
return True
Here in the above code I reached to ("iam here") but anonymous user is not printing instead showing the default message.
You can change error message in message property:
from rest_framework import permissions
class CustomerAccessPermission(permissions.BasePermission):
message = 'Your message'
def has_permission(self, request, view):
... # return True or False
Docs in : https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions
After that, the permission_denied(self, request, message=None, code=None) function in your view would be called which message is the attribute that you have declared in your permission class.
You can use that, or even pass another message:
from rest_framework.exceptions import PermissionDenied
class YourView(...):
permission_classes = [CustomerAccessPermission]
def permission_denied(self, request, message=None, code=None):
raise PermissionDenied(message)
This is a snippet from the Django rest framework documentation on writing custom permissions. I don't understand the meaning of the last line here:
class IsOwnerOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return True
return obj.owner == request.user
The method has_object_permission() returns True or False depending in the evaluation of obj.owner == request.user
I'm continuing my journey of testing my Django Rest Framework application as I add new views and more functionality. I must admit, at this stage, I'm finding testing harder than actually coding and building my app. I feel that there are far fewer resources on testing DRF available than there are resources talking about building a REST framework with DRF. C'est la vie, though, I soldier on.
My issue that I'm currently facing is that I'm receiving a 403 error when testing one of my DRF ViewSets. I can confirm that the view, and its permissions work fine when using the browser or a regular python script to access the endpoint.
Let's start with the model that is used in my ViewSet
class QuizTracking(models.Model):
case_attempt = models.ForeignKey(CaseAttempt, on_delete=models.CASCADE)
user = models.ForeignKey(CustomUser, on_delete=models.CASCADE)
answer = models.ForeignKey(Answer, on_delete=models.CASCADE)
timestamp = models.DateTimeField(auto_now_add=True)
Of note here is that there is a FK to a user. This is used when determining permissions.
Here is my test function. I have not included code for the entire class for the sake of brevity.
def test_question_retrieve(self):
"""
Check that quiz tracking/ID returns a 200 OK and the content is correct
"""
jim = User(username='jimmy', password='monkey123', email='jimmy#jim.com')
jim.save()
quiz_tracking = QuizTracking(answer=self.answer, case_attempt=self.case_attempt, user=jim)
quiz_tracking.save()
request = self.factory.get(f'/api/v1/progress/quiz-tracking/{quiz_tracking.id}')
# How do I refernce my custom permission using Permission.objects.get() ?
# permission = Permission.objects.get()
# jim.user_permissions.add(permission)
self.test_user.refresh_from_db()
force_authenticate(request, user=jim)
response = self.quiz_detail_view(request, pk=quiz_tracking.id)
print(response.data)
print(jim.id)
print(quiz_tracking.user.id)
self.assertContains(response, 'answer')
self.assertEqual(response.status_code, status.HTTP_200_OK)
In the above code, I define a user, jim and a quiz_tracking object owned by jim.
I build my request, force_authenticate the requst and the execute my request and store the response in response.
The interesting things to note here are:
- jim.id and quiz_tracking.user.id are the same value
- I receive a 403 response with
{'detail': ErrorDetail(string='You do not have permission to perform this action.', code='permission_denied')}
You may have noticed that I have commented out permission = Permission.objects.get() My understanding is that I need to pass this my Permission Class, which in my case is IsUser. However, there is no record of this in my DB and hence Permission.objects.get('IsUSer') call fails.
So my questions are as follows:
- How do I authenticate my request so that I receive a 200 OK?
- Do I need to add a permission to my user in my tests, and if so, which permission and with what syntax?
Below is my view and below that is my custom permission file defining IsUser
class QuickTrackingViewSet(viewsets.ModelViewSet):
serializer_class = QuizTrackingSerializser
def get_queryset(self):
return QuizTracking.objects.all().filter(user=self.request.user)
def get_permissions(self):
if self.action == 'list':
self.permission_classes = [IsUser, ]
elif self.action == 'retrieve':
self.permission_classes = [IsUser, ]
return super(self.__class__, self).get_permissions()
N.B. If I comment out def get_permissions(), then my test passes without problem.
My custom permission.py
from rest_framework.permissions import BasePermission
class IsSuperUser(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_superuser
class IsUser(BasePermission):
def has_object_permission(self, request, view, obj):
if request.user:
if request.user.is_superuser:
return True
else:
return obj == request.user
else:
return False
Cheers,
C
It seems that the problem is your permission IsUser.
You compare QuizTracking instance with user instance.
change this line
return obj == request.user
to
return obj.user.id == request.user.id
One off topic suggestion
You can write your IsUser class in the following way which is easier to read
class IsUser(BasePermission):
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
return request.user.is_superuser or obj.user.id == request.user.id
Note that has_object_permission is only called if the view level has_permission returns True.