Django OAuthToolkit Scopes per specific method - python

I'm using Django Rest Framework and OAuthTookit.
I want that the scope provided by the token should be HTTP Method specific. For eg:- GET, PUT, DELETE of the same APIView should have different scopes.
Following are my APIs.
class MyView(RetrieveUpdateDestroyAPIView):
permission_classes = [TokenHasScope]
required_scopes = ['scope1']
serializer_class = ModelSerializer
queryset = Model.objects.all()
Currently, the scope is set at the class level, which means to access all the GET, PUT & DELETE method, the token should have scope1.
I want that there should be different scope for different HTTP methods. How can I set different scope for different methods?

To handle this case, I think you need to implement a new permission class, something like this:
class TokenHasScopeForMethod(TokenHasScope):
def has_permission(self, request, view):
token = request.auth
if not token:
return False
if hasattr(token, "scope"):
# Get the scopes required for the current method from the view
required_scopes = view.required_scopes_per_method[request.method]
return token.is_valid(required_scopes)
And use it in your view like this:
class MyView(RetrieveUpdateDestroyAPIView):
permission_classes = [TokenHasScopeForMethod]
required_scopes_per_method = {'POST': ['post_scope'], 'GET': ['get_scope']}
serializer_class = ModelSerializer
queryset = Model.objects.all()

Perhaps You can use TokenMatchesOASRequirements permission class
class SongView(views.APIView):
authentication_classes = [OAuth2Authentication]
permission_classes = [TokenMatchesOASRequirements]
required_alternate_scopes = {
"GET": [["read"]],
"POST": [["create"], ["post", "widget"]],
"PUT": [["update"], ["put", "widget"]],
"DELETE": [["delete"], ["scope2", "scope3"]],
}

I like #clément-denoix answer, but would tweak it a bit.
Instead of reloading TokenHasScope.has_permission I would suggest to redefine
TokenHasScope.get_scopes as it is smaller method and perfectly fit for what you need. Also original has_permission method has additional logic that I prefer to preserve.
Something like this should do the trick:
from django.core.exceptions import ImproperlyConfigured
from oauth2_provider.contrib.rest_framework import TokenHasScope
class TokenHasScopeForMethod(TokenHasScope):
def get_scopes(self, request, view):
try:
scopes = getattr(view, "required_scopes_per_method")
return scopes[request.method]
except (AttributeError, KeyError):
raise ImproperlyConfigured(
"TokenHasScope requires the view to define the required_scopes_per_method attribute"
)

Related

How can I make "has_object_permission( )" work?

I'm trying to create an object level permission for a user. The structure of my ddbb model is the following one:
A Teacher owns a Classroom (id_teacher ForeignKey)
A Classroom owns some Students (id_classroom ForeignKey)
I want to let the access to Student information just for the teacher who owns the classroom where the Students are registered.
Here are the API code and the permission code:
class StudentAPI(RetrieveUpdateAPIView):
permission_classes = [GetStudentPermission, ]
def get(self, request):
student_ = Student.objects.get(username=request.GET['username'])
s_student_ = StudentSerializer(student_)
return Response(s_student_.data)
class GetStudentPermission(BasePermission):
message = 'La información de este estudiante está restringida para usted'
def has_object_permission(self, request, view, obj):
cls_ = Classroom.objects.filter(id=obj.id_classroom.id).first()
tch_ = Teacher.objects.get(classroom=cls_)
user_ = User.objects.get(id=tch_.id_user.id)
return bool(user_ == request.user)
It seems like permission classes is not working at all because I can access to the information of each student being registered with any user account.
Thank you beforehand
As per the note in the section Custom permissions [DRF docs]:
The instance-level has_object_permission method will only be called
if the view-level has_permission checks have already passed. Also
note that in order for the instance-level checks to run, the view code
should explicitly call .check_object_permissions(request, obj). If
you are using the generic views then this will be handled for you by
default. (Function-based views will need to check object permissions
explicitly, raising PermissionDenied on failure.)
Since you override get and implement it yourself check_object_permissions is never called, you can either do it yourself:
class StudentAPI(RetrieveUpdateAPIView):
permission_classes = [GetStudentPermission, ]
def get(self, request):
student_ = Student.objects.get(username=request.GET['username'])
self.check_object_permissions(self.request, student)
s_student_ = StudentSerializer(student_)
return Response(s_student_.data)
OR better yet your implementation of get is not much different than the builtin implementation that RetrieveUpdateAPIView already has, so you can forego overriding get and actually use the view directly:
class StudentAPI(RetrieveUpdateAPIView):
queryset = Student.objects.all()
serializer_class = StudentSerializer
lookup_field = 'username'
permission_classes = [GetStudentPermission, ]

How can I set the created instance's property in Django-Rest-Framework CreateAPIView?

How can I set the created instance's property in Django-Rest-Framework?
class ServerTaskCreateAPIView(CreateAPIView):
serializer_class = PhysicalServerTaskCreateSerializer
permission_classes = []
queryset = ServerTask.objects.all()
def perform_create(self, serializer):
# I want to set the ServerTask instance to be `Initialed`
# like instance.task_type = "Initialed"
serializer.save()
You see, I tried to rewrite the perform_create method, and find there is no instance's param. how to access the requirement?
edit
I know I can use APIView in the post method to achieve it, but that can not fit the simplicity and convenience of DRF. so I want in the DRF to do that, is it possible?
You can use this:
http://www.django-rest-framework.org/api-guide/serializers/#writing-create-methods-for-nested-representations
def create(self, validated_data):
validated_data['task_type'] = 'Initialed'
server_task = ServerTask.objects.create(**validated_data)
return server_task
Or in your case you may just want a default value on the model.

Django Rest Framework: Register multiple serializers in ViewSet

I'm trying to create a custom API (not using models), but its not showing the request definition in the schema (in consequence, not showing it in swagger). My current code is:
views.py
class InfoViewSet(viewsets.ViewSet):
#list_route(methods=['POST'])
def some_method(self, request):
data = JSONParser().parse(request)
serializer = GetInfoSerializer(data=data)
serializer.is_valid(raise_exception=True)
info = get_data_from_elsewhere(serializer.data)
return Response(info)
urls.py
router.register(r'^info', InfoViewSet, base_name='info')
serializers.py
class InfoSomeMethodSerializer(serializers.Serializer):
list_id = serializers.ListField(child=serializers.IntegerField())
password = serializers.CharField()
And it appears in swagger, but just the response part. How can I register the post parameters? I'm also not sure if I'm using DRF correctly (I'm new) so any correction will be appreciated.
--
edit:
I tried the serializer_class argument suggested by Linovia and didn't work, I got:
TypeError: InfoViewSet() received an invalid keyword 'serializer_class'
I tried overriding get_serializer_class method and didn't work either:
def get_serializer_class(self):
if self.action == 'some_method':
return InfoSomeMethodSerializer
For people running this into the future - when you add the serializer_class attribute to the #action decorator of a view which inherits from viewsets.ViewSet, it will indeed by default give you a TyperError, as OP mentioned:
TypeError: InfoViewSet() received an invalid keyword 'serializer_class'
To overcome this, simply add serializer_class = None as a class variable to your view.
Example of editing OPs code:
class InfoViewSet(viewsets.ViewSet):
# ↓ ADD THIS!
serializer_class = None
# Now you can add serializer_class without geting a TypeError ↓
#list_route(methods=['POST'], serializer_class=GetInfoSerializer)
def some_method(self, request):
data = JSONParser().parse(request)
serializer = GetInfoSerializer(data=data)
serializer.is_valid(raise_exception=True)
info = get_data_from_elsewhere(serializer.data)
return Response(info)

How can I add a non-model/queryset returning view with django-restframework when using DjangoObjectPermissions?

I have a view that I want to add to my django-restframework api that does not relate to any model. Though I'm using 'rest_framework.permissions.DjangoObjectPermissions' in DEFAULT_PERMISSION_CLASSES.
class EnumChoices(views.APIView):
def get(self, request):
enums = {}
return Response(enums)
Now Django complains about my view:
AssertionError at /api/enums/
Cannot apply DjangoModelPermissions on a view that does not have `.queryset` property or overrides the `.get_queryset()` method.
I need the permission class for almost all other views and do not want to get rid of it. How can I get around the mandatory attributes for the one view?
You can add a view-specific permission logic to overwrite the model permission check. Create a BasePermission class object and add it to your views permission_classes attribute. Don't forget IsAuthenticated unless you want to allow anonymous users too.
class EnumChoices(views.APIView):
class EnumPermission(permissions.BasePermission):
def has_permission(self, request, view):
# whatever permission logic you need, e.g.
return request.user.has_perm("planning.view_enums")
permission_classes = (permissions.IsAuthenticated, EnumPermission)
def get(self, request):
enums = {}
return Response(enums)
Now the view will ensure the user is authenticated and has the view_enums permission.
More info here: http://www.django-rest-framework.org/api-guide/permissions/#custom-permissions

How to support all REST operations for an endpoint in django rest framework

I have a subscription model that looks like this
class Subscription(models.Model):
name = models.CharField(max_length=100)
quantity = models.IntegerField(max_length=20)
stripe_id = models.CharField(max_length=100)
user = models.ForeignKey(User)
I would like to create an endpoint that allows POST, PATCH, DELETE, GET
So I did the following things
views.py
class SubscriptionDetail(viewsets.ModelViewSet):
serializer_class = SubscriptionSerializer
permission_classes = (IsAuthenticated,)
queryset = Subscription.objects.all()
serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
def update(self, instance, validated_data):
print "In update"
#how do I write create and delete?
urls.py
subscription = SubscriptionDetail.as_view({
'patch': 'update'
})
url(r'^rest-auth/subscription/$', subscription, name='something'),
Questions
Using the above when I send a PATCH request, I get an error. How can I fix this?
Expected view SubscriptionDetail to be called with a URL keyword
argument named "pk". Fix your URL conf, or set the .lookup_field
attribute on the view correctly.
While sending the patch request I would also like to send an 'email' field which is not on the subscription model. Is this possible to do? I need the email field in the POST (create) operation so that I know which user the subscription belongs to.
The easiest way is to do it this way.
keep the models class the same
views.py
from rest_framework import viewsets
#impost serializer and model class for subscription
class SubscriptionViewSet(viewsets.ModelViewSet):
serializer_class = SubscriptionSerializer
def get_queryset(self):
queryset = Subscription.objects.all()
#if you need to get subscription by name
name = self.request.QUERY_PARAMS.get('name', None)
if name is not None:
queryset = queryset.filter(name=name)
return queryset
serializers.py
class SubscriptionSerializer(serializers.ModelSerializer):
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
# django will handle get, delete,patch, update for you ....
# for customization you can use def update or def create ... to do whatever you need
# def create(self, validated_data):
# you can handle the email here
# and something like subscription= Subscription (name=validated_data['name'],vendor=validated_data['quantity']...)
# subscription.save()
# it will save whatever you want
urls.py
#use the router to handle everything for you
from django.conf.urls import patterns, include, url
from rest_framework import routers
#import your classes
router = routers.DefaultRouter()
router.register(r'subscription', views.SubscriptionViewSet,base_name='subscription')
urlpatterns = patterns('',
url(r'^', include(router.urls)),
)
For the creation of an Object you must implement the create function as described in the official documentation, found here. For patching you could use the partial argument from within you view class:
SubscriptionSerializer(subscription, data={'something': u'another', partial=True)
For deletion of the a Subscription, that could be done when you get the delete call as so in your view class:
if request.METHOD == 'DELETE':
subscription = Subscription.objects.get(pk=pk)
subscription.delete()
See this tutorial for complete example
Further more I think that you should include the "id" field in the SubscriptionSerialiser Meta class, otherwise it will be difficult to do the updates/deletions. I hope this helped a little.
Cheers,
Tobbe
When you want to use a method that allow make these operations you have to use a #detail_route() where you can say as well which methods will you use, like in the docs is said:
#detail_route(methods=['post'])
def set_password(self, request, pk=None):
user = self.get_object()
serializer = PasswordSerializer(data=request.data)
...
So to be able to use them you should add the next decorator
#detail_route(methods=['post', 'patch'])
To add another parameters you can do it for the .save() parameter. You just have to indicate the name of this and them just override your .save() model to check if that email belongs or not to the user that is trying to do the subscription. Here I paste you what the Django Rest docs says:
" Passing additional attributes to .save()
...
You can do so by including additional keyword arguments when calling .save(). For example:
serializer.save(owner=request.user)
Here I leave you the link for more information:
http://www.django-rest-framework.org/api-guide/serializers/#passing-additional-attributes-to-save
Using the above when I send a PATCH request, I get an error. How can I fix this?
Expected view SubscriptionDetail to be called with a URL keyword
argument named "pk". Fix your URL conf, or set the .lookup_field
attribute on the view correctly.
The error is caused because unlike create request, patch/update require a pk to know which object to update. That is why you have to supply the pk value for it. So, your url for PUT, DELETE andPATCH must have at least named parameter like this -
subscription = SubscriptionDetail.as_view({
'patch': 'update'
})
url(r'^rest-auth/subscription/(?<pk>(\d+))$', subscription, name='something'),
an example url will be - rest-auth/subscription/10 where 10 is the pk or id of the object. Django Rest Framework will then load the object internally to be updated.
While sending the patch request I would also like to send an 'email' field which is not on the subscription model. Is this possible to do? I need the email field in the POST (create) operation so that I know which user the subscription belongs to.
To add custom parameters, first declare the property in serializer, it is better to keep it required=False, so that other request does not throw error -
class SubscriptionSerializer(serializers.ModelSerializer):
custom_field = serialiers.BooleanField(required=False)
class Meta:
model = Subscription
fields = ('name','quantity', 'stripe_id')
def update(self, instance, validated_data):
print "In update"
so far this is enough for the django rest framework to accept the field custom_field and you will find the value in update method. To get the value pop it from the attributes supplied by the framework like this -
def update(self, instance, validated_data):
custom_field = validated_data.pop('custom_field', None)
if custom_field is not None:
# do whatever you like with the field
return super().update(instance, validated_data)
# for python < 3.0 super(SubscriptionSerializer, self).update(instance, validated_data)
When you overrided (I don't know if that's the proper conjugation of overriding a method) the update method, you stopped the ability to PUT or PATCH and object. Your new method only prints out "In update" but doesn't save the instance. Look at the update method from the serializer.ModelSerializer object:
def update(self, instance, validated_data):
raise_errors_on_nested_writes('update', self, validated_data)
for attr, value in validated_data.items():
setattr(instance, attr, value)
instance.save()
return instance
Notice the last few lines where the instance is saved with the values and then returned. Remove your update method on the SubscriptionSerializer object. This let's your parent object's create, update, retrieve, and delete methods do their magic which supports PATCH and PUT updates. The next problem is that your urls.py is using the Django rather than the REST framework router. Change it to this:
from rest_framework.routers import DefaultRouter
router = DefaultRouter()
router.register(r'subscription', SubscriptionDetail)
That should solve the patch update problem.
I don't think you can add an email field in your patch method without the attribute on the subscription model. That's just a guess on my part, and I may be wrong. Does the email field map to anything on any object? Can you use a ForeignKey to map it?
I hope that works for you, good luck!
In view.py you just need set the class with:
class SubscriptionDetail(mixins.CreateModelMixin,
mixins.ListModelMixin,
mixins.RetrieveModelMixin,
mixins.UpdateModelMixin,
generics.GenericAPIView):
and add this to fix .lookup_field :
def update(self, request, *args, **kwargs):
log.error("OBJ update kwargs= %s , data = %s" % (kwargs, str(request.data)))
pk = request.data.get('id')
if (kwargs.get('pk') is not None):
kwargs['pk'] = request.data.get('id')
self.kwargs['pk'] = request.data.get('id')
return super().update(request, *args, **kwargs)
and add support to methods do you want :
def post(self, request, *args, **kwargs):
return self.create(request, *args, **kwargs)
def get(self, request, *args, **kwargs):
return self.list(request, *args, **kwargs)
# def get(self, request, *args, **kwargs):
# return self.retrieve(request, *args, **kwargs)
def put(self, request, *args, **kwargs):
return self.update(request, *args, **kwargs)
# def patch(self, request, *args, **kwargs):
# return self.partial_update(request, *args, **kwargs)
#
# def delete(self, request, *args, **kwargs):
# return self.destroy(request, *args, **kwargs)
only tweak that remains is get for list or get for retrieve on element but should be easy now add something if we have one pk we may call self.retrieve else we may call self.list

Categories