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)
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()]
My (Angular) frontend performs jwt login to the Django Rest Framework backend through the provided jwt_views.TokenObtainPairView class.
However, this returns only access and refresh tokens, and I'd actually like to get more, such as username and the permissions the user has (so that the frontend can hide or disable any features the user can't use anyway).
What would be the best solution to achieve this?
Write my own token-generating view?
Subclass the existing view and overwrite the serializer with one that fetches user information?
Use a different method to transfer permissions or to log in?
The level of 'magic' that happens in Django confuses me as to what parameters and values I can set and overwrite.
Yes, you need to subclass TokenObtainPairView as well as TokenObtainPairSerializer.
from rest_framework_simplejwt.serializers import TokenObtainPairSerializer
from rest_framework_simplejwt.views import TokenObtainPairView
class MyTokenObtainPairSerializer(TokenObtainPairSerializer):
def validate(self, attrs):
attrs = super().validate(attrs)
return {
"username": self.user.username,
"email": self.user.email,
"permissions": self.user.user_permissions.values_list("codename", flat=True),
"groups": self.user.groups.values_list("name", flat=True),
**attrs,
}
class MyTokenObtainPairView(TokenObtainPairView):
serializer_class = MyTokenObtainPairSerializer
You can access the authenticated user in the serializer by calling self.user and hence you can access their permissions by self.user.user_permissions.all()
Also, do not forget to connect your new view class to the urls.py
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.
I am newbie to Django Rest Framework. I googled a lot and stuck on How to set User Role to User and strict to Permission.
My question is very basic but I don't know how to do it.
I want to create 2 types of Users:
1) Admin (Full Access)
2) Normal User (Limited Access)
I found some links but still on getting what to do:
Django rest-framework per action permission
django-rest-framework : setting per user permissions
you can separate users by is_staff property and use, standart rest permission isadminuser, for test you try this simple view
from rest_framework.permissions import IsAdminUser
from rest_framework.response import Response
from rest_framework.views import APIView
class ExampleView(APIView):
permission_classes = (IsAdminUser,)
def get(self, request, format=None):
content = {
'status': 'request was permitted'
}
return Response(content)
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