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],
)
Related
I’ver created some custom user groups in my Django app because I want to show them different admin fieldsets.
I thought they would work similarly to user.is_superuser.
But they don’t.
I have this def in my ModelAdmin:
def is_approver(user):
return user.groups.filter(name='approver').exists()
(I don’t know what that’s called, by the way. Do you just call it a “def”?)
So this works:
def get_fieldsets(self, request, obj=None):
if request.user.is_superuser:
return self.superuser_fieldset
I get the expected fieldset.
But this doesn’t work:
def get_fieldsets(self, request, obj=None):
if request.user.is_approver:
return self.approver_fieldset
However, this does work:
def get_fieldsets(self, request, obj=None):
if request.user.groups.filter(name='approvers').exists():
return self.approvers_fieldset
So, I guess my basic question is: why don’t my defs work like I expect?
(Python 3.9.7, Django 3.1)
add this code in the user model
#property
def is_approver(self):
return self.groups.filter(name='approver').exists()
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
I'm trying to check permissions for some API requests.
I've already set auth users and auth_user_user_permissions and auth_permissions tables like view_company add_company bla bla, but the problem is not that. The problem is when I'm trying yo use decorator which
#permission_required('API.view_company', raise_exception=True)
it said to me
AttributeError: 'CompanyDetailView' object has no attribute 'user'
Most probably it's looking for the user because its gonna check user_permission is it available to view companies or not but my view which I declared in urls.py (path('companies//', CompanyDetailView.as_view()),) has not have user object that's why error message returned attribute error, how can I solve this, thanks a lot
I tried to set example user in view class, in the beginning, it worked because view was looking for user object, i can not use that way because every request has different user
import rest_framework
from rest_framework import status
from django.contrib.auth.models import User
from rest_framework.views import APIView
from rest_framework.response import Response
from django.contrib.auth.decorators import permission_required
class CompanyDetailView(APIView):
#permission_required('api.view_company', raise_exception=True)
def get(self, request, id):
try:
request_data = {}
request_data['request_method'] = request.method
request_data['id'] = id
companies = Company.objects.get(id=id)
status = rest_framework.status.HTTP_200_OK
return Response(companies, status)
bla bla bla
url line was =
path('companies/<int:id>/', CompanyDetailView.as_view()),
my error message was : AttributeError: 'CompanyDetailView' object has no attribute 'user'
when i debug and i see request.user.has_perm('view_company')returned false but still api give responses, it suppose to say you are not allow to view companies
The mechanism of Django Views and Django Rest Framework Views are a bit different, that's why you've got that error message. permission_required will try to access user field of your view to check user permission using has_perm method. But APIView didn't have user field inside of it.
To get rid of this, you might want to use permissions which provided by Django Rest Framework to restrict the access.
But if you still want to use built-in permission of Django to restrict the access to your view, you could create a Permission class which will use has_perm to check user permission. Like so:
from rest_framework import permissions
from rest_framework import exceptions
class ViewCompanyPermission(permissions.BasePermission):
def has_permission(self, request, view):
if not request.user.has_perm('api.view_company'):
raise exceptions.PermissionDenied("Don't have permission")
return True
and use it on your view via permission_classes field:
class CompanyDetailView(APIView):
permission_classes = (ViewCompanyPermission, )
def get(self, request, id):
try:
request_data = {}
request_data['request_method'] = request.method
request_data['id'] = id
companies = Company.objects.get(id=id)
status = rest_framework.status.HTTP_200_OK
return Response(companies, status)
In case you want to replicas the permission_required behavior, you could do something like this:
from rest_framework import permissions
from rest_framework import exceptions
def permission_required(permission_name, raise_exception=False):
class PermissionRequired(permissions.BasePermission):
def has_permission(self, request, view):
if not request.user.has_perm(permission_name):
if raise_exception:
raise exceptions.PermissionDenied("Don't have permission")
return False
return True
return PermissionRequired
Then you can use it like:
class CompanyDetailView(APIView):
permission_classes = (permission_required("api.view_company", raise_exception=True), )
# ...
You can't easily use django permissions with django rest framework.
there is a tutorial about django-rest-framework permissions at:
https://www.django-rest-framework.org/api-guide/permissions/
Based on the description permission_required this decorator should be used for a function view, where first argument is request and you try to apply it for the class method where the first argument is self in your case instanse of the CompanyDetailView so you get the error. And you should use another way to check the permissions.
You can read some examples in here: decorators-on-django-class-based-views
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
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)