detail_route on viewset not following object level permissions - django-rest-framework

This is my ViewSet:
class PageViewSet(viewsets.ModelViewSet):
queryset = Page.objects.all()
serializer_class = PageSerializer
permission_classes = (IsAuthenticated, IsOwnerOrReadOnly,)
def perform_create(self, serializer):
serializer.save(owner=self.request.user, location=self.request.user.userextended.location)
#detail_route(methods=['post'])
def add(self, request, pk=None):
try:
page = Page.objects.get(pk=pk)
except:
content = {'Page': ['The page you are trying to add no longer exists.']}
return Response(content, status=status.HTTP_400_BAD_REQUEST)
page.users.add(request.user)
return Response(status=status.HTTP_204_NO_CONTENT)
And this is my IsOwnerOrReadOnly permission:
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Allow only the owner (and admin) of the object to make changes (i.e.
do PUT, PATCH, DELETE and POST requests. A user is an
owner of an object if the object has an attribute
called owner and owner is == request.user. If the
object is a User object or if the object does not have
an owner attribute, then return object == request.user.
"""
def has_permission(self, request, view):
print('In permission')
return True
def has_object_permission(self, request, view, obj):
print('In object level permission')
if request.method in permissions.SAFE_METHODS:
return True
if request.user.is_staff:
return True
try:
return obj.owner == request.user
except: # if obj does not have an owner property (e.g. users don't
# have owner properties).
return obj == request.user
The problem is, I can post to add-detail as an authenticated user even when I am not the owner of the page. When I do the post request, it only prints In permission twice and never prints In object level permission. My question is, since it is a detail_route and is clearly using a {lookup} object (see here which shows that it is using an object: http://www.django-rest-framework.org/api-guide/routers/), how come the has_object_permission() function from the permission class does not get called? How come only the regular has_permission() function gets called?
I'm hoping for someone to link to a documentation which verifies that even for detail_route, only has_permission gets called.
Edit: This is not a duplicate of Django rest framework ignores has_object_permission because I am using a ModelViewSet which inherits from GenericAPIView (as mentioned in the documentation: http://www.django-rest-framework.org/api-guide/viewsets/#modelviewset).

See this answer. There are some differences but the point is the same: check_object_permissions is not called.
Although you are inheriting from ModelViewSet you are not using its get_object in your add method to retrieve the page. It is get_object that calls check_object_permissions (not the router) for retrieve, update etc., so obviously it doesn't get called.
To fix it, do the following:
class PageViewSet(viewsets.ModelViewSet):
# ...
#detail_route(methods=['post'])
def add(self, request, pk=None):
page = self.get_object()
page.users.add(request.user)
return Response(status=status.HTTP_204_NO_CONTENT)
or just do self.check_object_permissions(page) yourself somewhere in your implementation.

Related

Accessing ViewSet object list to provide extra context to serializer

I am attempting to add context to a serializer within a ModelViewSet which is dependent on the current paged object list in context. I'll explain with an example.
I am building a viewsets.ModelViewSet that lists Users and a list of favorite_foods. However- the list of user's favorite foods in some external microservice accessible via API. Building a ViewSet to collect objects and performing HTTP requests on each is trivial, we can do something like this:
class UserViewSet(viewsets.ViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserSerializer(serializers.ModelSerializer):
favorite_foods = serializers.SerializerMethodField()
def get_favorite_foods(self, instance):
# performs an HTTP call and returns a list[] of foods.
return evil_coupled_microservice_client.get_user_food_list(instance)
class Meta:
model = User
fields = ('id', 'name', 'favorite_foods')
The issue is (aside from some ugly infrastructure coupling) this is going to make an HTTP request count equivalent to the page size. Instead- it would be great if I could prefetch the favorite food lists for all users on the page in a single HTTP call, and just add them into context, like this:
class UserViewSet(viewsets.ViewSet):
def get_serializer_context(self):
context = super().get_serializer_context()
users = <-- This is where I want to know what users are in the current filtered, paginated response.
users_food_dict = evil_coupled_microservice_client.get_many_users_food_list(users)
context.update({'usesr_foods': users_food_dict})
return context
However- it doesn't appear there is any way to fetch the object list that's going to be serialized. Although (I'm fairly sure) get_serializer_context is called after the queryset is filtered and paginated, I'm not sure how to access it without doing some really hacking re-compiling of the queryset based on the query_params and other pieces attached to the class.
I'll post my current solution. It's not terrible, but I'm still hoping for a cleaner built-in.
class UserViewSet(viewsets.ViewSet):
...
def list(self, request, *args, **kwargs):
# Overrwite `ListModelMixin` and store current set
queryset = self.filter_queryset(self.get_queryset())
page = self.paginate_queryset(queryset)
if page is not None:
self.current_queryset = page
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
self.current_queryset = queryset
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
This is untested so far (not sure about functionality on Detail endpoints for instance) allows for the current_queryset to be fetched within the serializer context.

Add temporary guest (Ephemeral user) to Django Rest Framework

I need the following:
Regular User creates a Guest . This guest must have the ability to POST and GET from two Views.
I've created the following Guest Model:
class Guest(models.Model):
reservation = models.ForeignKey(Reservation, related_name='guests', on_delete=models.CASCADE)
keys = models.ManyToManyField(Key)
email = models.EmailField(blank=False)
phone = models.DecimalField(decimal_places=0, max_digits=10)
name = models.CharField(max_length=64)
created = models.DateTimeField(default=timezone.now)
state = models.IntegerField(default=1,validators=[MaxValueValidator(1),MinValueValidator(0)])
class Meta:
ordering = ['created']
This guest "user" is a short-lived one (After a few days is deleted) . I'd like to avoid having to create an User instance for each guest.
My approach would be to allow anonymous users on ViewA and ViewB, then, i'd check request['guest'] to get the guest's ID , retrieve it from database and compare request['headers'] token value.
As for permissions on DRF (Pseudo-code to get the idea)
class IsGuestActive(permissions.BasePermission):
def has_permission(self, request, view):
if request.user.is_anonymous()
guest_id = request['guest']
token = request['headers'].token
return (Guest.objects.get(pk=guest_id, token=token) is not None)
I'm very new to Django, my main questions are:
is my approach viable?
Should i create an User with foreign key to Guest and use groups instead?
I really appreciate any tips on what's the best way to create Ephemeral users like i need to.
Edit (1)
Regarding safety, it really is dangerous using a duct tape type of solution, because no middleware would be responsible for authenticating a guest (Making the system prone to inadvertently giving permissions). But the approach of analyzing the headers within a permission class is not that unfeasible. From official documentation:
class BlocklistPermission(permissions.BasePermission):
"""
Global permission check for blocked IPs.
"""
def has_permission(self, request, view):
ip_addr = request.META['REMOTE_ADDR']
blocked = Blocklist.objects.filter(ip_addr=ip_addr).exists()
return not blocked
We can see here, that META is used to validate an access .
I'll need to dig further.
I found a solution, that i think is viable, to create a temporary controlled access to a View, without having to create a custom User:
Exclaimer: This method seems to be somewhat secure, considering the ephemeral nature of the guests using the system. It does not address the issue of controlling sessions, or retrieving access.
The use-case: You need a guest to access Views, but guest needs special authorization. This Guest will be short-lived.
middleware.py
Create a middleware where you define request.guest = None
class SimpleMiddleware:
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
request.guest = None
models.py
Now, we'll define our guest to have primary key as a token value:
class Guest(models.Model):
reservation = models.ForeignKey(Reservation, related_name='guests', on_delete=models.CASCADE)
token = models.CharField(max_length=40, primary_key=True)
...
class Meta:
ordering = ['created']
verbose_name = 'Guest'
verbose_name_plural = 'Guests'
def save(self, *args, **kwargs):
if not self.token:
self.token = self.generate_key()
return super().save(*args, **kwargs)
def generate_key(self):
return binascii.hexlify(os.urandom(20)).decode()
def __str__(self):
return self.token
In my case, i send the guest a link with a token as a parameter.
As you don't want to store the token on localstorage or any other place that isn't an httpOnly cookie to avoid XSS attacks, define a View to respond to the first click of the link on the email:
class SetGuest(APIView):
authentication_classes = []
permission_classes = [permissions.AllowAny]
def get(self, request, token, format=None):
response = HttpResponse("", status=308)
response['Location'] = "http://localhost/index.html?redirect&action=confirm&status=success"
response.set_cookie(key='guest_token', value=token, path='/', samesite='Lax', max_age=3600*24*7, secure=False, httponly=True)
return response
This View will redirect your guest to a page, and set up the CSRF and guest_token cookie.
Define the decorator creating a decorators.py:
def validate_guest(view_func):
def wrapper(self, request, *args, **kwargs):
try:
token = request.COOKIES.get('guest_token')
request.guest = Guest.objects.get(token=token)
except Exception as e:
logger.debug(str(e))
return redirect('/api/')
return view_func(self, request, *args, **kwargs)
return wrapper
Now, to protect the view so it only works for guests with a token:
class GuestGetKeys(APIView):
authentication_classes = []
permission_classes = [IsGuest]
dec_list = [ensure_csrf_cookie]
#validate_guest
def get(self, request, format=None):
return Response({'status': 'valid', 'success': 'Guest is logged'}, status=status.HTTP_200_OK)
One can achieve the same without using the decorator, but permissions.py instead.
The reason i prefer not to, is because through decorators i can control redirects in a cleaner way.
why defining the new dict value using a middleware? I tried modifying the request object directly on the Decorator and on the permissions class, it simply doesn't work.
There are a few short-comings from this approach, like session revoking issues, there's no other way of starting a "session" other than accessing the link sent to the guest, tokens are plain text and a database breach would allow anyone access as guests. Sessions don't expire, and so on.

Serializer `validate()` not getting called on `is_valid()` on POST

I want to create a non class-based form to facilitate uniform logging in by users across all front-end apps. Currently, it looks like this
My serializer class:
class EmployeeLoginSerializer(serializers.Serializer):
username = serializers.CharField(min_length=6)
password = serializers.CharField(min_length=6)
def validate_credentials(self, attrs):
print('validating')
try:
emp: Employee = Employee.objects.get(username=attrs['username'])
if crypto.verify_password(attrs['password'], emp.password_hash):
return attrs
except Exception:
raise serializers.ValidationError("Incorrect username or password")
raise serializers.ValidationError('Incorrect username or password')
My view class:
class TestView(APIView):
serializer_class = EmployeeLoginSerializer
def get(self, request, *args, **kwargs):
return Response({'Message': 'Get works'})
def post(self, request, *args, **kwargs):
print(request.POST)
serializer = self.serializer_class(data=request.POST)
if serializer.is_valid():
return Response({'Message': 'Credentials were correct'})
My issue is that serializer.is_valid() doesn't seem to be calling on validate automatically. I know I could just call serializer.validate() manually but all the docs and questions on StackOverflow seem to show validate() being called by is_valid() automatically so I get that feeling that that wouldn't be the best practice. Is there something I'm missing?
The is_valid() method will call validate() method of the serializer and validate_FIELD_NAME() methods.
In your code, the validate_credentials() seems a regular class method which won't detect by DRF since the credentials isn't a field on the serializer.

creating two models in one serializer of django rest framework

During registration of a user I would like to have both a User object and a EmailContact object created in one api call. The two objects should not be linked.
I have the following serializer:
class RegistrationSerializer(serializers.Serializer):
userserializer=UserAccountSerializer() #reuse existing modelserializer
emailcontactserializer=EmailContactSerializer() #reuse existing modelserializer
def create(self, validated_data):
emailcontact_data = validated_data.pop('emailcontactserializer')
user_data = validated_data.pop('userserializer')
emailcontact= EmailContact.objects.create(**emailcontact_data)
user= User.objects.create(**user_data)
return user
and the following Apiview:
class RegistrationAPIView(APIView):
permission_classes = (AllowAny,)
serializer_class = RegistrationSerializer
def post(self, request):
serializer = self.serializer_class(data=request.data)
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
The error I get is the following (occurs after the serializer.save()):
AttributeError at /api/register
Got AttributeError when attempting to get a value for field userserializer on serializer RegistrationSerializer.
The serializer field might be named incorrectly and not match any attribute or key on the User instance.
Original exception text was: 'User' object has no attribute 'userserializer'.
In your RegistrationSerializer.create() method, you're returning a User object. The serializer will try to serialize that object into this representation:
{
'userserializer': x,
'emailcontactserializer': y
}
But it's complaining because the User you returned doesn't have a userserializer field.
If you really want to return a User from this API call, you could make your RegistrationSerializer a ModelSerializer with Meta.model=User, and override perform_create to pop out the emailcontact_data. (I'd name the field something like RegistrationSerializer.email_contact to make the representation clearer, IMO the phrase "serializer" shouldn't be present on the client-visible portion of the API).
Alternatively, if you want to render both of your sub-serializers, you can create a RegistrationSerializer instance in RegistrationSerializer.create by passing in the data, something like
return RegistrationSerializer(data={'emailcontactserializer':
emailcontact_data, 'userserializer': user_data})

Require authorization for all but the listing view in a viewset

I have a ViewSet called BuildViewSet, with a bunch of detail views and one list view:
class BuildViewSet(viewsets.ModelViewSet):
queryset = Build.objects.all()
def list(self, request):
# Do some filtering on self.queryset based on user preferences
return super(BuildViewSet, self).list(request)
#detail_route(methods=['post'])
def transition(self, request):
…
# And a bunch of other methods, all prefixed with #detail_route
I set up REST Framework so the default authorization class is rest_framework.permissions.IsAuthenticated:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
However, I want my list() view to be available for everyone, even unauthenticated. I tried changing my list() method like this:
#list_route(permission_classes=(AllowAny,))
def list(self, request):
…
But this seems to have no effect:
AppError: Bad response: 401 UNAUTHORIZED (not 200 OK or 3xx redirect for http://localhost/api/v1/builds/)
'{"detail":"Authentication credentials were not provided."}
Changing the #detail_route to #permission_classes like this gives the same result:
#permission_classes((AllowAny,))
def list(self, request):
…
So it seems that list_route(…) is not the way to go, but in this case, what is?
You need to decorate the list method with the #permission_classes decorator, but the problem is that this decorator is only used for function-based views. Thus, you have two choices:
1) Convert the list view to a function-based view.
2) Authorize all views from the viewset by setting permission_classes = (AllowAny,) at the class level. In order to limit access to the other views, you will have to manually check the permissions using either a decorator or by calling the check_is_authenticated method:
def check_is_authenticated(self, request):
"""
Inspired from rest_framework.views.check_permissions
"""
if not IsAuthenticated.has_permission(request, self):
self.permission_denied(
request, message=getattr(permission, 'message', None)
)
Since all views that require a permission are already decorated with #detail_route, all you have to do is create a new #authenticated_detail_route decorator.
EDIT 3) Another, alternative solution would be to overload the check_permissions method:
def check_permissions(self, request):
if self.is_request_list(request):
return
return super(BuildViewSet, self).check_permissions(request)
The implementation of the is_request_list method is left as an exercise to the reader :-)
(seriously though, I'm not sufficiently familiar with django-rest-framework to offer an implementation. It would probably involve checking the request.method attribute)
EDIT As mentioned by the OP in a comment, in check_permissions the self.action attribute holds the "list" method name.

Resources