DRF permissions best practise DRY - python-3.x

Whats the best way to do view permissions in DRF based on user type currently?
In my structure there are several user_types and for example TEAM_LEADER cant create a team object but can see the list of teams. Which means for the same class view i want to use different permissions for POST and GET for example.
I'm looking to do this as dry as possible and i'm trying to follow the skinny view fat models design principle(also wondering if that's good practice to follow in 2021).
models.py for the user model
class User(AbstractBaseUser):
...fields here
objects = UserManager()
USERNAME_FIELD = "email"
def __str__(self):
return self.email
def has_perm(self, perm, obj=None):
if perm.Meta.verbose_name=="worksite" and perm.request.method =="POST":
if self.user_type <= self.DEPARTMENT_MANAGER:
return True
else:
return False
return True
views.py
class DashboardPermissions(BasePermission):
message="You dont have permission for this action"
def has_permission(self, request, view):
return request.user.has_perm(view.Meta.verbose_name)
class ViewName(CreateAPIView):
permission_classes = (IsAuthenticated,DashboardPermissions)
authentication_classes = ()
serializer_class = WorksiteSerializer
queryset = Worksite.objects.all()
class Meta:
verbose_name="view_name"
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
Bonus question would my solution create any performance issues?

Creating your custom Permission class is good practice. So that part looks OK to me. We could debate on whether the logic should be in the Permission or the User (like you did), but that's not a big deal.
If you want to have different permissions for different endpoints within your view, simply override the get_permissions method.
# Inherited method from APIView
def get_permissions(self):
"""
Instantiates and returns the list of permissions that this view requires.
"""
return [permission() for permission in self.permission_classes]
As you can see, for ALL services, it will fetch the permissions from self.permission_classes.
To use different permissions between GET/CREATE, you could create a dict of endpoint: [...permissions] and override get_permissions to fetch the one matching the current action
permissions = {
"create": [P1, P2,],
"get": [P1,]
}
def get_permissions(self):
action = self.it_is_somewhere_in_there
return [permission() for permissions in self.permissions[action]]

#JordanKowal's answer is correct, but as mentioned in the comments,
Also then i'd be repeating the permissions dict a lot ? in order to do it for every class of view right
For this you can create a mixin class. What it essentially allows you to do is move some code/feature that is to duplicated in multiple views to a standalone class and just inherit from it as per your convenience.
To extend on Jordan's answer, here's what a mixin class would look like:
class DefaultPermissionsMixin(object):
permissions = {
"create": [IsAuthenticated, DashboardPermissions],
"get": [DashboardPermissions]
}
def get_permissions(self):
# default `get_permissions` method
# reads `self.permission_classes`
perms = super().get_permissions()
if self.action in self.permissions.keys():
return perms + [p() for p in self.permissions[self.action]]
else:
return perms
class View1(CreateAPIView, DefaultPermissionsMixin):
# ...snip...
class View2(CreateAPIView, DefaultPermissionsMixin):
# i can overwrite here per my convenience
permissions = {
"create": [DashboardPermissions],
"delete": [],
}
# i can also define permissions the default way
# that will be enabled on all actions
permission_classes = [IsAuthenticated]
# ...snip...

Related

How do I use permission_classes in a custom method of a Viewset in DjangoRestFramework?

Suppose that I have a Viewset named UserViewset, and I have assigned IsAuthenticated permission to UserViewset Viewset. Now, I want to create a normal method (not an action method), and I want to assign another permission to that method which is IsAdminUser, how would I do that?
Below is the code with the method which I tried:
from rest_framework.viewsets import GenericViewSet
from rest_framework.permissions import IsAuthenticated, IsAdminUser
class UserViewset(GenericViewSet):
permission_classes = (IsAuthenticated,) # THIS IS THE DEFAULT PERMISSION I HAVE SET FOR THIS VIEWSET WHICH WILL APPLY TO ALL METHODS OR ACTION METHODS
def create(self, *args, **kwargs): # THIS IS MY CUSTOM METHOD
permission_classes = (IsAuthenticated, IsAdminUser) # I WANT SOMETHONG LIKE THIS, BUT IT DOES NOT WORK
.
.
.
You can override get_permission class
def get_permissions(self):
# check the action and return the permission class accordingly
if self.action == 'create':
return [IsAdminUser(),]
return [IsAuthenticated(), ]

DRF ViewSet extra action (`#action`) serializer_class

When I try to use Django Rest Framework extra actions on a viewset, I can not make the decorator's serializer_class work.
class ClientViewSet(ModelViewSet):
queryset = Client.objects.all()
serializer_class = ClientSerializer
def get_queryset(self):
# Do things
def get_serializer_class(self):
if self.action in ["create"]:
return CreateClientSerializer
elif self.action in ["retrieve"]:
return ClientDetailSerializer
return self.serializer_class
#action(detail=True, methods=["get"], serializer_class=ClientDetailSerializer)
def get_by_name(self, request, name=None):
"""
Get one Client searching by name.
#param request:
#param name: Client code
#return: Response
"""
queryset = get_object_or_404(Client, name__iexact=name)
serializer = self.get_serializer(queryset)
return Response(serializer.data)
So, even if the extra action is supposedly overriding the ViewSet default serializer class, I still get ClientSerializer instead of ClientDetailSerializer.
The official documentation states that...
The decorator allows you to override any viewset-level configuration such as permission_classes, serializer_class, filter_backends...:
My get_serializer_class override defaults to the ViewSet serializer_class attribute for my extra actions. If I understand correctly, this is basically what GenericAPIView get_serializer_class does under the hood:
def get_serializer_class(self):
"""
(...)
"""
assert self.serializer_class is not None, (
"'%s' should either include a `serializer_class` attribute, "
"or override the `get_serializer_class()` method."
% self.__class__.__name__
)
return self.serializer_class
I guess I'm missing something obvious here. Just can not figure out what...
Any help is appreciated. Thanks in advance :)
Why not use it like this? I'm guessing you're doing something wrong in get_serializer_class.
#action(detail=True, methods=["get"], serializer_class=ClientDetailSerializer)
def get_by_name(self, request, name=None):
"""
Get one Client searching by name.
#param request:
#param name: Client code
#return: Response
"""
object = get_object_or_404(Client, name__iexact=name)
serializer = ClientDetailSerializer(object)
return Response(serializer.data)
When you override the get_serializer_class without calling the super of this class, the super class doesn't run.
user this:
def get_serializer_class(self):
if self.action in ["create"]:
return CreateClientSerializer
elif self.action in ["retrieve"]:
return ClientDetailSerializer
return super().get_serializer_class()

How to do custom permission check for nested serializer with nested models having different permission in django

I'm building a webapp, where a user can create a team and add members and his/her projects. Everything is working fine, but now comes the permission part. One model will be Team and another Project. Right now i have written custom permission for both the models extending BasePermission.
The operation/permission would be :
User1 created a team Team1, can add any members and add his projects (no permission to add others project)
members of Team1 can add their own projects and edit (CRU) projects added by others. No permission for the members to delete Team1, only creator can delete the team.
A project can only be edited by the members of the team to which it is added. Others cannot. Only creator of the project can delete it.
Permissions:
from rest_framework import permissions
from .models import Team,Project
from rest_framework import serializers
class ProjectPermission(permissions.BasePermission):
message = "You do not have permission to perform this action with Project that doesn't belong to you or you are not a member of the team for this Project"
def has_object_permission(self, request,view, obj):
if not request.method in permissions.SAFE_METHODS:
if request.method != "DELETE":
if obj.team: #Team can be null when creating a project
return obj.created_by == request.user or request.user in obj.team.members.all()
return obj.created_by == request.user
return obj.created_by == request.user
return request.user.is_authenticated
def has_permission(self, request, view):
return request.user.is_authenticated
class TeamPermission(permissions.BasePermission):
message = "You do not have permission to perform this action with Team that is not created by you or you are not a member with full permission"
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS:
return request.user.is_authenticated
else:
if request.method != "DELETE":
if obj.created_by == request.user or request.user in obj.members.all():
return True
return obj.created_by == request.user
def has_permission(self, request, view):
if not request.method in permissions.SAFE_METHODS:
if request.method =="DELETE":
return True
Projects = request.data.get('Projects')
if request.method =="PUT" or request.method == "PATCH":
if len(Projects)==0:return True # for removing a list projects an empty array is passed, in the serializer set function "remove set(current projects - empty list)" is used to identify the projects to remove
if Projects:
perm = []
for Project in Projects:
try:
#this is the issue, if there are 100 projects to be added, it does 100 queries to fetch the object
perm.append(ProjectPermission().has_object_permission(request, view,Project.objects.get(id=Project)))
except Exception as e:
raise serializers.ValidationError(e)
if False in perm:
raise serializers.ValidationError(
{"detail":"You do not have permission to perform this action with Project that doesn't belong to you or you are not a member of the team for this Project"})
return True
else:
return request.user.is_authenticated
This line at the end causes 100 queries to db for 100 projects. i can do filter in by project list. but is there any alternatives ?
perm.append(ProjectPermission().has_object_permission(request, view,Project.objects.get(id=Project))
Views:
class ProjectView(viewsets.ModelViewSet):
'''
Returns a list of all the Projects. created by user and others
Supports CRUD
'''
queryset=Project.objects.select_related('team','created_by').all()
serializer_class=ProjectSerializer
permission_classes=[ProjectPermission]
class TeamView(viewsets.ModelViewSet):
"""
Returns a list of all the teams. created by user and others
Supports CRUD
"""
queryset=Team.objects.prefetch_related('members','projects').select_related('created_by').all()
serializer_class=TeamSerializer
permission_classes=[TeamPermission]
models:
class Team(models.Model):
team_name=models.CharField(max_length=50,blank=False,null=False,unique=True)
created_by=models.ForeignKey(User,on_delete=models.SET_NULL,null=True)
created_at=models.DateTimeField(auto_now_add=True)
members=models.ManyToManyField(User,related_name='members')
def __str__(self) -> str:
return self.team_name
class Meta:
ordering=['-created_at']
class Project(models.Model):
team=models.ForeignKey(Team,related_name='projects',blank=True,null=True,on_delete=models.SET_NULL)
project_name=models.CharField(max_length=50,blank=False,null=False,unique=True)
description=models.CharField(max_length=1000,null=True,blank=True)
created_by=models.ForeignKey(User,on_delete=models.SET_NULL,null=True)
created_at=models.DateTimeField(auto_now_add=True)
file_name=models.CharField(max_length=100,null=True,blank=True)
def __str__(self) -> str:
return self.project_name
class Meta:
ordering = ['-created_at']
Is there an efficient way to achieve what i need ? I read about adding permission in the models, but i have no idea how it can be done in this case. Any suggestion would be a great help
Let's break it down, any User can create a Team so there is one permission IsAuthenticated
Any Team member can CRU a Project so there are three permissions here:
IsAuthenticated & ( IsTeamOwner | IsTeamMember)
for deleting a Project, there are two permission (IsAuthenticated & IsProjectOwner) and so on so forth
so, the permissions could be something like that:
class IsTeamOwner(BasePermission):
message = "You do not have permission to perform this action"
def has_object_permission(self, request, view, obj):
return obj.created_by == request.user
class IsProjectOwner(BasePermission):
message = "You do not have permission to perform this action"
def has_object_permission(self, request, view, obj):
return obj.created_by == request.user
class IsTeamMember(BasePermission):
message = "You do not have permission to perform this action"
def has_object_permission(self, request, view, obj):
return request.user in obj.members.all()
Update
To check if the project id exists and is created by the current user or not:
try:
project = Project.objects.only('team').get(id=recieved_id, created_by=request.user)
except Project.DoesNotExist:
raise ProjectNotExist() # Create this exception
To check if the project id exists and belongs to the team that the current user is in or not:
try:
project = Project.objects.get(id=recieved_id).select_related('team')# try to use prefetch_related('team__members') also, I don't know it would work or not
if request.user not in project.team.members.all():
raise ProjectNotExist()
except Project.DoesNotExist:
raise ProjectNotExist()
and for more customization, after you checked if the project exists or not you could use bulk_update to update the team field in every project with only one query.

List Class View didn't return an HttpResponse

I've been trying to get a class-based list view to display all entries under a user's account (applicant), but when loading the page I'm given the following error:
The view jobassessment.views.view didn't return an HttpResponse object. It returned None instead.
To me that sounds like the URL dispatcher isn't running the correct view, but this is my URL file for both the whole site and the jobassessment application and I can't seem to spot the fault.
Site URL.py:
urlpatterns = [
path('admin/', admin.site.urls, name="admin"),
path('accounts/', include('django.contrib.auth.urls'), name="accounts"),
path('applicant/', include('userprofile.urls'), name="applicant"),
path('assessments/', include('jobassessment.urls')),
]
JobAssessment App's URL.py:
from django.urls import path
from . import views
urlpatterns = [
path("", views.AssessmentListView.as_view(), name="assessment"),
]
This is my ListView that is called:
class AssessmentListView(LoginRequiredMixin, generic.ListView):
model = Assessment
template_name ='assessments_index.html'
paginate_by = 5
def get(self, request, *args, **kwargs):
# Ensure they have first created an Applicant Profile
if not Applicant.objects.filter(user=self.request.user).exists():
messages.info(request, "You must create a profile before you can view any assessments.")
return redirect('profile_create_form')
def get_queryset(self):
return Assessment.objects.all().filter(applicant=Applicant.objects.filter(user=self.request.user)).order_by('-assessment_stage')
If Applicant of current login user not exists then your if condition fails and since there is no else
part in there so there is no HttpResponse returned from the view. So please add else part if applicant exists and return HttpResponse()
class AssessmentListView(LoginRequiredMixin, generic.ListView):
model = Assessment
template_name ='assessments_index.html'
paginate_by = 5
def get(self, request, *args, **kwargs):
# Ensure they have first created an Applicant Profile
if not Applicant.objects.filter(user=self.request.user).exists():
messages.info(request, "You must create a profile before you can view any assessments.")
return redirect('profile_create_form')
else:
return HttpResponse() #<------ add corresponding HttpResponse if Applicant exists.
def get_queryset(self):
return Assessment.objects.all().filter(applicant=Applicant.objects.filter(user=self.request.user)).order_by('-assessment_stage')
Following the django document on ListView filter it's better to handle it within get_queryset. So for your case it would be something like this:
class AssessmentListView(LoginRequiredMixin, generic.ListView):
model = Assessment
template_name ='assessments_index.html'
paginate_by = 5
def get_queryset(self):
# Ensure they have first created an Applicant Profile
if not Applicant.objects.filter(user=self.request.user).exists():
messages.info(request, "You must create a profile before you can view any assessments.")
return redirect('profile_create_form')
else:
return Assessment.objects.all().filter(applicant=Applicant.objects.filter(user=self.request.user)).order_by('-assessment_stage')

DRF request is not defined for getting current user id

So What I've been trying to do is to have my API view only return objects that have their attributes post_user to the current id of the logged in user. These post_user attributes are populated as whenever I post it populates the variable with the current user's id through my serializer.
However, I am not successful as it says request is not defined. I just want to get the current user's id so that I can use it to filter my object returns
views.py
# To retrieve and list all posts with DRF
class ListPosts(generics.ListCreateAPIView):
queryset = Posts.objects.get(post_user=request.user.id)
serializer_class = PostsSerializer
permission_classes = (permissions.IsAuthenticated,)
serializers.py
# serializer for posts to be taken
class PostsSerializer(serializers.ModelSerializer):
class Meta:
model = Posts
fields = ('id','post_title','post_content',)
def create(self, validated_data):
posts = Posts.objects.create(
post_title=validated_data['post_title'],
post_content=validated_data['post_content'],
# gets the id of the current user
post_user=self.context['request'].user.id,
)
posts.save()
return posts
error is in line
queryset = Posts.objects.get(post_user=request.user.id)
here request is not define at class declaration time. Solution is you can override the get_queryset method.
class ListPosts(generics.ListCreateAPIView):
queryset = Posts.objects.all()
serializer_class = PostsSerializer
permission_classes = (permissions.IsAuthenticated,)
def get_queryset(self, *args, **kwargs):
return Posts.objects.filter(post_user=self.request.user)
Inherit CreateModelMixin's features inside PostsSerializer and try to define your create() method like def create(request, *args, **kwargs).
Finally, you can try to get user id using request.user.id.
For a better documentation, you can check https://www.django-rest-framework.org/api-guide/generic-views/.
Also check what are Mixins and why do we use it (if you do not know).
For a little and brief definition, Mixins are just class with methods that can be mostly inherited and used by our views.
If you have any doubt, please comment.

Resources