TLDR: How do you permit specific Groups access to views using Django Rest Framework?
I'm in the process of building a web service with the Django Rest Framework. Only a (proper) subset of the views are intended to be available to customers.
So far, I've:
set the default permission to rest_framework.permissions.IsAdminUser
created a permission called is_customer (using the Django admin website)
created a Group called customers and added all relevant users to this group (using the admin website)
added the is_customer permission to customers (again using the admin website)
All of my views are function-based. To provide the appropriate permission to the customers group, I've tried
from rest_framework.decorators import api_view
from django.contrib.auth.decorators import permission_required
#api_view(["POST"])
#permission_required(["is_customer"])
def my_func(request):
# calculations
return Response(...)
EDIT: also tried the below method
from rest_framework import permissions
from rest_framework.decorators import api_view
class IsCustomer(permissions.BasePermission):
def has_permission(self, request, view):
if request.user.is_customer:
return True
return False
def has_object_permission(self, request, view, obj):
if request.user.is_customer:
return True
return False
#api_view(["POST"])
#permission_required([IsCustomer])
def my_func(request):
# calculations
return Response(...)
and
from rest_framework.decorators permission_classes, api_view
#api_view(["POST"])
#permission_classes(["is_customer"])
def my_func(request):
# calculations
return Response(...)
and neither seems to be working. What is the proper way to go about this? Thanks in advance for any advice!
The last block should work if you reference the class directly:
from rest_framework.decorators permission_classes, api_view
#api_view(["POST"])
#permission_classes([IsCustomer])
def my_func(request):
# calculations
return Response(...)
You can view the documentation with a similar example here:
https://www.django-rest-framework.org/api-guide/permissions/
The IsCustomer class was not set up properly. This is what the class should look like:
class IsCustomer(permissions.BasePermission):
def has_permission(self, request, view):
if 'auth.is_customer' in request.user.get_all_permissions():
return True
return False
def has_object_permission(self, request, view, obj):
if 'auth.is_customer' in request.user.get_all_permissions():
return True
return False
From there, using the #permission_classes([IsCustomer]) works as intended. Therefore the final form of the view is:
#api_view(["POST"])
#permission_classes([IsCustomer])
def testing_post(request):
try:
someData = someCalculations()
return Response(data=someData, status=status.HTTP_200_OK)
except ValueError as e:
return Response(e.args[0], status.HTTP_400_BAD_REQUEST)
Related
I wont be needing to write codes much,I just need an idea.I am trying to make users in two different categories.That is a User can either have a Profile or Shop both not both.
class Profile(models.Model):
user=models.OneToOneField(User,on_delete=models.CASCADE,primary_key=True,editable=True)
class Shop(models.Model):
user=models.OneToOneField(User,on_delete=models.CASCADE,primary_key=True,editable=True)
I want all users to either have one of both,They will register with the same form without being able to switch from one to another once they have registered for it.As at now what I'm doing is If i dont want a shop-owned User to have the previlege of a profile i do
def post_product(request):
try:
if bool(request.user.profile)==False:
.....
except:
pass
But I have to do this for all views that require previledges. I want something site-wide that i can put in settings or put at the view like #login_required .Please how can I go about this.Thanks
As per answer given by Prabhakar, you should use django auth groups.
But, if you want to write your custom permission class, you could also do it.
1) Assuming file name -> self_defined_permission.py
from rest_framework import permissions
from django.contrib.auth.models import Group
class ShopPermissions(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
'''
check user group. Assuming you keep users having profile in group 1 and shop in group 2
'''
shop_gp = Group.objects.get(id=2)
if shop_gp in request.user.groups.all():
return True
else:
return False
'''
similarly you could write permission class for profile owners
'''
2) In your class based view
from self_defined_permission import ShopPermissions
class SomeView(generics.ListCreateAPIView):
...
permission_classes = (ShopPermissions, )
3) In case you are using function based view.
from rest_framework.decorators import permission_classes
#permission_classes((ShopPermissions, ))
def some_view(request):
....
Try the user_passes_test decorator:
from django.contrib.auth.decorators import user_passes_test
def is_shop(user):
return Shop.objects.filter(user=user).exists()
def is_normal_user(user):
return Profile.objects.filter(user=user).exists()
#user_passes_test(is_shop)
def only_for_shops(request):
return render(request, 'edit_price_list.html')
I'm trying to set custom permissions on a class that extends viewsets.ModelViewSet and it appears that my permissions are not being evaluated. Here is my view:
from rest_framework import viewsets
from rest_framework.authentication import SessionAuthentication, BasicAuthentication
from rest_framework.permissions import IsAuthenticated
import models
import serializers
from permissions import IsAdminOrAuthenticatedReadOnly
class KPIViewSet(viewsets.ModelViewSet):
'''
API endpoint that allows KPI metadata to be viewed or edited
'''
authentication_classes = (BasicAuthentication,)
permission_classes = (IsAdminOrAuthenticatedReadOnly,)
queryset = models.KPI.objects.all()
serializer_class = serializers.KPISerializer
And here is my permission class:
from rest_framework.permissions import BasePermission, SAFE_METHODS
class IsAdminOrAuthenticatedReadOnly(BasePermission):
def has_permissions(self, request, view):
if request.method in SAFE_METHODS:
return request.user and request.user.is_authenticated()
return request.user and request.user.is_staff()
The problem I'm running into is that IsAdminOrAuthenticatedReadOnly never seems to get evaluated. I tested this both by forcing it to always return "False" and by switching the permission_classes value to "IsAuthenticated" in the view. In the former scenario, a request to the endpoint returns as if there were no authentication requirement. In the later, authentication is enforced as expected.
Any ideas what I'm missing?
The method name is has_permission not has_permissions (no s) ;)
I am trying to implement TokenAuthentication using the Rest Framework, but it seems that I can't add my own custom decorators to my ViewSets because they are evaluated BEFORE the authentication. Consider this:
from django.utils.decorators import method_decorator
from django.http.response import HttpResponseForbidden
def require_staff(View):
def staffOnly(function):
def wrap(request, *args, **kwargs):
if request.user.is_active and request.user.is_staff:
return function(request, *args, **kwargs)
else:
return HttpResponseForbidden()
return wrap
View.dispatch = method_decorator(staffOnly)(View.dispatch)
return View
When I try to implement this, it seems the decorator code fires first, so the authentication is never run.
#require_staff
class CustomerViewSet(ModelViewSet):
model = Customer
filter_class = CustomerFilter
filter_backends = (DjangoFilterBackend,)
Since request.user is never set, introducing the decorator breaks authentication.
I think the issue is that Authentication is occuring the rest_frameworks dispatch() function and it is not clear to me how I could add additional (say) custom security if authentication is done that late in the game.
Am I missing something here, or what is the proper way to implement this customization?
Someone suggested using Permissions for this instead. I assume they mean custom DRF Permissions, right?
Everything you need to know is DRF permissions is here: http://www.django-rest-framework.org/api-guide/permissions
DRF provides a built in permission that is similar to yours, called IsAdminUser
The IsAdminUser permission class will deny permission to any user,
unless user.is_staff is True in which case permission will be allowed.
This permission is suitable is you want your API to only be accessible
to a subset of trusted administrators.
To use this permission in a Class Based View:
class ExampleView(APIView):
permission_classes = (IsAdminUser,)
Now you have two options to do an extra check for user.is_active.
The first is override the IsAdminUser permission, like so:
from rest_framework import permissions
class IsActiveAndAdminUser(permissions.IsAdminUser):
"""Only allow a user who is Admin and Active to view this endpoint. """
def has_permission(self, request, view):
is_admin = super(IsAdminAndActiveUser, self).has_permission(request, view)
return request.user.is_active and is_admin
The second is to create an IsActiveUser permission, and chain them in your view.
IsActiveUser Permission:
from rest_framework import permissions
class IsActiveUser(permissions.BasePermission):
""" Only Active Users have permission """
def has_permission(self, request, view):
return request.user.is_active
Your new permission list in your class based view:
class ExampleView(APIView):
permission_classes = (IsActiveUser, IsAdminUser,)
If I want to make sure that a view is listed as having public access, is there a decorator equivalent to #public_access which would be the opposite of #login_required and make it clear that the view should be publicly accessible always?
One use case I have in mind is to automatically add "#csrf_exempt" to all public views in addition to making it clear in the code that the view should be publicly accessible.
Unfortunately, there's currently no built-in support for this in Django, leaving you at risk of exposing sensitive info when #login_required is accidentally forgotten.
Here's a solution from one of my projects:
middleware/security.py:
def public(function):
"""
Decorator for public views that do not require authentication
"""
orig_func = function
while isinstance(orig_func, partial): # if partial - use original function for authorization
orig_func = orig_func.func
orig_func.is_public_view = True
return function
def is_public(function):
try: # cache is found
return function.is_public_view
except AttributeError: # cache is not found
result = function.__module__.startswith('django.') and not function.__module__.startswith('django.views.generic') # Avoid modifying admin and other built-in views
try: # try to recreate cache
function.is_public_view = result
except AttributeError:
pass
return result
class NonpublicMiddleware(object):
def process_view_check_logged(self, request, view_func, view_args, view_kwargs):
return
def process_view(self, request, view_func, view_args, view_kwargs):
while isinstance(view_func, partial): # if partial - use original function for authorization
view_func = view_func.func
request.public = is_public(view_func)
if not is_public(view_func):
if request.user.is_authenticated(): # only extended checks are needed
return self.process_view_check_logged(request, view_func, view_args, view_kwargs)
return self.redirect_to_login(request.get_full_path()) # => login page
def redirect_to_login(self, original_target, login_url=settings.LOGIN_URL):
return HttpResponseRedirect("%s?%s=%s" % (login_url, REDIRECT_FIELD_NAME, urlquote(original_target)))
settings.py:
MIDDLEWARE_CLASSES = (
#...
'middleware.security.NonpublicProfilefullMiddleware',
#...
)
and, finally, view code:
from <projname>.middleware import publi
#public
def some_view(request):
#...
# Login required is added automatically
def some_private_view(request):
#...
Also, you may want to look at "Automatically decorating all views of a django project" blog post
As a previous poster mentioned, login not required is the default.
However, sometimes it's useful to block certain views from logged in users -- for instance, it makes no sense for a logged-in user to be able to use the site's signup page. In that case, you could do something like this, based off the existing login_required decorator
from django.contrib.auth.decorators import user_passes_test
from django.conf import settings
LOGGED_IN_HOME = settings.LOGGED_IN_HOME
def login_forbidden(function=None, redirect_field_name=None, redirect_to=LOGGED_IN_HOME):
"""
Decorator for views that checks that the user is NOT logged in, redirecting
to the homepage if necessary.
"""
actual_decorator = user_passes_test(
lambda u: not u.is_authenticated(),
login_url=redirect_to,
redirect_field_name=redirect_field_name
)
if function:
return actual_decorator(function)
return actual_decorator
A bit late, but another simple way to tackle this issue would be to rely on another decorator and add a lambda function:
from django.contrib.auth.decorators import user_passes_test
#user_passes_test(lambda u: u.is_anonymous)
You can use user_passes_test and add a requirement, anonymous_required. This would work like the opposite to login_required and you can use on your register and login page - it is a bit irritating for users to still see the login page, after the are logged in.
This is how you would do it:
from django.contrib.auth.decorators import user_passes_test
#Anonymous required
def anonymous_required(function=None, redirect_url=None):
if not redirect_url:
redirect_url = settings.LOGIN_REDIRECT_URL
actual_decorator = user_passes_test(
lambda u: u.is_anonymous,
login_url=redirect_url
)
if function:
return actual_decorator(function)
return actual_decorator
#Application of the Decorator
#anonymous_required
def register(request):
#Your code goes here
"Login not required" is the default. If you want to annotate that a view should never be login-restricted then you should do so in the docstring.
I use django-decorator-include to use the login_required decorator to secure an entire app, rather than one view at a time. My app's main urls.py looks like this:
path('my_secret_app/', decorator_include(login_required, ('my_secret_app.urls', 'my_secret_app'))),
This works great, except for when one of my apps has one view which needs to be public.
To get around this, I wrote my own login_required and login_not_required. My login_required is based on django's django.contrib.auth.decorators.login_required, but is slightly modified to actually care when a view is marked as not requiring login.
My project is called mysite.
My app is called my_secret_app.
My public view within my_secret_app is called MyPublicView.
My entire solution looks like this:
mysite/lib.py
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.decorators import user_passes_test
# A copy of django.contrib.auth.decorators.login_required that looks for login_not_required attr
def login_required(function=None, redirect_field_name=REDIRECT_FIELD_NAME, login_url=None):
actual_decorator = user_passes_test(
lambda u: u.is_authenticated,
login_url=login_url,
redirect_field_name=redirect_field_name
)
if function:
login_req = getattr(function, "login_required", True)
if login_req:
return actual_decorator(function)
else:
return function
else:
return actual_decorator
# Decorator to mark a view as not requiring login to access
def login_not_required(f):
f.login_required = False
return f
mysite/urls.py
from .lib import login_required
path('my_secret_app/', decorator_include(login_required, ('my_secret_app.urls', 'my_secret_app'))),
my_secret_app/views.py:
from django.utils.decorators import method_decorator
from mysite.lib import login_not_required
class MyPublicView(View):
#method_decorator(login_not_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def get(self, request):
...
def post(self, request, *args, **kwargs):
...
You should be able to do the same thing no matter if you're subclassing View, ListView, CreateView, UpdateView, TemplateView, or any of the other ones. It should also work if you're using a function as your view, though I haven't tried it:
#login_not_required
def MyPublicView(request):
...
#permission_classes([permissions.AllowAny])
How can make Django permission_required decorator not to redirect already logged-in users to login page, but display some message like Insufficient permissions?
Thank you.
A quick and dirty solution would be to write your own decorator to do this. Something like this:
decorator_with_arguments = lambda decorator: lambda *args, **kwargs: lambda func: decorator(func, *args, **kwargs)
#decorator_with_arguments
def custom_permission_required(function, perm):
def _function(request, *args, **kwargs):
if request.user.has_perm(perm):
return function(request, *args, **kwargs)
else:
request.user.message_set.create(message = "What are you doing here?!")
# Return a response or redirect to referrer or some page of your choice
return _function
You can then decorate your view thus:
#custom_permission_required('my_perm')
def my_view(request, *args, **kwargs):
#Do stuff
Since django 1.4 permission_required has a raise_exception parameter that you can set to True to have an unauthorized PermissionDenied exception raised
Eg. to give an exemple on a Class Based View:
from django.contrib.auth.decorators import permission_required
...
class MyView(TemplateView):
#method_decorator(permission_required('can_do_something', raise_exception=True))
def dispatch(self, *args, **kwargs):
return super(MyView, self).dispatch(*args, **kwargs)
Ref:permission_required decorator doc
I'm assuming this question requires two pieces
Users that are already logged in do not get redirected to the login page
Users that are not logged in do not get redirected
#Manoj Govindan's answer nor #Stefano's answer will not work. #Lidor's answer will work, but he has fully re-implemented the permission_required function.
Here is a simpler way:
#login_required
#permission_required('hi there', raise_exception=True)
def blah(request):
pass
With this, if the user is not logged in, they will be redirected. If they are but they don't have permissions, they will be down an error.
I had the same problem but learned about raise_exception parameter at the #permission_required decorator!
this parameter is False by default, but once you pass it the True value, it will automatically redirect the without permission user to the 403.html page! (if there was any 403.html page in the root of your project, otherwise shows the server 403 forbidden page!
read more in Django docs here
#permission_required('app_name.view_model', raise_exception=True)
def some_model_view(request):
...
You can write your own decorator to replace django's permission_required decorator:
from django.utils import six
from django.core.exceptions import PermissionDenied
from django.contrib.auth.decorators import user_passes_test
def permission_required(perm, login_url=None, raise_exception=True):
def check_perms(user):
if isinstance(perm, six.string_types):
perms = (perm, )
else:
perms = perm
if user.has_perms(perms):
return True
if raise_exception and user.pk:
raise PermissionDenied
return False
return user_passes_test(check_perms, login_url=login_url)
And use it the same way:
#permission_required('app.permission')
def view_page(request):
# your view function
Logged in users with no permission will get a 403 forbidden error. Those who are not logged in will be redirected to the login page.