My application has several user types admin, user and manager.
I have defined an endpoint for a Resource, which has several prepend_urls.
eg: the endpoints would be
/Profile/search/
/Profile/shortview/
/Profile/
How can I limit access to the endpoints such that
/Profile/search/ is accessible to admin, manager
/Profile/shortview/ is accessible to all
/Profile/ is accessible to admin only
I have thought of using my own class but for authentication and authorization but think they are applied to the entire resource not individual prepend_url endpoints.
authorization = MyAuthorization()
authentication= MyAuthentication()
Any help is appreciated
I'm going to suppose you have been configured your prepend_urls and hence you have wrapped a function called dispatch_search, so something like this will raise an exception if user is unauthorized to use the endpoint:
def dispatch_search(self, request, *args, **kwargs):
# check authorization here
self._meta.authorization.is_authorized(request)
Edited from here below
When inheriting from the DjangoAuthorization class, you also can override the methods:
read_detail(self, object_list, bundle)
read_list(self, object_list, bundle)
to raise an exception if user should not be able to read an specific resource or the resource list itself.
And your MyAuthorization class:
from tastypie.exceptions import Unauthorized
from tastypie.authorization import DjangoAuthorization
class MyAuthorization(DjangoAuthorization):
def is_authorized(self, request):
if request.user.is_superuser and 'search' in request.path:
return True
# more business logic here to check the other endpoints
raise Unauthorized('Unauthorized :(')
def read_list(self, object_list, bundle):
self.is_authorized(bundle.request) # call your custom validation
# Fallback to the DjangoAuthorization read_list
return super(MyAuthorization, self).read_list(object_list, bundle)
Refer to the docs for a complete list of functions you can override to add more business logic: http://django-tastypie.readthedocs.org/en/latest/authorization.html#the-authorization-api
Related
I am just a newbie with Django, python. I try to build 1 simple API Collection include CRUD basic, and authentication, authorization.
I have 1 simple Views like:
#api_view(['GET'])
#permission_classes([IsUser])
def get_test(request : Request):
return JsonResponse({"message": "success"}, status = status.HTTP_200_OK)
and IsUser is:
class IsUser(IsAuthenticated):
def has_permission(self, request : Request, view):
token = request.query_params.get('token')
if token:
role = jwt.decode(token.split(" ").__getitem__(1), key="secret_key",algorithms="HS256").get('role')
if role == 'User':
return True
else:
return False
return False
My purpose wants to parse the JWT token and authorization based on that. I wonder don't know if my way is really correct? Can anyone give me some comments as well as documentation to better understand this issue? I don't use any other lib because I want to code by myself at first to understand the flow.
Thanks for helping me.
If you are using the default JWT authentication of DRF then permission class IsAuthenticated will verify the the token for you and instead of specifying token in query parameter specify in headers.
However, if you want to allow a specific type(role) of user to access your endpoint. Then create a subclass of BasePermission class as show in the example.
Then simply add IsAuthenticated in #permission_classes([IsUser & IsAuthenticated]) to make it work.
Again, this would work only if you are using the default Group model.
Here is how you can import it from django.contrib.auth.models import Group
from rest_framework.permissions import BasePermission
USER = 'USER'
class IsUser(BasePermission):
def has_permission(self, request, view, user=None):
if not user:
user = request.user
return USER in [role.name for role in user.groups.filter()]
Context
My API accepts a jwt-token that I can pass to an external endpoint which will return 401 if invalid/expired or user information if still valid. Based on this I will either return 401 or filtered data belonging to the user. Also, POST request's need to involve writing who this resource belongs to for the GET to work. I tried to do this by overriding the get_queryset and perform_create methods.
My viewset looks something like this:
class ReportViewSet(AuthorizedUserBasedFilteredViewset):
queryset = Report.objects.all()
serializer_class = ReportSerializer
def perform_create(self, serializer):
try:
username = self.get_authorized_user()['user']['username']
except Exception as e:
return Response({'error': 'Token does not exist'}, status=HTTP_401_UNAUTHORIZED)
serializer.save(creatd_by=username)
def get_queryset(self):
try:
username = self.get_authorized_user()['user']['username']
except Exception as e:
return Response({'error': 'Token does not exist'}, status=HTTP_401_UNAUTHORIZED)
return Report.objects.filter(created_by=username)
This doesn't work because get_queryset expects a queryset as response.
Questions
How do I bubble up the authorize exception in get_queryset? Is there some other method I should be overriding to the authentication entirely? I still need to pass the username recieved to get_queryset for the authentication to be successful
Is there a better way to do this? I feel that the call to the external authentication API and setting the user to be accessible by the get_queryset and perform_create methods would ideally go somewhere else in the code. I looked at the RemoteUserAuthenticationBackend but that still involved creation of a User object but for this use case the user model is entirely external
Instead of
return Response({'error': 'Token does not exist'}, status=HTTP_401_UNAUTHORIZED)
use this
raise NotAuthenticated(detail='Token does not exist')
Hope above line has addressed your 1st question.
For 2nd question.
You can extend TokenAuthentication and then implement def authenticate_credentials(self, key): method. It is not a good idea to call external API to fetch user each time. Instead, you should get JTW token one time from external source and then pass JWT token in header like Authorization : Bearer cn389ncoiwuencr for each API call. Then you should decode JWT token in current system.
from rest_framework.authentication import TokenAuthentication
from django.contrib.auth import get_user_model
class CustomTokenAuthentication(TokenAuthentication):
keyword = 'Bearer' # token type
def authenticate_credentials(self, key):
#decode JWT.
username = get_username()
User = get_user_model()
user = User(username=username)
return user, key
Finally, add this class to settings.py file.
REST_FRAMEWORK = {
'DEFAULT_PERMISSION_CLASSES': [
'rest_framework.permissions.IsAuthenticated',
],
'DEFAULT_AUTHENTICATION_CLASSES': (
'path.of.CustomTokenAuthentication',
)
}
That will apply to all of your views. or you can use view specific auth class.
class Sample(ViewSet):
authentication_classes = (CustomTokenAuthentication,)
From now you can access user by request.user at views or viewset. That's it.
general django question for those who are more experienced than myself,
I'm reading through the code posted for a tutorial on thinkster.io:
https://github.com/howardderekl/conduit-django/tree/master/conduit/apps
There's an endpoint pertaining to the User model authenticion/models.py that requires an Authorization header for it to return user information defined here in authentication/views.py:
class UserRetrieveUpdateAPIView(RetrieveUpdateAPIView):
permission_classes = (IsAuthenticated,)
renderer_classes = (UserJSONRenderer,)
serializer_class = UserSerializer
def retrieve(self, request, *args, **kwargs):
serializer = self.serializer_class(request.user)
return Response(serializer.data, status=status.HTTP_200_OK)
My question is how/where is it (supposed to be) determined that an endpoint requires this Authorization. My thought is that it is tied to the permission_classes variable stated in the UserRetrieveUpdateAPIVIiew class above. I dug into the package location where this was imported from (from rest_framework.permissions import IsAuthenticated), but that doesn't appear to contain anything pertaining to an HTTP header:
class BasePermissionMetaclass(OperationHolderMixin, type):
pass
class BasePermission(metaclass=BasePermissionMetaclass):
"""
A base class from which all permission classes should inherit.
"""
def has_permission(self, request, view):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
def has_object_permission(self, request, view, obj):
"""
Return `True` if permission is granted, `False` otherwise.
"""
return True
...
...
...
class IsAuthenticated(BasePermission):
"""
Allows access only to authenticated users.
"""
def has_permission(self, request, view):
return bool(request.user and request.user.is_authenticated)
I'm looking for best practices on how to structure headers like this for HTTP methods in my backend. Any ideas on where I should look, somewhere in settings.py maybe?
Thanks!
Bonus question:
This header requires two strings in your request. First being 'Token', followed by a space, then the actual JWT for that user. Is this standard practice to use two strings like this? If so, what's the reasoning. I've seen this before with other seemingly arbitrary words used for the first string, 'Token'
As shown in the documentation :
REST framework will attempt to authenticate with each class in the list, and will set request.user and request.auth using the return value of the first class that successfully authenticates.
So if you use a token authentication scheme, the header Authorization: Token ... will result in the setup of the request.user and request.user.is_autenticated will be set to true for the view.
By combining it with the IsAuthenticated permission class, the view will only be accessible if the authorization token is set (and if you don't allow other authentication schemes)
For your second question, you can indeed put any string you want. Here DRF uses Token to make it clearer for which authentication scheme it is used. Since a lot of apps also uses a "Token", a lot of them use the same word. You can also often find Bearer.
Django REST Framework comes with a package for Token Authentication, which is probably what you are looking for:
https://www.django-rest-framework.org/api-guide/authentication/#tokenauthentication
With it, you can assign tokens to individual users, which they can use in lieu of a username and password to authenticate against endpoints. This token is provided in the HTTP header. As for your second question, by providing Token as a convention, it would allow for other custom authentication types:
https://www.django-rest-framework.org/api-guide/authentication/#custom-authentication
The permission_classes are what determine what, if any, authentication is necessary. Good luck!
I have a model, called rides, which I want to have access to my custom token authentication. I do not want this to be a made public to the whole viewset.
How can I add the authentication method to the create method? The below throws an error complaining I can't add a list_route to a create method as it exists already.
class RideViewSet(viewsets.ModelViewSet):
# POST /rides/
#list_route(methods=['post'], authentication_classes=[CustomTokenAuth])
def create(self, request, *args, **kwargs):
The decorator won't work on the ViewSet's list / create / ...
You'll need to deal with the authenticate by yourself.
Therefore you need to fill the DRF's request with:
request._authenticator as the auth backend that has been doing the auth
request.user, request.auth as the result of your auth backend's authenticate()
Lets say the API endpoint to GET a list of users is this
/api_auth/user/
But I want to restrict access to this list only to people with an api_key
/api_auth/user/?access_key=$omeRandomHash3252532
How do I implement such an access system using the Django Rest Framework?
Should I use Permissions to implement this?
This is not supported out of the box for django-rest-framework, however it can easily be implemented:
If you take a look at http://www.django-rest-framework.org/api-guide/authentication/ you'll see an Example of a custom authentication method. Baased on that, you'll need to implement something like this:
from django.contrib.auth.models import User
from rest_framework import authentication
from rest_framework import exceptions
class APIKeyAuthentication(authentication.BaseAuthentication):
def authenticate(self, request):
api_key = request.GET.get('api_key')
if not api_key:
return None
try:
user = get_user_from_api_key(api_key)
except User.DoesNotExist:
raise exceptions.AuthenticationFailed('No user for API KEY')
return (user, None)
The APIKeyAuthentication should be put on an authentication.py module and be configured with REST_FRAMEWORK setting on settings.py, like this
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'my_custom_package.authentication.APIKeyAuthentication',
)
}
Now, what the above does is that it checks if the api_key parameter is present (if not it will return None to check if the request can be authenticated differently -- if you don't want to check any other authentication classes then just raise an exceptions.AuthenticationFailed exception like we do when we dont find the user below. Now, we need to implement a get_user_from_api_key function that will return a User instance from an API_KEY. If the user that is correlated with the passed api_key is found then it will be returned, if not an exceptions.AuthenticationFailedexception will be thrown.
Concerning the get_user_from_api_key function, its implementation depends on your requirements. For instance, if you want to create a new api key for each user, you should create an APIKey model that will have an api_key CharField and a ForeignKey to the User that has this api_key. The get_user_from_api_key function then will query the APIKey model to get the user with the provided api_key.
Update
If you want to use the django-rest-framework permissions instead of the authentication, you may create an APIKeyPermission class like this:
from rest_framework import permissions
class APIKeyPermission(permissions.BasePermission):
def has_permission(self, request, view):
api_key = request.GET.get('api_key')
return check_permission(api_key, request)
Where the check_permission function will check if the api_key passed has permissions for that specific request. Please check the examples on http://www.django-rest-framework.org/api-guide/permissions/ for more info - you may instead choose to implement has_object_permission to implement object-level permissions instead of view-level permissions.
If you are able to set a header in your request you can use Rest Framework's Token Authentication.
Otherwise, if you need to put it in the URL as a GET-paramter you could make your own custom authentication class:
from rest_framework.authentication import TokenAuthentication
class MyAuthentication(TokenAuthentication):
def authenticate(self, request):
token = request.GET.get('api-key', None)
if token is None:
return None
return self.authenticate_credentials(token)