Django Rest Framework - Deny User from PUSH when U

2019-07-29 18:49发布

问题:

At the moment i have permissions set which prevent a user from GET, DELETE and PUT if they are not the Object Owner of Stock. But for some reason, the permissions do not work when the user performs a PUSH i.e. any user can PUSH a Note to a Stock even if they are not the Stock Owner.

Why? And how do i properly check that when a User PUSHs a Note, they must be the Owner of Stock?


This is an example data PUSH sent via HTTPie:

http -a testuser:testpw POST http://127.0.0.1:8000/api/v1/notes/ note="Testing API" stock="36"

Where "36" is the stock_id for an existing Stock.

Here is the stock_note/models.py:

from django.db import models
from django.utils import timezone
from django.contrib.auth.models import User
import uuid

class Stock(models.Model):
    '''
    Model representing the stock info.
    '''
    user = models.ForeignKey(User)
    book_code = models.CharField(max_length=14, null=True, blank=True)

    def __str__(self):
        return self.book_code

class Note(models.Model):
    '''
    Model representing the stock note.
    '''
    user = models.ForeignKey(User)
    note = models.TextField(max_length=560)
    stock = models.ForeignKey(Stock, related_name='notes')
    date_note_created = models.DateTimeField(default=timezone.now)

    def __str__(self):
        return self.note

This is the api/serializers.py:

from stock_note.models import Stock, Note
from rest_framework import serializers

class StockSerializer(serializers.ModelSerializer):
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())
    notes = serializers.PrimaryKeyRelatedField(read_only=True, many=True)

    class Meta:
        model = Stock
        fields = ('id', 'user', 'book_code', 'notes')

class NoteSerializer(serializers.ModelSerializer):
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = Note
        fields = ('user', 'note', 'stock')

This is the api/views.py:

from rest_framework import generics
from stock_note.models import Stock, Note
from api.serializers import StockSerializer, NoteSerializer
from rest_framework.permissions import IsAuthenticated
from api.permissions import IsOwner

# Create your views here.

class StockList(generics.ListCreateAPIView):
    serializer_class = StockSerializer
    permission_classes = (IsAuthenticated, IsOwner)

    def get_queryset(self):
        user = self.request.user
        return Stock.objects.filter(user=user)

    def perform_create(self, serializer):
        serializer.save()

    def perform_update(self, serializer):
        serializer.save()

class NoteList(generics.ListCreateAPIView):
    serializer_class = NoteSerializer
    permission_classes = (IsAuthenticated, IsOwner)

    def get_queryset(self):
        user = self.request.user
        return Note.objects.filter(user=user)

    def perform_create(self, serializer):
        serializer.save()

    def perform_update(self, serializer):
        serializer.save()

class StockListDetail(generics.RetrieveUpdateDestroyAPIView):
    serializer_class = StockSerializer
    permission_classes = (IsAuthenticated, IsOwner)
    lookup_url_kwarg = 'stock_id'

    def get_queryset(self):
        stock = self.kwargs['stock_id']
        return Stock.objects.filter(id=stock)

class NoteListDetail(generics.RetrieveUpdateDestroyAPIView):
    serializer_class = NoteSerializer
    permission_classes = (IsAuthenticated, IsOwner)
    lookup_url_kwarg = 'note_id'

    def get_queryset(self):
        note = self.kwargs['note_id']
        return Note.objects.filter(id=note)

This is the api/permissions.py:

from rest_framework import permissions

class IsOwner(permissions.BasePermission):
    def has_permission(self, request, view):
        return request.user and request.user.is_authenticated()

    def has_object_permission(self, request, view, obj):
        return obj.user == request.user

And finally this is the api/urls.py:

from django.conf.urls import url, include
from api import views

urlpatterns = [
    #Endpoint to allow GET and POST stocks.
    url(r'^v1/stocks/$', views.StockList.as_view()),
    #Endpoint to allow GET and POST a note to a stock.
    url(r'^v1/notes/$', views.NoteList.as_view()),
    #Endpoint to allow GET, POST, PUSH, DELETE a stocknote
    url(r'^v1/stocks/(?P<stock_id>[0-9]+)/$', views.StockListDetail.as_view()),
    #Endpoint to allow GET, POST, PUSH, DELETE a Note
    url(r'^v1/notes/(?P<note_id>[0-9]+)/$', views.NoteListDetail.as_view()),
]

UPDATE:

Following on from Tom's answer the NoteSerializer now looks like this which means a User is now only able to PUSH a Note if they are the Owner of Stock (the new addition is the validate_stock function). Keep note that there is one difference between Tom's answer and this code: instead of just checking the value, i am checking for the value.id. This is explained further in the comments of the validate_stock function:

class NoteSerializer(serializers.ModelSerializer):
    user = serializers.HiddenField(default=serializers.CurrentUserDefault())

    class Meta:
        model = Note
        fields = ('user', 'note', 'stock')

    def validate_stock(self, value):
        '''
        This function checks if the User is the owner of Stock
        before allowing the User to PUSH a Note to the Stock.
        '''

        # You have to get the object ID because otherwise you get following error when
        # you try to perform Stock.object.get(...):
        #TypeError: int() argument must be a string, a bytes-like object or a number, not 'Stock'
        value_id = value.id

        stock_obj = Stock.objects.get(pk=value_id)
        user = self.context['request'].user

        if not stock_obj.user == user:
            raise serializers.ValidationError("You do not have permission to perform this action.")
        return value

回答1:

When you POST to v1/notes/ the only permission check that will run is has_permission. There's no existing instance being referred to in the URL, so get_object isn't called on the view, and the has_object_permission check isn't called (there's no instance to call it with.)

What you need for this case is to enforce validation on the serializer class that ensures that the stock value must correspond to a Stock instance that is owned by the user.

Something along these lines...

def validate_stock(self, value):
    stock = Stock.objects.get(pk=value)
    user = self.context['request'].user
    if not stock.user == user:
        raise serializers.ValidationError(...)
    return value