Django - Custom Object Permission View - python

I'm trying to give shop owner permissions for a view. So I made a file in which I created different permissions. In my permission I first of all check if the user was logged in with a has_permission function. I am now trying to determine if a user actually owns the shop with the has_object_permission function. Unfortunately, I don't feel that my function was performed correctly.
I can always, despite my permission, make a request from any account, shop owner or not.
Here are the models I use:
models.py
class Shop(models.Model):
name = models.CharField(max_length=255)
category = models.ForeignKey(ShopCategory, on_delete=models.SET_NULL, null=True, blank=True)
description = models.TextField(blank=True, null=True)
path = models.CharField(max_length=255, unique=True, null=True, blank=True) # Set a null and blank = True for serializer
mustBeLogged = models.BooleanField(default=False)
deliveries = models.FloatField(default=7)
def __str__(self):
return self.name
class UserShop(models.Model):
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
shop = models.ForeignKey(Shop, on_delete=models.CASCADE)
def __str__(self):
return f"{self.user.name} {self.user.surname} - {self.shop.name}"
Here are my permissions :
utils.py
class IsOwner(BasePermission):
"""
Check if the user who made the request is owner.
Use like that : permission_classes = [IsOwner]
"""
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
try:
user_shop = UserShop.objects.get(user=request.user, shop=obj)
return True
except:
return False
class OwnerView(APIView):
"""
Check if a user is owner
"""
permission_classes = (IsOwner,)
Here is my view :
views.py
class ShopDetail(OwnerView):
"""Edit ou delete a shop"""
def put(self, request, path):
"""For admin or shop owner to edit a shop"""
shop = get_object_or_404(Shop, path=path)
serializer = ShopSerializer(shop, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors)
And here is my serializer :
serializers.py
class ShopSerializer(serializers.ModelSerializer):
class Meta:
model = Shop
fields = '__all__'
Thank you in advance for your help

As mentioned in the documentation for Custom permissions:
Note: The instance-level has_object_permission method will only be called if the view-level has_permission checks have already
passed. Also note that in order for the instance-level checks to run,
the view code should explicitly call
.check_object_permissions(request, obj). If you are using the
generic views then this will be handled for you by default.
(Function-based views will need to check object permissions
explicitly, raising PermissionDenied on failure.)
You have implemented the put method yourself and get the object yourself instead of using the get_object method (which calls check_object_permissions itself) so check_object_permissions is never called. Instead you should set lookup_field as path in your view class and use get_object:
class ShopDetail(OwnerView):
"""Edit ou delete a shop"""
queryset = Shop.objects.all()
lookup_field = 'path'
def put(self, request, path):
"""For admin or shop owner to edit a shop"""
shop = self.get_object()
serializer = ShopSerializer(shop, data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data)
return Response(serializer.errors)
Also to do this OwnerView should inherit from GenericAPIView:
from rest_framework.generics import GenericAPIView
class OwnerView(GenericAPIView):
"""
Check if a user is owner
"""
permission_classes = (IsOwner,)

Related

Only Owner of the Profile able to Update the data

Using class Based (APIView) in Django rest framework for Getting and Patch (Updating) UserInfo data.
views.py
class getUserInfo(APIView):
permission_classes = [permissions.IsAuthenticated]
def get(self, request, format=None):
user = request.user
userinfos = user.userinfo_set.all()
serializer = UserInfoSerializers(userinfos, many=True)
return Response(serializer.data)
def patch(self, request, pk, format=None):
user = UserInfo.objects.get(id=pk)
serializer = UserInfoSerializers(instance=user, data=request.data, partial=True)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
serializers.py
from django.contrib.auth.models import User
from .models import UserInfo
class UserSerializer(serializers.ModelSerializer):
class Meta:
model = User
fields = ('id', 'first_name', 'username')
class UserInfoSerializers(serializers.ModelSerializer):
user = UserSerializer(many=False, required=True)
class Meta:
model = UserInfo
fields = ('id', 'picture', 'profession', 'user')
Everything is working so far so good. Able to GET and PATCH (Update) logged-in user data.
While Testing the API in Postman, I found out that if User1 is logged in he can change the data of User2 by only using the pk of User2.
urls.py
urlpatterns = [
path('userinfo/', views.getUserInfo.as_view(), name="UserInfo"),
path('userinfo/<str:pk>/', views.getUserInfo.as_view()),
path('api/token/', views.MyTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
path('register/', views.RegisterView.as_view(), name='auth_register'),
]
Using rest_framework_simplejwt for Auth
models.py
from django.contrib.auth.models import User
class UserInfo(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, null=True)
picture = models.ImageField(upload_to="profile_pics", null=True)
profession = models.CharField(max_length=200, null=True)
def __str__(self):
return "%s's Profile Picture" % self.user
Any help would be appreciated
Don't use the primary key to get the user.You are using user = request.user to get the user on get method, use the same mechanism also on update. Then the login user can only update his/her info not others info or another way you can check the user = UserInfo.objects.get(id=pk) is same as the current user request.user . If not you can show an exception.
For Retrieving and Updating an object, you can use RetrieveUpdateAPIView
class GetUserInfo(generics.RetrieveUpdateAPIView):
permission_classes = [IsAuthenticated]
queryset = UserInfo.objects.all()
serializer_class = UserInfoSerializers
def get_object(self):
return self.request.user
Here we are getting an object, it will be called from get_object method. Instead of getting user using PK, we get the current user.
You can use same url for getting and updating the user, just change the method in postman while you hit the api. GET for retrieving and PATCH for partial update.
path('userinfo/', views.GetUserInfo.as_view(), name="UserInfo"),

How to associate user to a post request in Django drf

I have the following :
I am working with DRF, based JWT token.
I want to associate an experiment with a USER, i.e when a post request is arriving I want to be able to save that post request with the Foreginkey it needed for the author by the user whom sent the request.
The POST request is always authenticated and never anonymous, i.e request.user is always exist ( I can see it when debugging)
I tried to add the following
def create(self, request, **kwargs):
request.data["author"] = request.user
serializer = ExperimentsSerializers(data=request.data)
if serializer.is_valid():
serializer.save()
return....
But is_valid return always False ( the only time ts was true, was when I took out the author from the ExperimentsSerializers fields....
will be happy for any leads....
my code attached below
Model.py:
class User(AbstractUser):
pass
def __str__(self):
return self.username
class Experiments(models.Model):
name = models.CharField(max_length=40)
time = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE)
View.py:
filter_backends = [DjangoFilterBackend, filters.OrderingFilter]
serializer_class = ExperimentsSerializers
queryset = Experiments.objects.all()
filterset_fields = '__all__'
permission_classes = (permissions.IsAuthenticated,)
serializers.py
class ExperimentsSerializers(serializers.ModelSerializer):
class Meta:
model = models.Experiments
fields = '__all__'
You can just pass additional data with save arguments:
def create(self, request, **kwargs):
serializer = ExperimentsSerializers(data=request.data)
if serializer.is_valid():
serializer.save(author=request.user)
Note that you may need to specify author field as read_only so it would not be required in request body:
class ExperimentsSerializers(serializers.ModelSerializer):
class Meta:
model = models.Experiments
fields = '__all__'
read_only_fields = ['author']
One more approach can be to use
HiddenField with default value set to CurrentUserDefault
This way that field will not be exposed at the same time current user will be accessible and other operations can be done on that user context.
author = serializers.HiddenField(default=serializers.CurrentUserDefault())
Something like this:
class ExperimentsSerializers(serializers.ModelSerializer):
author = serializers.HiddenField(default=serializers.CurrentUserDefault())
class Meta:
model = models.Experiments
fields = '__all__'
Reference :
HiddenField - https://www.django-rest-framework.org/api-guide/fields/#hiddenfield
CurrentUserDefault - https://www.django-rest-framework.org/api-guide/validators/#currentuserdefault

Combining 2 custom permissions in django rest framework

I have a model called Showcase that users use to showcase projects, and also a collaboration model where users can add collaborators to the showcase. I am trying to implement a case where administrators in the showcase and the user in a collaboration can delete that collaboration.
To explain better, in a showcase model, there is a list of administrators that manage the showcase. they also can add collaborators (through the Collaborator model) to a showcase. The Collaborator has a user field which is the user contributed to the showcase.
I want that after a collaborator has been added, that user can either delete himself (in a case he doesnt want to be part of the showcase) or the administrators can delete that collaborator (in a case thay added a wrong user and want to delete him from that showcase)
models.py
class Showcase(models.Model):
title = models.CharField(max_length=50)
description = models.TextField(null=True)
skill_type = models.ForeignKey(Skill, on_delete=models.CASCADE)
user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.DO_NOTHING, related_name="Showcases")
content = models.TextField(null=True)
created_on = models.DateTimeField(auto_now_add=True)
updated_on = models.DateTimeField(auto_now=True)
voters = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="upvotes")
slug = models.SlugField(max_length=255, unique=True)
administrator = models.ManyToManyField(settings.AUTH_USER_MODEL, related_name="administrators", blank=True)
class Collaborator(models.Model):
post = models.ForeignKey(Showcase, on_delete=models.CASCADE, related_name="collaborated_showcases")
user = models.ForeignKey(settings.AUTH_USER_MODEL,
on_delete=models.CASCADE, related_name="collaborators")
skill = models.ForeignKey(Skill, on_delete=models.CASCADE, null=True, related_name="creative_type")
role = models.TextField(null=True)
created_on = models.DateTimeField(auto_now_add=True)
updated_on = models.DateTimeField(auto_now=True)
permission.py
class IsUser(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return False
return obj.user == request.user
class IsAdmin(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return False
return request.user.administrators.filter(pk=obj.pk).exists()
view.py
class CollaboratorDeleteView(APIView):
'''
Allow Administrators to delete a collaborator to a showcase
or allow the collaborator user to be able to delete himself
'''
permission_classes = [IsAdmin]
def delete(self, request, pk):
collaborator = get_object_or_404(Collaborator, pk=pk)
showcase = collaborator.post
try:
self.check_object_permissions(request, showcase)
collaborator.delete()
return Response(status=status.HTTP_204_NO_CONTENT)
except APIException:
return Response(status=status.HTTP_403_FORBIDDEN)
URLs
path("collaborator/<int:pk>/delete/", qv.CollaboratorDeleteView.as_view(), name="collaborator-delete-view"),
Right now I have been able to implement that administrators can remove the collaborator, but how can I add another permission for the user in the Collaborator model to be able to delete himself as a collaborator through that same view?
You can add as many permissions as you need to permission_classses attribute using & (and), | (or) and ~ (not) signs (doc):
class CollaboratorDeleteView(APIView):
'''
Allow Administrators to delete a collaborator to a showcase
or allow the collaborator user to be able to delete himself
'''
permission_classes = [IsAdmin|IsUser]
Both of this permissions will work now with OR logic.
Actually both permissions can be combined into single one. For example updating the permission like this:
class CanDeleteUser(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return False
return obj.user == request.user or ob.post.administrator.filter(pk=request.user.pk).exists()
Here I am checking either the request.user is obj.user or checking against administrators of the showcase object attached with obj variable.
Now I am only going to check permission for collaborator.
class CollaboratorDeleteView(APIView):
'''
Allow Administrators to delete a collaborator to a showcase
or allow the collaborator user to be able to delete himself
'''
permission_classes = [CanDeleteUser]
def delete(self, request, pk):
collaborator = get_object_or_404(Collaborator, pk=pk)
try:
self.check_object_permissions(request, collaborator)

Django REST and ModelViewSet filtering

I was previously using APIViews such as the following:
views.py
class AllProgramsApi(APIView):
def get(self, request):
user = self.request.user
userprograms = Program.objects.filter(user=user)
serializer = ProgramSerializer(userprograms, many=True)
return Response(serializer.data)
here's my model:
class Program(models.Model):
program_name = models.CharField(max_length=50)
program_description = models.CharField(max_length=250)
cycles = models.ManyToManyField(Cycle)
is_favourite = models.BooleanField(default="False")
user = models.ForeignKey(User, on_delete=models.CASCADE)
def get_absolute_url(self):
return reverse('programs:program', kwargs={'pk': self.pk})
def __str__(self):
return self.program_name
Now I've discovered ModelViewSet, which looks very convenient, but I can't seem to be able to filter for the user as I was previously doing in the APIView.
my attempt at views.py with ModelViewSet is the following and it works but I get all the content and not just the content related to a single user.
class AllProgramsApi(ModelViewSet):
serializer_class = ProgramSerializer
queryset = Program.objects.all()
How can I tweak the ModelViewSet so that it displays only the content related to the user who sends the request? What is the best method?
Thanks.
You can use get queryset method,if you know more refer the doc Filtering against the current user
class AllProgramsApi(ModelViewSet):
serializer_class = ProgramSerializer
queryset = Program.objects.all()
def get_queryset(self):
queryset = self.queryset
query_set = queryset.filter(user=self.request.user)
return query_set
there are permission_classes in django you can add permissions as per your requirements or you can create custom permissions
you would get better idea from django permission
or you can create your queryset by defining get_queryset method.

Django Rest Framework ModelSerializer Set attribute on create

When creating an object initially I use the currently logged-in user to assign the model field 'owner'.
The model:
class Account(models.Model):
id = models.AutoField(primary_key=True)
owner = models.ForeignKey(User)
name = models.CharField(max_length=32, unique=True)
description = models.CharField(max_length=250, blank=True)
Serializer to set owner:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = models.Account
fields = ('name', 'description')
def restore_object(self, attrs, instance=None):
instance = super().restore_object(attrs, instance)
request = self.context.get('request', None)
setattr(instance, 'owner', request.user)
return instance
It is possible for a different user in my system to update another's Account object, but the ownership should remain with the original user. Obviously the above breaks this as the ownership would get overwritten upon update with the currently logged in user.
So I've updated it like this:
class AccountSerializer(serializers.ModelSerializer):
class Meta:
model = models.Account
fields = ('name', 'description')
def restore_object(self, attrs, instance=None):
new_instance = False
if not instance:
new_instance = True
instance = super().restore_object(attrs, instance)
# Only set the owner if this is a new instance
if new_instance:
request = self.context.get('request', None)
setattr(instance, 'owner', request.user)
return instance
Is this the recommended way to do something like this? I can't see any other way, but I have very limited experience so far.
Thanks
From reviewing #zaphod100.10's answer. Alternatively, in the view code (with custom restore_object method in above serializer removed):
def post(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.DATA, files=request.FILES)
if serializer.is_valid():
serializer.object.owner = request.user
self.pre_save(serializer.object)
self.object = serializer.save(force_insert=True)
self.post_save(self.object, created=True)
headers = self.get_success_headers(serializer.data)
return Response(serializer.data, status=status.HTTP_201_CREATED,
headers=headers)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
Basically you want the owner to be set on creation and not on subsequent updates. For this I think you should set the owner in the POST view. I think it is more logical and robust that way. Update is done via PUT view so your data should always be correct since no way on updation the owner can be changed if the owner is not editable on PUT.
For making the views you can use DRF's generic class based views. Use the RetrieveUpdateDeleteView as it is. For ListCreateView override the post method. Use a django model form for validating the data and creating an account instance.
You will have to copy the request.DATA dict and insert 'owner' as the current user.
The code for the POST method can be:
def post(self, request, *args, **kwargs):
data = deepcopy(request.DATA)
data['owner'] = request.user
form = AccountForm(data=data)
if form.is_valid():
instance = form.save(commit=false)
instance.save()
return Response(dict(id=instance.pk), status=status.HTTP_201_CREATED)
return Response(form.errors, status=status.HTTP_400_BAD_REQUEST)
Potential other option using pre_save which I think seems to be intended for just this kind of thing.
class AccountList(generics.ListCreateAPIView):
serializer_class = serializers.AccountSerializer
permission_classes = (permissions.IsAuthenticated)
def get_queryset(self):
"""
This view should return a list of all the accounts
for the currently authenticated user.
"""
user = self.request.user
return models.Account.objects.filter(owner=user)
def pre_save(self, obj):
"""
Set the owner of the object to the currently logged in user as this
field is not populated by the serializer as the user can not set it
"""
# Throw a 404 error if there is no authenticated user to use although
# in my case this is assured more properly by the permission_class
# specified above, but this could be any criteria.
if not self.request.user.is_authenticated():
raise Http404()
# In the case of ListCreateAPIView this is not necessary, but
# if doing this on RetrieveUpdateDestroyAPIView then this may
# be an update, but if it doesn't exist will be a create. In the
# case of the update, we don't wish to overwrite the owner.
# obj.owner will not exist so the way to test if the owner is
# already assigned for a ForeignKey relation is to check for
# the owner_id attribute
if not obj.owner_id:
setattr(obj, 'owner', self.request.user)
I think this is the purpose of pre_save and it is quite concise.
Responsibilities should be split here, as the serializer/view only receives/clean the data and make sure all the needed data is provided, then it should be the model responsibility to set the owner field accordingly. It's important to separate these two goals as the model might be updated from elsewhere (like from an admin form).
views.py
class AccountCreateView(generics.CreateAPIView):
serializer_class = serializers.AccountSerializer
permission_classes = (permissions.IsAuthenticated,)
def post(self, request, *args, **kwargs):
# only need this
request.data['owner'] = request.user.id
return super(AccountCreateView, self).post(request, *args, **kwargs)
models.py
class Account(models.Model):
# The id field is provided by django models.
# id = models.AutoField(primary_key=True)
# you may want to name the reverse relation with 'related_name' param.
owner = models.ForeignKey(User, related_name='accounts')
name = models.CharField(max_length=32, unique=True)
description = models.CharField(max_length=250, blank=True)
def save(self, *args, **kwargs):
if not self.id:
# only triggers on creation
super(Account, self).save(*args, **kwargs)
# when updating, remove the "owner" field from the list
super(Account, self).save(update_fields=['name', 'description'], *args, **kwargs)

Categories

Resources