I looked at the code for CreateAPIView and nowhere in the creation process are permissions checked. So I decided to check them in the perform_create hook as follows:
class CourseList(generics.CreateAPIView):
"""
Create a course.
A user can create a course if he/she is an instructor in
the academy the course is to be created in.
"""
serializer_class = CourseRef
queryset = Course.objects.all()
permission_classes = (IsAuthenticated, IsInstructorInAcademy)
def perform_create(self, serializer):
self.check_object_permissions(self.request, serializer.validated_data.get('academy'))
serializer.save()
where the permission looks like:
class IsInstructorInAcademy(BasePermission):
def has_object_permission(self, request, view, academy):
return request.user.instructor_set.filter(academy=academy).exists()
When I try to create a course, the initial permission check runs fine and the course is saved but immediately afterwards I get the following error:
ValueError at /api/courses/
Cannot query "Mathematics 11A": Must be "Academy" instance.
and this is raised at the permissions check, as a result of which the POST returns a 500 ISE.
(This is slightly hacky because I'm passing in the academy instance in the permissions check in the perform_create hook whereas the view really concerns courses. I considered using a permission IsInstructorInAcademyOfObject but for that, I would need to pass in an object which I cannot get unless the serializer is saved.)
What is the best way to check for permissions here?
You have an issue because you're tying business logic checks with permissions.
Instead of calling check_object_permission, perform your own check once it's deserialized and raise a PermissionDenied if it fails.
Related
I'm trying to make a custom permission using this guide
views.py
class CustomModelList(generics.ListAPIView):
queryset = CustomModel.objects.all()
serializer_class = CustomModelSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsCustomOrReadOnly]
def get(self, request, format=None):
# some logic
def post(self, request, format=None):
# some logic
Just for experiment I've created this permission not to apply anyway
pesmissions.py
class IsCustomOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
return False
But when POST request sends to server it takes no effect -- I'm able to create new model instance.
I think that since you are using a list view, custom object level permissions are not checked automatically.
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the filtering documentation for more details.
You can try overriding the has_permission method instead and see if that works, or check the permissions manually.
I am trying to make an api backend of something like reddit. I want to ensure that whoever is creating a post (model Post) within a particular subreddit is a member of that subreddit (subreddit model is Sub). Here is my latest effort, which works but seems pretty sloppy, and the serializer for some context.
Post permissions.py
class IsMemberOfSubOrReadOnly(BasePermission):
def has_permission(self, request, view):
if request.method in permissions.SAFE_METHODS:
return True
elif request.data:
# prevent creation unless user is member of the sub
post_sub_pk = get_pk_from_link(request.data['sub'])
user = request.user
user_sub_pks = [sub.pk for sub in user.subs.all()]
if not (post_sub_pk in user_sub_pks):
return False
return True
Post serializers.py
from .models import Post
from redditors.models import User
from subs.models import Sub
class PostSerializer(serializers.HyperlinkedModelSerializer):
poster = serializers.HyperlinkedRelatedField(
view_name='user-detail',
#queryset=User.objects.all(),
read_only=True
)
sub = serializers.HyperlinkedRelatedField(
view_name='sub-detail',
queryset=Sub.objects.all()
)
class Meta:
model = Post
fields = ('url', 'id', 'created', 'updated', 'title', 'body',
'upvotes', 'sub', 'poster')
The issue with this approach is that since 'sub' is a hyperlinkedRelatedField on the Post serializer what I get back from request.data['sub'] is just the string hyperlink url. I then have a function, get_pk_from_link that uses regex to read the pk off the end of the url. Then I can use that to grab the actual model I want and check things. It would be nice if there were a more direct way to access the Sub model that is involved in the request.
I have tried searching the fields of the arguments that are available and I can't find a way to get to the Sub object directly. Is there a way to access the Sub model object through the hyperlink url?
I have also solved this problem by just using a serializer field validator (not shown above), but I am interested to know how to do it this way too. Maybe this is just a bad idea and if so please let me know why.
You are right, parsing the url is not the way to go. Since you want to perform the permission check before creating a Post object, I suspect you cannot use object level permissions either, because DRF does not call get_object in a CreateAPIView (since the object does not exist in the database yet).
Considering this is a "business logic" check, a simpler approach would be to not have that permission class at all and perform the check in your perform_create hook in your view (I had asked a similar question about this earlier):
from rest_framework.exceptions import PermissionDenied
# assuming you have a view class like this one for creating Post objects
class PostList(generics.CreateApiView):
# ... other view stuff
def perform_create(self, serializer):
sub = serializer.get('sub') # serializer is already validated so the sub object exists
if not self.request.user.subs.filter(pk=sub.pk).exists():
raise PermissionDenied(detail='Sorry, you are not a member of this sub.')
serializer.save()
This saves you hassle of having to perform that url parsing as the serializer should give you the Sub object directly.
Which is the proper way to test object based permissions?
Sample:
from rest_framework import permissions
class IsOfficeAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
office = obj
return office.admin == request.user
Which are the "asserts" that I shouldn't miss?
Do I need to create a view?
To your questions:
it's up to you to write the logic which will allow a user to access the object. As a result, you have to return a Boolean.
yes. You will specify to the view which permission classes you want to apply. In case of object permissions they will be queried on detail routes (get, update, delete)
I'm developing a REST API which takes POST requests from some really brain-dead software which can't PATCH or anything else. The POSTs are to update Model objects which already exist in the database.
Specifically, I'm POSTing data for objects with a related field (a SlugRelatedField, as the POSTer knows the 'name' attribute but NOT the 'pk'). However, I need to return a 404 if the POSTer sends data where the 'name' returns nothing on the SlugRelatedField (e.g. the related object does not exist). I've been through this with a debugger but it seems that DRF uses some Django signals magic to do it The Way DRF Does Itâ„¢, which is to return a 400 BAD REQUEST. I don't know how to modify this - only when it's the above condition and not a true 400-worthy POST - into a 404.
By the way, pre_save() in my view is NOT executing during execution of the failing test.
Here's the serializer:
class CharacterizationSerializer(serializers.ModelSerializer):
"""
Work-in-progress for django-rest-framework use. This handles (de)serialization
of data into a Characterization object and vice versa.
See: http://www.django-rest-framework.org/tutorial/1-serialization
"""
creator = serializers.Field(source='owner.user.username')
sample = serializers.SlugRelatedField(slug_field='name',
required=True,
many=False,
read_only=False)
class Meta:
model = Characterization
# leaving 'request' out because it's been decided to deprecate it. (...maybe?)
fields = ('sample', 'date', 'creator', 'comments', 'star_volume', 'solvent_volume',
'solution_center', 'solution_var', 'solution_minimum', 'solution_min_stddev',
'solution_test_len',)
And here's the view where pre_save isn't being run in the given test (but does get run in some others):
class CharacterizationList(generics.ListCreateAPIView):
queryset = Characterization.objects.all()
serializer_class = CharacterizationSerializer
permission_classes = (AnonPostAllowed,) # #todo XXX hack for braindead POSTer
def pre_save(self, obj):
# user isn't sent as part of the serialized representation,
# but is instead a property of the incoming request.
if not self.request.user.is_authenticated():
obj.owner = get_dummy_proxyuser() # this is done for CharacterizationList so unauthed users can POST. #todo XXX hack
else:
obj.owner = ProxyUser.objects.get(pk=self.request.user.pk)
# here, we're fed a string sample name, but we need to look up
# the actual sample model.
# #TODO: Are we failing properly if it doesn't exist? Should
# throw 404, not 400 or 5xx.
# except, this code doesn't seem to be run directly when debugging.
# a 400 is thrown; DRF must be bombing out before pre_save?
obj.sample = Sample.objects.get(name=self.request.DATA['sample'])
And for good measure, here's the failing test:
def test_bad_post_single_missing_sample(self):
url = reverse(self._POST_ONE_VIEW_NAME)
my_sample_postdict = self.dummy_plqy_postdict.copy()
my_sample_postdict["sample"] = "I_DONT_EXIST_LUL"
response = self.rest_client.post(url, my_sample_postdict)
self.assertTrue(response.status_code == 404,
"Expected 404 status code, got %d (%s). Content: %s" % (response.status_code, response.reason_phrase, response.content))
If I put a breakpoint in at the self.rest_client.post() call, the response already has a 400 in it at that point.
You can use a Django Shortcut for that, getting the obj.sample:
from django.shortcuts import get_object_or_404
obj.sample = get_object_or_404(Sample, name=self.request.DATA['sample'])
Instead of using pre_save why not override post in your API view:
def post(self, request, *args, **kwargs):
...other stuff
try:
obj.sample = Sample.objects.get(name=self.request.DATA['sample'])
...or whatever other tests you want to do
except:
return Response(status=status.HTTP_404_NOT_FOUND)
response = super(CharacterizationList, self).post(request, *args, **kwargs)
return response
Make sure you import DRF's status:
from rest_framework import status
Also, note you will likely want to be more specific with the Exceptions you catch. Django's get method will return either DoesNotExist if nothing matches or MultipleObjectsReturned if more than one object matches. The relevant documentation:
Note that there is a difference between using get(), and using
filter() with a slice of [0]. If there are no results that match the
query, get() will raise a DoesNotExist exception. This exception is an
attribute of the model class that the query is being performed on - so
in the code above, if there is no Entry object with a primary key of
1, Django will raise Entry.DoesNotExist.
Similarly, Django will complain if more than one item matches the
get() query. In this case, it will raise MultipleObjectsReturned,
which again is an attribute of the model class itself.
I'm nearing what I think is the end of development for a Django application I'm building. The key view in this application is a user dashboard to display metrics of some kind. Basically I don't want users to be able to see the dashboards of other users. Right now my view looks like this:
#login_required
#permission_required('social_followup.add_list')
def user_dashboard(request, list_id):
try:
user_list = models.List.objects.get(pk=list_id)
except models.List.DoesNotExist:
raise Http404
return TemplateResponse(request, 'dashboard/view.html', {'user_list': user_list})
the url for this view is like this:
url(r'u/dashboard/(?P<list_id>\d+)/$', views.user_dashboard, name='user_dashboard'),
Right now any logged in user can just change the list_id in the URL and access a different dashboard. How can I make it so a user can only view the dashboard for their own list_id, without removing the list_id parameter from the URL? I'm pretty new to this part of Django and don't really know which direction to go in.
Just pull request.user and make sure this List is theirs.
You haven't described your model, but it should be straight forward.
Perhaps you have a user ID stored in your List model? In that case,
if not request.user == user_list.user:
response = http.HttpResponse()
response.status_code = 403
return response
I solve similiar situations with a reusable mixin. You can add login_required by means of a method decorator for dispatch method or in urlpatterns for the view.
class OwnershipMixin(object):
"""
Mixin providing a dispatch overload that checks object ownership. is_staff and is_supervisor
are considered object owners as well. This mixin must be loaded before any class based views
are loaded for example class SomeView(OwnershipMixin, ListView)
"""
def dispatch(self, request, *args, **kwargs):
self.request = request
self.args = args
self.kwargs = kwargs
# we need to manually "wake up" self.request.user which is still a SimpleLazyObject at this point
# and manually obtain this object's owner information.
current_user = self.request.user._wrapped if hasattr(self.request.user, '_wrapped') else self.request.user
object_owner = getattr(self.get_object(), 'author')
if current_user != object_owner and not current_user.is_superuser and not current_user.is_staff:
raise PermissionDenied
return super(OwnershipMixin, self).dispatch(request, *args, **kwargs)
You need to have some information stored about what list or lists a user can access, and then include that in the user_list lookup. Let's assume the simple case where List has a single owner, a foreign key to the User model. That's a many-to-one relationship between lists and users; no list is owned by more than one user, but a user can have multiple lists. Then you want something like this:
try:
user_list = models.List.objects.get(pk=list_id, owner=request.user)
except models.List.DoesNotExist:
raise Http404
Whether to return 404 or 403 is to some extent a matter of opinion; the definition for 403 says:
If the request method was not HEAD and the server wishes to make public why the request has not been fulfilled, it SHOULD describe the reason for the refusal in the entity. If the server does not wish to make this information available to the client, the status code 404 (Not Found) can be used instead.
http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.4
If you do return a 404, you can use the django shortcut function get_object_or_404 instead of the explicit try/except - there's nothing wrong with doing it explicitly, but the need is common enough that there's a convenience function to do it.