Django REST Framework - Allow staff to access all endpoints - python

I'm building a DRF API, and I would like to allow staff members (is_staff == True) to access all of the REST endpoints, while still providing custom permission checking per ViewSet. Ideally, this would be a global setting, but I'm not against setting it up per ViewSet.
Here are the things I've tried:
Option 1: Check on every custom permission
from rest_framework import permissions
class SomeModelPermission(permissions.BasePermission):
def has_permission(self, request, view):
if request.user.is_staff:
return True
# other logic
def has_object_permission(self, request, view, obj):
if request.user.is_staff:
return True
# other logic
This works, but I'd rather not repeat so much code.
Option 2: Bitwise operators
I tried removing the is_staff logic from the custom permission above, and adding this to the ViewSet:
from rest_framework import permissions, viewsets
class SomeModelViewSet(viewsets.ModelViewSet):
permission_classes = (permissions.IsAdminUser|SomeModelPermission,)
However, this actually does not enforce permissions as I'd like, because IsAdminUser inherits from BasePermission, which is defined as:
class BasePermission(object):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
return True
IsAdminUser doesn't define its own has_object_permission, so it will always return True when checking object permissions, resulting in unintended object access.
Any ideas? I was hoping there would be some way I could set a global permissions check that would return True when the user is a staff member, and defer to the custom permissions otherwise. But reading through how permissions are determined, I'm not sure that this is possible.

Bitwise solution:
How about creating your own IsAdminUser which also defines has_object_permission ? You could just inherit from the existing one:
from rest_framework.permissions import IsAdminUser as BaseIsAdminUser
class IsAdminUser(BaseIsAdminUser):
def has_object_permission(self, request, view, obj):
# Just reuse the same logic as `has_permission`...
return self.has_permission(request, view)
Then you can do what you attempted above, with the bitwise operator:
from rest_framework import permissions, viewsets
from your_own_project.permissions import IsAdminUser
class SomeModelViewSet(viewsets.ModelViewSet):
permission_classes = (IsAdminUser|SomeModelPermission,)
Another solution:
A bit "hacky" in some ways, but you could try to create your own permission types on the fly.
So the end result would look something like:
class SomeModelViewSet(viewsets.ModelViewSet):
permission_classes = skip_for_staff((SomeModelPermission, SomeOtherPermission, ...))
With the implementation something similar to:
class StaffAllowedMixin:
def has_permission(self, request, view):
if request.user.is_staff:
return True
return super().has_permission(request, view)
def has_object_permission(self, request, view, obj):
if request.user.is_staff:
return True
return super().has_object_permission(request, view, obj)
def skip_for_staff(permission_classes):
# You can probably also use a comprehension here, but for clarity:
staff_allowed_classes = []
for permission_class in permissions(
staff_allowed_classes.append(
# Create a new type (class) with name StaffAllowed<...>
type(f"StaffAllowed{permission_class}",
# Inherit from the mixin above, and from the original class
(StaffAllowedMixin, permission_class),
# empty dictionary means you don't want to override any attributes
{})
)
return tuple(staff_allowed_classes)
Essentially, for each permission class, you create a new class with the extra mixin that takes precedence and checks if the user is staff.
But you do that on the fly, where your permissions are used, instead of having to predefine it for every permission.

There is a permission class for admin users.
Here is an example of that:
class deletecompletedreopenjobAPIView(RetrieveUpdateAPIView):
queryset = Job.objects._all()
serializer_class = JobCompletedDeleteStatusSerializer
lookup_field = 'job_id'
permission_classes = [IsOwnerOrReadOnly | **IsAdminUser**]
authentication_classes = (authentication.TokenAuthentication,)
My custom IsOwnerOrReadOnly is as follows:
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.user == request.user

Related

Can not switch from assigning permission_classes to use decorator #permission_classes

The below is an api that required authen. It works
class some_random_api(generics.GenericAPIView):
permission_classes = (IsAuthenticated, )
def get(self,request):
return HttpResponse("Called successfully")
However, I dont like declaring variable. After looking up this document, https://www.django-rest-framework.org/api-guide/permissions/ . I find an alternative way by using decorator. So I change my code into this.
from rest_framework.decorators import permission_classes
class some_random_api(generics.GenericAPIView):
#permission_classes(IsAuthenticated)
def get(self,request):
return HttpResponse("You call random test api")
Now this API does not check Authen
The decorator #permission_classes is only applicable to function-based API views as documented.
REST framework provides a set of additional decorators which can be added to your views. These must come after (below) the #api_view decorator.
The available decorators are:
...
#permission_classes(...)
...
Also here:
Or, if you're using the #api_view decorator with function based views.
#api_view(['GET'])
#permission_classes([IsAuthenticated])
def example_view(request, format=None):
...
An alternative to defining a fixed list permission_classes = (IsAuthenticated, ) is to override the get_permissions() and assign different permissions for the different HTTP methods as described here.
class some_random_api(generics.GenericAPIView):
def get_permissions(self):
if self.request.method == "GET": # If using viewsets, the self.action can be used e.g. <if self.action == "retrieve":>
permission_classes = [permissions.IsAuthenticated]
else:
permission_classes = [permissions.IsAdminUser]
return [permission() for permission in permission_classes]
def get(self, request):
return HttpResponse("You call random test api")
Related questions:
Django Rest Framework - GenericViewSet with Authentication/Permission decorator
Django REST Framework - Separate permissions per methods

How do I create links in Django such that other users can't access them?

I'm pretty confused about how do I prevent users' from accessing the data of other users.
The case at hand :
I'm creating a Notes + To-Do app in which a user logs in, creates their notes and tasks.
How to create links to those notes such that they aren't accessible by other users? As in the correct syntax for UserPassesTestMixin.
In the To-Do app, how do I keep the tasks of one user unique to them? Similarly for the note app, how do I achieve that?
Not sure what you mean by "create links". For what you describe, the links don't change for people that have access or not. The difference if that a user that owns note 5 and goes to /note/5/, they should be able to see their note, but if another user goes to /note/5/ they should either 1) get a 404 error (Note not found) or 403 (Permission Denied) just be redirected to another page (say, the home page), maybe with a message.
Using Class based views, this is easy to do.
Prevent access to views
from django.core.exceptions import PermissionDenied
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
class LoginRequiredAccessMixin(object):
# This will ensure the user is authenticated and should
# likely be used for other views
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super(LoginRequiredAccessMixin, self).dispatch(request, *args, **kwargs)
class AccessMixin(LoginRequiredAccessMixin):
def get_object(self, queryset=None):
obj = get_object_or_404(Note, pk=self.kwargs['id'])
# Assumes you have a notes.user, but change to created_by
# or whatever is your user field name
if obj.user == self.request.user:
# User owns object
return obj
raise PermissionDenied("User has no access to this note")
class NoteView(AccessMixin, DetailView):
# This is a regular DetilView, but with the Mixin,
# you are overwriting the get_object() function.
# If you don't want the Mixin, then you can just add
# get get_object() function here. Except that with the
# Mixin, you can reuse it for your UpdateView, DeleteView
# and even across both your notes and task views
model = Note
template_name = 'note/details.html'
def get_context_data(self, **kwargs):
context = super(NoteView, self).get_context_data(**kwargs)
# Add any special context for the template
return context
If instead you want to just direct users to another page, you would do something like:
from django.utils.decorators import method_decorator
from django.contrib.auth.decorators import login_required
from django.contrib import messages
class NoteView(DetailView):
model = Note
template_name = 'note/details.html'
def get_context_data(self, **kwargs):
context = super(NoteView, self).get_context_data(**kwargs)
# Add any special context for the template
return context
#method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
note = self.get_objet()
if note and not note.user == self.request.user:
messages.error(
self.request,
'You are not allowed to access this Note'
)
return HttpResponseRedirect('/home')
return super(NoteView, self).dispatch(request, *args, **kwargs)
You didn't supply any code so I cannot be more specific, but hopefully you get an idea of the two techniques. The first is usually a cleaner solution, and the Mixin I show can be shared across both your Note views and ToDo Tasks records, assuming they use the same user/created_by field name.
In case you are using functions (FBV) you could use if request.user == item.user
#login_required
def post_edit(request, post_id):
item = Post.objects.get(pk=post_id)
if request.user == item.user:
CBV - Class Based View - using UserPassesTestMixin
class PostUpdateView(LoginRequiredMixin, UserPassesTestMixin, UpdateView):
# [...]
You can use the decorator in Django called "user_passes_test"
You can import like:
from django.contrib.auth.decorators import user_passes_test
For detail check docs here

Using django custom permission on view defs

I have written a custom permission
class IsObjectOwner(permissions.BasePermission):
def has_permission(self, request, view):
if ...
.....
return False
else:
return True
And I am trying to apply this on a few particular view definitions.
URLs.py
url(r'^get_possible_moves/', include('GameView.get_possible_moves')),
url(r'^get_available_pieces/', include('GameView.get_available_pieces')),
url(r'^remove_pieces/', include('GameView.remove_pie
ces')),
views.py
class GameView(viewsets.ModelViewSet):
queryset = GameModel.objects.all()
serializer_class = GameSerializer
def get_available_pieces(self,request,*args,**kwargs):
permission_classes(IsObjectOwner,)
....
def get_possible_moves(self,request,*args,**kwargs):
permission_classes(IsObjectOwner,)
.....
def remove_pieces(self,request,*args,**kwargs):
permission_classes(IsObjectOwner,)
......
.......
But the permission is not working on the view and the permission doesn't seem to be getting invoked. I would like to know how to implement custom permission on view definition, any help is appreciated.
Note: There are other definitions too in this view, on which I do not want to impose the permissions. So, I cannot put the permissions at view level.
And this is not a game of chess.
Set the permissions_classes to the entire viewset and not just to the functions.
class GameView(viewsets.ModelViewSet):
permission_classes = (IsObjectOwner,)
queryset = GameModel.objects.all()
serializer_class = GameSerializer
def get_available_pieces(self,request,*args,**kwargs):
....
return
def get_possible_moves(self,request,*args,**kwargs):
....
return
def remove_pieces(self,request,*args,**kwargs):
....
return

Check permissions on a related object in Django REST Framework

I have defined the following models
class Flight(models.Model):
...
class FlightUpdate(models.Model):
flight = models.ForeignKey('Flight', related_name='updates')
...
and the following viewset using the NestedViewsetMixin in the REST Framework Extensions
class FlightUpdateViewSet(mixins.ListModelMixin,
mixins.CreateModelMixin,
NestedViewSetMixin,
viewsets.GenericViewSet):
"""
API Endpoint for Flight Updates
"""
queryset = FlightUpdate.objects.all()
serializer_class = FlightUpdateSerializer
def create(self, request, *args, **kwargs):
flight = Flight.objects.get(pk=self.get_parents_query_dict()['flight'])
...
So, to access the FlightUpdates associated with a Flight, the URL is /flights/1/updates/.
I want to ensure that people can only create FlightUpdates if they have the permissions to change the Flight object with which the FlightUpdate is associated.
How would I go about performing the extra check when adding a FlightUpdate? I've tried adding something like this in the viewset, but I'm not sure if it's the best way.
if not request.user.has_perm('flights.change_flight', flight):
raise PermissionError()
Note: I'm using django-rules for the object-level permissions implementation.
I solved this problem by implementing a custom permissions class.
from django.core.exceptions import ObjectDoesNotExist
from rest_framework.permissions import BasePermission, SAFE_METHODS
from .models import Flight
class FlightPermission(BasePermission):
def has_permission(self, request, view):
if request.method in SAFE_METHODS:
return True
try:
flight = Flight.objects.get(pk=view.kwargs['parent_lookup_flight'])
except ObjectDoesNotExist:
return False
return request.user.has_perm('flights.change_flight', flight)

How to get OR permissions instead of AND in REST framework

It seems that permission classes are ANDed when REST framework checks permissions. That is every permission class needs to return True for permission to be granted. This makes things like "if you are a superuser, you can access anything, but if you are a regular user you need explicit permissions" a bit hard to implement, you cannot just return False, it will fail the whole stack. Is there a way to maybe short-circuit permissions? Something like "if this permission is granted, stop checking?" or some other way to deal with cases like that?
Now DRF allows permissions to be composed using bitwise operators: & -and- and | -or-.
From the docs:
Provided they inherit from rest_framework.permissions.BasePermission, permissions can be composed using standard Python bitwise operators. For example, IsAuthenticatedOrReadOnly could be written:
from rest_framework.permissions import BasePermission, IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView
class ReadOnly(BasePermission):
def has_permission(self, request, view):
return request.method in SAFE_METHODS
class ExampleView(APIView):
permission_classes = (IsAuthenticated|ReadOnly,)
def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
Edited: Please note there is a comma after IsAuthenticated|ReadOnly.
I think you might be able to use django-rules library here. Link
It is a rule based engine very similar to decision trees and it can be easily integrated with permissions_class framework of DRF.
The best part is you can perform set operations on simple permissions and create complex permissions from them.
Example
>>> #rules.predicate
>>> def is_admin(user):
... return user.is_staff
...
>>> #rules.predicate
>>> def is_object_owner(user, object):
return object.owner == user
Predicates can do pretty much anything with the given arguments, but must always return True if the condition they check is true, False otherwise.
Now combining these two predicates..
is_object_editable = is_object_owner | is_admin
You can use this new predicate rule is_object_editable inside your has_permissions method of permission class.
You need to build your own custom http://www.django-rest-framework.org/api-guide/permissions/#custom-permissions as described in the docs.
Something like:
from rest_framework import permissions
class IsAdminOrStaff(permissions.BasePermission):
message = 'None of permissions requirements fulfilled.'
def has_permission(self, request, view):
return request.user.is_admin() or request.user.is_staff()
Then in your view:
permission_classes = (IsAdminOrStaff,)
Aside from the custom permission which is simpler approach mentioned in the earlier answer, you can also look for an existing 3rd party that handle a much complex permission handling if necessary.
As of Feb 2016, those handling complex condition permission includes:
rest_condition
djangorestframework-composed-permissions
One way would be to add another permission class which combines existing classes the way you want it, e.g.:
class IsAdmin(BasePermission):
"""Allow access to admins"""
def has_object_permission(self, request, view, obj):
return request.user.is_admin()
class IsOwner(BasePermission):
"""Allow access to owners"""
def has_object_permission(self, request, view, obj):
request.user.is_owner(obj)
class IsAdminOrOwner(BasePermission):
"""Allow access to admins and owners"""
def has_object_permission(*args):
return (IsAdmin.has_object_permission(*args) or
IsOwner.has_object_permission(*args))
Here is a generic solution:
from functools import reduce
from rest_framework.decorators import permission_classes
from rest_framework.permissions import BasePermission
def any_of(*perm_classes):
"""Returns permission class that allows access for
one of permission classes provided in perm_classes"""
class Or(BasePermission):
def has_permission(*args):
allowed = [p.has_permission(*args) for p in perm_classes]
return reduce(lambda x, y: x or y, allowed)
return Or
class IsAdmin(BasePermission):
"""Allow access to admins"""
def has_object_permission(self, request, view, obj):
return request.user.is_admin()
class IsOwner(BasePermission):
"""Allow access to owners"""
def has_object_permission(self, request, view, obj):
request.user.is_owner(obj)
"""Allow access to admins and owners"""
#permission_classes((any_of(IsAdmin, IsOwner),))
def you_function(request):
# Your logic
...
The easiest way would be to separate them out with | in permission_classes attribute or get_permissions method, but if you have complex rules, you can specify those rules in the check_permissions method in the viewsets class that you are defining. Something like this:
class UserProfileViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = ProfileSerializerBase
def create(self, request, *args, **kwargs):
# Create rules here
def get_permissions(self):
if self.action == "destroy":
# Only Super User or Org Admin can delete record
permission_classes = [SAPermission, OAPermission]
def check_permissions(self, request):
"""
Original check_permissions denies access if any one of the permission
classes returns False, changing it so that it would deny access only if
all classes returns False
"""
all_permissions = []
messages = []
code = []
for permission in self.get_permissions():
all_permissions.append(permission.has_permission(request, self))
messages.append(getattr(permission, "message", None))
code.append(getattr(permission, "code", None))
if True in all_permissions:
return
message = ",".join(i for i in messages if i)
self.permission_denied(
request,
message=message if message else None,
code=code[0],
)

Categories

Resources