Override the IsAuthenticated permission class with one that checks firebase - python

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):
...

Related

How to use has_object_permission to check if a user can access an object in function based views

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

Django-REST: custom permission doesn't work

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.

DRF and Token authentication with safe-deleted users?

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)

How would I override the perform_destroy method in django rest framework?

DRF currently has functionality that throws a 404 if an object doesn't exist in the db. For example
Request: /delete/1234
Response: 204 (success)
Request 2: /delete/1234
Response: 404 (not found)
This logic is very problematic for my mobile apps and i would like to change it so that I override the 404-not-found functionality. In other words, I want my request to be idempotent. For example:
Request: /delete/1234
Response: 204 (success)
Request 2: /delete/1234
Response: 204 (success)
I've been looking at the docs but i'm not really sure how to override the get_object_or_404 functionality.
I believe, if there is no object to delete, ideally it should return 404 as DRF does.
For your requirement the following code will do the trick:
from rest_framework import status,viewsets
from rest_framework.response import Response
from django.http import Http404
class ExampleDestroyViewset(viewset.ModelViewSet):
def destroy(self, request, *args, **kwargs):
try:
instance = self.get_object()
self.perform_destroy(instance)
except Http404:
pass
return Response(status=status.HTTP_204_NO_CONTENT)
To implement the custom functionality you need to override get_object() method in the viewset. Follow the links get_object and perform_destroy
class ExampleDestroyViewset(viewset.ModelViewSet):
queryset = # queryset
serializer_class = # serializer class
def get_queryset(self):
# write custom code
def perform_destroy(self, instance):
# write custom code

django-rest-framework: globally restrict requests to GET?

I am using django-rest-framework and I would like to to allow only GET requests to my API.
Is there some global setting for this?
Currently I'm decorating individual views, like this:
#api_view(['GET'])
def my_api_view(request, format=None):
# get data
You can write a custom Permission class IsRequestMethodGet which will grant access to only GET requests.
To implement the custom permission IsRequestMethodGet, override the BasePermission class and implement .has_permission(self, request, view) method. The method should return True if the request should be granted access, and False otherwise.
from rest_framework import permissions
class IsRequestMethodGet(permissions.BasePermission):
"""
The request is a GET request.
"""
def has_permission(self, request, view):
return request.method == 'GET' # Returns True if GET request
So, a request will be granted access if the request method is GET, otherwise not.
Now, you need to include this custom permission class in your global settings.
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': (
'my_app.permissions.IsRequestMethodGet',
)
}

Categories