Search code examples
pythondjangodjango-viewshttp-authentication

How to Don't Repeat Yourself (DRY)


I have basic authorization in two places in my code. I want to separate this as one function and not repeat code.

My app:

from django.shortcuts import render
from django.views.generic import View
from django.http import HttpResponse, Http404
from django.contrib.auth import authenticate
from django.core.exceptions import PermissionDenied
import base64

from notes.models import Note, load_initial_data


class NoteListView(View):



    def filter_queryset(self, query_set):
        query_params = self.request.GET
        if 'board' in query_params:
            query_set = query_set.filter(board=query_params['board'])
        return query_set

    def get(self, request):
        load_initial_data()
        query_set = self.filter_queryset(Note.objects.all())
        basic_auth = True
        # this lines below !
        if basic_auth:
            if 'HTTP_AUTHORIZATION' in request.META:
                auth = request.META['HTTP_AUTHORIZATION'].split()
                if len(auth) == 2:
                    if auth[0].lower() == "basic":
                        uname, passwd = base64.b64decode(auth[1]).split(':')
                        user = authenticate(username=uname, password=passwd)
                        if user is not None and user.is_active:
                            request.user = user
                            if not request.user.is_staff:
                                raise PermissionDenied
                            return HttpResponse(query_set)

            response = HttpResponse()
            response.status_code = 401
            response['WWW-Authenticate'] = 'Basic realm="%s"' % "Basic Auth Protected"
            return response
        else:
            return HttpResponse(query_set)



class NoteView(View):

    def get_object(self, obj_id):
        try:
            return Note.objects.get(id=int(obj_id))
        except IndexError:
            raise Http404

    def get(self, request, note_id):
        load_initial_data()
        basic_auth = True
        #this lines below
        if basic_auth:
            if 'HTTP_AUTHORIZATION' in request.META:
                auth = request.META['HTTP_AUTHORIZATION'].split()
                if len(auth) == 2:
                    if auth[0].lower() == "basic":
                        uname, passwd = base64.b64decode(auth[1]).split(':')
                        user = authenticate(username=uname, password=passwd)
                        if user is not None and user.is_active:
                            request.user = user

                            return HttpResponse(self.get_object(note_id))

            response = HttpResponse()
            response.status_code = 401
            response['WWW-Authenticate'] = 'Basic realm="%s"' % "Basic Auth Protected"
            return response
        else:
            return HttpResponse(self.get_object(note_id))

I repeat code in class NoteListView in get func and in class NoteView. I don't know how to separate this functionality. I marked the repeated lines with comment. Any suggestions?


Solution

  • I’ll skip the obligatory joke about not repeating myself, but following up on Utkbansal’s comment, you can either create your own Mixin class or create your own base view from which both views derive. i.e., object inheritance. That said, the easiest (and dare I say—fanciest!) way to do this is by subclassing the PermissionRequiredMixin:

    from django.contrib.auth.mixins import PermissionRequiredMixin
    
    class BasicAuthRequired(PermissionRequiredMixin):
        def __init__(self):
            super(BasicAuthRequired, self).__init__()
            self.basic_auth = True
    
        def has_permission(self):
            if self.basic_auth:
                if 'HTTP_AUTHORIZATION' not in request.META:
                    return False
                auth = request.META['HTTP_AUTHORIZATION'].split()
                if len(auth) != 2 or auth[0].lower() != "basic":
                    return False
                uname, passwd = base64.b64decode(auth[1]).split(':')
                user = authenticate(username=uname, password=passwd)
                if not user or not user.is_active:
                    return False
                self.request.user = user # from `View`
                return user.is_staff
            return True # some other type of auth
    

    Now in your views, you can just do the following where we can be assured that basic auth has been verified and handled properly, and just handle the positive case:

    class NoteView(BasicAuthRequired, View):
        def get_object(self, obj_id):
            try:
                return Note.objects.get(id=int(obj_id))
            except IndexError:
                raise Http404
    
        def get(self, request, note_id):
            load_initial_data()   
            return HttpResponse(self.get_object(note_id))