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.

Assuming I understood the problem correctly, you want to check if the requesting user belongs to a certain clinic before saving the data and if not show some permissions error.

Here is what I think:
Add user as a foreign key on both Doctor and Patient models.

When you are making a request, include the clinic id in the URL itself. You can access the clinic id in the request kwargs.
The url would look something like: my_app/add_user_info/12345/ where 12345 is clinic id.

Your decorator would look something like:

def has_POST_permission():
    if request.method == 'POST":
        clinic_id = request.kwargs.get('clinic_id')
        # assuming this check is for doctors
        # or can also use "get" instead of "filter" in a try/except block
        requesting_doctor = Doctor.objects.filter(user = request.user, clinic_id=clinic_id).first()  
        if requesting_doctor:
            return True   # return your view that saves the data
    return False  # throw permission denied error

Another option would be to use doctor/patient ID as a field on the UserGroupMap model instead along with clinic id. Then you can replace querying the Doctor model in the above decorator with UserGroupMap.

Just a thought: If you know who the requesting user is going to be you can avoid having the UserGroupMap model altogether

(Note: You might have to modify the decorator code I wrote above as it is not tested, I hope I was able to convey my idea correctly)