class UserViewSet(viewsets.ModelViewSet):
def list(self, request):
users = User.objects.all()
serializer = UserSerializer(users, many=True)
return Response(serializer.data, status=status.HTTP_200_OK)
def create(self, request):
serializer = UserSerializer(data=request.data)
if serializer.is_valid(raise_exception=True):
pass
def retrieve(self, request, pk):
user = get_object_or_404(User, pk=pk)
self.check_object_permissions(request, user)
serializer = UserSerializer(user)
return Response(serializer.data, status=status.HTTP_200_OK)
def get_permissions(self):
if self.action == "list":
permission_classes = [
IsAdminUser,
]
elif self.action == "create":
permission_classes = [AllowAny]
else:
permission_classes = [AccountOwnerPermission | IsAdminUser ]
return [permission() for permission in permission_classes]
and custom permission is:
class AccountOwnerPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print(object)
print(request.user)
return obj == request.user
first i dont get object permission but with help of #brian-destura at this question i fixed that part the previous question
now the problem is when i chain 2 permission together it behave like AllowAny i check them one by one and both permissions work fine, one of them allow admin and one of them allow owner but when they are or together it mess everything up
When chaining permissions like
permission_classes = [AccountOwnerPermission, IsAdminUser]
it behaves like an AND operator between the permission classes
The best option is to create a new permission that allows either the permission logic.
class AdminOrAccountOwnerPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj == request.user or request.user.id_admin
or this when the permissions used have long complex code to keep code DRY:
class AdminOrAccountOwnerPermission(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return AccountOwnerPermission().has_object_permission(request, view, obj) or IsAdminUser().has_object_permission(request, view, obj)
EDIT:
Address the question from the comments, the reason why it behaves like AllowAny.
AccountOwnerPermission has has_object_permission but no has_permission. On the other hand, IsAdminUser has has_permission but no has_object_permission implemented.
When those functions are not implemented, the functions return True by default(from BasePermission). As a result, when running has_permission, AccountOwnerPermission always returns True When running has_object_permission, IsAdminUser is always returning True.
Implementing AccountOwnerPermission.has_permission would give the expected behavior.
#Kyell already described the problem and his answer should be accepted
But I'll try to add some details:
When we chain two permission classes DRF creates one new OR class:
>>> from rest_framework.permissions import IsAdminUser
>>> or_class = [IsAdminUser | IsAdminUser]
>>> len(or_class)
1
>>> print(or_class)
[<rest_framework.permissions.OperandHolder object at 0x1096d5fa0>]
>>>
Django documentation says that has_object_permission (check permissions for exact object) method running after has_permission (check permissions for whole view class)
Let's see how looks these methods inside chained OR class:
>>> import inspect
>>> or_instance = or_class[0]()
>>> print(inspect.getsource(or_instance.has_permission))
def has_permission(self, request, view):
return (
self.op1.has_permission(request, view) or
self.op2.has_permission(request, view)
)
>>> print(inspect.getsource(or_instance.has_object_permission))
def has_object_permission(self, request, view, obj):
return (
self.op1.has_object_permission(request, view, obj) or
self.op2.has_object_permission(request, view, obj)
)
So we can see that DRF check both of has_permission and after that both of has_object_permission
has_permission check may be skipped because we run has_object_permission after.
But! has_object_permission is not implemented inside IsAdminUser permission class but it is implemented inside parent BasePermission class and looks like:
class BasePermission(metaclass=BasePermissionMetaclass):
def has_object_permission(self, request, view, obj):
return True
So IsAdminUser always return True on has_object_permission. In usual cases IsAdminUser should fail on has_permission, but in your OR class has_permission passed because it is not implemented inside AccountOwnerPermission class
Simplest solution will be to add has_permission method to AccountOwnerPermission class:
class AccountOwnerPermission(permissions.BasePermission):
def has_permission(self, request, view, obj):
return False
Related
I have one modelviewset and couple of extra actions for it.
class TestViewset(
mixins.RetrieveModelMixin,
mixins.ListModelMixin,
mixins.UpdateModelMixin,
GenericViewSet,
):
permission_classes = [CustomPermissionClass]
def get_serializer_class(self) -> Type[ModelSerializer]:
if self.action == "list" or self.action == "get":
return FirstSerializer
return SecondSerializer
#action(
detail=True,
methods=["POST"],
url_path="test-path",
url_name="test-path",
serializer_class=MyCustomSerializer,
)
def test_path(self, request: Request, **kwargs) -> Response:
print(self.get_serializer()) # Despite setting the 'MyCustomSerializer' it still gives me 'SecondSerializer'
return Response(status=status.HTTP_200_OK)
If I remove the get_serializer_class method and give only one serializer it works.
What could be the best solution for this or is this any bug in drf?
Ideally the extra action should use the serializer class that has been provided but its not.
There might be a better approach for this, but off the top of my head, you can fix this by changing your get_serializer_class to:
def get_serializer_class(self) -> Type[ModelSerializer]:
if self.action == "list" or self.action == "get":
return FirstSerializer
return self.serializer_class or SecondSerializer
This will ensure the serializer_class that is defined in your action is used, if the action is neither list nor get.
I'm trying to use the following permission class in many apps in the project, the only change needed is the model class that user data is checked from.
Permission class:
class IsAuthorOrForbidden(permissions.BasePermission):
"""
Check if the requesting user the author or not
"""
def __init__(self, modelClass):
self.modelClass = modelClass
def has_permission(self, request, view):
# get the needed model instance or 404 if not available
instance = get_object_or_404(self.modelClass, pk=view.kwargs['pk'])
# Check if the requesting user is the author
if instance.user == request.user:
return True
return False
The permission class in the view class:
class TextNoteGenerateShareKeyAPIView(generics.UpdateAPIView):
"""
Generate a new text note share key
"""
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated,
IsAuthorOrForbidden(modelClass=TextNote))
...
When I run the tests, i get this error:
return [permission() for permission in self.permission_classes]
TypeError: 'IsAuthorOrForbidden' object is not callable
Is it possible to do it like this or should I write this permission class in every app in my project?
You can use object level permission:
class IsAuthorOrForbidden(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return obj.user == request.user
and add it to the view like this:
permission_classes = (IsAuthenticated,
IsAuthorOrForbidden)
Explanation on why the error is occurring
To understand the error, you need to see the implementation of get_permissions method in GitHub source code:
#copy pasted from GitHub
def get_permissions(self):
"""
Instantiates and returns the list of permissions that this view requires.
"""
return [permission() for permission in self.permission_classes]
Here it is making a list of objects from self.permission_classes, meaning permission_classes are suppose to have Class, not objects.
You are adding a IsAuthorOrForbidden object to the permission classes, where you need to add the class. If you want to put a override, then it should be done here:
class TextNoteGenerateShareKeyAPIView(generics.UpdateAPIView):
def get_permissions(self):
return [IsAuthenticated(), IsAuthorOrForbidden(modelClass=TextNote)]
But this is a hackish solution where solution in the original answer is the proper way to implement in DRF.
For those who are using "viewsets" and want to allow only the author to access the content/items you can simply use a mixin like below and use that mixin in whichever viewsets that requires.
# mixin
class FilterByUserMixin:
def get_queryset(self, *args, **kwargs):
queryset = super().get_queryset(*args, **kwargs)
return queryset.filter(user=self.request.user)
# viewsets
class WhateverViewSet(FilterByUserMixin, viewsets.ModelViewSet):
One way to make the OP's code work is to add a __call__ method to the permission class that just returns the object.
class IsAuthorOrForbidden(permissions.BasePermission):
def __call__(self):
return self
This will avoid the TypeError that was being thrown when the permission_classes are being instantiated into objects, by simply returning the object when it is called. This allows for passing arguments to the permission_classes like the OP wanted.
I am not very comfortable using class-based views but I am aware of their perks so I am forcing myself to start using them more often.
There's this view that receives a path param: manage/:id to manage a particular entity.
class MyView(TemplateView):
template_name = '...'
def get_context_data(self, **kwargs):
context = super(MyView, self).get_context_data(**kwargs)
context['entity'] = get_object_or_404(Entity, pk=self.args[0])
return context
An Entity includes a list of authorized users to perform special actions. This view, MyView is one of those special actions.
I tried making a decorator for the view but it required finding the Entity first so I wasn't sure how to work that out.
Now, I have a check_permission(request, entity) function that checks if the current user is one of these authorized ones.
My question is where should I call this function in the class-based views like MyView which will be any of these views considered "special actions"?
Should I call it just from get_context_data()?
put it into dispatch(). It could look like this:
class MyView(TemplateView):
template_name = '...'
def dispatch(self, request, *args, **kwargs):
entity = get_object_or_404(Entity, pk=args[0])
if not check_permission(request, entity):
raise Http404
return super(MyView, self).dispatch(request, *args, **kwargs)
You can check permissions in the dispatch as yedpodtrzitko has said. I think it's also a good idea to throw it inside of a mixin that you can put on your views.
Here's an example:
from django.core.exceptions import PermissionDenied
class ViewPermissionsMixin(object):
"""Base class for all custom permission mixins to inherit from"""
def has_permissions(self):
return True
def dispatch(self, request, *args, **kwargs):
if not self.has_permissions():
raise PermissionDenied
return super(ViewPermissionsMixin, self).dispatch(
request, *args, **kwargs)
class MyCustomPermissionMixin(ViewPermissionsMixin):
def has_permissions(self):
# here you will have access to both
# self.get_object() and self.request.user
return self.request.user in self.get_object().special_list_of_people
Now you can throw MyCustomPermissionMixin on your view:
class MyView(MyCustomPermissionMixin, TemplateView):
# ...
In your case, since you're using a TemplateView, you should also make a get_object() method that returns the object that you want to deal with. Template views don't have this method by default.
Finally, just want to say that you will love Django's class based views once you learn some more about how to use them.
Take a look at Django Braces, it's a solid set of mixins which are designed around permissions.
How specifically you deal with permissions depends largely on implementation. I've done it in dispatch() before which is the way Braces does it but if it's specific to an object or queryset, I'll do it in the actual get_object or get_queryset methods as part of a DetailView.
For example if you had a creator associated with an Entity, you could override get_object to check the current logged in user is the Entity's creator.
class EntityView(LoginRequiredMixin, DetailView):
model = Thing
def get_object(self, **kwargs):
return Entity.objects.get_object_or_404(
pk=kwargs['entity_id'],
creator=self.request.user
)
Note: LoginRequiredMixin is a part of Braces. Very slick.
Are declaring variables in dispatch method valid in following sample code?
if not is there better coding standards in CBV? my idea is avoiding redeclare the same variable in every http methods
class MyClsName(View):
template_name = "setup/code_install.html"
def dispatch(self, request, *args, **kwargs):
self.name = request.session['name']
self.obj = MyModel.objects.get(Name=self.name)
return super(MyClsName, self).dispatch(request, *args, **kwargs)
def get(self, request):
# obj = MyModel.objects.get(Name=self.name) --> to avoid
context = {'account': self.obj}
return render(request, self.template_name, context)
def post(self, request):
# obj = MyModel.objects.get(Name=self.name) --> to avoid
context = {'account': self.obj}
return render(request, self.template_name, context)
This is not invalid, but it is definitely not taking advantage of the features of class-based views. It is rarely necessary to overwrite either of dispatch or get/post. Instead, you should choose a more appropriate view to subclass, and override the specific methods it provides. In this case, since you want to display a single object, the appropriate base class is DetailView, and the method to override is get_object.
class MyClsName(DetailView):
template_name = "setup/code_install.html"
def get_object(self, queryset=None):
return MyModel.objects.get(Name=self.request.session['name'])
Note no need to define dispatch, get, or post at all.
In DRF, I have a simple ViewSet like this one:
class MyViewSet(viewsets.ViewSet):
def update(self, request):
# do things...
return Response(status=status.HTTP_200_OK)
When I try a PUT request, I get an error like method PUT not allowed. If I use def put(self, request): all things work fine. Accordingly to the docs I should use def update(): not def put():, why does it happen?
PUT needs id in URL by default
Sometimes there is the difference between POST and PUT, because PUT needs id in URL
That's why you get the error: "PUT is not Allowed".
Example:
POST: /api/users/
PUT: /api/users/1/
Hope it'll save a lot of time for somebody
Had a similar "Method PUT not allowed" issue with this code, because 'id' was missing in the request:
class ProfileStep2Serializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ('middle_initial', 'mobile_phone', 'address', 'apt_unit_num', 'city', 'state', 'zip')
class Step2ViewSet(viewsets.ModelViewSet):
serializer_class = ProfileStep2Serializer
def get_queryset(self):
return Profile.objects.filter(pk=self.request.user.profile.id)
Turned out that i have missed 'id' in the serializer fields, so PUT request was NOT able to provide an id for the record. The fixed version of the serializer is below:
class ProfileStep2Serializer(serializers.ModelSerializer):
class Meta:
model = Profile
fields = ('id', 'middle_initial', 'mobile_phone', 'address', 'apt_unit_num', 'city', 'state', 'zip')
This answer is right, Django REST framework: method PUT not allowed in ViewSet with def update(), PUT is not allowed, because DRF expects the instance id to be in the URL. That being said, using this mixin in your ViewSet is probably the best way to fix it (from https://gist.github.com/tomchristie/a2ace4577eff2c603b1b copy pasted below)
class AllowPUTAsCreateMixin(object):
"""
The following mixin class may be used in order to support PUT-as-create
behavior for incoming requests.
"""
def update(self, request, *args, **kwargs):
partial = kwargs.pop('partial', False)
instance = self.get_object_or_none()
serializer = self.get_serializer(instance, data=request.data, partial=partial)
serializer.is_valid(raise_exception=True)
if instance is None:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extra_kwargs = {self.lookup_field: lookup_value}
serializer.save(**extra_kwargs)
return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer.save()
return Response(serializer.data)
def partial_update(self, request, *args, **kwargs):
kwargs['partial'] = True
return self.update(request, *args, **kwargs)
def get_object_or_none(self):
try:
return self.get_object()
except Http404:
if self.request.method == 'PUT':
# For PUT-as-create operation, we need to ensure that we have
# relevant permissions, as if this was a POST request. This
# will either raise a PermissionDenied exception, or simply
# return None.
self.check_permissions(clone_request(self.request, 'POST'))
else:
# PATCH requests where the object does not exist should still
# return a 404 response.
raise
This is because the APIView has no handler defined for .put() method so the incoming request could not be mapped to a handler method on the view, thereby raising an exception.
(Note: viewsets.ViewSet inherit from ViewSetMixin and APIView)
The dispatch() method in the APIView checks if a method handler is defined for the request method.If the dispatch() method finds a handler for the request method, it returns the appropriate response. Otherwise, it raises an exception MethodNotAllowed.
As per the source code of dispatch() method in the APIView class:
def dispatch(self, request, *args, **kwargs):
...
...
try:
self.initial(request, *args, **kwargs)
# Get the appropriate handler method
if request.method.lower() in self.http_method_names:
# here handler is fetched for the request method
# `http_method_not_allowed` handler is assigned if no handler was found
handler = getattr(self, request.method.lower(),
self.http_method_not_allowed)
else:
handler = self.http_method_not_allowed
response = handler(request, *args, **kwargs) # handler is called here
except Exception as exc:
response = self.handle_exception(exc)
self.response = self.finalize_response(request, response, *args, **kwargs)
return self.response
Since .put() method handler is not defined in your view, DRF calls the fallback handler .http_method_not_allowed. This raises an MethodNotAllowed exception.
The source code for .http_method_not_allowed() is:
def http_method_not_allowed(self, request, *args, **kwargs):
"""
If `request.method` does not correspond to a handler method,
determine what kind of exception to raise.
"""
raise exceptions.MethodNotAllowed(request.method) # raise an exception
Why it worked when you defined .put() in your view?
When you defined def put(self, request): in your view, DRF could map the incoming request method to a handler method on the view. This led to appropriate response being returned without an exception being raised.
def update(self, request, pk=None):
data_in = request.data
print(data_in)
instance = self.get_object()
serializer = self.get_serializer(instance, data=request.data, partial=False)
serializer.is_valid(raise_exception=True)
if instance is None:
lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field
lookup_value = self.kwargs[lookup_url_kwarg]
extra_kwargs = {self.lookup_field: lookup_value}
serializer.save(**extra_kwargs)
return Response(serializer.data, status=status.HTTP_201_CREATED)
serializer.save()
data_out = serializer.data
return Response(serializer.data)
Using Django viewsets you can easily map the method in the url e.g.
path('createtoken/', CreateTokenView.as_view({'post': 'create', 'put':'update'}))
Then in your class override the methods as you please:
class CreateTokenView(viewsets.ModelViewSet):
queryset = yourSet.objects.all()
serializer_class = yourSerializer
def create(self, request, *args, **kwargs):
#any method you want here
return Response("response")
def update(self, request, *args, **kwargs):
# any method you want here
return Response("response")
I have multiple objects that are working with ModelViewSet and all have different (unique) fields for lookup.
So I came up with another solution for this question by defining put on a parent class and a new field lookup_body_field that can be used to associate payload with existing object:
class CustomViewSet(viewsets.ModelViewSet):
lookup_body_field = 'id'
def put(self, pk=None):
lookup_value = self.request.data.get(self.lookup_body_field)
if not lookup_value:
raise ValidationError({self.lookup_body_field: "This field is mandatory"})
obj = self.get_queryset().filter(**{self.lookup_body_field: lookup_value}).last()
if not obj:
return self.create(request=self.request)
else:
self.kwargs['pk'] = obj.pk
return self.update(request=self.request)
class MyViewSetA(CustomViewSet)
model = ModelA
lookup_body_field = 'field_a' # Unique field on ModelA
class MyViewSetB(CustomViewSet)
model = ModelB
lookup_body_field = 'field_b' # Unique field on ModelA
Suppose
you have registered a route like this (in urls.py)
router = DefaultRouter()
router.register(r'users', UserViewSet, basename='user-viewset')
urlpatterns += router.urls
and your restapi routs starts with /api/.
ViewSets generates follow routs.
create a user (send user object in body)
POST
/api/users/
get list of users
GET
/api/users/
get a user
GET
/api/users/{id}/
update a user (full object update)
PUT
/api/users/{id}/
partial update for a user
PATCH
/api/users/{id}/
delete a user
DELETE
/api/users/{id}/