How to disable the browsable API for non staff users (is_staff=False)? - django-rest-framework

in my case I am using Django REST Framework (DRF) as internal api. it is not intended to be consumed by regular users. therefore I would like to disable it for regular users.
an admin (is_staff=True) should be able to access it and see it:
https://restframework.herokuapp.com/
a non staff user (is_staff=False) should just get the JSON response of a GET request like:
https://restframework.herokuapp.com/?format=json
he should not(!) see the browsable api. this applies for the root view and all endpoints.
to configure this, I applied the following:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.SessionAuthentication'],
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'],
}
my endpoints are the following (to keep the example simple I just show 1):
# api/urls.py
from django.urls import include, path
from rest_framework import routers
from . import views
app_name = 'api'
router = routers.DefaultRouter() # browseable api
router.register('segments', views.SegmentViewSet)
# there are a lot more...
urlpatterns = [
path('', include(router.urls)),
]
based on answer https://stackoverflow.com/a/58894198/420953 my settings.py looks like this:
# settings.py
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': ['rest_framework.authentication.SessionAuthentication'],
'DEFAULT_PERMISSION_CLASSES': ['rest_framework.permissions.IsAuthenticated'],
# enable JSON renderer by default
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
and my api/views.py:
# api/views.py
from django_filters import rest_framework as drf_filters
from rest_framework import filters, renderers, viewsets
from . import serializers
from segment.models import Segment
class StaffBrowsableAPIMixin:
def get_renderers(self):
"""
add BrowsableAPIRenderer if user is staff (regular users see JSONRenderer response)
"""
# explicitly set renderer to JSONRenderer (the default for non staff users)
rends = [renderers.JSONRenderer]
if self.request.user.is_staff:
# staff users see browsable API
rends.append(renderers.BrowsableAPIRenderer)
return [renderer() for renderer in rends]
class SegmentViewSet(StaffBrowsableAPIMixin, viewsets.ReadOnlyModelViewSet):
queryset = Segment.objects.all()
serializer_class = serializers.SegmentSerializer
this works fine for all endpoints (when a regular user calls the endpoint via GET, they only see the JSON, not the browsable API). Unfortunately it does not work for APIRootView (the root view of the api, e.g. https://restframework.herokuapp.com/).
how to get this to work for APIRootView as well?

I believe you can lock the base URL of your API down pretty simply (the mixin should probably be moved to another file but just kept everything together for clarity):
# api/urls.py
from django.urls import include, path
from rest_framework import permissions, renderers, routers
from . import views
app_name = 'api'
class StaffBrowsableAPIMixin:
def get_renderers(self):
"""
add BrowsableAPIRenderer if user is staff (regular users see JSONRenderer response)
"""
# explicitly set renderer to JSONRenderer (the default for non staff users)
rends = [renderers.JSONRenderer]
if self.request.user.is_staff:
# staff users see browsable API
rends.append(renderers.BrowsableAPIRenderer)
return [renderer() for renderer in rends]
class CustomAPIRootView(StaffBrowsableAPIMixin, routers.APIRootView):
permission_classes = (permissions.IsAdminUser,)
class CustomDefaultRouter(routers.DefaultRouter):
APIRootView = CustomAPIRootView
router = CustomDefaultRouter() # browseable api
router.register('segments', views.SegmentViewSet)
# there are a lot more...
urlpatterns = [
path('', include(router.urls)),
]
The permission_classes will handle not showing any of your endpoints to non-Admin users but the Browsable API template will still be shown. To remove that as well, you need to change the renderer using the StaffBrowsableAPIMixin.
Original Answer
One way to do this is using DRF's renderer settings and methods.
In your settings.py:
REST_FRAMEWORK = {
# Only enable JSON renderer by default.
'DEFAULT_RENDERER_CLASSES': [
'rest_framework.renderers.JSONRenderer',
],
}
And in your views.py:
from rest_framework import generics, renderers
class StaffBrowsableMixin(object):
def get_renderers(self):
"""
Add Browsable API renderer if user is staff.
"""
rends = self.renderer_classes
if self.request.user and self.request.user.is_staff:
rends.append(renderers.BrowsableAPIRenderer)
return [renderer() for renderer in rends]
class CustomListApiView(StaffBrowsableMixin, generics.ListAPIView):
"""
List view.
"""
# normal stuff here
Basically, use StaffBrowsableMixin for any APIView you want the BrowsableAPI to be enabled for staff.
Similar question, as linked above in comments, and my answer there as well: https://stackoverflow.com/a/58762483/4599228

Related

How to use django groups and perms in django-rest-framework

How to handle complex required permissions in any API using django-rest-framework?
For example, you have three tier support operators who have access to APIs, but they should only have access to respective endpoint and as the last tier, superadmin should have everything in hands.
How to solve this simple problem by django group and permissions?
There are lots of answers being related to AND or OR the perms in permission_classes like
def get_permissions(self):
return (
IsAuthenticated & (IsCommitteeHead|IsCommitteeMember|IsAdminUser)
)()
or
permission_classes = [IsAuthenticated &(IsCommitteeHead|IsCommitteeMember|IsAdminUser)]
but I think hard coding always makes the code a complete mess!
Create new permission model permission from django base model permission like this:
from rest_framework.permissions import DjangoModelPermissions
class FullDjangoModelPermissions(DjangoModelPermissions):
perms_map = {
'GET': ['%(app_label)s.view_%(model_name)s'],
'OPTIONS': ['%(app_label)s.view_%(model_name)s'],
'HEAD': ['%(app_label)s.view_%(model_name)s'],
'POST': ['%(app_label)s.add_%(model_name)s'],
'PUT': ['%(app_label)s.change_%(model_name)s'],
'PATCH': ['%(app_label)s.change_%(model_name)s'],
'DELETE': ['%(app_label)s.delete_%(model_name)s'],
}
And in view class like this:
class SomeAwuful(GenericAPIView):
...
permission_classes = [FullDjangoModelPermissions]
...
Then you can go in Django admin dashboard and set who has access to what, or creating group to access features, like this:

Django Rest Framework Routing: Reverse for 'orders' not found. 'orders' is not a valid view function or pattern name

I'm not quite sure I'm understanding how routing works in DRF. I went through the documentation but still haven't grasp the differences.
I've got the following view:
from django.shortcuts import render
from django.shortcuts import get_object_or_404
from rest_framework.viewsets import ViewSet
from rest_framework.response import Response
from .models import Order
from .serializer import OrderSerializer
class OrderAPIViewSet(ViewSet):
def post(self, request):
print(request)
and this is urls.py within my app:
from django.urls import include, path
from rest_framework import routers
from .views import OrderAPIViewSet
router = routers.DefaultRouter()
router.register(r'orders', OrderAPIViewSet, basename='order')
urlpatterns = router.urls
and this is the main urls.py:
from django.contrib import admin
from django.urls import path, include
from django.conf.urls.static import static
from django.conf import settings
urlpatterns = [
path('admin/', admin.site.urls),
path('', include('products.urls')),
path('', include('orders.urls'))
] + static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)
but when I try to access the orders endpoint via a simple test:
ORDERS_URL = reverse('orders')
class PublicOrderApiTests(TestCase):
"""
Test the public facing Order API
"""
def setUp(self):
self.client = APIClient()
def test_sample(self):
data = {
"product_id": 1,
"price": 5.80
}
res = self.client.post(ORDERS_URL, data)
print(res)
I'm getting the following error:
django.urls.exceptions.NoReverseMatch: Reverse for 'orders' not found.
'orders' is not a valid view function or pattern name.
what am I doing wrong? the endpoint for products works just fine but not for orders.
Found the issue or issues in this case:
In my test I had defined the wrong parameter to get the url, so instead of ORDERS_URL = reverse('orders') it needed to beORDERS_URL = reverse('order-list') . *-list is used for GET /orders/ and POST /orders/ while *-detail is for all other endpoints: PUT /orders/{id} GET /orders/{id} etc
The method in my view was post but ViewSetwhich I'm inheriting from doesn't have that method, it has the create method instead, so I needed to rename my post to create

How can i use Simple JWT for custom User Model

I am managing my User Model (for customers), but i don't know how can i use simple-jwt for my Customer Model with it's custom Login View.
models.py
from django.db import models
class Customer(models.Model):
name = models.CharField(max_length=100)
email = models.EmailField(max_length=100)
password = models.CharField(max_length=100)
and also, i want to manage my customer session by saving the Refresh Token,
can anyone please tell me how can i achieve that.
After some research, I came to the conclusion that Simple JWT doesn't have any special support for user models that are not derived from AbstactUserModel.
It means if you are managing the Customer model, either you need to extend it from AbstactUserModel or you should use jwt instead of Simple jwt.
First install simple jwt with the command
pip install djangorestframework-simplejwt
You must then configure your django project to use the libary with following option in settings.py
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
...
'rest_framework_simplejwt.authentication.JWTAuthentication',
)
...
}
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=5),
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'ROTATE_REFRESH_TOKENS': False,
'BLACKLIST_AFTER_ROTATION': True,
'UPDATE_LAST_LOGIN': False,
'ALGORITHM': 'HS256',
'SIGNING_KEY': settings.SECRET_KEY,
'VERIFYING_KEY': None,
'AUDIENCE': None,
'ISSUER': None,
'AUTH_HEADER_TYPES': ('Bearer',),
'AUTH_HEADER_NAME': 'HTTP_AUTHORIZATION',
'USER_ID_FIELD': 'id',
'USER_ID_CLAIM': 'user_id',
'AUTH_TOKEN_CLASSES': ('rest_framework_simplejwt.tokens.AccessToken',),
'TOKEN_TYPE_CLAIM': 'token_type',
'JTI_CLAIM': 'jti',
'SLIDING_TOKEN_REFRESH_EXP_CLAIM': 'refresh_exp',
'SLIDING_TOKEN_LIFETIME': timedelta(minutes=5),
'SLIDING_TOKEN_REFRESH_LIFETIME': timedelta(days=1),
}
In your root urls.py add the following
from rest_framework_simplejwt.views import (
TokenObtainPairView,
TokenRefreshView,
)
urlpatterns = [
...
path('api/token/', TokenObtainPairView.as_view(), name='token_obtain_pair'),
path('api/token/refresh/', TokenRefreshView.as_view(), name='token_refresh'),
...
]
Simple JWT by default only sends user id in the token. You can customize it further to send more information in your serializers.py by doing the following
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
class CustomTokenObtainPairSerializer(TokenObtainPairSerializer):
"""Customizes JWT default Serializer to add more information about user"""
#classmethod
def get_token(cls, user):
token = super().get_token(user)
token['name'] = user.name
token['email'] = user.email
token['is_superuser'] = user.is_superuser
token['is_staff'] = user.is_staff
return token
Then in your views.py add the following
from users import serializers
class CustomTokenObtainPairView(TokenObtainPairView):
# Replace the serializer with your custom
serializer_class = serializers.CustomTokenObtainPairSerializer
You can read more here
I could fix this problem adding 'rest_framework_simplejwt' to INSTALLED_APPS after the app that has the custom user model.
in your views.py file, import this
from rest_framework_simplejwt.tokens import RefreshToken
next, create a refresh token in def post - the one you use for logging in
def post(self, request):
...
refresh = RefreshToken.for_user(user)
...
return the generated refresh token and access token in the response body
return Response(
{
'refresh': str(refresh),
'access': str(refresh.access_token),
},
status = status.HTTP_200_OK
)
What I have suggested is a simple and super basic implementation of what I believe you are looking for. I got it from here. But if you want a better and more customized, neater implementation, refer this article.

Djoser for DRF with Knox tokens

I'm trying to use djoser with token authentication, but using django-rest-knox tokens.
I have set the TOKEN_MODEL to knox.models.AuthToken, and the rest framework's DEFAULT_AUTHENTICATION_CLASSES to knox.auth.TokenAuthentication.
I naïvely thought that this would be enough, but it seems that Djoser's inbuilt serializers (create token, and token), don't work properly with the knox tokens. I tried overriding them with custom serializers, but I didn't get anywhere (which is not to say it's not possible, just that I'm bad at this).
It occurred to me that perhaps I should try using Knox's own login views... Is that possible, or can they not be mixed like that? (I'm mainly asking because I don't want to get it to 'work', but find that I've actually introduced a security hole in doing so).
Settings:
DJOSER = {
"TOKEN_MODEL": "knox.models.AuthToken",
"SERIALIZERS": {"token": "users.serializers.TokenSerializer"},
}
Where users.serializers.TokenSerializer is:
class TokenSerializer(serializers.ModelSerializer):
auth_token = serializers.CharField(source="token_key")
class Meta:
model = settings.TOKEN_MODEL
fields = ("auth_token",)
This is only slightly modified from the original Djoser TokenSerializer. It was throwing an error that AuthToken objects did not have a key attribute. Knox tokens seem to call it token_key, so I replaced the line:
auth_token = serializers.CharField(source="key") with auth_token = serializers.CharField(source="token_key")
Now, it doesn't throw an error, but it returns an empty token. Inspecting the actual db shows that it has saved a token with the correct user and creation time, but with 'null' for digest, salt, and token_key
Yes, it is possible to mixin's Djoser's and knox's additional view point. For that we are going to create an app name auth from where we are going to serve all authenticational related end-points. Now our project structure is like
MainProject
-auth
--__init__.py
--urls.py
-mainapp
....
Now in our auth app's urls we are going to serve our necessary end-points for authentication. For that we are going to take help from Djoser's urls link and Knox's urls link
And our auth's urls.py will be like following
from django.conf.urls import url, include
from django.contrib.auth import get_user_model
from djoser import views as djsoer_views
from knox import views as knox_views
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register('users', djsoer_views.UserViewSet)
User = get_user_model()
djoser_urlpatterns = [
url(
r'^users/create/?$',
djsoer_views.UserCreateView.as_view(),
name='user-create'
),
url(
r'^users/delete/?$',
djsoer_views.UserDeleteView.as_view(),
name='user-delete'
),
url(
r'^users/activate/?$',
djsoer_views.ActivationView.as_view(),
name='user-activate'
),
url(
r'^{0}/?$'.format(User.USERNAME_FIELD),
djsoer_views.SetUsernameView.as_view(),
name='set_username'
),
url(r'^password/?$', djsoer_views.SetPasswordView.as_view(), name='set_password'),
url(
r'^password/reset/?$',
djsoer_views.PasswordResetView.as_view(),
name='password_reset'
),
url(
r'^password/reset/confirm/?$',
djsoer_views.PasswordResetConfirmView.as_view(),
name='password_reset_confirm'
),
url(r'^$', djsoer_views.RootView.as_view(), name='root'),
url(r'^', include(router.urls)), ### If you want to add user view set
]
knox_urlpatterns = [
url(r'login/', knox_views.LoginView.as_view(), name='knox_login'),
url(r'logout/', knox_views.LogoutView.as_view(), name='knox_logout'),
url(r'logoutall/', knox_views.LogoutAllView.as_view(), name='knox_logoutall'),
]
urlpatterns = knox_urlpatterns + djoser_urlpatterns
Now we are going to add this urls under our main_app's urls
from django.urls import path
from django.conf import settings
auth_urls = include('auth.urls')
urlpatterns = [
path('api/auth/', auth_urls),
......
]
Now we are going to able to access every end-point like login as api/auth/login/ or user-create as api/auth/user/create/ etc.
What i can see that djoser add some additional UserViewset end-point by default mostly you may not like this , you should include what is really needed for you.

Django rest framework not indexing a custom CBV in Api Root

In Django-rest-framework I have a simple CBV
class LocationList(APIView):
"""
List all locations (id and name)
"""
def get(self, request, format=None):
# Make connection to SQL server db
dbargs = dict(
DRIVER='{FreeTDS}',
SERVER=django_settings.DB_HOST,
PORT=django_settings.DB_PORT,
DATABASE=django_settings.DB_NAME,
UID=django_settings.DB_USER,
PWD=django_settings.DB_PWD,
)
cnxn = pyodbc.connect(**dbargs)
# Query db
curs = cnxn.cursor()
select_locations_cmd = 'SELECT list_id, cast(list_name as text) FROM location_lists;'
curs = curs.execute(select_locations_cmd)
# Serialize
sdata = [dict(list_id=lid, list_name=lname) for lid, lname in curs.fetchall()]
# Close cnxn
cnxn.close()
return Response(sdata)
As you can see all it does is queries an external database, manually serializes the result and returns it in a django-rest-framework Response object.
In my urls.py I have
router = routers.DefaultRouter()
router.register(r'someothermodel', SomeOtherModelViewSet)
urlpatterns = [url(r'^', include(router.urls)),
url(r'^locationlists/$', LocationList.as_view(), name="weather-location-lists"),
]
This works OK, but what I'm concerned about is that when I visit the root API url, it only shows the endpoint for someothermodel, which was registered via the router and used a standard ViewSet. It doesn't list the locationlists endpoint at all. I can visit the /locationlists endpoint in the browser (or make a GET request otherwise to it without issue), but it's not indexed.
How can I index it at the root? So it appears alongside
Api Root
HTTP 200 OK
Allow: GET, HEAD, OPTIONS
Content-Type: application/json
Vary: Accept
{
"someothertask": "http://127.0.0.1:8000/someothertask/",
}
The page you are referring to is powered by the router and in your case LocationList isn't registered via the router. As such it doesn't appear in the endpoint list.
As #Linovia points out, routers only deal with viewsets. With a few changes however this is simple to achieve:
# views.py
from rest_framework.viewsets import ViewSet
class LocationList(ViewSet):
def list(self, request, format=None): # get -> list
...
# urls.py
router = routers.DefaultRouter()
router.register(r'someothermodel', SomeOtherModelViewSet)
router.register(r'locationlists', LocationList, base_name='weather-location')
urlpatterns = [
url(r'^', include(router.urls)),
]
You should now see:
{
"someothertask": "http://127.0.0.1:8000/someothertask/",
"locationlists": "http://127.0.0.1:8000/locationlists/",
}
It is worth noting that the reverse name for your view has now changed from weather-location-lists to weather-location-list, but hopefully this is minor change to the places this may have been used.
Routers only work with ViewSet

Resources