Override request.user in djangorestframework-simplejwt - django-rest-framework

There are two user models in my project:
class User(AbstractUser):
id = models.AutoField(primary_key=True)
email = models.EmailField(unique=True)
...
class ProjectMember(UserModel):
project = models.ForeignKey("project.Project")
I use djangorestframework-simplejwt for authorization and it gives me a User instance in request.user inside a view, here is an example:
from rest_framework.permissions import IsAuthenticated
from rest_framework.request import Request
from rest_framework.views import APIView
class CurrentUserView(ListAPIView):
permission_classes = [IsAuthenticated]
def get(self, request: Request):
# there will be User instance
current_user = request.user
# Will raise an exception
current_user.project
# some other code here
...
I have a user, but I cannot access project, because it defined in ProjectMember and not accessible via User.
I found out that I can get ProjectMember instance by checking the special attribute:
def get(self, request: Request):
# there will be ProjectMember instance
current_user = request.user.projectmember
# I can access project now
current_user.project
# some other code here
...
But now I have to repeat this code in every view I use my current user. How can I override the request.user for it to be always a ProjectMember (if it is an instance of ProjectMember, of course)?

I found the solution while writing the question.
Override JWTAuthentication like this:
# users/auth/custom_auth.py
from rest_framework_simplejwt.authentication import JWTAuthentication
class CustomAuth(JWTAuthentication):
def authenticate(self, request):
user, access = super().authenticate(request)
if hasattr(user, 'projectmember'):
user = user.projectmember
return user, access
And in settings.py change:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'rest_framework_simplejwt.authentication.JWTAuthentication'
],
}
to path to new class (users/auth/custom_auth.py:CustomAuth in my case):
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [
'users.auth.custom_auth.CustomAuth'
],
}

Related

Set permissions on Graphene Relay Node and Connection fields

How can I require authentication/authorization on the tier Node field and allTiers Connection field query below?
# schema.py
class TierNode(DjangoObjectType):
class Meta:
model = Tier
filter_fields = []
interfaces = (graphene.relay.Node,)
class Query(graphene.ObjectType):
tier = relay.Node.Field(TierNode)
all_tiers = DjangoFilterConnectionField(TierNode)
You can define a resolver for those fields with auth decorator like so:
from graphql_jwt.decorators import login_required
class Query(graphene.ObjectType):
tier = relay.Node.Field(TierNode)
all_tiers = DjangoFilterConnectionField(TierNode)
#login_required
def resolve_tier(root, info, **kwargs):
# code for resolving here
This is just using the login_decorator that comes with graphql_jwt but it will work for your custom decorators too if you defined them.
Furthermore, this also works for when you're resolving a field for TierNode:
class TierNode(DjangoObjectType):
class Meta:
model = Tier
filter_fields = []
interfaces = (graphene.relay.Node,)
some_property = graphene.Field("types.SomePropertyType")
#login_required
def resolve_some_property(root, info, **kwargs):
# code for resolving here
You can define authorization or/and authentication decorator like this:
from functools import wraps
def authorize_required(role):
def decorator(func):
#wraps(func)
def wrapper(instance, info, *args, **kwargs):
current_user = info.context.user
if not current_user.is_authenticated:
raise Exception("Authentication credentials were not provided")
if not authorize(instance, current_user, role):
raise Exception(
f"{current_user} has no access to {instance} with required {role=}"
)
return func(instance, info, *args, **kwargs)
return wrapper
return decorator
def authorize(instance, user, role) -> bool:
# check if user can have access to instance
# if there is requirement to have certain role
And use it in schema definition:
class TierNode(DjangoObjectType):
class Meta:
model = Tier
filter_fields = []
interfaces = (graphene.relay.Node,)
class Query(graphene.ObjectType):
tier = relay.Node.Field(TierNode)
all_tiers = DjangoFilterConnectionField(TierNode)
#authorize_required('user')
def resolve_tier(self, info, **args):
# some resolve code
#authorize_required('admin')
def resolve_all_tiers(self, info, **args):
# some resolve code

How to send PUT request to ModelViewSet without passing a primary key in the url?

I am particularly interested in using ModelViewSet
for solving the challenge of updating the logged in user's profile. I am using the following definition:
from rest_framework import viewsets
class ProfileRetrieveUpdate(viewsets.ModelViewSet):
serializer_class = UserProfileSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
def get_queryset(self):
return UserProfile.objects.filter(user=self.request.user)
def perform_update(self, serializer):
serializer.save(user=self.request.user)
By overriding get_queryset, I am able to get the expected behavior, i.e. when I access the endpoints (say profile/), I get the profile of the logged in user. However, if I have to update the profile, I can only access PUT by going to profile/6/. Is there a way I can get ModelViewSet to expose PUT at the same endpoint i.e. profile/?
You can register ModelViewSet HTTP method under any path you want.
path(
"profile",
ProfileRetrieveUpdate.as_view(
{"put": "partial_update", "get": "retrieve"}
),
name="profile-retrieve-update",
)
You will have to adjust other things as well as you don't provide pk.
Instead of overriding get_queryset method you need to adjust get_object for your needs.
from rest_framework import viewsets
from rest_framework.generics import get_object_or_404
class ProfileRetrieveUpdate(viewsets.ModelViewSet):
serializer_class = UserProfileSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
queryset = UserProfile.objects.all()
def get_object(self):
obj = get_object_or_404(self.queryset, user=self.request.user)
self.check_object_permissions(self.request, obj)
return obj
def perform_update(self, serializer):
serializer.save(user=self.request.user)
Now if you PUT profile/ it should work as expected.

How to solve django.url.exceptions.NoReverseMatch with kwargs?

I am trying to update an object that has already been created.
Following is my urls.py:
from django.conf.urls import url
from django.urls import include, path
from rest_framework import routers
from . import admin_views, temp_views, views
app_name = "transactions"
router = routers.SimpleRouter()
router.register(r"transactions", views.TransactionViewSet)
router.register(r"offerings", views.OfferingViewSet)
router.register(r"bank_accounts", views.BankAccountViewSet)
router.register(r"merchants", views.MerchantViewSet)
Following is my views.py:
class MerchantViewSet(GetPrefixedIDMixin, viewsets.ModelViewSet):
"""POST support for /merchants/."""
print ("in MerchantViewSet")
queryset = models.Merchant.objects.all()
serializer_class = serializers.CreateMerchantSerializer
lookup_field = "id"
lookup_value_regex = f"{models.Merchant.id_prefix}_[a-f0-9]{32}"
permission_classes = [permissions.MerchantPermission]
def get_queryset(self):
"""Filter the queryset based on the full merchant name or starting with a letter."""
queryset = models.Merchant.objects.all()
search_param = self.request.query_params.get("search", None)
if search_param:
if search_param.startswith("^"):
queryset = queryset.filter(name__istartswith=search_param[1:])
else:
queryset = queryset.filter(name__icontains=search_param)
return queryset
Following is the test I am trying to write:
class MerchantsViewSetTest(tests.BigETestCase): # noqa
#classmethod
def setUpClass(cls): # noqa
super(MerchantsViewSetTest, cls).setUpClass()
cls.application = tests.get_application()
tests.create_group("merchant")
cls.consumer_user = tests.create_consumer()
cls.admin = tests.create_administrator()
cls.merchant_geraldine = models.Merchant.objects.create(
name="Test Account 1",
contact_name="Geraldine Groves",
contact_email="geraldine#example.com",
contact_phone_number="+35310000000",
)
cls. merchant_barbara = models.Merchant.objects.create(
name="Account 2",
contact_name="Barbara",
contact_email="barbara#example.com",
contact_phone_number="+35310000432",
)
def test_edit_merchant(self): # noqa
# url = reverse("bige_transactions:merchant-list", kwargs={"id": self.merchant_geraldine.prefixed_id},)
url = reverse("bige_transactions:merchant-list", kwargs={"id": self.merchant_geraldine.prefixed_id})
# payload
data = {"name": "Edited"}
# verify anonymous cannot edit a user
self.put(url, data, status_code=401)
Following is my permissions.py:
class MerchantPermission(BasePermission): # noqa
def _has_get_permission(self, request, view): # noqa
access_token = get_access_token(request)
# we allow the request if the user is an admin
if is_administrator(access_token.user):
return True
return False
def _has_put_permission(self, request, view): # noqa
print ("entered put permissions")
access_token = get_access_token(request)
# we allow the request if the user is an admin
if is_administrator(access_token.user):
print ("Admin detected!")
return True
return False
def has_permission(self, request, view): # noqa
# is it a supported method
method = request.method.lower()
if method in view.http_method_names:
if method == "post":
return True
elif method == "get":
return self._has_get_permission(request, view)
elif method == "put":
return self._has_put_permission(request, view)
# always deny by default
raise exceptions.MethodNotAllowed(request.method)
So basically what I am trying to do is to add a functionality whereby I can update a merchant's detail. From my understanding from reading the Django Rest Framework documentation, the router.register method in my urls.py should set up all the URL's for create, list, update, retrieve and destroy. With my current test method, I get :
django.urls.exceptions.NoReverseMatch: Reverse for 'merchant-list' with keyword arguments '{'id': 'merch_b6c983ec082b4c7eb321b335fe9b122c'}' not found. 1 pattern(s) tried: ['merchants/$']
I am able to create, list and search users (didn't add those methods here as they are not relevant and don't want to make this question longer than it needs to be). I changing the url to
url = reverse("bige_transactions:merchant-list") + "?id=" + self.merchant_geraldine.prefixed_id
which does print out the URL, and I do not get the reverse() method error anymore, but I get a 404 error. So I am kind of stuck trying to implement this and I cannot find any examples on how to use the update() method that is built in ModelViewSet other than mentions of that it can be used.
When you register your viewset using router.register(r"merchants", views.MerchantViewSet) behind the scene you will get two different reverse urls:
merchants-list -> actual url will be ^merchants/$
merchants-detail -> actual url will be ^merchants/<pk>/$
In your case since you defined lookup_field="id" if might be <id> instead of <pk>. So your code needs to use the detail url like this:
url = reverse("bige_transactions:merchant-detail", kwargs={'id': self.merchant_geraldine.prefixed_id})
For more info check related docs

Access user object in decorator from request object for a logged in user

In my DRF app driven with APIView(), I want to add a single decorator. The decorator is:
from django.core.exceptions import PermissionDenied
from payment.models import Purchase
def client_has_paid(function):
'''
Has the client paid or have active subscription fee?
'''
def wrap(request, *args, **kwargs):
iccd = request.user.user_profile.iccd
filters = {'iccd': iccd , 'active': 1 }
try:
purchase = Purchase.objects.get(**filters)
return function(request, *args, **kwargs)
except:
raise PermissionDenied
wrap.__doc__ = function.__doc__
wrap.__name__ = function.__name__
return wrap
The error is in line request.user.user_profile.iccd which states user_profile don't exist (it does exist). doing
print(request.user)
gives out AnnonymousUser
Without the decorator, the API does print the correct user information as long as the passed token is valid.
The API that uses it is:
#method_decorator(client_has_paid, name='dispatch')
class AddIngredient(APIView):
permission_classes = [TokenHasReadWriteScope]
def post(self, request, cropped_land_id, format=None):
You can directly create drf style permission class and use it in your decorator, which would be more convenient. Just try this:
from rest_framework import permissions
class CustomPermission(permissions.BasePermission):
def has_permission(self, request, view):
iccd = request.user.user_profile.iccd
filters = {'iccd': iccd , 'active': 1 }
try:
purchase = Purchase.objects.get(**filters)
return True
except:
raise False
and use it in your view like:
class AddIngredient(APIView):
permission_classes = [CustomPermission]
def post(self, request, cropped_land_id, format=None):

'type' object is not iterable with Django Rest Framework and django oauth2-toolkit

Well I am trying to create new access token for the login user on creation with custom authentication class in views.
Serializer.py
class UserCreateSerializer(ModelSerializer):
def create(self, validated_data):
user = User.objects.create_user(validated_data['username'],
validated_data['email'],
validated_data['password'])
return user
class Meta:
model = User
fields = ('username', 'email' ,'password')
views.py
class User_Create_view(CreateAPIView):
serializer_class = UserCreateSerializer
queryset = User.objects.all()
permission_classes = [AllowAny]
authentication_classes = Has_Access_Token
def create(self, request):
serializers =self.serializer_class(data=request.data)
if serializers.is_valid():
# pdb.set_trace()
serializers.save()
# Has_Access_Token.access_token(Has_Access_Token())
return Response(serializers.data)
return Response(status=status.HTTP_202_ACCEPTED))
permission.py
class Has_Access_Token(BaseAuthentication):
def access_token(self):
app = Application.objects.get(name="testing")
tok = generate_token()
pdb.set_trace()
acce_token=AccessToken.objects.get_or_create(
user=User.objects.all().last(),
application=app,
expires=datetime.datetime.now() + datetime.timedelta(days=365),
token=tok)
return acce_token
#method_decorator(access_token)
def authenticate(self):
return request
If I use the Decorator
File "/usr/lib/python2.7/functools.py", line 33, in update_wrapper
setattr(wrapper, attr, getattr(wrapped, attr))
AttributeError: 'tuple' object has no attribute 'module'
If I am Not using the Decorator
File "/home/allwin/Desktop/response/env/local/lib/python2.7/site-packages/rest_framework/views.py", line 262, in get_authenticators
return [auth() for auth in self.authentication_classes]
TypeError: 'type' object is not iterable
The Problem I am facing is that when i use the Has_Access_Token fuction implicitly after serializer.save() Access token is generated in admin with respect to user but that's not effective method, so I need to override the custom authentication_class in views.
Could somebody please suggest some ways to tackle this issue or perhaps let me know the decorator correction with the above code.
Thanks in advance.
While setting REST_FRAMEWORK.DEFAULT_AUTHENTICATION_CLASSES in settings.py file, the customAuthencationClass must like below:
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': [ # need to be list not tuple
'CustomAuthentication',
],
}

Resources