Enforcing permissions through a related model in Django Rest Framework

Viewed 200

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:

  1. 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.
  2. 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?

2 Answers

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.
Related