Changing the default user while using REST framework JWT - python

I have a proxied user model:
class TheDude(User):
class Meta:
proxy = True
And I'm using the Django REST framework JWT to do JWT auth in the REST API.
I'd like to get the user object from the request but currently it's a User object. Because it's proxied I can't use AUTH_USER_MODEL. I've tried doing a middleware component to override the user in the request but it's not set at that stage. I've also tried using JWT_RESPONSE_PAYLOAD_HANDLER however my function isn't called so I can't set it there either.
If I want to be able to get TheDude object when I call request.user instead of User in my views, how would I do this while authing using the REST framework JWT Auth library?
EDIT
I've added
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
...
'myapp.authentication.MyCustomJWTAuthentication',
)
...
}
to my settings.py and my
class MyCustomJWTAuthentication(JWTAuthentication):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user_model = TheDude
Which is called correctly however when I get the user from request in my serialiser, it's still of type User and not TheDude
class TestSerializer(serializers.ModelSerializer):
user_test = serializers.SerializerMethodField('get_user_test')
def get_user_test(self, obj):
print(type(self.context['request'].user))

It should be possible to use the proxy model by overriding the JWTAuthentication authentication class and setting your proxy user model as the user_model like so:
from rest_framework_simplejwt.authentication import JWTAuthentication
class MyCustomJWTAuthentication(JWTAuthentication):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user_model = TheDude
Say you add this class on myapp/authentication.py, you can then apply this custom authentication class as one of the default authentication classes in your REST_FRAMEWORK settings:
REST_FRAMEWORK = {
...
'DEFAULT_AUTHENTICATION_CLASSES': (
'myapp.authentication.MyCustomJWTAuthentication',
...
)
...
}
Or just apply it to certain views you want:
from myapp.authentication import MyCustomJWTAuthentication
class CertainAPIViewThatNeedsTheDude(APIView):
authentication_classes = (MyCustomJWTAuthentication, )
This should in turn give you a request.user that is a TheDude instance.

Related

Django custom authentication

I am using Django REST Framework in my app. I need authentication but not the default one. I have a model:
class Worker(models.Model):
token = models.CharField(...)
ip = models.GenericIPAddressField(...)
created_date = models.DateTimeField(...)
last_update = models.DateTimeField(...)
Worker sends messages through my API to view WorkerView which inherits from Django REST Framework's APIView. Token is send in request's header:
class WorkerView(APIView):
def post(self, request):
# some not important code
I have an authenticating method:
def authenticate(request):
try:
ip = request.META.get("REMOTE_ADDR", None)
token = request.META.get("HTTP_AUTHORIZATION", None)
...
I thought about two solutions:
Make a mixin class and inherit it in my WorkerView:
class WorkerView(AuthenticationMixin, APIView)
...
Make a class decorator from my authenticate method and use it like this:
#authenticate
class WorkerView(APIView)
But in both cases I need to pass request argument to authenticate method.
How to do that? Or maybe there's a better solution to my problem?
Why don't You create a custom authentication class as defined here?
http://www.django-rest-framework.org/api-guide/authentication/#custom-authentication

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

Django-tastypie pass request.user to custom save method

Since my model's custom save method takes request.user as an argument I'm unable to do POST/PUT requests.
TypeError at /api/obsadmin/observation/23
save() takes at least 2 arguments (1 given)
I'm using SessionAuthentication() and have included the CSRF token.
Here's the relevant model part:
def save(self, user, owner=None, *args, **kwargs):
self.updated_by = user.id
super(ObsModel, self).save(*args, **kwargs)
And the resource:
class ObservationResource2(ModelResource):
comments = fields.ToManyField(CommentResource2, 'comments', full=True, null=True)
class Meta:
queryset = Observation.objects.filter(is_verified=True)
authentication = SessionAuthentication()
authorization = DjangoAuthorization()
resource_name = 'observation'
always_return_data = True
I've just achieved this same end goal by using the built-in hydrate methods to modify the data prior to saving. The current request is available in bundle.request inside the hydrate methods. See the docs here.
I have a Friend model exposed via FriendResource that I want to link to the creating Django user via a user ForeignKey field.
My example Resource code:
class FriendResource(ModelResource):
class Meta:
queryset = Friend.objects.all()
resource_name = 'friend'
excludes = ['slug',]
authentication = SessionAuthentication()
authorization = DjangoAuthorization()
always_return_data = True
def get_object_list(self, request):
return super(FriendResource, self).get_object_list(request).filter(user=request.user)
def hydrate(self, bundle):
bundle.obj.user = bundle.request.user
return bundle
Hope that helps!
You could override the default save() method on your ModelResource subclass. Looking at the default implementation shows that save() is called with a bundle object which has both the request and the object to be saved.
Unfortunately, there's no easy way to change this without copying most of that code because changing a Django model's save() signature is fairly uncommon. You might be able to do something like this, although I'd recommend testing it carefully:
from functools import partial
try:
old_save = bundle.obj.save
bundle.obj.save = partial(old_save, user=bundle.request.user)
return super(FooResource, self).save(bundle)
finally:
bundle.obj.save = old_save
References:
obj_create: docs source
obj_update: docs source
save: source

access session users in models

Is There any possible way to access session users in models,
Iam override the save method in models which needs user to check role and then save,
And also using djangorestframework for apis which calls save method in models ,
Thanks In Advance
You would pass your save function the request object from one of your views.
class MyModel(models.Model):
def save(self, *args, **kwargs):
request = kwargs.pop('request', None)
print request # this is the request object if it was passed into save()
# do whatever you'd like with request here.
super(MyModel, self).save(*args, **kwargs)
def myview(request):
MyModel.objects.save(request=request)

Categories