Django: Object level permissions DRY - django

Object level permissions
Example from http://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/#object-level-permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the snippet.
return obj.owner == request.user
My need: Queryset of all objects a user can edit
I want to have a django-orm queryset which contains all objects which a given user can edit.
I guess I could solve this by creating a complex django-orm filter (with OR and distinct)
Not DRY
But that's not DRY. That's not DRY because I need to code the stuff twice. One time in has_object_permission() and one time in the django-orm filter.
Question
How to solve my need (queryset of all objects a user can edit) without duplication the permission checking?

If you want that hard to keep things DRY, you'll have to load the entire database entries and apply permission check to every one.
I doubt that's what you really want.
Sometime you can't keep things DRY.
It's the same when you display data to a user. You'll usually apply basic permissions implicitly when performing the query and then ensure the full permissions are valid or not.

Related

Can someone explain when/how to use Queryset modification vs Permissioning with Django Rest Framework?

I am struggling to understand how permissioning in DRF is meant to work. Particularly when/why should a permission be used versus when the queryset should be filtered and the difference between has_object_permission() & has_permission() and finally, where does the serializer come in.
For example, with models:
class Patient(models.Model):
user = models.OneToOneField(User, related_name='patient')
class Appointment(models.Model):
patient = models.ForeignKey(Patient, related_name='appointment')
To ensure that patients can only see/change their own appointments, you might check in a permission:
class IsRelevantPatient(BasePermission):
def has_object_permission(self, request, view, obj):
if self.request.user.patient == obj.appointment.patient:
return True
else:
return False
But, modifying the queryset also makes sense:
class AppointmentViewSet(ModelViewSet):
...
def get_queryset(self):
if self.request.user.is_authenticated:
return Appointment.objects.filter(patient=self.request.user.patient)
What's confusing me is, why have both? Filtering the queryset does the job - a GET (retrieve and list) only returns that patient's appointments and, a POST or PATCH (create or update) only works for that patient's appointments.
Aside from this seemingly redundant permission - what is the difference between has_object_permission() & has_permission(), from my research, it sounds like has_permission() is for get:list and post:create whereas has_object_permission() is for get:retrieve and patch:update. But, I feel like that is probably an oversimplification.
Lastly - where does validation in the serializer come in? For example, rather than a permission to check if the user is allowed to patch:update an object, You can effectively check permissions by overriding the update() method of the serializer and checking there.
Apologies for the rambling post but I have read the docs and a few other question threads and am at the point where I am probably just confusing myself more. Would really appreciate a clear explanation.
Thanks very much.
First, difference between has_object_permission() and has_permission() :
has_permission() tells if the user has the permission to use the view or the viewset without dealing with any object in the database
has_object_permission() tells if the user has the permission to use the view or the viewset based on a specific object in the database.
The important note thaw is that DRF wont perform the test itself in the case of object level permission, but you have to do it explicitly by calling check_object_permission() somewhere in your view (doc here).
The second important note is that DRF will not filter the result of the query based on object permission. If you want the query to be filtered, then you have to do it yourself (by overriding get_queryset() like you did or using a filter backend), that's the difference.
The serializer has nothing to do with permission neither with filtering. It handles objects one by one, applying validation (not permission) on each field of each objects.

Best practice for permissions/filters in Django Rest Framework

I'm new in DRF and can't understand the best way to give acess only to information owned by user. I have a view:
class InvoiceAPIView(generics.ListAPIView):
serializer_class = InvoiceSerializer
permission_classes = [permissions.IsAuthenticated]
def get_queryset(self):
return Invoice.objects.filter(company__user=self.request.user)
Is it good enough to use filter method or maybe I should add custom permission or even go to serialization. Thanks!
If you need to limit list of objects with only those owned by current user then overriding queryset is OK.
Permission classes using to check, well, permissions based on view or object level. It would be good to use permissions if you need allow all users to see all objects, but only owner can edit specific object.

What url structure should I use when retrieving users via a token?

I'm using Django Rest Framework to handle token authentication for a mobile application for a school. In particular, when a student logs in, the mobile application sends a token to my Django backend, which then combines data from its database and some external data from another source. I found it easiest to use a generic RetrieveAPIView to accomplish what I needed.
My code is working, and my main question is around the url. For most retrievals, we usually have the primary key as well (e.g. /students/SOME-ID), but in this case, I'm using the token to retrieve the user rather than the primary key. In fact, if SOME-ID passed in was different from the Token, the user associated with the Token would be returned anyway (which seems kinda strange).
I'm wondering whether it is better to have my url route be just (/students) instead though this seems to be a list rather than a retrieve operation.
WHAT I HAVE NOW
http://localhost:8000/api/v1/revision/students/1
IS THIS BETTER
http://localhost:8000/api/v1/revision/students/
CODE
class StudentView(generics.RetrieveAPIView):
model = Student
serializer_class = StudentSerializer
# combines data from both current and legacy database
def retrieve(self, request, pk=None):
obj = get_object_or_404(Student, user=request.user)
# KIV -> unsure if this is the best way to combine data from legacy and current database
# or should it be done in the serializer
data = StudentSerializer(obj).data
# combines existing data stored in database with legacy data from database
legacy_data = SOME_EXTERNAL_API_SERVICE.get_student_info(obj)
data['avatar'] = legacy_data['avatar']
data['coins'] = legacy_data['coins']
return Response(data)
I would definitely not use /students/id/ with the behaviour you're describing: This URL should always return the student with the given id of error (depending on whether the user fetching this resource is allowed to do so). You might want to use this URL for admins to view students in the future.
And for the same reason, I wouldn't use /students/ because I'd expect it to return a list of all students, or at least the list of all students the particular logged in user is allowed to see. This might fit your purpose now (where the logged in user can only see himself), but maybe not in the future if you create new roles that can view more students.
There are two approaches here:
Either you treat this as a filter on all the students: /students/?current=true which I personally find ugly because you're not actually filtering on the total set of students.
Or you treat this as a special case: /students/current using a special keyword for fetching this one specific student.
I would choose the latter one because it is more descriptive and easier to understand when looking at the API. Note of course that id can never be 'current' in this case, which is why some people discourage this kind of special resource queries and opt for the first option.
Definitely, the url http://localhost:8000/api/v1/revision/students/ looks better.
But you don't need to write this in a RetrieveAPIView, you could always do this in base APIView,
class StudentView(APIView):
def get(self, request, *args, **kwargs):
obj = get_object_or_404(Student, user=request.user)
data = StudentSerializer(obj).data
legacy_data = SOME_EXTERNAL_API_SERVICE.get_student_info(obj)
data['avatar'] = legacy_data['avatar']
data['coins'] = legacy_data['coins']
return Response(data)
By using like this, you can avoid the extra pk keyword argument from your url.

Django admin for User's objects

I would like to allow a User to have an admin interface to their own Video objects. I was planning on writing some views that allowed things like setting attributes like "published" or deleting objects.
I've started looking into using django's Admin site - but that seems like it might be overly complicated for what I want (just deleting/ setting the published attribute).
Is one approach better than the other? Writing something from scratch or using the Admin site?
If I were to write something from scratch - what is the correct way to achieve ModelAdmin style actions (ie. delete_selected(queryset, request))
This is exactly what the admin should be used for! How could it be too complicated? Even writing a handful of lines of HTML would take longer.
If you built this yourself, no matter how simple, you'll have to define views that list objects, validate input, check permissions, write HTML, implement some kind of multiple action system that maps to python code, ....
Assuming you don't want to do that:
You're going to want to look into making multiple admin sites and filtering admin results to only those that belong to the user via overriding the queryset method on a ModelAdmin
# pasted from docs
class MyModelAdmin(admin.ModelAdmin):
def queryset(self, request):
qs = super(MyModelAdmin, self).queryset(request)
if request.user.is_superuser:
return qs
return qs.filter(author=request.user)

Django admin -- restricting access by user

I was wondering if the django admin page can be used for external users.
Let's say that I have these models:
class Publisher(models.Model):
admin_user = models.ForeignKey(Admin.User)
..
class Publication(models.Model):
publisher = models.ForeignKey(Publisher)
..
I'm not exactly sure what admin_user would be -- perhaps it could be the email of an admin user?
Anyways. Is there a way allow an admin user to only add/edit/delete Publications whose publisher is associated with that admin user?
-Thanks!
-Chris
If you need finer-grained permissions in your own applications, it should be noted that Django's administrative application supports this, via the following methods which can be overridden on subclasses of ModelAdmin. Note that all of these methods receive the current HttpRequest object as an argument, allowing for customization based on the specific authenticated user:
queryset(self, request): Should return a QuerySet for use in the admin's list of objects for a model. Objects not present in this QuerySet will not be shown.
has_add_permission(self, request): Should return True if adding an object is permitted, False otherwise.
has_change_permission(self, request, obj=None): Should return True if editing obj is permitted, False otherwise. If obj is None, should return True or False to indicate whether editing of objects of this type is permitted in general (e.g., if False will be interpreted as meaning that the current user is not permitted to edit any object of this type).
has_delete_permission(self, request, obj=None): Should return True if deleting obj is permitted, False otherwise. If obj is None, should return True or False to indicate whether deleting objects of this type is permitted in general (e.g., if False will be interpreted as meaning that the current user is not permitted to delete any object of this type).
[django.com]
I see chris's answer was useful at the time question was asked.
But now it's almost 2016 and I guess it gets more easier to enable restricted access of Django Admin panel to end user.
Django authentication system provides:
Groups: A generic way of applying labels and permissions to more than one user.
Where one can add specific permissions and apply that group to user via admin panel or with writing codes.
After adding user to those specific groups, Admin need to enable is_staff flag for those users.
User will be able access restricted registered models in admin.
I hope this helps.
django admin can, to a certain extent, be restricted. For a given user, first, they must have admin rights in order to log into the admin site. Anyone with this flag set can view all admin pages. If you want to restrict viewing, you're out of luck, because that just isn't implemented. From there, each user has a host of permissions, for create, update and delete, for each model in the admin site. The most convenient way to handle this is to create groups, and then assign permissions to the groups.