Combining 2 custom permissions in django rest framework - python

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)

Related

Raise conditional permission error in get_queryset DRF

I want to get all users of an organization by uuid. I am following REST standards so I want my url to look like 'organizations/uuid/users/'. If super admin hits this API, it should be allowed but If an admin user tries using this API, then it should only allow if the admin belongs to the same organization for which users are requested. I used ListAPIView generic view class and I was able to get list of all users in an organization from admin of different organization but it still returns info when it should return 403 error.
urls.py
path('organizations/<uuid:pk>/users/', OrganizationUsersView.as_view()),
views.py
class OrganizationUsersView(ListAPIView):
serializer_class = UserSerializer
permission_classes = (IsAuthenticated, IsSuperAdmin|IsAdmin,)
def get_queryset(self):
uuid = self.kwargs['pk']
if self.request.user.role == 'admin':
if self.request.user.org.id != uuid:
return IsOrganizationAdmin()
org = Organization.objects.get(id=uuid)
return User.objects.filter(org=org)
models.py
class Organization(models.Model):
name = models.CharField(max_length=255, null=False)
class User(AbstractBaseUser):
....
other fields
....
org = models.ForeignKey(Organization, on_delete=models.CASCADE, null=False, related_name='users')

how to access specific choice in django choice field for conditional statements

I have an Account model which extends django's standard User model:
class Account(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE)
joined_groups = models.ManyToManyField(Group, related_name='joined_group', blank=True)
EMAIL_PREFERENCES = [
('user_emails', 'User Emails'),
('group_emails', 'Group Emails'),
('leader_emails', 'Leader Emails'),
]
email_preferences = MultiSelectField(
verbose_name = 'Email Preferences',
choices=EMAIL_PREFERENCES,
blank=True,
max_choices=3,
max_length=255,
default=['user_emails', 'group_emails', 'leader_emails']
)
I also have many celery tasks that send email notifications for when Users create, update, delete, join, or leave Groups. However, I want these emails to only be sent if the User wants them to.
When a new User registers, their email preferences default to accepting all emails, but they have the ability to change their preferences in this view:
class EmailPreferencesUpdate(UpdateView):
model = Account
form_class = EmailPreferencesUpdateForm
template_name = 'accounts/update_email_preferences.html'
def form_valid(self, form):
instance = form.save()
email = instance.user.email
update_email_preferences_notification.delay(email)
return HttpResponseRedirect(reverse('user_detail', args=[str(instance.pk)]))
My issue is I'm trying to have conditionals, before I run my celery tasks, to see if the User allows this type of email, but I can't figure out how to access the User's choices to see if that specific choice was selected.
For example, I have a UserUpdate view:
class UserUpdate(generic.UpdateView):
model = User
form_class = UserUpdateForm
template_name = 'accounts/update_user.html'
def get_object(self):
return self.request.user
def form_valid(self, form):
instance = form.save()
user = self.request.user
form.update_user_notification()
return HttpResponseRedirect(reverse('user_detail', args=[str(instance.pk)]))
and I want to add an if statement when the form is validated to check email preferences of said User. I'm assuming it's something like this:
def form_valid(self, form):
instance = form.save()
user = self.request.user
if user.account.email_preferences.includes('user_emails')
form.update_user_notification()
return HttpResponseRedirect(reverse('user_detail', args=[str(instance.pk)]))
But that didn't work. I'm just not sure how to access the choices field to see if a User has a specific choice selected.
It actually wasn't so complicated. All I have to do is see if the type of email exists in the User's choices:
def form_valid(self, form):
instance = form.save()
user = self.request.user
if 'user_emails' in user.account.email_preferences:
form.update_user_notification()
return HttpResponseRedirect(reverse('user_detail', args=[str(instance.pk)]))
and this worked. We'll see if it works for actually updating the email notification settings of a User, because I'm not sure what runs first, the celery task or updating of email preferences.

Django - Custom Object Permission View

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

Django: Custom permissions for group-based model

I'm new to Django and StackOverflow so please be a bit chill with me! I will update the code and everything as per requests.
I'm currently working on a system where students can join groups called Societies. These Societies have a ManyToManyField relationship with Users. I have been trying to think of a way to build a system where within each Society, there are Leaders and general members. Leaders can promote members to be fellow leaders, delete posts in the society, or even kick members of the society.
I would love some guidance in the right direction on how to structure (or re-structure!) my code. Specifically, I would like to know how to incorporate a "Leader" group of users with custom permissions like the one described above, that I can then use in my templates. Thank you!
class Society(models.Model):
name = models.CharField(max_length = 200)
description = models.TextField()
members = models.ManyToManyField(User, verbose_name=("Members"))
def __str__(self):
return f"{self.name}"
class SocPost(models.Model):
title = models.CharField(max_length = 100)
content = models.TextField()
date_posted = models.DateTimeField(default=timezone.now)
author = models.ForeignKey(User, on_delete=models.CASCADE)
society = models.ForeignKey(Society, on_delete=models.CASCADE, related_name="posts")
def __str__(self):
return self.title
def get_absolute_url(self):
return reverse('post-detail', kwargs={'society_id':self.society.id, 'pk':self.pk})
Simplest solution is to use a through model:
class SocietyMembership(models.Model)
member = models.ForeignKey(User, related_name='society_membership')
society = models.ForeignKey('Society')
is_leader = models.BooleanField(default=False)
class Society(models.Model):
name = models.CharField(max_length = 200)
description = models.TextField()
members = models.ManyToManyField(
User,
verbose_name=("Members"),
related_name='societies',
through=SocietyMembership
)
def __str__(self):
return f"{self.name}"
This avoids extra queries injected by ContentTypes and the need to create one leader group per society in order to separate permissions from one group to the next.
Update:
Model update:
class Society(models.Model):
name = models.CharField(max_length=200)
description = models.TextField()
members = models.ManyToManyField(
User,
verbose_name=_("Members"),
related_name="societies",
through=SocietyMembership,
)
#property
def leaders(self):
return self.memberships.filter(is_leader=True)
def user_is_leader(self, user: User):
return self.memberships.filter(is_leader=True, member=user).exists()
def is_member(self, user: User):
return self.members.filter(pk=user.id).exists()
def __str__(self):
return f"{self.name}"
def enroll(self, user: User, as_leader: bool = False):
# Django >= 2.2, which it should be in July 2020
self.members.add(user, through_defaults={"is_leader": as_leader})
def promote(self, user: User) -> bool:
try:
membership = self.memberships.get(member=user)
except SocietyMembership.DoesNotExist:
return False
membership.is_leader = True
membership.save()
return True
def kick(self, user: User):
self.memberships.filter(member=user).delete()
Example of a views with permission check:
from django.views import generic
from django.contrib.auth.mixins import UserPassesTestMixin, LoginRequiredMixin
from . import models
class SocietyList(LoginRequiredMixin, generic.ListView):
model = models.Society
template_name = "main/society-list.html"
context_object_name = "societies"
class SocietyDetail(UserPassesTestMixin, generic.DetailView):
model = models.Society
template_name = "main/society-detail.html"
context_object_name = "society"
def test_func(self):
# self.object will be an instance of Society
if not hasattr(self, "object"):
setattr(self, "object", self.get_object())
return self.object.is_member(self.request.user)
It seems like you want your 'leaders' to be an admin, denoted by the 'is_superuser' field in the default django user model. What you described are typical admin activities in Django (and can easily be done in the django admin panel). Create one of these superusers by executing python manage.py createsuperuser and then you will have access to the admin panel where you can customize everything you need, and promote more users to admin level. If you are looking for some level of leadership between basic user and admin, you can create users with custom permissions dependent on the user_permissions field, denoted by some sort of variable which you add to the django default user model using a 1:1 relationship.

How to handle multiple user types with Django 1.7 user model

I am very new to Python and Django. I am trying to setup user model for different roles like Agents, Brokers, Company and Customer. Each of these roles can register with the site as a user. Then Agents and Brokers can have public facing profile pages.
Do I have to use custom user model or built in user model will work? I have additional properties like license, location, languages, photo etc for Agents and Brokers.
class MyUser(AbstractBaseUser):
AGENTS = 'AG'
BROKERS = 'BR'
COMPANY = 'CP'
CUSTOMER = 'CM'
ROLE_IN_CHOICES = (
(AGENTS, 'Agent'),
(BROKERS, 'Broker'),
(COMPANY, 'Company'),
(CUSTOMER, 'Customer'))
first_name = models.CharField(max_length=100, blank=True)
second_name = models.CharField(max_length=100, blank=True)
middle_name = models.CharField(max_length=100, blank=True)
dob = models.DateField(blank=True, null=True)
phone = models.CharField(max_length=10)
secondary_phone = models.CharField(max_length=10, blank=True, null=True)
......
#property
def is_agent(self):
return self.role in (self.AGENTS)
#property
def is_customer(self):
return self.role in (self.CUSTOMER)
#property
def is_broker(self):*
return self.role in (self.BROKER)
#property
def is_company(self):
return self.role in (self.COMPANY)
....
Can I not use base User model and achieve same? Am I on write track?
How do I create public facing pages for these roles (Agents, Brokers)?
This is my first attempt with Python and Django. I am using Django 1.7.7 with Python 3.4
You should extend from the Django User model instead and add the extra fields you need:
from django.contrib.auth.models import User as Auth_User
class User(Auth_User):
# add your extra fields here like roles, etc
phone = CharField(max_length=20, null=True, blank=True)
# add your extra functions
def extra_user_function(self):
return "This is an extra function"
This way you have your own fields and also the Django User fields...
After migrating, if you check the database, you'll have auth_user and your_app_user tables.
Just bare in mind that request.user will only give you the super fields... In order to get the extended class you'll need
User.objects.get(id=request.user.id)
The latest will only have the extra fields and its id will be the same as the auth.User if you don't add any auth_user by itself.
Attention: this is important!
Otherwise request.user.id and your_app.User.id don't match, therefore User.objects.get(id=request.user.id) won't work and you'll have to query the db to find the your_app.User.id
User.object.get(user_ptr_id = request.user.id)
Other things to consider
This will work:
# you_app.User objects gets vars from auth.User
user = User.objects.get(id=request.user.id)
first_name = user.first_name
But this won't work
# auth.User trying to get a function from your_app.User
user = request.user
user.extra_user_function()
So the User model could be something like this:
import os
from django.contrib.auth.models import User as Django_User
from django.db.models import CharField, ImageField
class User(Django_User):
phone = CharField(max_length=20, null=True, blank=True)
observations = CharField(max_length=2048, null=True, blank=True)
picture = ImageField(upload_to='users', default='default/avatar.jpg')
class Meta:
# adding extra permissions (default are: add_user, change_user, delete_user)
permissions = (
("access_user_list", "Can access user list"),
("access_user", "Can access user"),
)
ordering = ["-is_staff", "first_name"]
Instead of creating roles on the user, Django already has groups, so you should use them.
The groups follow the same logic:
from django.contrib.auth.models import Group as Auth_Group
from django.db import models
class Group(Auth_Group):
observations = models.CharField(max_length=2048, null=True, blank=True)
def get_users_in_group(self):
return self.user_set.filter(is_active=1).order_by('first_name', 'last_name')
def count_users_in_group(self):
return self.user_set.count()
def __unicode__(self):
return self.name
class Meta:
permissions = (
("access_group_list", "Can access group list"),
("access_group", "Can access group"),
)
ordering = ["name"]
You can clear / add users to a group:
user.groups.clear()
user.groups.add(group)
Clear / add permissions to the group:
group.permissions.clear()
group.permissions.add(permission)
There is also a decorator to check if a user has permissions
from django.contrib.auth.decorators import permission_required
#permission_required(("users.change_user","users.access_user",))
def your_view(request):
...
I've tried many things in the past, but I guess this is the way to go.
If you really need roles, and a user can have more than one role, mayb the best thing would be to create a model Role and add that to the user has a ManyToMany Field
roles = ManyToManyField(Role)
but you could do that with groups

Categories

Resources