I know a lot of people already asked about handling with current user but I couldn't find solution so I post this.
What I want to do is to get, put and delete current user without providing pk.
I want to set endpoint like users/my_account
My current code is here
class MyAccountDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = CustomUser.objects.all()
serializer_class = UserSerializer
def get(self, request):
serializer = UserSerializer(request.user)
return Response(serializer.data)
And now I can get current user's info but when I try to update or delete the current user,
AssertionError: Expected view MyAccountDetail to be called with a URL keyword argument named "pk". Fix your URL conf, or set the .lookup_field attribute on the view correctly.
How can I solve this?
Update
urlpatterns = [
path('users/my_account', views.MyAccountDetail.as_view()),
]
In this case, you will need to override get_object() method in your MyAccountDetail view. For example:
from rest_framework.permissions import IsAuthenticated
class MyAccountDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = CustomUser.objects.all()
serializer_class = UserSerializer
permission_classes = (IsAuthenticated,)
def get_object(self):
return self.request.user
You need to do that, because by default get_object method looks for lookup_url_kwarg or lookup_field in the URL, and from that it will try to fetch the object using pk or whatever you have configured in lookup_field or lookup_url_kwarg.
FYI, I have added a permission class as well, because without it, self.request.user will be an anonymous user, hence will throw error.
Related
This question is similar to this one: Using different authentication for different operations in ModelViewSet in Django REST framework, but it didn't work for me.
I've got the following viewset:
class UserViewSet(viewsets.ModelViewSet):
serializer_class = UserSerializer
queryset = UserProfile.objects.none()
permission_classes = [SpecialPermission]
SpecialPermission looks like this:
class SpecialPermission(IsAuthenticated):
def has_permission(self, request, view):
if request.method == 'POST':
return True
return super().has_permission(request, view)
REST framework settings:
"DEFAULT_AUTHENTICATION_CLASSES": ["backend.api.authentication.ExpiringTokenAuthentication"],
"DEFAULT_PERMISSION_CLASSES": ["rest_framework.permissions.IsAuthenticated"],
I want to everybody to be able to post to UserViewSet but every other method should require Authentication. However, with the code above I get an Unauthorized Response on post.
What do I need to change?
Although it can be done, this requirement imo does not justify this ifology as auth/user related stuff should be clean and secure.
Instead extract POST method from this viewset to its own class.
class UserViewSet(mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
mixins.DestroyModelMixin,
mixins.ListModelMixin,
GenericViewSet):
serializer_class = UserSerializer
queryset = UserProfile.objects.none()
permission_classes = [SpecialPermission]
class CreateUserView(CreateAPIView):
serializer_class = UserSerializer
queryset = UserProfile.objects.none()
authentication_classes = []
if you really want to disable authentication in this viewset I'd rather recommend this
def get_authenticators(self):
if self.action == 'create':
return []
return super().get_authenticators()
That's more explicit than your solution.
I figured it out: Making perform_authentication lazy solved my problem. Now I can post but authentication still runs on all other methods where it is needed.
def perform_authentication(self, request):
"""
Perform authentication on the incoming request.
Note that if you override this and simply 'pass', then authentication
will instead be performed lazily, the first time either
`request.user` or `request.auth` is accessed.
"""
pass
I want users to have access only to the records that belong to them, not to any other users' records so
I've created the following view:
class AddressViewSet(viewsets.ModelViewSet):
authentication_classes = (TokenAuthentication,)
permission_classes = [IsAuthenticated, IsOwner]
queryset = Address.objects.all()
def retrieve(self, request, pk):
address = self.address_service.get_by_id(pk)
serializer = AddressSerializer(address)
return Response(serializer.data, status=status.HTTP_200_OK)
I want only the owner of the records to have access to all the methods in this view ie retrieve, list, etc (I'll implement the remaining methods later) so I created the following permissions.py file in my core app:
class IsOwner(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
print('here in has_object_permission...')
return obj.user == request.user
this wasn't working, so after going through stackoverflow answers I found this one Django Rest Framework owner permissions where it indicates that has_permission method must be implemented. But as you can see in that answer, it's trying to get the id from the view.kwargs but my view.kwargs contains only the pk and not the user. How can I fix this? Do I need to implicitly pass the user id in the request url? that doesn't sound right.
Here's the test I'm using to verify a user cannot access other user's records:
def test_when_a_user_tries_to_access_another_users_address_then_an_error_is_returned(self):
user2 = UserFactory.create()
addresses = AddressFactory.create_batch(3, user=user2)
address_ids = [address.id for address in addresses]
random_address_id = random.choice(address_ids)
url = reverse(self.ADDRESSES_DETAIL_URL, args=(random_address_id,))
res = self.client.get(url, format='json')
print(res.data)
Currently just using the test to check the data returned, will implement the assertions later on.
Edit
So I added has_permission method to IsOwner:
def has_permission(self, request, view):
return request.user and request.user.is_authenticated
if I put a print statement here it gets printed, but doesn't seem to be hitting the has_object_permission method, none of the prints I added there are being displayed
This answer was the right one for me.
It says:
The has_object_permission is not called for list views. The
documentation says the following:
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.
Link to documentation
Note: The instance-level has_object_permission method will only be called if the view-level has_permission checks have already passed.
You need to write the has_permission too in order to make your custom permission works.
Here is the official docs and mentioned it. It should works after you add in has_permission.
As mentioned in the docs, permissions are checked on self.get_object method call.
def get_object(self):
obj = get_object_or_404(self.get_queryset(), pk=self.kwargs["pk"])
self.check_object_permissions(self.request, obj)
return obj
Which basically is all retrieve method does in ModelViewSet
def retrieve(self, request, *args, **kwargs):
instance = self.get_object()
serializer = self.get_serializer(instance)
return Response(serializer.data)
Whatever it is you do in self.address_service.get_by_id(pk) should either be moved to self.get_object or call self.check_object_permissions(self.request, obj) in retrieve method.
In the basic scenario this is all you need. There's no need to overwrite retrieve method.
class AddressViewSet(viewsets.ModelViewSet):
serializer_class = AddressSerializer
authentication_classes = (TokenAuthentication,)
permission_classes = [IsAuthenticated, IsOwner]
queryset = Address.objects.all()
I try to implement rest_framework for Django 2.
I have a URL that should show diffrent content for authentificated users.
Anonymous users just will get a limited view, authentificated will see everything.
In the documentation I can only methods that will deny everything, but not a if else clause.
Basicly I try something like this:
class StoryViewSet(viewsets.ModelViewSet):
if IsAuthenticated == True:
queryset = Story.objects.all()
else:
queryset = Story.objects.filter(story_is_save=True)
serializer_class = StorySerializer
Obviously IsAuthenticated is not a True/False statement i can query.
Any ideas how I could do this easily?
Thanks
You need to override get_queryset method:
class StoryViewSet(viewsets.ModelViewSet):
serializer_class = StorySerializer
def get_queryset(self):
if self.request.user.is_authenticated():
queryset = Story.objects.all()
else:
queryset = Story.objects.filter(story_is_save=True)
return queryset
I'm implementing 'users/me/'. (I alse read the the article, but I'm try to add some specific function)
I made a function in UserViewSet:
#list_route()
def me(self, request):
# ... find user ...
request.path = reverse('user-detail', kwargs={'pk': user.id})
self.partial_update(request)
It raises AssertionError: Expected view UserViewSet to be called with a URL keyword argument named "pk". Fix your URL conf, or set the `.lookup_field` attribute on the view correctly.
print(request.path) is /users/2/, so reverse is working.
urls.py:
router = routers.DefaultRouter()
router.register(r'users', UserViewSet, base_name='user')
How could I deliver a pk? I have no idea what is the mistake I made.
UserViewSet code:
class UserViewSet(viewsets.ModelViewSet):
queryset = User.objects.all()
serializer_class = UserSerializer
#list_route(methods=['patch'], permission_classes=[IsAuthenticated])
def me(self, request):
user = get_object_or_404(Token, key=request.auth).user
request.path = reverse('user-detail', kwargs={'pk': user.id})
self.partial_update(request, pk=user.id)
serializer = self.get_serializer(user)
return Response(serializer.data)
I would not implement it using a list_route but rather using another approach: overriding get_object(self) on the view:
def get_object(self):
if self.kwargs.get(self.lookup_field, None) == 'me':
# ... find user probably using self.get_queryset() or return self.request.user
self.check_object_permissions(self.request, user) # may not be required, see get_object link
return user
return super().get_object()
This make you API react exactly the same whenever you use 'me' or the user pk and actions will be the same (get, put, patch, ... detailed_routes)
Notes:
this only works if you are using a GenericView or subclass like ModelViewset
I use python3 super syntax, do not forget to adapt it if you are using python2
I am trying to use DjangoModelPermissions and it does not seem to work properly.
This is the code:
class TestViewSet(viewsets.ModelViewSet):
model = Test
serializer_class = serializers.TestSerializer
permission_classes = (permissions.DjangoModelPermissions,)
def create(self, request):
response_data = {}
response_data['type'] = 'error'
data=json.loads(request.raw_post_data)
test = Test.objects.create(name=data['name'],\
description=data['description'],\
start_date=data['start_date'],\
end_date=data['end_date'])
#save changes
test.save()
return Response({'status': 'ok', "result": test.id})
I don't think it makes any difference in this case but I am using django_mongodb_engine.
I have a user that has no permissions, and it is able to create Test instances. On the other hand, how can I block also GET so just users with the right permissions can perform that action?
Thanks
The reason for DjangoModelPermissions is not working here is clearly explained in the DRF docs
"This permission must only be applied to views that have a .queryset property or get_queryset() method."
Check the docs here
The solution is:
Add queryset to your model
class TestViewSet(viewsets.ModelViewSet):
serializer_class = serializers.TestSerializer
permission_classes = (permissions.DjangoModelPermissions,)
queryset = Test.objects.all()
or if you want to override the default queryset method use this method as you like
def get_queryset(self):
return super().get_queryset()
Also, I noticed you don't have to specify the model in your ModelViewSet. If you specify your Test model in TestSerializer you only have to specify the serializer in ModelViewSet that's how DRF works
My problem was the same. The user could create new instance in the database despite of the permission class. I looked into the django-guardian and found that this back-end should be default:
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend',
)
So I added it in my settings.py file and now it works and a user without a permission cannot create new instance. I hope it helps.
You need to have django-guardian with DRF for DjangoModelPermissions to work correctly. It's also mentioned in the original docs http://www.django-rest-framework.org/api-guide/permissions under the DjangoModelPermissions paragraph
If it still doesn't work as it should then let us know