How to set Authentication and Permission on django URLs for imported views - django

I want to use django REST framework's inbuilt API documentation. The problem is that I need it to be private with a login and I couldn't manage thus far. I do the following to create my API doc as documented:
from rest_framework.documentation import include_docs_urls
API_TITLE = "Cool title"
API_DESCRIPTION = "Badass description"
urlpatterns = [
path('docs/', include_docs_urls(title=API_TITLE, description=API_DESCRIPTION)
That creates my docs, which is fantastic. But it is accessible by everyone even though I have set my permissions and authentications for all the endpoints to private. I did this like this in my configs:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_jwt.authentication.JSONWebTokenAuthentication',
'rest_framework.authentication.SessionAuthentication',
'rest_framework.authentication.BasicAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAdminUser',
)
}
But even with this set I can access all site of the docs.
So I am looking for a way to protect my URLs which I imported and thus have no classes or methods at my disposal to protect. (So I can't use any decorators on them). Then I tried using the login required decorator on my URL like so:
path('docs/', login_required(include_docs_urls(title=API_TITLE, description=API_DESCRIPTION))),
but it throws me the following error: django.template.exceptions.TemplateDoesNotExist: registration/login.html
Is there a way of protecting such URLs?
Help is very much appreciated! Thanks a lot in advance!
EDIT: I figured I could pass permission classes as argument to my URL, and indeed Pycharm marks it as an option. So I put:
path('docs/', include_docs_urls(title=API_TITLE, description=API_DESCRIPTION, permission_classes= "rest_framework.permissions.IsAdminUser")),
That throws me the error str object not callable. Any ideas on how I could pass my permission classes there maybe?

For anyone ever having this problem. It is possible by doing the following:
from rest_framework.permissions import IsAdminUser
path('api/docs', include_docs_urls(
title=API_TITLE, description=API_DESCRIPTION, permission_classes=[IsAdminUser]
))
I wish this inbuilt API documentation tool was documented better. Took me a while to even find out that this exists....

Related

Set up DRF Browsable API Root with all api urls

In my urls.py I have many rest framework urls:
path(
"api/",
include([
path("users/", api_views.users, name="users"),
path("proposals/", api_views.Proposals.as_view(), name="proposals"),
path("requests/", api_views.Requests.as_view(), name="requests"),
#...
])
)
I can visit the individual endpoints in the browser and access the browsable API, but I want to set up a browsable API Root where I can see all the available endpoints. The DRF tutorial has an example in which they list the urls they want in the root, but I have a lot of urls and I want all of them to show up. Is there a way to include all the urls in my "api/" path?
You should try to use drf_yasg package. Here's a documentation for that.

DigitalOcean Django Rest Framework Authentication credentials were not provided

My live site decided to throw a 403 Forbidden error yesterday on authenticated users when calling an Ajax API and I've trying to troubleshoot with no success. The localhost on my machine works fine when DEBUG = True in my settings.py, but the same code throws the following error:
HTTP 403 Forbidden
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"detail": "Authentication credentials were not provided."
}
My rest framework setting in settings.py:
## REST framework default permissions
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
Since the browsable API requires SessionAuthentication, I tried the following with no success:
## REST framework default permissions
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.SessionAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
]
}
I did look at Django Rest Framework Docs and it seems to suggest that if my user is logged in, the Ajax calls after login should work fine. Am I missing something? Really appreciate your input
UPDATE 1:
When I run the command:
sudo journalctl -u gunicorn -n 25
One of the things I see is gunicorn[820]: Session data corrupted
I did restart the server, hoping that by logging back in, the new session data will be generated, but the same message is displayed. The logged in user is still not able to view the data the ajax call is trying to fetch. How do I resolve the sessions data corrupted message. I am guessing this affects the DRF authenticating the request
My problem was with the migrations and so I ended up backing up my database, deleting all tables, recreating the tables using Django migrations. Some migration file must have been not in sync for some reason. I really don't know if I messed up manually manipulating something or if it was a bug. Anyways, water under the bridge...

Custom Grouping on OpenAPI endpoints with Django Rest Framework

I have a Django project and I am using Django REST framework. I am using drf-spectacular
for OpenAPI representation, but I think my problem is not tied to this package, it's seems a more generic OpenAPI thing to me (but not 100% sure if I am right to this).
Assume that I have a URL structure like this:
urlpatterns = [
path('admin/', admin.site.urls),
path('api/', include([
path('v1/', include([
path('auth/', include('rest_framework.urls', namespace='rest_framework')),
path('jwt-auth/token/obtain', CustomTokenObtainPairView.as_view(), name='token_obtain_pair'),
path('jwt-auth/token/refresh', CustomTokenRefreshView.as_view(), name='token_refresh'),
path('home/', include("home.urls"))
]))
])),
# OpenAPI endpoints
path('swagger/', SpectacularSwaggerView.as_view(url_name='schema-swagger-json'), name='schema-swagger-ui'),
path('swagger.yaml/', SpectacularAPIView.as_view(), name='schema-swagger-yaml'),
path('swagger.json/', SpectacularJSONAPIView.as_view(), name='schema-swagger-json'),
path('redoc/', SpectacularRedocView.as_view(url_name='schema-swagger-yaml'), name='schema-redoc'),
]
In the corresponding swagger UI view, I get all endpoints grouped under api endpoint, e.g.:
If add more endpoints under v1, all go under the api endpoint.
What I want to achieve is, to have the endpoints in Swagger grouped differently, e.g. by app. So I'd have home, jwt, another_endpoint, instead of just api, so it will be easier to navigate in Swagger (when I add more endpoints, with the current method it's just showing a massive list of URLs, not very user friendly).
I've read that those groups are being extracted from the first path of a URL, in my case this is api, so if I change the URL structure, I could achieve what I need.
But isn't there another way of doing this? I want to keep my URL structure, and customize how I display this with OpenAPI, so in the end I have a swagger that groups the endpoints by app, so it's easier to navigate and find what you are looking for.
you are making it harder than it needs to be. In the global settings you can specify a common prefix regex that strips the unwanted parts. that would clean up both operation_id and tags for you. In your case that would probably be:
SPECTACULAR_SETTINGS = {
'SCHEMA_PATH_PREFIX': r'/api/v[0-9]',
}
that should result in tags: home, jwt-auth, swagger.json, swagger.yaml
the tags on #extend_schema is merely a convenience to deviate from the default where needed. it would be cumbersome to do this for every operation. see the settings for more details:
https://drf-spectacular.readthedocs.io/en/latest/settings.html
for even more elaborate tagging you can always subclass AutoSchema and override get_tags(self) to your liking. cheers!
Turns out that you can control this by changing the tags in a view, as per OpenAPI specification: https://swagger.io/docs/specification/grouping-operations-with-tags/
So, with drf-spectacular, you can use the extend_schema decorator to achieve this, e.g.:
from drf_spectacular.utils import extend_schema
class CustomTokenObtainPairView(TokenObtainPairView):
"""
Takes a set of user credentials and returns an access and refresh JSON web
token pair to prove the authentication of those credentials.
"""
#extend_schema(
operation_id="jwt_obtain",
....
tags=["aTestTag"]
)
def post(self, request, *args, **kwargs):
# whatever
So you have to use this decorator to extend the schema in each view that you want to put into a custom group.

Cannot access admin panel after setting up django-oauth-toolkit

I had a pet-api (testing api) without authentication. I'm trying to learn how to implement oath2 to add security to my app.
I'd like to access the models of my app through a request call using
the API but also through the Django Admin Panel.
I'm following this tutorial: https://medium.com/#halfspring/guide-to-an-oauth2-api-with-django-6ba66a31d6d
for setting up: django-oauth-toolkit
Tutorial says I should add this code to settings:
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend' # To keep the Browsable API
'oauth2_provider.backends.OAuth2Backend',
)
But when I run server, and try to access /admin, I get:
ModuleNotFoundError at /admin/login/
No module named 'django.contrib.auth.backends.ModelBackendoauth2_provider'; 'django.contrib.auth.backends' is not a package
If I comment:
# 'django.contrib.auth.backends.ModelBackendoauth2_provider';
I can access the interface for the login, but says my user or password are wrong (they are not).
Commenting both lines I can access the admin panel without problems:
#AUTHENTICATION_BACKENDS = (
# 'django.contrib.auth.backends.ModelBackend' # To keep the Browsable API
# 'oauth2_provider.backends.OAuth2Backend',
#)
There is an error in code,
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend' # To keep the Browsable API
'oauth2_provider.backends.OAuth2Backend',
)
There is a comma (,) missing after 'django.contrib.auth.backends.ModelBackend' so it is taking both lines as single line, as you can see in the error.
So you needed to do just
AUTHENTICATION_BACKENDS = (
'django.contrib.auth.backends.ModelBackend', # To keep the Browsable API
'oauth2_provider.backends.OAuth2Backend',
)
Now it will work...
It's okay, mine is working good now without it. I am also following that guide. Just continue http://127.0.0.1:8000/o/applications.
BTW, I am also commenting the ALLOWED_HOSTS = ['0.0.0.0'] and on the users/views.py, I changed all the http://0.0.0.0:8000 to http://127.0.0.1:8000.
And I now get these:
{
"access_token": "C2qukd1zWz9aGSp652qbnpYjoT6ZRx",
"expires_in": 36000,
"token_type": "Bearer",
"scope": "read write",
"refresh_token": "UoI0r9J09F3kcXGO1q3KsYoGHQ9DBw"
}

How to make a POST simple JSON using Django REST Framework? CSRF token missing or incorrect

Would appreciate someone showing me how to make a simple POST request using JSON with Django REST framework. I do not see any examples of this in the tutorial anywhere?
Here is my Role model object that I'd like to POST. This will be a brand new Role that I'd like to add to the database but I'm getting a 500 error.
{
"name": "Manager",
"description": "someone who manages"
}
Here is my curl request at a bash terminal prompt:
curl -X POST -H "Content-Type: application/json" -d '[
{
"name": "Manager",
"description": "someone who manages"
}]'
http://localhost:8000/lakesShoreProperties/role
The URL
http://localhost:8000/lakesShoreProperties/roles
DOES work with a GET request, and I can pull down all the roles in the database, but I can not seem to create any new Roles. I have no permissions set. I'm using a standard view in views.py
class RoleDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Role.objects.all()
serializer_class = RoleSerializer
format = None
class RoleList(generics.ListCreateAPIView):
queryset = Role.objects.all()
serializer_class = RoleSerializer
format = None
And in my urls.py for this app, the relevant url - view mappings are correct:
url(r'^roles/$', views.RoleList.as_view()),
url(r'^role/(?P<pk>[0-9]+)/$', views.RoleDetail.as_view()),
Error message is:
{
"detail": "CSRF Failed: CSRF token missing or incorrect."
}
What is going on here and what is the fix for this? Is localhost a cross site request? I have added #csrf_exempt to RoleDetail and RoleList but it doesn't seem to change anything. Can this decorator even be added to a class, or does it have to be added to a method?
Adding the #csrf_exempt decorate, my error becomes:
Request Method: POST
Request URL: http://127.0.0.1:8000/lakeshoreProperties/roles/
Django Version: 1.5.1
Exception Type: AttributeError
Exception Value:
'function' object has no attribute 'as_view'
Then I disabled CSRF throughtout the entire app, and I now get this message:
{"non_field_errors": ["Invalid data"]} when my JSON object I know is valid json. It's a non-field error, but I'm stuck right here.
Well, it turns out that my json was not valid?
{
"name": "admin",
"description": "someone who administrates"
}
vs
[
{
"name": "admin",
"description": "someone who administrates"
}
]
Having the enclosing brackets [], causes the POST request to fail. But using the jsonlint.com validator, both of my json objects validate.
Update: The issue was with sending the POST with PostMan, not in the backend. See https://stackoverflow.com/a/17508420/203312
CSRF is exempted by default in Django REST Framework. Therefore, curl POST request works fine. POSTMAN request call returned CSRF incorrect because POSTMAN included csrf token if it is found in Cookies. You can solve this by cleaning up Cookies.
It's from your REST Framework settings. in your settings.py file, your REST_FRAMEWORK should have the following.
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework.authentication.TokenAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.AllowAny',
),
}
This will set your REST Framework to use token authentication instead of csrf authentication. And by setting the permission to AllowAny, you can authenticate only where you want to.
You probably need to send along the CSRF token with your request. Check out https://docs.djangoproject.com/en/1.7/ref/contrib/csrf/#csrf-ajax
Update: Because you've already tried exempting CSRF, maybe this could help (depending on which version of Django you're using): https://stackoverflow.com/a/14379073/977931
OK, well now of course I take back what I said. CSRF does work as intended.
I was making a POST request using a chrome plugin called POSTMAN.
My POST request fails with CSRF enabled.
But a curl POST request using
curl -X POST -H "Content-Type: application/json" -d '
{
"name": "Manager",
"description": "someone who manages"
}' http://127.0.0.1:8000/lakeshoreProperties/roles/
works fine...
I had to take off the braces, i.e., [], and make sure there is a slash after the 's' in roles, i.e., roles/, and csrf enabled did not throw any errors.
I'm not sure what the difference between calling using POSTMAN is vs using curl, but POSTMAN is run in the web browser which is the biggest difference. That said, I disabled csrf for the entire class RoleList but one identical request works with Curl, but fails with POSTMAN.
To give an update on current status, and sum up a few answers:
AJAX requests that are made within the same context as the API they are interacting with will typically use SessionAuthentication. This ensures that once a user has logged in, any AJAX requests made can be authenticated using the same session-based authentication that is used for the rest of the website.
AJAX requests that are made on a different site from the API they are communicating with will typically need to use a non-session-based authentication scheme, such as TokenAuthentication.
Therefore, answers recommending to replace SessionAuthentication with TokenAuthentication may solve the issue, but are not necessarily totally correct.
To guard against these type of attacks, you need to do two things:
Ensure that the 'safe' HTTP operations, such as GET, HEAD and OPTIONS cannot be used to alter any server-side state.
Ensure that any 'unsafe' HTTP operations, such as POST, PUT, PATCH and DELETE, always require a valid CSRF token.
If you're using SessionAuthentication you'll need to include valid CSRF tokens for any POST, PUT, PATCH or DELETE operations.
In order to make AJAX requests, you need to include CSRF token in the HTTP header, as described in the Django documentation.
Therefore, it is important that csrf is included in header, as for instance this answer suggests.
Reference: Working with AJAX, CSRF & CORS, Django REST framework documentation.
As you said your URL was
http://localhost:8000/lakesShoreProperties/roles
Postman has some issues with localhost.
Sending the POST to 127.0.0.1:8000/your-api/endpoint instead did the trick for me.
the old Postman is having a problem with csrf tokens because it does not working with cookies.
I suggest for you to switch to the new version of postman, it works with cookies and you will not face this problem again.
if you have set AllowAny permission and you facing with csrf issue
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.AllowAny'
]
}
then placing following in the settings.py will resolve the issue
REST_SESSION_LOGIN = False