Django Rest Framework custom Permission Class with ManyToManyField - django

I am trying to write a permission class for django rest api view.
this is my models
from django.db import models
from django.contrib.auth.models import User
class Profile(models.Model):
name = models.CharField(max_length=50)
auth_user = models.OneToOneField(User, on_delete=models.CASCADE)
class Group(models.Model):
admin = models.ManyToManyField(Profile, related_name='g_admin')
members = models.ManyToManyField(Profile, related_name='g_mess')
I want only group admin can perform an action in particular pages, for this i am writing a permission class:
class IsGroupAdmin(BasePermission):
"""
Allows access only to Group Admin.
"""
def has_permission(self, request, view):
# return bool() do something here
I am going through trouble to write the permission class to check if group admin or not. i am facing issue coz, this is many to many field.
can anyone help me to write this?

You can get the User from request.user. I don't know how you can get the Group from request (it may be in the request payload, it depends on your design) but lets assume that you pass the Group to the permission class, then it would be something like that:
class IsGroupAdmin(BasePermission):
"""
Allows access only to Group Admin.
"""
def __init__(self, group):
super().__init__()
self.group = group
def has_permission(self, request, view):
return self.group.profile_set.filter(auth_user=request.user).any()
Note: If you can get the group from request so you don't need to pass group to the IsGroupAdmin class and this would be much easier. But if you cant do that and you'r going to use just the above permission class, you'r going to need the partial function. so you need to import it:
from functools import partial
And then in the views, initiate permission_classes like that:
permission_classes = [partial(IsGroupAdmin, group)]
However, I suggest you to use the easier way, cause I think the group must be in request. Lets say that group is in the request payload, then you must implement IsGroupAdmin as:
class IsGroupAdmin(BasePermission):
"""
Allows access only to Group Admin.
"""
def has_permission(self, request, view):
group = request.data.get('group', '')
return group.profile_set.filter(auth_user=request.user).any()
And use it just like a normal permission. (no need to use partial function)

your data structure is wrong
admin and members should be m2m with user not with Profile.
for permission, you have to make a query like
Group.objects.filter(admin__auth_user__id=request.id) the true else return flase.
use through table it will reduce your work and make some sort of flag which gives you an idea that request user is the admin or not(in User model use signal) so every time you don't have to fire a query in the database.

Related

Enforcing permissions through a related model in Django Rest Framework

I'm working on building out permissions for an API built with Django REST Framework. Let's say I have the following models:
from django.db import models
class Study(models.Model):
pass
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
I have basic serializers and views for both of these models. I'll be using per-object permissions to grant users access to one or more studies. I want users to only be able to view Results for a Study which they have permissions to. There are two ways I can think of to do this, and neither seem ideal:
Keep per-object permissions on Results in sync with Study. This is just a non-starter since we want Study to always be the source of truth.
Write a custom permissions class which checks permissions on the related Study when a user tries to access a Result. This actually isn't too bad, but I couldn't find examples of others doing it this way and it got me thinking that I may be thinking about this fundamentally wrong.
Are there existing solutions for this out there? Or is a custom permissions class the way to go? If so, do you have examples of others who've implemented this way?
As you stated, you can make custom permission as per the second way:
And include the permission in your view:
I am considering your study model with some parameter course, based on that i am writing the solution you can consider for any element in study model
models.py
from django.db import models
class Study(models.Model):
course = models.CharField(max_length=50)
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
In permission.py
from rest_framework import permissions
class ResultOrReadOnly(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in permissions.SAFE_METHODS and obj.study.course == request.GET.get('course') :
# read only requests
return True
else:
# other requests such as post, patch, put
return obj.study == request.GET.get('course')
And include ,
class ReviewDetail(viewsets.ViewSet):
permission_classes =[ResultOrReadOnly]
And in urls.py,
Modify it to accept the URL parameter course
I would create a new field called enrolled_users in the Study model to indicate which all user has access to the particular Study object.
from django.db import models
from django.conf import settings
class Study(models.Model):
enrolled_users = models.ManyToManyField(
settings.AUTH_USER_MODEL,
related_name="studies"
)
# other fields
class Result(models.Model):
study = models.ForeignKey(Study)
value = models.IntegerField(null=False)
Then, it will very easy in DRF to filter the queryset in the views
# views.py
from .models import Study, Result
from rest_framework.permissions import IsAuthenticated
class StudyModelViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return Study.objects.filter(enrolled_users=self.request.user)
class ResultModelViewSet(viewsets.ModelViewSet):
permission_classes = (IsAuthenticated,)
def get_queryset(self):
return Result.objects.filter(study__enrolled_users=self.request.user)
Notes
This will handle the object permission (Detail View) request as well
This will not return a status code of HTTP 403, but HTTP 404
Here I used the ModelViewSet class, but, you can use any views, but the filter plays the role.

Identify type of logged in user

I have a custom user setup like this:
class CustomUser(AbstractUser):
pass
class Employee(CustomUser):
user = models.OneToOneField(settings.AUTH_USER_MODEL)
# other fields
In settings.py, I then add the following key:
AUTH_USER_MODEL = 'myapp.CustomUser'
I want to identify who logged in redirect them to appropriate views or urls.
In my account activation view, after the logging them in I redirect them to their appropriate page like this
if hasattr(user, 'employee'):
return redirect('edit_employee', slug=user.employee.slug)
else:
return redirect('index')
But this doesn't feel that right as I need to use this in other places like showing a different profile page link in the templates.
How do I better identify the regular user and employee in views and templates?
AFAIK you should not store different types of users in different tables. It will make your life pretty hard when defining relationships between other models and your users model.
My suggested approach would be having different profile models for different types of users and using a generic FK or some sort of other similar approaches to find out the user type and get their profile.
class CustomUser(AbstractUser):
USER_TYPE_EMPLOYEE = 'employee'
USER_TYPES = (
(USER_TYPE_EMPLOYEE, _('Employee')),
)
user_type = models.CharField(max_length=max(map(len, map(operator.itemgetter(0), CustomUser.USER_TYPES))), choices=CustomUser.USER_TPYES)
#property
def profile_model(self):
return {
USER_TYPE_EMPLOYEE: EmployeeProfile
}[self.user_type]
#property
def profile(self):
return self.profile_model.objects.get_or_create(user_id=self.pk)
class EmployeeProfile(models.Model):
user = models.OneToOneField(settings.AUTH_USER_MODEL, related_name='employee_profile')
The idea of extending the User Model to create an Employee Model doesn't seem good to me. Instead of this, you can use Django Group Model and add the user to the employee group. In this way, you can easily check if a user belongs to Employee group or not.
Additionally, you can also use django permissions here.
Assign your custom permissions to the employee group and restrict other users to view employee pages.

Django Custom Permission for Authorization

I am working on a Django Project, where one model (lets say Document) has the following field:
#In models.py
class Document (models.Model):
choice = (('Yes','Yes'), ('No','No'))
authorized = models.CharField (max_length=3, choices=choice, default='No')
Now, as a normal user creates a Document object, the authorized field is no. However, the superior needs to authorize the same before it is actually approved. Now, is there a permission system in django where one user can create an object but not authorize, whereas some other user - who has the permission - can authorize? If not, is the only way to do it is to create a custom field in user model and check it every time?
First of all, why you need to store possible values in CharField and not in BooleanField? I think you should consider changing to BooleanField.
You can do that by providing custom ModelAmin class in admin.py:
from django.contrib import admin
from .models import Document
#admin.register(Document)
class DocumentModelAdmin(admin.ModelAdmin):
def get_form(self, request, obj=None, **kwargs):
if not request.user.is_superuser:
self.exclude = ['authorized']
return super(DocumentModelAdmin, self).get_form(request, obj, **kwargs)
So now on admin page of Document if it is not a superuser, user won't see authorized field. You can change that code for checking if it should be specific user, or has some permissions, or if user belongs to some Group and etc.
UPDATE
If you want it to be in general views, you can just pass different forms to users, depend on their roles|permissions|groups(i don't know how your so called senior is different from rest of the users). So the answer would be: create two forms, then pass on of them in template based on your request.user attributes.
Django has awesome auth system. I couldn't understand you scenario.
But you could try something like this below
By default every Model object comes with three Permission object like (add_document, change_document and delete_document in your case).
If you want some custom permission you can add it in model Meta class like this:
You can add these permission to User object or Group object.
models.py
class Document (models.Model):
######
class Meta:
permissions = (("Can see document dashbaord", "see_document" ),)
and run python manage.py migrate to create new Permission object with codename as "see_document".
You can implement permissions in request handled by view like this:
view.py
from django.contrib.auth.mixins import PermissionRequiredMixin, permission_required
# For function based view
#pemission_required('document.see_document')
def someview(request):
######
pass
# For class based views
class SomeView(PermissionRequiredMixin, BaseView):
permission_required = 'document.see_document'
This could redirect any user with out the permssion to permission denied page. For more go through this https://docs.djangoproject.com/en/1.10/topics/auth/

How can I create on the fly a Auth Group when creating another object from Admin site?

I have a model which create some kind of object (let's say a Course).
I add new objects from the admin front-end
When I create an object, I would like to automatically create a Auth Group with a certain name (not important, but the name of the group must be the same that a created object field).
Is it possible? I have read something about Admin actions, but haven't found anything clear.
You can easily create a group by doing the following:
from django.contrib.auth.models import Group
newgroup = Group.objects.create(name=course.name)
You can put this code in your models like this (or maybe create a custom model manager):
from django.contrib.auth.models import User, Group
class Course(models.Model):
name = models.CharField(max_length=100)
#classmethod
def create(course, name):
newcourse = course(title=name)
# create the group for the course
newgroup = Group.objects.create(name=newcourse.name)
return newcourse
Then, you can create your course:
course = Course.create(name="Django: The Web framework for perfectionists with deadlines")
Daniel it seems a good approximation. I have managed a solution as well overriding the method save_model for the model CourseAdmin, inside the admin.py. Thanks a lot, your implementation seems cleaner!
Here is what I have done:
----- admin.py -----
from django.contrib.auth.models import Group
class CourseAdmin(admin.ModelAdmin):
def save_model(self, request, obj, form, change):
g = Group(name=obj.code)
g.save()
obj.save() # Without this line the object is not saved into the Course model!
admin.site.register(CourseAdmin)

Is it possible to limit of object creation of a model in admin panel?

I just want to know that is it possible to limit the number of objects of a model in admin panel?
It is that, for example, I have a model named 'Homepage' and in the admin panel I don't want a user can create more than one instance of Homepage.
Is there a way I can do this?
If it's just the admin that you want to affect (and don't want to affect the database model), you can create a custom ModelAdmin subclass:
class HomePageAdmin(admin.ModelAdmin):
def add_view(self, request):
if request.method == "POST":
# Assuming you want a single, global HomePage object
if HomePage.objects.count() > 1:
# redirect to a page saying
# you can't create more than one
return HttpResponseRedirect("foo")
return super(HomePageAdmin, self).add_view(request)
# ...
admin.site.register(HomePage, HomePageAdmin)
An alternative strategy to do the same thing is to create a custom ModelForm for HomePage, with a clean method that enforces the single HomePage requirement. This will make your requirement appear as a validation error, rather than as a redirect (or as a database error):
from django import forms
from django.forms.util import ErrorList
class HomePageModelForm(forms.ModelForm):
def clean(self):
if HomePage.objects.count() > 1:
self._errors.setdefault('__all__', ErrorList()).append("You can only create one HomePage object.")
return self.cleaned_data
# ...
class HomePageAdmin(admin.ModelAdmin):
form = HomePageModelForm
# ...
admin.site.register(HomePage, HomePageAdmin)
If it's "one HomePage per user", you will need HomePage to have a ForeignKey to User and adapt the above. You may also need to store the current User object in threadlocals in order to access it from HomePageModelForm.clean
If you want to limit Homepage to one for every user, then you could use one-to-one relation, with OneToOneField. As for limiting to N - a pre_save signal might be useful.
Try
class HomePage(models.Model):
user = models.ForeignKey(User, unique=True)
homepage = models.CharField(max_length=100, unique=True)
class Meta:
unique_together = (("user", "homepage"),)