Django REST framework checks unnecessary permissions - python

Am I using django rest framework (v3.4.6) object level permissions. However, I cannot figure out a few things. First I created a custom permission that checks if user works for a specific shop:
class Works4Shop(BasePermission):
def has_object_permission(self, request, view, obj):
profile = request.user.profile
if obj.shop in profile.shops.all():
return True
else:
return False
I then added permission_classes = (Works4Shop,) to a new custom view class ShopItemsView(APIView)
Now starts the curious part first I read that I need to explicitly check for object level permissions by calling self.check_object_permissions(request,obj). However what I see is that getting any object through model manager enforces the policy on retrieved objects. Well not exactly, it does call the has_object_permission(self, request, view, obj) but it ignores the result. The issue is the performance, this kind of thing creates to many unnecessary selects to DB. Can anyone explain this? I can also post logs from the DB.

So the answer was more simple than I thought. Basically this problem only occurs when using the browsable API. During rendering of the template there are many template tags for forms that use each kind of request specified in the View class (post,put,delete, etc.) and the object permissions is checked for each form individually. When I used pure json format everything started to work as it should, no unnecessary checks etc.
There is however one issue. The browsable api checks permissions for every object fetched which in turn creates an issue: you don't know what is the class of obj in has_object_permission(self, request, view, obj) so you should make an explicit check or the APIView will throw a TypeError

Related

Django queryset permissions

I am building a quite complex Django application to be used on top of and email scanning service. The Django application is written using Python 3.5+
This application primarily uses Django Rest Framework to handle communication with the frontend in the browser.
The issue that I am currently having is that I try to implement the concept of a System Administrator, Domain Administrator and Application User
The System Administrator is basically the "normal" django superuser and is therefore capable of doing everything and see every record in the system.
The Domain Administrator is user who manages one or more email domains. I keep track of this using a Many2Many relationship between the users and the domains. The idea would then be to predefine a filter, so that the log of messages processed, will be automatically filtered to show only messages where the sender domain or the recipient domain equal a domain in the list of domains that the given user is assigned to.
The same would be true for blacklisting/whitelisting policies.
If the Domain Administrator is not assigned to any domains, then no data is shown.
The Application User is basically any authenticated user with one or more domains assigned to them, using the same Many2Many relationship as the Domain Administrator. If no domains are assigned, then no data is shown.
I have found some other solution here on Stackoverflow on making the request.user available to the QuerySet in the ModelManager, but that does not seem like the correct way to handle it.
I have looked at django-guardian, django-authority and django-permissions, but none of them seem to be affecting the QuerySet or the resulting list of objects.
Does anyone have a suggestion for Django package/addon that can be used to handle this or maybe an idea for how this could be handled?
I'm the author of django-cancan library https://github.com/pgorecki/django-cancan which strives to solve the exact problem you are describing.
The philosophy is as following: first, you determine per-user abilities, then in a view, you can check user abilities for a given object, model, or you can retrieve a queryset based on those abilities.
The declaration part looks like this:
def declare_abilities(user, ability):
if not user.is_authenticated:
# Allow anonymous users to view only published articles
ability.can('view', Article, published=True)
else:
# logged in user can view any article...
ability.can('view', Article)
# ... and change his own
ability.can('change', Article, author=user)
# ... and add new ones
ability.can('add', Article)
if user.is_superuser:
# Allow superuser to view and change any article
ability.can('view', Article)
ability.can('change', Article)
Then you can you can check for abilites on a per-object level:
def article_detail_view(request, pk):
article = Article.objects.get(pk=pk)
if request.ability.can('view', article):
...
or on a model level:
def article_create_view(request, pk):
if request.ability.can('add', Article):
...
or get a queryset with accessible objects:
def another_list_view(request, pk):
articles = request.ability.queryset_for('view', Article)
...
DRF's GenericAPIView has a get_queryset method that you can override to perform custom filtering:
def get_queryset(self):
qs = super(YourView, self).get_queryset()
return self.filter_queryset_for_user(qs, request.user)
def filter_queryset_for_user(self, qs, user):
pass # Your logic here
This is not necessarily a bad idea; DRF docstrings recommend overriding this:
You may want to override this if you need to provide different
querysets depending on the incoming request.
I think you are misunderstanding the concept of permission in Django. django-guardian, django-authority and django-permissions these all packages are for handling permission inside your Django application. What permission does is it checks certain model or instance of model, if the user has permission to view that particular model or object, otherwise it will return 403 Unauthorized response. Permission does not change or filter your queryset to return only the valid results.
Rather if you want to apply filter your queryset, you can do so by the above answer, or you can move that code to a Mixin to follow DRY Style. For Mixin Reference you can see this link:
https://reinout.vanrees.org/weblog/2015/06/03/10-mixins.html
https://www.youtube.com/watch?v=0513I6_f2Tc
My answer to this question also provides an alternative to filter your queryset by subclassing rest_framework.filters.BaseFilterBackend and implement filter_queryset() based on your permission pattern , which would be suitable for more complicated use cases.

Django decorator #transaction.non_atomic_requests not working in a ViewSet method

I recently ran into the need to disable transaction requests in one of my views, in order to be able to call db.connection.close() and connect() during requests in an effort to improve performance.
I have a DRF ViewSet, and used the following very simple view to verify that the non_atomic_requests decoractor seems to have no effect. ATOMIC_REQUESTS=True is enabled in settings.py, and DEBUG=False.
from django.db import transaction
#transaction.non_atomic_requests
def create(self, *args, **kwargs):
m = MyModel(stuff="hello")
m.save()
raise Exception('exception! row should still be saved though')
return Response()
After calling the view, I open Django shell, and verify that the amount of rows in the db has not grown, even though it should have. Also opening a debugger during the request to halt execution after the line m.save(), I can observe in Django shell that a new row is not visible yet.
If I set ATOMIC_REQUESTS=False in settings.py, the code works as expected, and the number of rows in the db is grown by one, even if an error is raised before returning from the view.
When ATOMIC_REQUESTS=False, using #transaction.atomic decorator does work as expected though. So as a workaround, I could use it to set every other view as atomic instead...
I am currently thinking this is a bug in the framework. Can anybody verify my findings, or point out if I am misunderstanding how this decorator is meant to function?
I am using Python 3.6, Django 2.0 and DRF 3.7.7.
As documented, non_atomic_requests only works if it's applied to the view itself.
In your case, create is a viewset method, it is not the view itself. With a regular class based view in Django, you'd need to wrap the dispatch method using method_decorator.
#method_decorator(transaction.non_atomic_requests, name='dispatch')
class MyViewSet(ViewSet):
...
def create(self, *args, **kwargs):
...
I'm not familiar enough with the rest framework internals to say whether this will work or not. Note that it will disable atomic requests for all views handled by the viewset, not just the create method.
The non_atomic_requests method has this limitation because the Django request handler has to inspect the view before it runs so that it knows whether to run it in a transaction. The transaction.atomic decorator does not have the same requirement - Django can simply start the transaction as soon as it enters an atomic function or block.
In case you are using db other than 'default':
you need to explicitly mention the 'using' property.
otherwise, it would default to 'default'
transaction.non_atomic_requests(using='db_name')
in case of class based views-
either apply it on dispatch in views:
#method_decorator(transaction.non_atomic_requests(using='db_name'), name='dispatch')
class MyViewSet(ViewSet):
...
or apply it on as_view method in urls
path(r'endpoint/', transaction.non_atomic_requests(using='db_name')(MyViewSet.as_view()), name='myview')

Django - Login User without model

I am trying to implement an SSO login, deriving all the authorization rights from saml response:
class SAMLServiceProviderBackend(object):
def authenticate(self, saml_authentication=None):
if not saml_authentication: # Using another authentication method
return None
if saml_authentication.is_authenticated():
attributes = saml_authentication.get_attributes()
user = User(username=saml_authentication.get_nameid())
user.first_name = attributes['givenName']
user.last_name = attributes['sn']
return None
in views I got something like
...
user = authenticate(saml_authentication=auth)
login(self.request, user)
...
login fails because of the missing save() method. The only way would be to inherit from User and override the save method. Trying this, I got the next errors with is_authenticated, get_and_delete_messages, and so on
Is there an easy way to insert a user object into session, without saving the user to database?
Something like:
request.session['user'] = authenticate(saml_authentication=auth)
I guess should be possible with some limitations, eg. you cannot save your data with a user being a FK.
I have tried this myself, I suspect that you can dynamically create a user instance in the authenticate() method just don't call user.save(), or overwrite the save() method to do nothing.
You might also need to hook up the user record between requests, so you might need to write your own serializer for the user and load that construct the user instance back from session.
I'm fairly certain that Django's session objects are required for the authentication backend. i.e. if you login, then Django needs to store a reference to this fact somewhere. Django generally uses it's database to do this, however, if you're not using a database with your app then you can look at caching sessions instead:
https://docs.djangoproject.com/en/1.11/topics/http/sessions/#using-cached-sessions
(I'm assuming you're not using a database judging by your question)
More importantly however, depending on your needs, you may need to look at creating / configuring, a custom User Model in order to get your backend to work:
https://docs.djangoproject.com/en/1.11/topics/auth/customizing/#substituting-a-custom-user-model

Django: Filter request results to only contain data related to the requesting user

I'm a Django beginner (though I do have experience in web development using Sails.js + Angular) so bear with me.
I have an existing application that uses REST API in communicating between Sails.js backend and AngularJS frontend. Now, we've found the backend to be unsuited for our purposes, and we're going to swap to using Django in near-future. Sails.js automatically creates the REST methods for the controllers while Django doesn't, so I suppose I'm going to use something like Django Rest Framework to create the API.
So yeah, I've found corresponding features for most things. The on thing I haven't found yet is a replacement for a Sails.js feature called "policies". They are functions that can be executed on queries to certain controller actions, and can be defined as model-specific, model-controller action-specific, and request type specific. For example, you can have an "authAccess" policy that checks that the user of a request is authenticated, and the policy gets executed before the actual requested controller method gets executed. You can also use these to modify request objects before passing them to the controller. Now to my actual problem:
Let's say I have a User model that has a many-to-one relation with another model, let's call it Book, meaning a user can own many books, but a book can only have one owner. Goody good. Now, we have a logged-in user that is making a query to find all of his books. He makes a GET request to /book. I want to ensure that the returned Book objects are filtered so that ONLY HIS BOOKS are returned.
So basically in Sails I was able to write a policy that altered the request parameters to be like {user: loggedInUser} so the resulting Book results were automatically filtered. This was handy, since I was able to use the same policy to filter other models, too, like DVD or Friend or whatnot. My question is - what would be the best way to implement the same functionality in Django?
Have a look at the documentation:
http://www.django-rest-framework.org/api-guide/filtering/#filtering-against-the-current-user
Most likely you are better off overwriting the get_queryset method in a model viewset. And you can make this a generic approach by creating a base class for your views, something like:
from rest_framework import generics, viewsets, mixins, generics
class OwnerModelViewSet(viewsets.ModelViewSet):
def get_queryset(self):
"""
This view should return a list of all the records
for the currently authenticated user.
"""
return self.model.objects.filter(user=self.request.user)
All your model viewset classes can inherit from that class. It would require the foreign key field to be always named "user" though. If that is not the case here is a slightly hacky way how you could find a foreign key field to the User table. Use with care.
from django.db.models.fields.related import ForeignKey
from accounts.models import User
def _get_related_user(self, obj):
'''
Search for FK to user model and return field name or False if no FK.
This can lead to wrong results when the model has more than one user FK.
'''
for f in self.model._meta.fields:
if isinstance(f, ForeignKey) and f.rel.to == User:
return f.name
return False

Django Rest Framework permissions of POST object

I'm implementing a custom permission class for a Hyperlinked serializer that uses HyperlinkeRelatedFields and such.
The check is very roughly:
def has_permission(self, request,view):
if request.method in permissions.SAFE_METHODS:
return True
if not request.user.is_authenticated():
return False
if request.user.is_staff:
return True
# POST: 'author' is a URL due to serializer being Hyperlinked
# meaning we have to translate URL to model (like the serializer)
# to perform the check
if url_to_user(request.DATA['author']) == request.user:
return True
Given the comment in the code, it seems like it might be better to have the serializer do this check during validation, though that would shift concerns. It's almost like the validation class needs a method that is passed the new object before save() is called on it to check the POST/PUT was acceptable from a permissions point of view rather than cram HTTP/permissions related checks in to the validation in the serializer.
url_to_user is serialization specific, and checks in the validator portion of serializers would be request/http specific rather than just sanity/integrity checks of the new model.
It seems like a common thing to do so i'm curious which route others have taken and if there is a "more correct" approach i'm missing.
Thanks in advance.
Given that the comment says POST, I'll assume it's just the creation of a new object you're dealing with.
Since the author must be the request.user there's no need to have this as a writable field. I'd just set the object's author attribute to the current user in pre_save.
Similarly if you want to handle PUT you can limit the QuerySet to the current user too:
def get_queryset(self):
return MyClass.objects.filter(author=self.request.user)
(Looking at the request.method in permissions.SAFE_METHODS you may need to do this based on request method...)
That way a user cannot create an object not attached to themselves and cannot update one they do not already own.
However, as ever, TIMTOWTDI; there's nothing wrong with the permissions based route. (I'm not sure there's a final answer here.)

Categories

Resources