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.
Related
I wrote a permission:
class IsParticipant(permissions.BasePermission):
def has_permission(self, request, view):
return True
def has_object_permission(self, request, view, obj):
if isinstance(obj, Message):
return request.user in obj.chat.chat_participants.all()
elif ....
return False
I checked, conditions work correctly (now I need just the first if condition), the program enters the if condition, and returns False or True (I've written print() function, so it showed in terminal that program returns False of True depending on values). But it doesn't change anything, even if returns False, it doesn't forbid anything for user. It even doesn't raise any default error.
Here is the view class (although I don't think it is necessary here):
class WriteMessageCreateAPIView(generics.CreateAPIView):
permission_classes = [IsAuthenticated, IsParticipant]
serializer_class = WriteMessageSerializer #the model is named as 'Message'
def perform_create(self, serializer):
serializer.validated_data['author'] = self.request.user
serializer.validated_data['chat_id'] = self.kwargs['pk']
return super(WriteMessageCreateAPIView, self).perform_create(serializer)
So the questions are: Why the permission class doesn't do anything? How can I fix this issue?
Try by doing
permission_classes = [IsParticipant]
It will work I think, I had the same issue few days ago and I found out that the
permission_classes = [ ] is giving the 'or' of the classes and not the 'and', I mean if either of the permission class is true, then it is returning true. (or permitting the user)
You may directly add what you wanna do in views.py by overriding the post function. (or whichever is needed)
if you use many permissions need to indicate how they relate to each other
https://www.django-rest-framework.org/community/3.9-announcement/#composable-permission-classes
for example:
permission_classes = (IsAuthenticated | IsParticipant)
I solved the problem by adding
obj = get_object_or_404(Chat, id=self.kwargs['pk'])
self.check_object_permissions(self.request, obj)
to my perform_create function in WriteMessageCreateAPIView class.
Now it works: checks if the user is a participant of the chat group or not. If not, denies permission related with that chat group for the user.
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()
My questions is, can I create methods in a form?
I want to create a method in a UserChangeForm it should control some things.
And how can I call this method then in the save() if commit?
In the forms.form I must create a save() too?
I tried this:
class UserChangeForm(forms.Form):
#fields for the form
def create_club_contact(self):
user = super(UserChangeForm, self).save(commit=False)
if information is None:
#create clubcontact
else:
#update clubcontact
return clubcontact
def clean_password2(self):
#check password
return password2
def save(self):
data = self.cleaned_data
user = TennisClub(link=data['link'], name=data['name'])
user.save()
contact = self.create_club_contact()
user.club_contact = contact
user.save()
On this way I'm always getting this Error
'super' object has no attribute 'save'
I think the forms.Form needs a save() method, but I don't know, how to do it right.
In the docs, I have not seen such a example.
Thanks for helping.
class MyForm(forms.Form):
def __init__(self):
self.member = False
# example of a method with return statement
def my_method(self):
return something
# example for a void method, no return value
def another_method(self):
self.member = True
def save(self):
my_variable = self.my_method()
self.another_method()
# continue with your logic
EDIT:
I think you don't have the problem with creating methods and calling them. The above is probably clear to you. But it seems that you have another problem, but you can't question it right.
Your form inherits from Form. The parent class Form has a method save() and you want to override it.
Put this in the end of your method save() and try again:
return super(UserChangeForm, self).save()
I'm trying to make my User model RESTful via Django Rest Framework API calls, so that I can create users as well as update their profiles.
However, as I go through a particular verification process with my users, I do not want the users to have the ability to update the username after their account is created. I attempted to use read_only_fields, but that seemed to disable that field in POST operations, so I was unable to specify a username when creating the user object.
How can I go about implementing this? Relevant code for the API as it exists now is below.
class UserSerializer(serializers.HyperlinkedModelSerializer):
class Meta:
model = User
fields = ('url', 'username', 'password', 'email')
write_only_fields = ('password',)
def restore_object(self, attrs, instance=None):
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User
def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]
Thanks!
It seems that you need different serializers for POST and PUT methods. In the serializer for PUT method you are able to just except the username field (or set the username field as read only).
class UserViewSet(viewsets.ModelViewSet):
"""
API endpoint that allows users to be viewed or edited.
"""
serializer_class = UserSerializer
model = User
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method == 'PUT':
serializer_class = SerializerWithoutUsernameField
return serializer_class
def get_permissions(self):
if self.request.method == 'DELETE':
return [IsAdminUser()]
elif self.request.method == 'POST':
return [AllowAny()]
else:
return [IsStaffOrTargetUser()]
Check this question django-rest-framework: independent GET and PUT in same URL but different generics view
Another option (DRF3 only)
class MySerializer(serializers.ModelSerializer):
...
def get_extra_kwargs(self):
extra_kwargs = super(MySerializer, self).get_extra_kwargs()
action = self.context['view'].action
if action in ['create']:
kwargs = extra_kwargs.get('ro_oncreate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_oncreate_field'] = kwargs
elif action in ['update', 'partial_update']:
kwargs = extra_kwargs.get('ro_onupdate_field', {})
kwargs['read_only'] = True
extra_kwargs['ro_onupdate_field'] = kwargs
return extra_kwargs
Another method would be to add a validation method, but throw a validation error if the instance already exists and the value has changed:
def validate_foo(self, value):
if self.instance and value != self.instance.foo:
raise serializers.ValidationError("foo is immutable once set.")
return value
In my case, I wanted a foreign key to never be updated:
def validate_foo_id(self, value):
if self.instance and value.id != self.instance.foo_id:
raise serializers.ValidationError("foo_id is immutable once set.")
return value
See also: Level-field validation in django rest framework 3.1 - access to the old value
My approach is to modify the perform_update method when using generics view classes. I remove the field when update is performed.
class UpdateView(generics.UpdateAPIView):
...
def perform_update(self, serializer):
#remove some field
rem_field = serializer.validated_data.pop('some_field', None)
serializer.save()
I used this approach:
def get_serializer_class(self):
if getattr(self, 'object', None) is None:
return super(UserViewSet, self).get_serializer_class()
else:
return SerializerWithoutUsernameField
UPDATE:
Turns out Rest Framework already comes equipped with this functionality. The correct way of having a "create-only" field is by using the CreateOnlyDefault() option.
I guess the only thing left to say is Read the Docs!!!
http://www.django-rest-framework.org/api-guide/validators/#createonlydefault
Old Answer:
Looks I'm quite late to the party but here are my two cents anyway.
To me it doesn't make sense to have two different serializers just because you want to prevent a field from being updated. I had this exact same issue and the approach I used was to implement my own validate method in the Serializer class. In my case, the field I don't want updated is called owner. Here is the relevant code:
class BusinessSerializer(serializers.ModelSerializer):
class Meta:
model = Business
pass
def validate(self, data):
instance = self.instance
# this means it's an update
# see also: http://www.django-rest-framework.org/api-guide/serializers/#accessing-the-initial-data-and-instance
if instance is not None:
originalOwner = instance.owner
# if 'dataOwner' is not None it means they're trying to update the owner field
dataOwner = data.get('owner')
if dataOwner is not None and (originalOwner != dataOwner):
raise ValidationError('Cannot update owner')
return data
pass
pass
And here is a unit test to validate it:
def test_owner_cant_be_updated(self):
harry = User.objects.get(username='harry')
jack = User.objects.get(username='jack')
# create object
serializer = BusinessSerializer(data={'name': 'My Company', 'owner': harry.id})
self.assertTrue(serializer.is_valid())
serializer.save()
# retrieve object
business = Business.objects.get(name='My Company')
self.assertIsNotNone(business)
# update object
serializer = BusinessSerializer(business, data={'owner': jack.id}, partial=True)
# this will be False! owners cannot be updated!
self.assertFalse(serializer.is_valid())
pass
I raise a ValidationError because I don't want to hide the fact that someone tried to perform an invalid operation. If you don't want to do this and you want to allow the operation to be completed without updating the field instead, do the following:
remove the line:
raise ValidationError('Cannot update owner')
and replace it with:
data.update({'owner': originalOwner})
Hope this helps!
More universal way to "Disable field update after object is created"
- adjust read_only_fields per View.action
1) add method to Serializer (better to use your own base cls)
def get_extra_kwargs(self):
extra_kwargs = super(BasePerTeamSerializer, self).get_extra_kwargs()
action = self.context['view'].action
actions_readonly_fields = getattr(self.Meta, 'actions_readonly_fields', None)
if actions_readonly_fields:
for actions, fields in actions_readonly_fields.items():
if action in actions:
for field in fields:
if extra_kwargs.get(field):
extra_kwargs[field]['read_only'] = True
else:
extra_kwargs[field] = {'read_only': True}
return extra_kwargs
2) Add to Meta of serializer dict named actions_readonly_fields
class Meta:
model = YourModel
fields = '__all__'
actions_readonly_fields = {
('update', 'partial_update'): ('client', )
}
In the example above client field will become read-only for actions: 'update', 'partial_update' (ie for PUT, PATCH methods)
This post mentions four different ways to achieve this goal.
This was the cleanest way I think: [collection must not be edited]
class DocumentSerializer(serializers.ModelSerializer):
def update(self, instance, validated_data):
if 'collection' in validated_data:
raise serializers.ValidationError({
'collection': 'You must not change this field.',
})
return super().update(instance, validated_data)
Another solution (apart from creating a separate serializer) would be to pop the username from attrs in the restore_object method if the instance is set (which means it's a PATCH / PUT method):
def restore_object(self, attrs, instance=None):
if instance is not None:
attrs.pop('username', None)
user = super(UserSerializer, self).restore_object(attrs, instance)
user.set_password(attrs['password'])
return user
If you don't want to create another serializer, you may want to try customizing get_serializer_class() inside MyViewSet. This has been useful to me for simple projects.
# Your clean serializer
class MySerializer(serializers.ModelSerializer):
class Meta:
model = MyModel
fields = '__all__'
# Your hardworking viewset
class MyViewSet(MyParentViewSet):
serializer_class = MySerializer
model = MyModel
def get_serializer_class(self):
serializer_class = self.serializer_class
if self.request.method in ['PUT', 'PATCH']:
# setting `exclude` while having `fields` raises an error
# so set `read_only_fields` if request is PUT/PATCH
setattr(serializer_class.Meta, 'read_only_fields', ('non_updatable_field',))
# set serializer_class here instead if you have another serializer for finer control
return serializer_class
setattr(object, name, value)
This is the counterpart of getattr(). The
arguments are an object, a string and an arbitrary value. The string
may name an existing attribute or a new attribute. The function
assigns the value to the attribute, provided the object allows it. For
example, setattr(x, 'foobar', 123) is equivalent to x.foobar = 123.
class UserUpdateSerializer(UserSerializer):
class Meta(UserSerializer.Meta):
fields = ('username', 'email')
class UserViewSet(viewsets.ModelViewSet):
def get_serializer_class(self):
return UserUpdateSerializer if self.action == 'update' else super().get_serializer_class()
djangorestframework==3.8.2
I would suggest also looking at Django pgtrigger
This allows you to install triggers for validation. I started using it and was very pleased with its simplicity:
Here's one of their examples that prevents a published post from being updated:
import pgtrigger
from django.db import models
#pgtrigger.register(
pgtrigger.Protect(
operation=pgtrigger.Update,
condition=pgtrigger.Q(old__status='published')
)
)
class Post(models.Model):
status = models.CharField(default='unpublished')
content = models.TextField()
The advantage of this approach is it also protects you from .update() calls that bypass .save()
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