I'm using a Django package named django-safedelete that allows to delete users without removing them from the database.
Basically, it adds a delete attribute to the model, and the queries like User.objects.all() won't return the deleted models.
You can still query all objects using a special manager. For example User.objects.all_with_deleted() will return all users , including the deleted ones. User.objects.deleted_only() will return the deleted ones.
This works as expected, except in one case.
I'm using Token Authentication for my users with Django Rest Framework 3.9, and in my DRF views, I'm using the built-in permission IsAuthenticated.
Code of a basic CBV I'm using:
class MyView(APIView):
permission_classes = (IsAuthenticated,)
def get(self, request):
return Response(status=HTTP_200_OK)
Code of the DRF implementation of IsAuthenticated permission:
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)
The problem
When a user is soft deleted, he's still able to authenticate using its token.
I'm expecting the user to have a 401 Unauthorized error when he's soft deleted.
What's wrong?
The DRF already uses the is_active property to decide if the user is able to authenticate. Whenever you delete a user, just be sure to set is_active to False at the same time.
For django-safedelete:
Since you're using django-safedelete, you'll have to override the delete() method to de-activate and then use super() to do the original behavior, something like:
class MyUserModel(SafeDeleteModel):
_safedelete_policy = SOFT_DELETE
my_field = models.TextField()
def delete(self, *args, **kwargs):
self.is_active = False
super().delete(*args, **kwargs)
def undelete(self, *args, **kwargs):
self.is_active = True
super().undelete(*args, **kwargs)
Note that this works with QuerySets too because the manager for SafeDeleteModel overrides the QuerySet delete() method. (See: https://github.com/makinacorpus/django-safedelete/blob/master/safedelete/queryset.py)
The benefit to this solution is that you do not have to change the auth class on every APIView, and any apps that rely on the is_active property of the User model will behave sanely. Plus, if you don't do this then you'll have deleted objects that are also active, so that doesn't make much sense.
Why?
If we look into the authenticate_credentials() method of DRF TokenAuthentication [source-code], we could see that,
def authenticate_credentials(self, key):
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active:
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
Which indicates that it's not filtering out the soft deleted User instances
Solution?
Create a Custom Authentication class and wire-up in the corresponding view
# authentication.py
from rest_framework.authentication import TokenAuthentication, exceptions, _
class CustomTokenAuthentication(TokenAuthentication):
def authenticate_credentials(self, key):
model = self.get_model()
try:
token = model.objects.select_related('user').get(key=key)
except model.DoesNotExist:
raise exceptions.AuthenticationFailed(_('Invalid token.'))
if not token.user.is_active or not token.user.deleted: # Here I added something new !!
raise exceptions.AuthenticationFailed(_('User inactive or deleted.'))
return (token.user, token)
and wire-up in views
# views.py
from rest_framework.views import APIView
class MyView(APIView):
authentication_classes = (CustomTokenAuthentication,)
permission_classes = (IsAuthenticated,)
def get(self, request):
return Response(status=HTTP_200_OK)
Related
I want to know the major difference between self.request.user (when using generic view View) and request.user (when using a user defined view), at times when I use django's generic View (django.views.generic import View) which has access to the django User model, I'd interact with the authenticated user as request.user and self.request.user without having any problem.
For instance, in a django views.py:
from django.contrib.auth import get_user_model
User = get_user_model()
class GetUserView(View):
def get (self, request, *args, **kwargs):
user_qs = User.objects.filter(username=request.user.username)
#Do some other stuffs here
class AnotherGetUserView(View):
def get(self, request, *args, **kwargs):
user_qs = User.objects.filter(username=self.request.user.username)
#Do some other stuffs here
I realize that the two queryset works fine but I can't still figure out the best to use.
There is no difference. Before triggering the get method, it will run the .setup(…) method [Django-doc] which will set self.request = request as well as self.args and self.kwargs to the positional and named URL parameters respectively. Unless you override this, self.request will thus always refer to the same object as request, and the documentation explicitly says that you should not override the .setup(…) method without making a call to the super method which will thus set self.request.
It however does not make much sense to do User.objects.filter(username=request.user.username): you already have the user object: that is request.user, so here you will only make extra database requests. user_qs is simply a queryset with one element: request.user. You can thus for example use request.user.email to obtain the email address of the logged in user.
You might want to use the LoginRequiredMixin [Django-doc] to ensure that the view can only be called if the user has been logged in, so:
from django.contrib.auth.mixins import LoginRequiredMixin
class GetUserView(LoginRequiredMixin, View):
def get (self, request, *args, **kwargs):
# …
I am using firebase(pyrebase library) for my authentication with a django backend and a react frontend.For this to work I had to override the DRF auth class TokenAuthentication with my FirebaseAuthentication. But I still get 401 unauthorised when I try to access a view since I also need to override the drf permission class isAuthenticated.But I have been searching for a way to do this with python without success.Any help would be appreciated. Below is a snippet of the permission class and where its applied on my views
DRF permissions.py
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)
views.py
class FinanceTransactionList(GenericAPIView):
authentication_classes = [FirebaseAuthentication]
permission_classes = [IsAuthenticated]
#classmethod
#encryption_check
def post(self, request, *args, **kwargs):
...
To implement custom permission, override BasePermission and implement either, or both, of the following methods:
.has_permission(self, request, view)
.has_object_permission(self, request, view, obj)
The methods should return True if the request should be granted access, and False otherwise.
If you need to test if a request is a read operation or a write operation, you should check the request method against the constant SAFE_METHODS, which is a tuple containing 'GET', 'OPTIONS', and 'HEAD'. For example:
if request.method in permissions.SAFE_METHODS:
# Check permissions for the read-only request
else:
# Check permissions for writing request
Custom permissions will raise a PermissionDenied exception if the test fails. To change the error message associated with the exception, implement a message attribute directly on your custom permission. Otherwise, the default_detail attribute from PermissionDenied will be used. Similarly, to change the code identifier associated with the exception, implement a code attribute directly on your custom permission - otherwise, the default_code attribute from PermissionDenied will be used.
from rest_framework import permissions
class CustomerAccessPermission(permissions.BasePermission):
message = 'Firebase Auth Required.'
def has_permission(self, request, view):
...
I have a note object which can be accessd from notes/{pk}. If the method is GET or a read only method I was to allow anyone access to the note as long as the note is public (note.is_private = False)
I've implemented this as:
#api_view(['GET', 'DELETE', 'PUT'])
def detail_notes(request, pk):
try:
note = Note.objects.get(pk=pk)
except Note.DoesNotExist:
return Response(status=status.HTTP_404_NOT_FOUND)
if request.method == 'GET':
response = NoteSerializer(note)
return Response(response.data)
...
If the method is PUT or DELETE I want to allow access to the note only if the current user is the owner of the note. I implemented this permission (according to the docs) as follows:
class IsOwnerOrIsPublic(BasePermission):
def has_object_permission(self, request, view, obj):
user = obj.user
privacy = obj.is_private
if request.method in SAFE_METHODS:
return not privacy # is the note public and is this a read only request ?
return request.user == obj.user
However, when I add the #permission_classes([IsOwnerOrIsPublic]) decorator to my view the permission doesn't restrict access to an unauthorized user. I'm able to view any note with a pk.
I tried explicitly calling IsOwnerOrIsPublic.has_object_permissions(), with this code in my view:
authorized = IsOwnerOrIsPublic.has_object_permission(request, note)
if not authorized:
return Response(status=HTTP_401_UNAUTHORIZED)
But I get the error has_object_permission() missing 2 required positional arguments: 'view' and 'obj' (obviously), and I do not know what other arguments to pass in. For example, what is the view argument?
How do I make this permission work on this view? Alternatively, how do I make this constraint work?
Note that my solution is based on generic views which is more simpler to implement.
Try the following view class:
from rest_framework import generics
from django.shortcuts import get_object_or_404
from rest_framework.authentication import TokenAuthentication
from rest_framework.permissions import IsAuthenticated
...
# generics.RetrieveUpdateDestroyAPIView will allow Get, put, delete
class NoteDetailAPIView(generics.RetrieveUpdateDestroyAPIView):
"""
Retrieve, update, or delete note by its author
Retrieve only for readers
"""
# I'll assume you are using token authentication
authentication_classes = (TokenAuthentication,)
permission_classes = (IsAuthenticated, IsOwnerOrIsPublic,)
serializer_class = NoteSerializer
def get_object(self):
note = get_object_or_404(Note, pk=self.kwargs['pk'])
self.check_object_permissions(self.request, note)
return note
I'm trying to make a custom permission using this guide
views.py
class CustomModelList(generics.ListAPIView):
queryset = CustomModel.objects.all()
serializer_class = CustomModelSerializer
permission_classes = [IsAuthenticatedOrReadOnly, IsCustomOrReadOnly]
def get(self, request, format=None):
# some logic
def post(self, request, format=None):
# some logic
Just for experiment I've created this permission not to apply anyway
pesmissions.py
class IsCustomOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
return False
But when POST request sends to server it takes no effect -- I'm able to create new model instance.
I think that since you are using a list view, custom object level permissions are not checked automatically.
Also note that the generic views will only check the object-level permissions for views that retrieve a single model instance. If you require object-level filtering of list views, you'll need to filter the queryset separately. See the filtering documentation for more details.
You can try overriding the has_permission method instead and see if that works, or check the permissions manually.
How can I make a ModelViewSet accept the POST method to create an object? When I attempt to call the endpoint I get a 405 'Method "POST" not allowed.'.
Within views.py:
class AccountViewSet(viewsets.ModelViewSet):
"""An Account ModelViewSet."""
model = Account
serializer_class = AccountSerializer
queryset = Account.objects.all().order_by('name')
Within serializers.py:
class AccountSerializer(serializers.ModelSerializer):
name = serializers.CharField(required=False)
active_until = serializers.DateTimeField()
class Meta:
model = Account
fields = [
'name',
'active_until',
]
def create(self, validated_data):
with transaction.atomic():
Account.objects.create(**validated_data)
within urls.py:
from rest_framework import routers
router = routers.SimpleRouter()
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
Do I need to create a specific #action? my attempts to do so have yet to be successful. If that is the case what would the url = reverse('app:accounts-<NAME>') be such that I can call it from tests? I haven't found a full example (urls.py, views.py, serializers.py, and tests etc).
I discovered what the issue was, I had a conflicting route. There was a higher level endpoint registered before the AccountViewSet.
router.register(
prefix=r'v1/auth',
viewset=UserViewSet,
base_name='users',
)
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
Django runs through each URL pattern, in order, and stops at the first one that matches the requested URL.. I should have been ordered this way:
router.register(
prefix=r'v1/auth/accounts',
viewset=AccountViewSet,
base_name='accounts',
)
router.register(
prefix=r'v1/auth',
viewset=UserViewSet,
base_name='users',
)
despite the fact that reverse('appname:acccounts-list') worked, the underlying URL router still thought I was calling the UserViewSet.
From the docs:
A ViewSet class is simply a type of class-based View, that does not provide any method handlers such as .get() or .post(), and instead provides actions such as .list() and .create().
And here is a list of supported actions:
def list(self, request):
pass
def create(self, request):
pass
def retrieve(self, request, pk=None):
pass
def update(self, request, pk=None):
pass
def partial_update(self, request, pk=None):
pass
def destroy(self, request, pk=None):
pass
So no post is not directly supported but create is.
So your end point would be v1/auth/accounts/create when using the a router instead v1/auth/accounts/post.
I honestly prefer using class based or function based views when working with DRF. It resembles regular django views more closely and makes more sense to me when working with them. You woul write you views and urls pretty much like regular django urls and views.