Django Rest Framework permissions of POST object - python

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.)

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 ModelForm 'instance' param not working as expected

I am using Django's forms to validate API PATCH requests. In the "view" (which I use in quotes because it isn't really a view directly, it is a restless Resource, but that should be irrelevant here) which handles this patch request, self.data contains a dictionary of changes to some of the fields of the License object. I want to instantiate a ModelForm with the instance of the object to be changed. Clearly, though, I am misunderstanding how this works. See below:
def handle_patch(self, pk):
license = License.objects.get(id=pk)
form = LicenseResourceForm(self.data, instance=license)
if not form.is_valid():
print(form.errors)
If I pass a few fields as data to the above function, form.errors complains about every other required field of the License model, meaning I'm clearly not understanding how setting an instance on a ModelForm works.
I added a few debug prints to Django's ModelForm code itself in the clean() method, and as it begins to do the cleaning process, I can see that self.instance is populated with the instance of License that I expect, which confuses me - the ModelForm object knows the instance, but isn't using it to "fill in the blanks" so to speak.
So what am I misunderstanding? I must be doing this wrong.
EDIT I realize that some of you may want to see the LicenseResourceForm itself, so here it is, including my debug print:
class LicenseResourceForm(ModelForm):
"""Form for License Resource create and change endpoints."""
class Meta(object):
model = License
fields = ['customer', 'service', 'enabled', 'not_valid_before', 'not_valid_after']
def clean(self):
try:
print(self.instance)
super().clean()
except Exception as e:
print(e)
Django forms aren't meant for API use and don't understand PATCH semantics. They are meant for the workflow of a user entering or changing data in a web form, which will always post all the data to the backend. Therefore, all fields listed in the fields attribute of the form will be checked against the data, and any missing fields will be validated as blank.
You could probably fix this by doing something clever to dynamically set the list of fields based on the data supplied, but really you should use the appropriate tool for validating your data. I don't know restless, but django-rest-framework has serializers which can be used for this.

Django REST framework checks unnecessary permissions

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

Django: What is the first method that is called in by as_view() in form views

I have a URL conf as /book/(?P<book_id>\d+)/(?P<page_id>\d+)/edit/ wheredjango.views.generic.edit.FormView as handler to this endpoint.
As soon as the request is made, I have to check whether there is a Book object is present with book_id and the page number is present in respective Book using page_id.
Where exactly would be the appropriate place to do this processing? I would also like to store them as instance attributes for further processing wherever required.
Edit
Also, the tricky part here is that the form_class used here is totally dependent on the objects found!
Currently in our function based views, a decorator is used to the method which does the job. In class based views, I am not sure what's the place to do it.
In typical function base view
def get_object_or_404(view_function):
def decorator(request, book_id, page_id):
return view_function(request, book, page)
return decorator
#get_book_or_404
def edit_book(self, book, page):
# process the page
return HttpResponse()
I am basically figuring out the simple approach equivalent of get_book_or_404 for class based views
It doesn't matter, and you shouldn't care, what is the first method called in the view. The point is to put logic where it belongs. In this case, since you're concerned with getting instances, you should probably override get_object. In that method you can return the book object, but also store both book and page objects as instance attributes.

Excluding fields in generic CRUD views

I have a model named Domain which looks like this:
class Domain(models.Model):
"""
Model for storing the company domains
"""
user = models.ForeignKey(
User
)
host = models.CharField(
null=False, verbose_name="Host", max_length=128, unique=True
)
I'd like to use Django's generic views for doing CRUD operations on this. There is one field in this model that needs user input but the foreign key field doesn't need any user input. How can I exclude that field from the form that my generic view generates but assign it the value of the current authenticated user.
Thanks.
Have a look at Russel's answer to a similar question on the django-users group earlier this week.
Quoting the answer*:
Forms and Views solve different problems.
The View is solving the problem of "how do I handle this request and
convert it into a response?". The Form is solving the problem of "How
do I convert the POST data in this request into a model object (or a
change to a model object)?".
Very roughly, a view is doing the following:
View gets a request
View works out whether this is a GET or a POST
If its a POST, View asks the Form to turn the Post into a model change
Form returns success or failure
View responds to the success or failure of the Form.
View returns a response.
The functionality of the Form is a complete subset of the
functionality of the View -- and for this reason, it's a completely
interchangable internal component.
Now, in simple situations, it's possible for a View to guess all the
defaults for the form -- all it needs to know is that you're dealing
with a Foo model, and it can construct a default Foo ModelForm.
However, if you have more sophisticated form requirements, you're
going to need a customized Form.
We could have implemented this by exposing all the options of
ModelForm on the View class; but in order to keep everything clean, we
kept the ModelForm isolated, and provided the View with a way to
specify which Form class it's going to use.
So - to cover your use case of excluding fields, you define a
ModelForm that excludes the fields, then let the CreateView know the
form you want to use:
class CampaignForm(forms.ModelForm):
class Meta:
model = Campaign
exclude = ('user', 'name', 'content_inlined')
class CreateCampaignView(CreateView):
form_class = CampaignForm
template_name = "forms/create.html"
I'm guessing when you say "fix a values for a field", you mean setting
the values of user, name and content_inlined before you save the new
Campaign instance; to do this, you need to inject some extra code into
the form processing logic of the form:
class CreateCampaignView(CreateView):
form_class = CampaignForm
template_name = "forms/create.html"
def form_valid(self, form):
form.instance.user = ... (something meaningful.. e.g., self.request.user)
return super(CreateCampaignView, self).form_valid(form)
This overrides the default behavior when the form is valid, and sets
the extra values. The super() implementation of form_valid() will then
save the instance.
For the record, this could also be done by overriding the save()
method on the ModelForm -- however, if you do that, you lose the
request object, which you will need if you're trying to set the
instance values to something that is request-sensitive.
*the original answer set self.object.user instead of form.instance.user. This gives an AttributeError so I have changed it above.

Categories

Resources