How do you add permissions to a model so that any user can add a new instance, but only a logged in user can add a particular attribute?
Django models.py:
class Ingredient(models.Model):
name = models.CharField(max_length=100, unique=True)
recipes = models.ManyToManyField(Recipe, related_name='ingredients', blank=True)
DRF views.py:
class IngredientViewSet(viewsets.ModelViewSet):
queryset = Ingredient.objects.all()
serializer_class = IngredientSerializer
permission_classes = (IsUserForRecipeOrBasicAddReadOnly,)
DRF permissions.py:
class IsUserForRecipeOrBasicAddReadOnly(permissions.BasePermission):
"""
Custom permission to only allow logged in users to add an ingredient AND associate its recipe(s).
"""
message = 'You must be logged in to add an Ingredient to a Recipe.'
# using this method so I can access the model obj itself
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
if request.method in permissions.SAFE_METHODS:
return True
# Check if we are creating, and if the recipes are included, and if they are not a user. If so, return False
if request.method == 'POST' and obj.recipes.all().count() > 0 and request.user.is_anonymous:
return False
else:
return True
I see the appropriate calls/prints to the custom permission class, but I can still make a POST request with a list of recipe id's and it does not error with the message.
Notes -
I am getting two POST requests, both with the same information/print statements, and then a third to the GET (once it is added, it shows the newly created instance - which is correct behavior, but I don't know why two POSTs are going through)
I think a better approach would be to use 2 different serializer(one of them hase Recipes as a writable field and the other does not), then override get_serializer_class:
class yourMOdelviewset():
...
...
def get_serializer_class(self):
if self.action == 'create':
if self.request.user.is_authenticated:
return SerializerThatHasRecipesAsAWriteableField
else:
return SerializerThatHasNot
return super().get_serializer_class()
p.s. Drf uses object level permission for retrieving or updating (basically there should be an object already), since in create there is no object yet, drf never checks the object level permission.
The solution proposed by #changak is a good one. Including this as a more direct solution to the question posed. In DRF, has_object_permission is explicitly for an object already in the database, but you can use has_permission. From the docs, this excerpt explains why you don't see has_object_permission being called:
Note: The instance-level has_object_permission method will only be
called if the view-level has_permission checks have already passed.
In has_permission, you still have access to the data, and can add a check. Assuming your IngredientSerializer has a recipes field, you can check with something like this:
class IsUserForRecipeOrBasicAddReadOnly(permissions.BasePermission):
"""
Custom permission to only allow logged in users to add an ingredient AND associate its recipe(s).
"""
message = 'You must be logged in to add an Ingredient to a Recipe.'
def has_permission(self, request, view):
if view.action != 'create':
# Handle everything but create at the object level.
return True
if not request.data.get('recipes'):
return True
return request.user and request.user.is_authenticated()
Related
TL;DR
How to write a custom permission class to check object-level permissions (has_object_permission) before the api-level ones (has_permission)?
Definitions
Suppose we are building an online annotation tool. We have Images belonging to Projects and Users working on annotating Images. Users have a role field -- Guests have read-only access, Developers may change the Image fields and Owners may change both Projects and Images.
The models:
class Image(models.Model):
data = JSONField()
project = models.ForeignKey(Project)
class Project(models.Model):
pass
class User(AbstractUser):
ROLE_GUEST, ROLE_DEVELOPER, ROLE_OWNER = range(3)
ROLE_CHOICES = [(ROLE_GUEST, "Guest"), (ROLE_DEVELOPER, "Developer"), (ROLE_OWNER, "Owner")]
role = models.IntegerField(default=ROLE_GUEST, choices=ROLE_CHOICES)
Suppose we wish to restrict access to and within some Projects. We are adding m2m model UserProjectRole:
class UserProjectRole(models.Model):
user = models.ForeignKey(User)
project = models.ForeignKey(Project)
project_role = ... # same as in User
Now we wish to set up permissions in a way that, if user requests access to API endpoints regarding some Project:
if there is no UserProjectRole with this user and this project, permissions defaults to his global role
if there is such UserProjectRole, the permissions are using his project_role instead of role
Problem
We are implementing custom permission class:
# permissions.py
from rest_framework import permissions
class AtLeastDeveloper(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.is_staff or request.user.role >= User.ROLE_DEVELOPER
class AtLeastOwner(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.is_staff or request.user.role >= User.ROLE_OWNER
class InProjectPermissions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if isinstance(obj, Project):
project = obj
elif isinstance(obj, Image):
project = obj.project
else:
raise ValueError(obj)
upr = UserProjectRole.objects.filter(user = request.user, project = project).first()
if upr is None:
return False
if isinstance(obj, Project):
return upr.role == User.ROLE_OWNER
return upr.role >= User.ROLE_DEVELOPER
# views.py
class ProjectGetDeleteUpdateView(RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticated, AtLeastOwner, InProjectPermissions]
...
class ImageGetDeleteUpdateView(RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticated, AtLeastDeveloper, InProjectPermissions]
...
The problem here is that has_permission is called before has_object_permissions (as it is stated in https://www.django-rest-framework.org/api-guide/permissions/#custom-permissions). Therefore, if the User's global role is not sufficient, he will be denied access (IsAtLeastSomething.has_permissions will return False and therefore InProjectPermissions.has_object_permissions will not be executed).
Please advise on how to write a custom permission class to check object-level permissions before the api-level ones.
TLDR; - Try overriding check_object_permissions(request, obj)
As per DRF documentations :
Before running the main body of the view each permission in the list
is checked. If any permission check fails an
exceptions.PermissionDenied or exceptions.NotAuthenticated exception
will be raised, and the main body of the view will not run.
Which means all the permissions which we have mentioned in permission_classes MUST pass. If anyone of them also fails, then it'll not authenticate.
So for our custom need we can override check_object_permissions(request, obj) and write our custom authentication.
So, that's what i came up with.
I could, as suggested by #Hafnernuss, get access to the object in question by view.get_object(id) (taking id from request.GET/POST), but this is exactly what i wished to avoid: manual tackling with internals of structures.
#umair-mohammad suggested to override view.check_object_permissions, but again, that i wished to avoid, because it meant that all my views should be inherited from one base class. In most cases this is OK and thank you for your answer -- that indeed solves the problem -- but i really wanted to separate this logic into permissions.py without touching the views.
So, let's instead override the base permission class:
class AbstractPermission(permissions.BasePermission):
def has_permission(self, request, view):
return True
def has_permission_by_global_role(self, request, view):
raise NotImplementedError()
def has_object_permission(self, request, view, obj):
if isinstance(obj, Project):
project = obj
elif isinstance(obj, Image):
project = obj.project
else:
raise ValueError(obj)
# if user has global role big enough -- permit
if self.has_permission_by_global_role(request, view):
return True
# otherwise -- assess his 'local' role (as before)
upr = UserProjectRole.objects.filter(user = request.user, project = project).first()
if upr is None:
return False
if isinstance(obj, Project):
return upr.role == User.ROLE_OWNER
return upr.role >= User.ROLE_DEVELOPER
Then we inherit our AtLeastSomething classes, changing has_permission to has_permission_by_global_role:
class AtLeastOwner(AbstractPermission):
def has_permission_by_global_role(self, request, view):
return request.user.is_staff or request.user.role >= User.ROLE_OWNER
...
Thus, in views.py, we only need to specify permission_classes based on a permitted global role -- in our case, that is AtLeastOwner for Project and AtLeastDeveloper for Images.
I believe this is the most pythonic approach to this. One can further make this cleaner by providing each model with something like .allowed_roles() method.
I'm still struggling with django-filter. I have my filter defined below
class MasterListFilter(django_filters.FilterSet):
project = django_filters.ModelChoiceFilter(
label='Projects',
name='project_fkey',
queryset=Project.objects.filter(deleted__isnull=True)
)
class Meta:
model = Task
fields = ['project']
#property
def qs(self):
parent = super(MasterListFilter, self).qs
user = get_current_user()
return parent.filter(master=True, deleted__isnull=True, user_fkey=user.id)
This works perfectly fine. However I also want to filter the dropdown filter (ie the Project queryset) by the current user. As my user is logged in and authenticated, I believe the user details should be attached to the request.
According to the django-filter docs
The FilterSet may be initialized with an optional request argument. If
a request object is passed, then you may access the request during
filtering. This allows you to filter by properties on the request,
such as the currently logged-in user or the Accepts-Languages header.
So it would seem that the request is there, but I can't work out how to access it as an argument of the FilterSet, nor have I been able to find any examples in the docs or anywhere else in my travels as to how to do it. So if anyone can give me any clues at all, I really would appreciate the help.
Edit
Thanks Willem for the information and advice. Very much appreciated. However I think I may not have explained myself clearly. The problem is not filtering the qs
#property
def qs(self):
parent = super(MasterListFilter, self).qs
user = get_current_user()
return parent.filter(master=True, deleted__isnull=True, user_fkey=user.id)
this bit works fine although I will change it to use the self.request.user as suggested along with capturing any requests that are None. This portion returns my results table that gets rendered in my hmtl page. In this case it is a list of tasks that belong to various projects. What I want to be able to do is give the users a dropdown list at the top of the page which has a list of projects that they can choose from and thereby filter the results table by individual projects. (Projects being the parent model.) This part of the code:
class MasterListFilter(django_filters.FilterSet):
project = django_filters.ModelChoiceFilter(
label='Projects',
name='project_fkey',
queryset=Project.objects.filter(deleted__isnull=True)
)
does achieve this to a point in that it gives a list of all projects that have, in this case, not been deleted. Unfortunately the users are able to create their own projects, each of which has a foreign key back to the user who created it. Therefore, in addition to displaying projects that have not been deleted, I also want to show only the projects that belong to the current user.
No doubt I am missing something here, but my understanding is that django_filters.FilterSet has the request as a property, but if I try to use 'user = self.request.user' in this part of the class, I get an error saying self is not defined (and looking at it, it clearly isn't.) Frankly I'm now a bit stumped and really need some advice on this part of the code.
In short: you can access the request with self.request. If no request is given, then self.request is None.
The request is an attribute of the self. So you can obtain this with self.request.user:
#property
def qs(self):
parent = super(MasterListFilter, self).qs
user = self.request.user # unsafe (!) since request can be None!
return parent.filter(master=True, deleted__isnull=True, user_fkey=user.id)
Note however that the request can be None. So it is better to guard against that, like:
#property
def qs(self):
parent = super(MasterListFilter, self).qs
if self.request:
user = self.request.user
else:
user = None
if user and user.is_authenticated():
return parent.filter(master=True, deleted__isnull=True, user_fkey=user.id)
else:
# do something if no request, or no logged in user
# for example
return parent.filter(master=True, deleted__isnull=True)
Or in a more compact form:
#property
def qs(self):
parent = super(MasterListFilter, self).qs
filters = dict(master=True, deleted__isnull=True)
user = getattr(self.request, 'user', None)
if user and user.is_authenticated():
filters['user_fkey'] = user.id
return parent.filter(**filters)
Since obtaining the user is a rather common operation, we can implement a mixin for this:
class UserFilterMixin(object):
#property
def current_user(self):
return getattr(self.request, 'user', None)
You can then use the mixin, and thus obtain the user with self.current_user.
To filter your list of projects by the request.user, you need to provide a callable as the queryset argument. I'm not familiar with your project, but the code should look something like:
def requested_projects(request):
if request is None:
return Projects.objects.none()
return Project.objects.filter(deleted__isnull=True, user_fkey=request.user)
class MasterListFilter(django_filters.FilterSet):
project = django_filters.ModelChoiceFilter(
label='Projects',
name='project_fkey',
queryset=requested_projects,
)
...
I've got source and target rule-based transition decorators working well in django-fsm (Finite State Machine). Now I'm trying to add permissions handling. This seems straightforward, but it seems that no matter what I do, the transition is executed, regardless the user's permissions or lack thereof. I've tried with Django permission strings, and I've tried with lambda, per the documentation. I've tried all of these:
#transition(field=state, source='prog', target='appr', permission='claims.change_claim')
and
#transition(field=state, source='prog', target='appr', permission=lambda instance, user: not user.has_perm('claims.change_claim'),)
and, just as a double-check, since permission should respond to any callable returning True/False, simply:
#transition(field=state, source='prog', target='appr', permission=False)
def approve(self):
Which should raise a TransitionNotAllowed for all users when accessing the transition. But nope - even basic users with no permissions can still execute the transition (claim.approve()).
To prove that I've got permission string right:
print(has_transition_perm(claim.approve, request.user))
prints False. I am doing validation as follows (works for source/target):
class ClaimEditForm(forms.ModelForm):
'''
Some users can transition claims through allowable states
'''
def clean_state(self):
state = self.cleaned_data['state']
if state == 'appr':
try:
self.instance.approve()
except TransitionNotAllowed:
raise forms.ValidationError("Claim could not be approved")
return state
class Meta:
model = Claim
fields = (
'state',
)
and the view handler is the standard:
if request.method == "POST":
claim_edit_form = ClaimEditForm(request.POST, instance=claim)
if claim_edit_form.is_valid(): # Validate transition rules
What am I missing? Thanks.
The problem turned out to be that the permission property does validation differently from the source/target validators. Rather than the decorator raising errors, you must evaluate the permissions established in the decorator elsewhere in your code. So to perform permission validation from a form, you need to pass in the user object, receive user in the form's init, and then compare against the result of has_transition_perm. So this works:
# model
#transition(field=state, source='prog', target='appr', permission='claims.change_claim')
def approve(self):
....
# view
if request.method == "POST":
claim_edit_form = ClaimEditForm(request.user, request.POST, instance=claim)
....
# form
from django_fsm import has_transition_perm
class ClaimEditForm(forms.ModelForm):
'''
Some users can transition claims through allowable states
(see permission property on claim.approve() decorator)
'''
def __init__(self, user, *args, **kwargs):
# We need to pass the user into the form to validate permissions
self.user = user
super(ClaimEditForm, self).__init__(*args, **kwargs)
def clean_state(self):
state = self.cleaned_data['state']
if state == 'appr':
if not has_transition_perm(self.instance.approve, self.user):
raise forms.ValidationError("You do not have permission for this transition")
I'm trying to enforce a permission with Django Rest Framework where a specific user cannot post an object containing a user id which is not his.
For example i don't want a user to post a feedback with another id.
My model is something like :
class Feedback(Model):
user = ForeignKey(User)
...
I try to put a permission on my view which would compare the feedback.user.id with the request.user.id, the right work ok on a post on an object and return false, but it's still posting my object... Why?
The View
class FeedbackViewSet(ModelViewSet):
model = Feedback
permission_classes = (IsSelf,)
serializer_class = FeedbackSerializer
def get_queryset(self):
....
The Permission
class IsSelf(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
#return eval(obj.user.id) == request.user.id
return False
I've commented the line to show where the problem lies.
Again the function is correctly called and returns False, but there's just no PermissionDenied raised.
While at it, i'm wondering if this is actually the way to implement this behaviour, and if not, what would be...?
Thanks.
Your problem is that has_object_permission is only called if you're trying to access a certain object. So on creation it is never actually used.
I'd suggest you do the check on validation. Example:
class FeedbackSerializer(HyperlinkedModelSerializer):
def validate(self, attrs):
user = self.context['request'].user
if attrs['user'].id != user.id:
raise ValidationError('Some exception message')
return attrs
If you have some other super serializer class then just change it.
Now that I think of it if the user field must always be the posting user, then you should just make that field read-only and set it on pre_save() in the viewset class.
class FeedbackViewSet(ModelViewSet):
def pre_save(self, obj, *args, **kwargs):
if self.action == 'create':
obj.user = self.request.user
And in the serializer set the user field read-only
class FeedbackSerializer(HyperlinkedModelSerializer):
user = serializers.HyperlinkedRelatedField(view_name='user-detail', read_only=True)
....
I don't know if this is still open...
However, in order to work, you should move that line from "has_object_permission" to "has_permission", something like this:
class IsSelf(permissions.BasePermission):
def has_permission(self, request, view, obj):
if request.method == 'POST':
#your condition
Worked for me.
As it was stated in the selected answer
has_object_permission is only called if you're trying to access a certain object
so you have to place your condition under has_permission instead.
I suppose similar problem would have been discussed here, but I couldn't find it.
Let's suppose I have an Editor and a Supervisor. I want the Editor to be able to add new content (eg. a news post) but before publication it has to be acknowledged by Supervisor.
When Editor lists all items, I want to set some fields on the models (like an 'ack' field) as read-only (so he could know what had been ack'ed and what's still waiting approval) but the Supervisor should be able to change everything (list_editable would be perfect)
What are the possible solutions to this problem?
I think there is a more easy way to do that:
Guest we have the same problem of Blog-Post
blog/models.py:
Class Blog(models.Model):
...
#fields like autor, title, stuff..
...
class Post(models.Model):
...
#fields like blog, title, stuff..
...
approved = models.BooleanField(default=False)
approved_by = models.ForeignKey(User)
class Meta:
permissions = (
("can_approve_post", "Can approve post"),
)
And the magic is in the admin:
blog/admin.py:
...
from django.views.decorators.csrf import csrf_protect
...
def has_approval_permission(request, obj=None):
if request.user.has_perm('blog.can_approve_post'):
return True
return False
Class PostAdmin(admin.ModelAdmin):
#csrf_protect
def changelist_view(self, request, extra_context=None):
if not has_approval_permission(request):
self.list_display = [...] # list of fields to show if user can't approve the post
self.editable = [...]
else:
self.list_display = [...] # list of fields to show if user can approve the post
return super(PostAdmin, self).changelist_view(request, extra_context)
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # same thing
else:
self.fields = ['approved']
return super(PostAdmin, self).get_form(request, obj, **kwargs)
In this way you can use the api of custom permission in django, and you can override the methods for save the model or get the queryset if you have to. In the methid has_approval_permission you can define the logic of when the user can or can't to do something.
Starting Django 1.7, you can now use the get_fields hook which makes it so much simpler to implement conditional fields.
class MyModelAdmin(admin.ModelAdmin):
...
def get_fields(self, request, obj=None):
fields = super(MyModelAdmin, self).get_fields(request, obj)
if request.user.is_superuser:
fields += ('approve',)
return fields
I have a system kind of like this on a project that I'm just finishing up. There will be a lot of work to put this together, but here are some of the components that I had to make my system work:
You need a way to define an Editor and a Supervisor. The three ways this could be done are 1.) by having an M2M field that defines the Supervisor [and assuming that everyone else with permission to read/write is an Editor], 2.) make 2 new User models that inherit from User [probably more work than necessary] or 3.) use the django.auth ability to have a UserProfile class. Method #1 is probably the most reasonable.
Once you can identify what type the user is, you need a way to generically enforce the authorization you're looking for. I think the best route here is probably a generic admin model.
Lastly you'll need some type of "parent" model that will hold the permissions for whatever needs to be moderated. For example, if you had a Blog model and BlogPost model (assuming multiple blogs within the same site), then Blog is the parent model (it can hold the permissions of who approves what). However, if you have a single blog and there is no parent model for BlogPost, we'll need some place to store the permissions. I've found the ContentType works out well here.
Here's some ideas in code (untested and more conceptual than actual).
Make a new app called 'moderated' which will hold our generic stuff.
moderated.models.py
class ModeratedModelParent(models.Model):
"""Class to govern rules for a given model"""
content_type = models.OneToOneField(ContentType)
can_approve = models.ManyToManyField(User)
class ModeratedModel(models.Model):
"""Class to implement a model that is moderated by a supervisor"""
is_approved = models.BooleanField(default=False)
def get_parent_instance(self):
"""
If the model already has a parent, override to return the parent's type
For example, for a BlogPost model it could return self.parent_blog
"""
# Get self's ContentType then return ModeratedModelParent for that type
self_content_type = ContentType.objects.get_for_model(self)
try:
return ModeratedModelParent.objects.get(content_type=self_content_type)
except:
# Create it if it doesn't already exist...
return ModeratedModelParent.objects.create(content_type=self_content_type).save()
class Meta:
abstract = True
So now we should have a generic, re-usable bit of code that we can identify the permission for a given model (which we'll identify the model by it's Content Type).
Next, we can implement our policies in the admin, again through a generic model:
moderated.admin.py
class ModeratedModelAdmin(admin.ModelAdmin):
# Save our request object for later
def __call__(self, request, url):
self.request = request
return super(ModeratedModelAdmin, self).__call__(request, url)
# Adjust our 'is_approved' widget based on the parent permissions
def formfield_for_dbfield(self, db_field, **kwargs):
if db_field.name == 'is_approved':
if not self.request.user in self.get_parent_instance().can_approve.all():
kwargs['widget'] = forms.CheckboxInput(attrs={ 'disabled':'disabled' })
# Enforce our "unapproved" policy on saves
def save_model(self, *args, **kwargs):
if not self.request.user in self.get_parent_instance().can_approve.all():
self.is_approved = False
return super(ModeratedModelAdmin, self).save_model(*args, **kwargs)
Once these are setup and working, we can re-use them across many models as I've found once you add structured permissions for something like this, you easily want it for many other things.
Say for instance you have a news model, you would simply need to make it inherit off of the model we just made and you're good.
# in your app's models.py
class NewsItem(ModeratedModel):
title = models.CharField(max_length=200)
text = models.TextField()
# in your app's admin.py
class NewsItemAdmin(ModeratedModelAdmin):
pass
admin.site.register(NewsItem, NewsItemAdmin)
I'm sure I made some code errors and mistakes in there, but hopefully this can give you some ideas to act as a launching pad for whatever you decide to implement.
The last thing you have to do, which I'll leave up to you, is to implement filtering for the is_approved items. (ie. you don't want un-approved items being listed on the news section, right?)
The problem using the approach outlined by #diegueus9 is that the ModelAdmin acts liked a singleton and is not instanced for each request. This means that each request is modifying the same ModelAdmin object that is being accessed by other requests, which isn't ideal. Below is the proposed solutions by #diegueus9:
# For example, get_form() modifies the single PostAdmin's fields on each request
...
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not has_approval_permission(request, obj):
self.fields = [...] # list of fields to show if user can't approve the post
else:
self.fields = ['approved', ...] # add 'approved' to the list of fields if the user can approve the post
...
An alternative approach would be to pass fields as a keyword arg to the parent's get_form() method like so:
...
from django.contrib.admin.util import flatten_fieldsets
class PostAdmin(ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if has_approval_permission(request, obj):
fields = ['approved']
if self.declared_fieldsets:
fields += flatten_fieldsets(self.declared_fieldsets)
# Update the keyword args as needed to allow the parent to build
# and return the ModelForm instance you require for the user given their perms
kwargs.update({'fields': fields})
return super(PostAdmin, self).get_form(request, obj=None, **kwargs)
...
This way, you are not modifying the PostAdmin singleton on every request; you are simply passing the appropriate keyword args needed to build and return the ModelForm from the parent.
It is probably worth looking at the get_form() method on the base ModelAdmin for more info: https://code.djangoproject.com/browser/django/trunk/django/contrib/admin/options.py#L431