简体   繁体   中英

Implementing a custom authentication in DRF which can read request.data

I have a foreign key on my models like Patient, and Doctor, which point to a Clinic class. So, the Patient and Doctor are supposed to belong to this Clinic alone. Other Clinics should not be able to see any detail of these Models.

The models look like this:

class Clinic(models.Model):
    clinicid = models.AutoField(primary_key=True, unique=True)
    name = models.CharField(max_length=60, unique=True)
    label = models.SlugField(max_length=25, unique=True)
    email = models.EmailField(max_length=100, default='')
    mobile = models.CharField(max_length=15, default='')
    ...

class Doctor(models.Model):
    # Need autoincrement, unique and primary
    docid = models.AutoField(primary_key=True, unique=True)
    name = models.CharField(max_length=200)
    username = models.CharField(max_length=15)
    regid = models.CharField(max_length=15, default="", blank=True)
    ...
    linkedclinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)

class Patient(models.Model):
    cstid = models.AutoField(primary_key=True, unique=True)
    date_of_registration = models.DateField(default=timezone.now)
    name = models.CharField(max_length=35, blank=False)
    ageyrs = models.IntegerField(blank=True)
    agemnths = models.IntegerField(blank=True)
    dob = models.DateField(null=True, blank=True)
    ...
    linkedclinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)

class UserGroupMap(models.Model):
    id = models.AutoField(primary_key=True, unique=True)
    user = models.ForeignKey(
        User, related_name='target_user', on_delete=models.CASCADE)
    group = models.ForeignKey(UserGroup, on_delete=models.CASCADE)
    clinic = models.ForeignKey(Clinic, on_delete=models.CASCADE)
    ...

From my Vue app, I will post using Axios to the django app which uses DRF, and thus get serialized data of Patients and Doctors. It all works fine if I try to use the following sample code in function view:

@api_view(['GET', 'POST'])
def register_patient_vue(request):
    if request.method == 'POST':
        print("POST details", request.data)
        data = request.data['registration_data']
        serializer = customerSpecialSerializer(data=data)
        if serializer.is_valid():
            a = serializer.save()
            print(serializer.data)
            return Response(serializer.data, status=status.HTTP_201_CREATED)
        else:
            print("Serializer is notNot valid.")
            print(serializer.errors)
            return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST)

Sample output:

POST details {'registration_data': {'name': 'wczz', 'ageyrs': 21, 'agemonths': '', 'dob': '', 'gender': 'unspecified', 'mobile': '2', 'email': '', 'alternate': '', 'address': '', 'marital': 'unspecified', 'city': '', 'occupation': '', 'linkedclinic': 10}}
data: {'name': 'wczz', 'ageyrs': 21, 'agemonths': '', 'dob': '', 'gender': 'unspecified', 'mobile': '2', 'email': '', 'alternate': '', 'address': '', 'marital': 'unspecified', 'city': '', 'occupation': '', 'linkedclinic': 10}

However, I need to authenticate the request by special custom authentication. I have another class called UserGroupMap which has Foreign Keys for both User and Clinic, so that if there is a match for a filter for the clinic and user, in the map, it will authenticate. Else it should fail authentication and the data should not be retrieved or serializer saved.

In my previous simple pure django project I used to employ a custom permission function, and decorating my view with it:

@handle_perm(has_permission_level, required_permission='EDIT_CLINICAL_RECORD', login_url='/clinic/')
def some_function(request, dept_id):
    ....
    Some code which runs after authentication

And it would use the following:

def handle_perm(test_func, required_permission=None, login_url=None, redirect_field_name=REDIRECT_FIELD_NAME):
    """
    Decorator for views that checks that the user passes the given test,
    redirecting to the log-in page if necessary. The test should be a callable
    that takes the user object and returns True if the user passes.
    """

    def decorator(view_func):
        @wraps(view_func)
        def _wrapped_view(request, *args, **kwargs):
            print(f"Required permission level is {required_permission}")
            if has_permission_level(request, required_permission):
                print("User has required permission level..Allowing entry.")
                return view_func(request, *args, **kwargs)
            print("FAILED! User does not have required permission level. Access blocked.")
            path = request.build_absolute_uri()
            resolved_login_url = resolve_url(login_url or settings.LOGIN_URL)
            # If the login url is the same scheme and net location then just
            # use the path as the "next" url.
            login_scheme, login_netloc = urlparse(resolved_login_url)[:2]
            current_scheme, current_netloc = urlparse(path)[:2]
            if ((not login_scheme or login_scheme == current_scheme) and
                    (not login_netloc or login_netloc == current_netloc)):
                path = request.get_full_path()
            from django.contrib.auth.views import redirect_to_login
            return redirect_to_login(
                path, resolved_login_url, redirect_field_name)
        return _wrapped_view
    return decorator

def has_permission_level(request, required_permission, clinic=None):
    print("has_permission_level was called.")
    user = request.user
    print(f'user is {user}')
    
    clinic=clinic_from_request(request)
    print(f"has_permission_level called with clinic:{clinic}")
    
    if clinic is None:
        print("clinic is none")
        return HttpResponseRedirect('/accounts/login/')

    group_maps = UserGroupMap.objects.filter(user=user, clinic=clinic)
    print(f"No: of UserGroupMap memberships: {len(group_maps)}")
    if len(group_maps) < 1:
        # There are no UserGroupMap setup for the user. Kindly set them up.\nHint:Admin>Manage users and groups>Users
        return False
    # Now checking Group memberships whether the user has any with permisison

    for map in group_maps:
        rolesmapped = GroupRoleMap.objects.filter(group=map.group)
        if len(rolesmapped) < 1:
            print(f"No permission roles.")
        else:
            for rolemap in rolesmapped:
                print(f"{rolemap.role}", end=",")
                if rolemap.role.name == required_permission:
                    print(
                        f"\nAvailable role of [{map.group}] matched required permission of [{required_permission}] in {clinic.name} [Ok]")
                    return True
    return False

I need to build a custom authentication using DRF, so that it reads the POSTed data, and checks the linkedclinic value, and employs similiar logic.

I started like this:

def has_permission_POST(request, required_permission, clinic=None):
    print("has_permission_POST was called.")
    user = request.user
    print(f'user is {user}')

    if request.method == 'POST':
        print(request)
        print(dir(request))
        print("POST details: POST:", request.POST, "\n")
        print("POST details: data:", request.data, "\n")
        ....
        # Further logic to check the mapping

        return True
        
    else:
        print("Not a valid POST")
        return Response("INVALID POST", status=status.HTTP_400_BAD_REQUEST)

# And decorating my DRF view:
@handle_perm(has_permission_POST, required_permission='EDIT_CLINICAL_RECORD', login_url='/clinic/')
@api_view(['GET', 'POST'])
def register_patient_vue(request):
    if request.method == 'POST':
        print("POST details", request.data)
        data = request.data['registration_data']

The problem is that if I run this, then, has_permission_POST cannot get the value of request.data, which contains the data posted from my frontend. I can work around this, by adding the @api_view(['GET', 'POST']) decorator to has_permission_POST. But that introduces another error, a failed assertion:

AssertionError: Expected a `Response`, `HttpResponse` or `HttpStreamingResponse` to be returned from the view, but received a `<class 'bool'>`

This happens from has_permission_POST once it is decorated with @api_view.

So my problems:

  1. How to implement a custom authentication for my use case?
  2. If I am going about this right, by using this custom has_permission_level, how can I get the request.data in this function before my actual api view is called, so that I can read the clinic id and do the checks for permission that I need.

I have taken a look at the CustomAuthentication provided by DRF, but could not find out how to get the request.data parameters in the custom class.

Thanks to @MihaiChelaru, I was able to find a solution to my problem.

I created a custom Permission class by extending permissions.BasePermission, and using my custom logic in the special has_permission function. I went a step further and implemented checking of Token from the request. Once token is authenticated, the user can be got from the matching token from the Token table. I found that in the custom permission class, I could read the full request.data paramter passed by Vue and Postman. Once I read that, I could easily implement the custom checking of User permissions that my custom models had.

class CustomerAccessPermission(permissions.BasePermission):
    message = 'No permission to create new patient records'

    def has_permission(self, request, view):
        bearer_authorizn = request.META.get('HTTP_AUTHORIZATION')
        try: #Different apps like POSTMAN, and Vue seem to use different strings while passing token
            token = bearer_authorizn.split("Bearer ")[1]
        except Exception as e:
            try:
                token = bearer_authorizn.split("Token ")[1]
            except Exception as e:
                raise NotAuthenticated('Did not get token in request')
        try:
            token_obj = Token.objects.get(key=token)
        except self.model.DoesNotExist:
            raise AuthenticationFailed('Invalid token')
        if not token_obj.user.is_active:
            raise AuthenticationFailed('User inactive or deleted')
        print("Username is %s" % token_obj.user.username)
        print("POST details", request.data)
        linkedclinic_id = request.data['data']['linkedclinic']
        clinic = Clinic.objects.get(clinicid=int(linkedclinic_id))
        print("Clinic membership requested:", clinic)
        group_maps = UserGroupMap.objects.filter(user=user, clinic=clinic)
        print(f"No: of UserGroupMap memberships: {len(group_maps)}")
        if len(group_maps) > 1:
            return True
        return False


@api_view(['POST'])
@permission_classes([CustomerAccessPermission])
def register_patient_vue(request):
    logger.info('In register_patient_vue...')
    ...

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM