Django REST Framework how to allow DELETE only without pk - python

I need to create an endpoint:
DELETE /users (deleted ids in the request body)
So, i created a class UserViewSet, that extends mixins.DestroyModelMixin and GenericViewSet and implemented the method def delete(self, request), it works for request DELETE /users, but the problem is that user can send DELETE /users/5 request and then i got an error 'Not found` (user with the current pk is not found, because i don't have it).
I want to forbid send DELETE /users/{id} requests and allow only DELETE /users.
How can i do this?
I tried to use decorator #action(methods=['delete'], detail=True) for the delete method, but its doesn't change anything.
Also i tried to use this signature:
def delete(self, request, pk=None):
if pk:
return Response(data='PK is not allowed in DELETE method')
For some reason it doesn't even match this method with DELETE/{id} and i still get the error 'Not found'.
UPD. I delete mixins.DestroyModelMixin from the extend classes and now DELETE /users works but DELETE/{id} gives me 404 (Page not found) when i try to send DELETE with id.
I would like something like method not allowed
UPD2.
Code:
def destroy(self, request, pk=None):
raise exceptions.MethodNotAllowed('DELETE with PK')
Works for me, but i think this is the bad solution...

You get 404 (Page not found) because you don't have /users/<int:id> in your URLConf.

Related

Django Rest Framework gives 302 in Unit tests when force_login() on detail view?

I'm using Django Rest Framework to serve an API. I've got a couple tests which work great. To do a post the user needs to be logged in and I also do some checks for the detail view for a logged in user. I do this as follows:
class DeviceTestCase(APITestCase):
USERNAME = "username"
EMAIL = 'a#b.com'
PASSWORD = "password"
def setUp(self):
self.sa_group, _ = Group.objects.get_or_create(name=settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME)
self.authorized_user = User.objects.create_user(self.USERNAME, self.EMAIL, self.PASSWORD)
self.sa_group.user_set.add(self.authorized_user)
def test_post(self):
device = DeviceFactory.build()
url = reverse('device-list')
self.client.force_login(self.authorized_user)
response = self.client.post(url, data={'some': 'test', 'data': 'here'}, format='json')
self.client.logout()
self.assertEqual(status.HTTP_201_CREATED, response.status_code)
# And some more tests here
def test_detail_logged_in(self):
device = DeviceFactory.create()
url = reverse('device-detail', kwargs={'pk': device.pk})
self.client.force_login(self.authorized_user)
response = self.client.get(url)
self.client.logout()
self.assertEqual(status.HTTP_200_OK, response.status_code, 'Wrong response code for {}'.format(url))
# And some more tests here
The first test works great. It posts the new record and all checks pass. The second test fails though. It gives an error saying
AssertionError: 200 != 302 : Wrong response code for /sa/devices/1/
It turns out the list view redirects the user to the login screen. Why does the first test log the user in perfectly, but does the second test redirect the user to the login screen? Am I missing something?
Here is the view:
class APIAuthGroup(InAuthGroup):
"""
A permission to allow all GETS, but only allow a POST if a user is logged in,
and is a member of the slimme apparaten role inside keycloak.
"""
allowed_group_names = [settings.KEYCLOAK_SA_WRITE_PERMISSION_NAME]
def has_permission(self, request, view):
return request.method in SAFE_METHODS \
or super(APIAuthGroup, self).has_permission(request, view)
class DevicesViewSet(DatapuntViewSetWritable):
"""
A view that will return the devices and makes it possible to post new ones
"""
queryset = Device.objects.all().order_by('id')
serializer_class = DeviceSerializer
serializer_detail_class = DeviceSerializer
http_method_names = ['post', 'list', 'get']
permission_classes = [APIAuthGroup]
Here is why you are getting this error.
Dependent Libraries
I did some searching by Class Names to find which libraries you were using so that I can re-create the problem on my machine. The library causing the problem is the one called keycloak_idc. This library installs another library mozilla_django_oidc which would turn out to be the reason you are getting this.
Why This Library Is Causing The Problem
Inside the README file of this library, it gives you instructions on how to set it up. These are found in this file. Inside these instructions, it instructed you to add the AUTHENTICATION_BACKENDS
AUTHENTICATION_BACKENDS = [
'keycloak_oidc.auth.OIDCAuthenticationBackend',
...
]
When you add this authentication backend, all your requests pass through a Middleware defined inside the SessionRefresh class defined inside mozilla_django_oidc/middleware.py. Inside this class, the method process_request() is always called.
The first thing this method does is call the is_refreshable_url() method which always returns False if the request method was POST. Otherwise (when the request method is GET), it will return True.
Now the body of this if condition was as follows.
if not self.is_refreshable_url(request):
LOGGER.debug('request is not refreshable')
return
# lots of stuff in here
return HttpResponseRedirect(redirect_url)
Since this is a middleware, if the request was POST and the return was None, Django would just proceed with actually doing your request. However when the request is GET and the line return HttpResponseRedirect(redirect_url) is triggered instead, Django will not even proceed with calling your view and will return the 302 response immediately.
The Solution
After a couple of hours debugging this, I do not the exact logic behind this middleware or what exactly are you trying to do to provide a concrete solution since this all started based off guess-work but a naive fix can be that you remove the AUTHENTICATION_BACKENDS from your settings file. While I feel that this is not acceptable, maybe you can try using another library that accomplishes what you're trying to do or find an alternative way to do it. Also, maybe you can contact the author and see what they think.
So i guess you have tested this and you get still the same result:
class APIAuthGroup(InAuthGroup):
def has_permission(self, request, view):
return True
Why do you use DeviceFactory.build() in the first test and DeviceFactory.create() in the second?
Maybe a merge of the two can help you:
def test_get(self):
device = DeviceFactory.build()
url = reverse('device-list')
response = self.client.get(url)
self.assertEqual(status.HTTP_200_OK, response.status_code)
Is this a problem with the setUp() method? From what I see, you may be setting self.authorize_user to a user that was already created on the first test.
Instead, I would create the user on each test, making sure that the user doesn't exist already, like so:
user_exists = User.objects.filter(username=self.USERNAME, email=self.EMAIL).exists()
if not user_exists:
self.authorize_user = User.objects.create_user....
That would explain why your first test did pass, why your second didn't, and why #anupam-chaplot's answer didn't reproduce the error.
Your reasoning and code looks ok.
However you are not giving the full code, there must be error you are not seeing.
Suspicious fact
It isn't be default 302 when you are not logged in.
(#login_required, etc redirects but your code doesn't have it)
Your APIAuthGroup permission does allow GET requests for non-logged-in user ( return request.method in SAFE_METHODS), and you are using GET requests (self.client.get(url))
So it means you are not hitting the endpoint that you think you are hitting (your get request is not hitting the DevicesViewSet method)
Or it could be the case you have some global permission / redirect related setting in your settings.py which could be DRF related..
eg :
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Guess
url = reverse('device-detail', kwargs={'pk': device.pk})
might not point to the url you are thinking..
maybe there's another url (/sa/devices/1/) that overrides the viewset's url. (You might have a django view based url)
And I didn't address why you are getting redirected after force_login.
If it's indeed login related redirect, all I can think of is self.authorized_user.refresh_from_db() or refreshing the request ..
I guess some loggin related property (such as session, or request.user) might point to old instance .. (I have no evidence or fact this can happen, but just a hunch) and you better off not logging out/in for every test case)
You should make a seperate settings file for testing and add to the test command --settings=project_name.test_settings, that's how I was told to do.

Django (DRF) fails to POST data to url with similar path

In my url conf I have two similar patterns:
urlpatterns = [
path('chat/', views.chat), # create chat
path('chat/message/', views.message), # create message
]
The second path works as expect, however, when I try to POST data to chat/ I get error 405 and {"detail":"Method \"POST\" not allowed."} error message. The code in the view works, if I modify chat/ to something more specific like chat/create/ then everything works fine. However, this is not what I want to do. I thought django would match the first URL that matches the request. Why is this happening? It this bug or expected behavior?
I have run into a similar issue. I created a new nested_route decorator that acts a lot like list_route and detail_route. I usually redirect to another viewset to handle the nested path. The issue was that the stream was being read too early (by the parent viewset dispatch), so I needed to ensure that the initialize_request function was only called once for a given request.
Working off of #rsalmaso's comment above, I overrode the initialize_request method in the children viewsets with the following.
def initialize_request(self, request, *args, **kwargs):
if not isinstance(request, Request):
request = super().initialize_request(request, *args, **kwargs)
return request
This works fine, but I think having some sort of attribute for either ignoring the initialize_request function within dispatch or having a global check to only run initialize_request iff isinstance(request, rest_framework.request.Request) == False. I'm happy to prepare the PR with tests if that could be acceptable.

How to handle GET and POST needs different numbers of argument in web.py framework

When develop an restful style API server with web.py micro framework . i have an issue about multiple URL with a handle class and http method funcation needs defferent arguments. for example:
URL:
url =['/api/users', 'User',
'/api/users/(\d+)$, 'User']
Class:
class User(object):
def GET(self, id=None):
Pass
def POST(self):
Pass
Issues:
When use postman call /api/uses/1 with POST method
Will happen an exception. How to fix it?
As suggested in a comment, make POST method's signature the same as GET:
def POST(self, id=None):
...
Your issue is due to the fact that your POST request /api/users/1 includes the final ID. That means webpy matches the pattern /api/users/(\d+)$, which is then handled by class User. If you GET that url, it goes to <User>.GET(), if you POST that url, it goes to <Users>.POST()... (DELETE goes to <User>.DELETE(), etc.)
Because the matching pattern includes a regex capture (\d+), that matched value will be passed as an additional argument to the function.... whichever function is called.
You can use a non-capturing regex: (?:\d+)$ which will require a match in the URL, but will not cause the matching value to be passed as a argument, so:
url = ('/api/users', 'User',
'/api/users/(?:\d+)$', 'User')
class User(object):
def GET(self, id=None):
pass
def POST(self):
pass
Now, /api/users and /api/users/1 will go to User class, and get handled by GET or POST depending on the request. Note that because you're no longer capturing the id your GET(self, id=None): still works, but will always have None for the id (because it's not captured!)
Most likely, what you want is the simple: make the arguments the same.
Or you can use flexible number of arguments:
def GET(*arg):
self = arg[0]
id = arg[1]

Django pre_save signal and ModelAdmin custom error message

I have a model whose pre_save() signal is connected to a remove service (json, REST, etc.) in the following way:
before saving locally, query the remote service, asking for remote insertion
remote service does its things, the main being checking that a relevant entry does not already exist.
On success (HTTP 201), all is good, the local model uses the remote service response to populate the db.
on failure, the service returns an HTTP 400 (the status code is debatable but that is for a different question on SO :-) )
The error response is in the following form:
{'local_model_field': [u'This element already exists']}
The local model pre_save signal then raises a ValidationError:
raise ValidationError(json_response['local_model_field'][0])
This works nicely.
Now, on the django admin, when I try to simulate the remote insertion of an already-existing object, I get a 500 page, which is fine but not ideal.
Is there any way to have the pre_save() error bubble all the way up to the ModelAdmin and be displayed as a standard error message, populated with the relevant content?
I have tried the following but to no avail:
def changeform_view(self, request, object_id=None, form_url='', extra_context=None):
"""
trying to display the right message when pre_save() fails on model save() method (after asking CC)
"""
try:
return super(ObjectAdmin, self).changeform_view(request, object_id, form_url, extra_context)
except IntegrityError as e:
self.message_user(request, e, level=messages.ERROR)
return HttpResponseRedirect(form_url)
Is a ValidationError the right thing to do? Knowing that the pre_save() must lock out any chance of ending with with duplicates both locally and remotely. The main reason is that the local/remote object creation can be made form the admin but also from other website instances/types (front-end, end-user facing, for example).
Thanks
Not sure if this is still relevant.
But the way I solved this is by creating a ModelForm:
class AuthorAdminForm(forms.ModelForm):
def clean(self):
# or some api calls here
if self.instance.id > 4:
self.instance.name = "4+"
else:
ValidationError("Id is bigger that 4")
return super().clean()
And then by adding the form to the admin model:
class AuthorAdmin(admin.ModelAdmin):
form = AuthorAdminForm
This clean() method will ensure that you could add/modify fields before you hit the model save() method and still throw a ValidationError if something goes south like a 404 error when hitting a url

Django url validation

HI,
I want to validate my urls whether they are post or get with caring the proper data.So i want to validate these urls before they call to respective views.So i am willing to write the some kind of middleware between view and urls so that i can keep safe the system.I am not aware how do i pass the data through middleware code to view.In middle ware i will write the unittest code.which will validate the urls if valid then will pass to the respective view other wise happy to say 404 .So can any buddy suggest me how do i handle the case.Or may be their is another alternative best way to do this validation.
Thanks to all.
You should really be checking for request type in your views, and not in a middleware. As I mentioned in the comments above, you can't tell whether a request is a POST message from the URL alone, let alone determine what POST data it carries.
Checking the request type within a view is very straight-forward -- simple check that request.method is equal to "GET" or "POST".
If you're doing this often, a short cut would be to create a decorator which does this check for you. For example, the following decorator checks that a GET request was used to call this view, or else return an HttpResponseBadRequest object (status code 400):
# untested code, use with care
def require_GET(view_func):
def wrap(request, *args, **kwargs):
if request.method != "GET":
return HttpResponseBadRequest("Expecting GET request")
return view_func(request, *args, **kwargs)
wrap.__doc__ = view_func.__doc__
wrap.__dict__ = view_func.__dict__
wrap.__name__ = view_func.__name__
return wrap
You can then simply prepend your view function with #require_GET and the check will be done whever the view is called. E.g.
#require_GET
def your_view(request):
# ...
You can do the same for POST.
Here's an example decorator checking for POST request which takes an optional list of fields that must be provided with the POST request.
# again, untested so use with care.
def require_POST(view_func, required_fields=None):
def wrap(request, *args, **kwargs):
if request.method != "POST":
return HttpResponseBadRequest("Expecting POST request")
if required_fields:
for f in required_fields:
if f not in request.POST:
return HttpResponseBadRequest("Expecting field %s" % f)
return view_func(request, *args, **kwargs)
wrap.__doc__ = view_func.__doc__
wrap.__dict__ = view_func.__dict__
wrap.__name__ = view_func.__name__
return wrap
Use like this:
#require_POST
def another_view(request):
# ...
or:
#require_POST(required_fields=("username", "password"))
def custom_login_view(request):
# ...
Update
OK, my bad. I've just reinvented wheel.
Django already provides the #require_GET and #require_POST decorators. See django.views.decorators.http.
Like others said, you must do it in your view, or maybe you must say what you are trying to do for the best...
Anyway, you can not create a responce object in process_request , you can only add variables or change variables on the related request, like the sessionid variable used by django, or any such thing... Or update any existing request variables...
So, you must use process_view, which is triggered after process_request and just before your related view function is executed.Since you have request object at hand, you can check GET or POST data by using request.GET or request.POST.
For doing this, you must add your middle class to MIDDLEWARE_CLASSES in settings.py and write a proper middleware process_view function. For writing middlewares see middleware documentation and check existing middlewares of django. Or tell me what you are rtying to do...

Categories

Resources