How to assign Django object ownership without explicitly declaring an owner field on all models?

Viewed 4082

I'm currently trying to figure out per user object permissions for our Django website API.

I have several models with sensitive information, that I need to be able to filter on a user basis.

For a simplified example of one of the models:

  • Restaurant, main customer of the website.

  • User, each user gets assigned a restaurant when the user account is created. As such, a restaurant can have many users and they all should only be able to access that restaurant's information.

  • Oven, which belong to a specific restaurant. A restaurant can have many ovens.

  • Recipe, which belong to an oven. An oven can have many different recipes.

  • Recipe Results, which belong to a recipe. There can be many different Recipe Results belonging to the same Recipe (different ingredients tried, etc).

There are at least 12+ different models. All models from a particular restaurant have to be hidden from other restaurants, we don't want them to be able to look at other restaurant recipes after all!

Not all models have a user = models.ForeignKey(User)

Without having to go into each one of my models and declaring owner = models.ForeignKey(User), is there a way to filter them in my API List Views and Detail Views?

Currently my List API View looks like this (simplified example):

class RecipeResultsListAPIView(ListAPIView):
    queryset = RecipeResults.objects.all()
    queryset = queryset.prefetch_related('oven')
    serializer_class = RecipeResultsListSerializer 
    filter_backends = (DjangoFilterBackend,)
    filter_fields = ('id', 'time', 'oven', 'recipe_name', 'recipe_description')
    pagination_class = ExpertPageNumberPagination

    def list(self, request):
       user = User.objects.get(username=request.user)
       restaurant = Restaurant.objects.get(user=user)
       ovens = Oven.objects.filter(restaurant=restaurant)
       queryset = RecipeResults.objects.filter(oven__in=ovens)
       serializer = RecipeResultsListSerializer(queryset, many=True, context={'request':request})
       return Response(serializer.data)

And the model for that looks like this:

class RecipeResults(models.Model):
    time = models.DateTimeField()
    oven = models.ForeignKey(Oven, on_delete=models.CASCADE)
    recipe_name = models.CharField(max_length=20)
    recipe_description = models.CharField(max_length=50)

    def __str__(self):
        return str(self.time) + ': ' + self.recipe_name + ' = ' + self.recipe_description

    def __key(self):
        return self.oven, self.time, self.recipe_name

    def __eq__(self, y):
        return isinstance(y, self.__class__) and self.__key() == y.__key()

    def __hash__(self):
        return hash(self.__key())

    class Meta:
        unique_together=(('time','recipe_name', 'oven-'),)

Specifically looking at the modified list method, currently this works properly to filter API call results to display only those Recipe Results that belong to the user that is logged in.

What I'm trying to figure out is if there's an easier way to do this, as for each model I would have to trace back ownership to the specific restaurant which would get confusing fast as I have 12+ different models.

What I'm not sure is if declaring "owner = models.ForeignKey(User)" on each of those models is the way to go. It feels like it would create many extra steps when retrieving the data.

I have also tried

class IsOwnerOrAdmin(BasePermission):
    """
    Custom permission to only allow owners of an object to see and edit it.
    Admin users however have access to all.
    """
    def has_object_permission(self, request, view, obj):
        # Permissions are only allowed to the owner of the snippet
        if request.user.is_staff:
            return True
        return obj.user == request.user

But this didn't seem to filter properly, and besides, not all of the models have a user field assigned to them.

Please keep in mind I'm a junior developer and I'm learning a lot as I go. I'm only working on the API side of the company. The website and schema is already a work in progress and other systems depend on it, and so I'm trying not to modify the schema or models too much (I would like to avoid this if possible, but will do it if it's the only way). I was also brought in just to work on the API at first. The company understands I'm a junior developer and I'm extremely grateful to have been given the opportunity to grow while learning this project, but this one issue seems to be giving me a lot more trouble than actually building the rest of the API for the website.

I would greatly appreciate any help I can get with this!

1 Answers

I think you might benefit from model inheritance in this case.

You can define a base model for your owner-affected objects.

An example can look like:

class OwnedModel(models.Model):
    owner = models.ForeignKey(User)

    class Meta:
        abstract = True

Then you can simply add this as the base for your other models:

class SomeModel(OwnedModel):
    """
    This class already has the owner field
    """

A big downside of this approach is that you will still need a migration that will alter every involved table.

If you aren't allowed to do that, you might be able to do it with a loose, non relational approach, for example with django's permission model. You can assign automatically generated permission strings, eg: myapp.mymodel.pkey:

A final alternative is this third party source app that handles things for you: django-guardian

Related