Add temporary guest (Ephemeral user) to Django Rest Framework - 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.

Related

DRF Filter PrimaryKeyField Based on Current User

I have a view set up to return a list of books to a user, which is retrieved from a simple book model based on the currently logged-in user. However, I also have ReadingSession model which has a foreign key relationship to both the Book, and the User.
When I'm retrieving the books for the user, I'd like to, at the very least, return a list of primary keys that I can use to get the length of in my client.
The following code will get the full set of readingsessions in my BookSerializer:
from rest_framework import serializers
from books.models import Book
class BookSerializer(serializers.ModelSerializer):
readingsession_set = serializers.PrimaryKeyRelatedField(
many=True, read_only=True)
class Meta:
model = Book
fields = ["id", "title", "author", "publisher",
"publish_date", "description", "category",
"language", "small_thumbnail", "thumbnail",
"readingsession_set"]
However, the problem with this is that it will return all of the readingsessions, regardless of whether or not the session belongs to that user.
I'd like to be able to filter that so that it will only return the readingsessions for the current user. Something along the lines of:
readingsession_set = serializers.PrimaryKeyRelatedField(queryset=ReadingSession.objects.filter(user=user), read_only=True)
But I've tried various ways of trying to pass the user (self.request.user) from the APIView but none seem to work. I've tried passing a context, and tried passing extra **kwargs in __init__ but none seem to work.
Is there a way of achieving this? Or am I taking the wrong approach?
Thanks
The user is not present on the serializer's declaration but during its instantiation.
Therefore, you can filter querysets by user within the __init__ method.
from rest_framework import serializers
from bar.models import Foo
class RandomSerializer(serializers.Serializer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
user_foos = Foo.objects.filter(user=self._user)
self.fields['foo_ids'] = serializers.PrimaryKeyRelatedField(
required=False,
many=True,
read_only=False,
queryset=user_foos,
default=user_foos)
#property
def _user(self):
request = self.context.get('request', None)
if request:
return request.user
Don't forget to pass the request object to the serializer in the context (if necessary, e.g., using a simple APIView.
from rest_framework import views
class RandomView(views.APIView):
serializer_class = RandomSerializer
def post(self, request):
serializer = self.serializer_class(
data=request.data, context={'request': request})
# ...
serializer = RandomSerializer(data=request.data, context={'request': request}
You can access the user of the request on the serializer by means of the context.
As mentioned in the documentation, you can always do:
serializer = AccountSerializer(account, context={'request': request})
Thus, you will be able to use self.context['request'].user inside your serializer.
Hope that's what you're after.

last_login does not update by API in django rest

I created a simple endpoint to create and update users. It's work fine except for the field last_login that not update when a user login by API.
My example:
My urls:
router.register(r"user", foo.UserViewSet)
My Serializer:
class UserSerializer(serializers.ModelSerializer):
password = serializers.CharField(write_only=True)
def create(self, validated_data):
user = User.objects.create(
username=validated_data['username'],
first_name=validated_data['first_name'],
last_name=validated_data['last_name'],
email=validated_data["email"],
last_login=validated_data["last_login"],
)
user.set_password(validated_data['password'])
user.save()
return user
class Meta:
model = User
exclude = (
"groups",
"user_permissions",
)
My View:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
pagination_class = StandardPagination
serializer_class = UserSerializer
model = User
def update(self, request, *args, **kwargs):
user = self.get_object()
serializer = self.get_serializer(
user, data=request.data, partial=True
)
serializer.is_valid(raise_exception=True)
self.perform_update(serializer)
return Response(serializer.data)
When I log with any user (in API) the field last_login is always null. By django admin is ok.
Update
I had some progress and problems putting the code in my UserViewSet:
def perform_authentication(self, request):
user = request.user
user.last_login = timezone.now()
user.save()
return user
Ok. last_login is registering, but only in user endpoint and every request in this endpoint.
As far as my own investigation on this issue goes, it turns out that last_login field is tightly coupled with default Django authentication system (session authentication), that is why this field is updated when you use Django Admin, as Admin uses session authentication.
In your case, it looks like some other type of authentication is used, and the signal which updates this field is not triggered. See the link below where the signal is handled:
https://github.com/django/django/blob/5f8495a40ab1554e81ac845484da890dd390e1d8/django/contrib/auth/models.py#L14
So, in order to update this field properly you should probably do it manually.
Here is one more discussion on this matter:
last_login field is not updated when authenticating using Tokenauthentication in Django Rest Framework
Maybe it could be the default setting, try this in settings.py:
SIMPLE_JWT = {
// ...
'UPDATE_LAST_LOGIN': True,
// ...
}
source: https://django-rest-framework-simplejwt.readthedocs.io/en/latest/settings.html

Django rest framework - Raising exception / Handling empty results while filtering

I have a user profile class and am checking if a user exists and if not want to create that user.
Am using the filter class for userprofile so that the client can call :
http://localhost:8000/users/?email=a#b.com
and if the result is empty will create a user with the email address.
Is there a way to intercept the query result and raise an exception when its empty and handle that to create the user.
If there is a better way would like to be corrected as well.
class UserQueryFilter(django_filters.rest_framework.FilterSet):
email = django_filters.CharFilter(name="user__email")
username = django_filters.CharFilter(name="user__username")
class Meta:
model = UserProfile
fields = ['email', 'username']
class UserViewSet(viewsets.ReadOnlyModelViewSet):
queryset = UserProfile.objects.all()
serializer_class = UserSerializer
filter_class = UserQueryFilter
Any help is appreciated.
Thanks
Anand
Django Rest Framework provide a functionality that is disabled by default. Maybe it could give you another approach to resolve your problem: PUT as create
In other hand, if you really need to create the user through a GET request with a querystring, you can use a MethodFilter from django-filters, for example:
class UserFilters(FilterSet):
user = MethodFilter(action='filter_user')
class Meta:
model = User
fields = ['user']
def filter_user(self, queryset, value):
if not value:
# Here Raise Exception
else:
# Check if the user exists, if not create it
users = queryset.filter(Q(username=value) | Q(email=value))
if not users.exists:
user = User.objects.create(...)
return queryset.filter(pk=user.id)
else:
return users
Hope this can help you. I'm not pretty sure about it works in that exact way but it's the idea.
Personally, I recommend you that try to execute that tasks through a more appropriate request like POST or PUT and manage in the corresponding method.

detail_route on viewset not following object level permissions

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.

Django REST framework restrict posting and browsable Api-fields

I use the Django Rest framework together with an JavaScript app. I have some difficulties to get the posting of new data items right with the generic ModelViewSet
Most importantly I want to restrict what a poster can submit
(they should only be allowed to post items that have the user_id of this user (the authenticated user of the session).
I don't know when/where I should check for this? Is this a validation problem?
How I understand the permission classes is that they restrict the method (Post/Get) or check for user groups.
Also my user field in the item model is a foreign key to the user model
so the browsable api suggest in the Html-form a dropdown with the information about other users. (their email adresses and some other fields).
My data items look like this
[{
"id": 792,
"name": "test",
"category": 1,
"value": 5,
"user": "33"
}]
Here is my Serializer and the Viewset:
class ItemSerializer(serializers.ModelSerializer):
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
I can't believe this issue with the DRF Create/Update (Post/Put) form isn't more widely discussed.
It's a huge data privacy issue - e.g. One can restrict the List API view to only show items owned by a User via overriding the get_queryset method inside as below:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
But as OP notes, when accessing the API Create/Post or Update/Put form for the ItemViewSet, there is seemingly no easy way to restrict the user options to the user itself.
I had a similar issue myself building a survey platform, where I want to restrict choice of survey/question/options etc. to those owned by the user, and prevent users from inadvertently seeing each other's data.
Jocelyn's answer works for the OP's particular situation where we already know that the Item.user must equal request.user, so we override this on the perform_create method.
But Jocelyn's solution is insufficient for situations where you do not know in advance what the relationship between model instances will be (e.g. in my case where a new question objected could be added to any one of a user's surveys).
The solution I came up with was the nuclear option: do away with the Viewset altogether for Create and Update functionality, and use a set of custom views.APIView classes instead, as below (adapted for the case of the OP, only showing Create).
class ItemCreateView(views.APIView):
def post(self, request, format=None):
post_user_id = int(request.data['user'].split('/')[-2])
request_user_id = request.user.id
serializer = ItemSerializer(data=request.data, context={'request': request})
if post_user_id == request_user_id:
if serializer.is_valid():
serializer.save()
return Response(serializer.data, status=status.HTTP_201_CREATED)
else:
return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)
else:
return Response('Not Allowed: Owner is not User', status=status.HTTP_401_UNAUTHORIZED)
Please note, I'm using a HyperlinkedModelSerializer rather than a plain ModelSerializer, hence the need for .split('/')[-2] to grab the post_user_id
Handling the user field
First set the user field to be readonly:
# serializers.py
class ItemSerializer(serializers.ModelSerializer):
user = serializers.ReadOnlyField()
class Meta:
model = Item
fields = ('id',
'name',
'category',
'value',
'user',
)
Then auto-set the user id on creation:
# views.py
class ItemViewSet(viewsets.ModelViewSet):
serializer_class = ItemSerializer
def get_queryset(self):
return Item.objects.filter(user=self.request.user)
def perform_create(self, serializer):
serializer.save(user=self.request.user.customer)
Handling permissions
Just use standard permissions mechanism to define a custom one :
# permissions.py
from rest_framework import permissions
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
return (request.user.is_authenticated() and
(obj.user == request.user.customer))
...and use it in your viewset :
# views.py
from permissions import IsOwner
class ItemViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwner]
...

Resources