Django Rest framework Permissions

hoonki·2022년 3월 29일
0

오늘은 오랜만히 Django 포스팅이다! 인프라 쪽 이슈랑 사내 사업 종료때문에 문서화 관련되어 일이 짜잘짜잘하게 너무 많아서 피쳐 개발 진행을 못하다가 이제서야 피쳐 개발을 시작한다.

오늘 다룰 내용은 Django Rest framework의 Permissions 기능이다.

내가 새롭게 구현하는 기능은 LMS 조직 및 학급 관리 시스템이다. 조직 및 학급 관리 시스템의 경우 특정 API를 호출하는데 특정한 “권한" 이 있어야 한다. “권한”에 대한 모델링을 다음과 같이 수행하였다.

특정 사용자는 Tenant혹은 Organization 혹은 EasyClass라는 모델에 대해 Role 즉 역할을 가지게 된다. 역할은 다수의 Privilege 즉 권한으로 구성되어 있다.

이제 특정 API 에서 어떤 사용자가 특정 권한을 가지고 있는지 여부를 검사하기 위해 코드를 짜려고 했다. 기존 레거시 코드도 그렇고 심플하게 생각했을 때 구성할 수 있는 코드는 다음과 같다.

class CreateView(generics.ListCreateAPIView):
	def perform_create(self, serializer):
	    if self.request.user.has_some_permission():
	        return serializer.save(user=self.request.user)
	
	    raise PermissionDenied()

근데 이렇게 코드를 짜면 권한을 검사하는 모든 view에 분기문이 들어가게 되고, 권한을 검사하는 로직이 user라는 모델의 도메인 로직에 포함되게 된다. 물론 이 것도 괜찮은 방법일 수 있지만, user라는 도메인이 일반적으로 하는 일이 많은데 시스템 상 존재하는 다양한 권한에 대한 로직을 가지게 되는 것은 좋지 않아보인다.

이런 경우에 Django Rest Framework에서는 permissions 기능을 도입해볼 수 있다. Django permissions 에 대해 알아보자.

공식 문서에 따르면 permissions는 어떤 요청이 인증되고 혹은 접근이 거부되는지를 결정한다. 그리고 항상 view의 시작점에서 검증이 된다고 한다.

Rest framework에서 permissions를 사용하려면 APIView 기준으로 permission_classes에 필요한 permission을 명시하면 된다.

from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework.views import APIView

class ExampleView(APIView):
    permission_classes = [IsAuthenticated]

    def get(self, request, format=None):
        content = {
            'status': 'request was permitted'
        }
        return Response(content)

자 그러면 APIView는 명시된 permission_classes를 어떻게 처리하는지 로직을 살펴보자.

class APIView(View):

    # The following policies may be set at either globally, or per-view.
    renderer_classes = api_settings.DEFAULT_RENDERER_CLASSES
    parser_classes = api_settings.DEFAULT_PARSER_CLASSES
    authentication_classes = api_settings.DEFAULT_AUTHENTICATION_CLASSES
    throttle_classes = api_settings.DEFAULT_THROTTLE_CLASSES
    permission_classes = api_settings.DEFAULT_PERMISSION_CLASSES

	...

	def get_permissions(self):
        """
        Instantiates and returns the list of permissions that this view requires.
        """
        return [permission() for permission in self.permission_classes]

	...
	def check_permissions(self, request):
        """
        Check if the request should be permitted.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_permission(request, self):
                self.permission_denied(
                    request,
                    message=getattr(permission, "message", None),
                    code=getattr(permission, "code", None),

	...
	def initial(self, request, *args, **kwargs):
        """
        Runs anything that needs to occur prior to calling the method handler.
        """
        self.format_kwarg = self.get_format_suffix(**kwargs)

        # Perform content negotiation and store the accepted info on the request
        neg = self.perform_content_negotiation(request)
        request.accepted_renderer, request.accepted_media_type = neg

        # Determine the API version, if versioning is in use.
        version, scheme = self.determine_version(request, *args, **kwargs)
        request.version, request.versioning_scheme = version, scheme

        # Ensure that the incoming request is permitted
        self.perform_authentication(request)
        self.check_permissions(request)
        self.check_throttles(request)

APIView에는 기본적으로 permission_classes 변수를 가지고 있고, default 값으로 Django setting에서 우리가 설정한 값을 가진다. Default 값을 통해 전체 API에 인증절차를 적용할 수 있다. (good)

check_permissions 로직을 보면 모든 permission들에 대해 has_permission을 호출하여 만약 특정 permission이 만족되지 않는다면 permission_denied 처리를 한다. 이 때 has_permission은 간단하게 False, True를 리턴한다.

그리고 check_permissions로직은 method handler가 호출되기 전에 호출된다.

즉, APIView를 상속한 구현체에서 permission_classes 배열에 사용할 클래스들만 명시하면 나머지는 부모 클래스에서 그 때 그 때 Permission 클래스 객체를 만들고, 클래스의 has_permission() 함수를 통해 권한을 체크하는 것이다.

다음은 가장 기본적인 BasePermission 클래스이다.

class BasePermission(metaclass=BasePermissionMetaclass):
    """
    A base class from which all permission classes should inherit.
    """

    def has_permission(self, request, view):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

	def has_object_permission(self, request, view, obj):
        """
        Return `True` if permission is granted, `False` otherwise.
        """
        return True

우리가 어떤 권한을 Custom하려면 위 BasePermission을 상속받아 구현하면 된다.


자 이제 맨 처음으로 돌아가서, 나는 권한 관리 모델을 설계했고 해당 모델링한 결과를 바탕으로 어떤 사용자가 어떤 tenant, organization, class 등에 대해 특정 권한이 있는지 검증해야한다. has_permission, has_object_permissionhas_object_permission 을 구현하였는데 그 이유는 권한 검증을 위해 request에 있는 user 말고 추가적인 모델이 필요하기 때문이다.

class OrganizationEditPermission(permissions.IsAuthenticated):
    def has_object_permission(self, request, view, obj: Tenant):
        _ = view
        user = request.user

        if not (
            Privilege.objects.filter(name=Privilege.Type.ORGANIZATION_EDIT)
            .filter(role__user=user)
            .filter(role__roleowner__tenant=obj)
            .exists()
        ):
            return False

        return True

구현한 클래스를 가지고 다음과 같이 View에서 사용하였다.

class OrganizationView(generics.CreateAPIView):

    serializer_class = OrganizationCreateSerializer
    permission_classes = [OrganizationEditPermission]

    @transaction.atomic
    def perform_create(self, serializer):
        data = serializer.data
				# if permission denied raise Error.
        self.check_object_permissions(self.request, data["tenant"])

처음 코드를 짰을 때 if문으로 권한을 체크하는 것이 보기 싫어서, permission_classes에만 Custom Permission을 정의하면 추가적인 코드가 필요 없을 줄 알았지만, tenant, organization 등 user말고 타 모델의 참조가 필요하기 때문에 어쩔수 없이 check_object_permissions를 통해서 명시적으로 permission 체크를 하였다.

그러나 이 Django Rest framework에서 제공하는 Permissions를 활용하여 개발하면 Permission에 관련된 코드들이 한 곳에 모여 소프트웨어의 응집도가 높아진다는 장점을 가질수 있다는 생각이 들었다. 특히 내가 지금 구성하고 있는 LMS 시스템의 경우 다양한 권한들이 존재하는데 권한검증에 대한 로직이 한곳에 모일 수 있는 장점을 가지게 되고, permission_classes를 통해 이 API를 호출하는 데 필요한 권한이 무엇인지 한눈에 볼 수 있게 된다.

따라서 나는 이번 LMS 시스템 권한 검증 로직을 개발하는데 Django Rest framework에서 제공하는 Permissions 기능을 적극 활용해보려고 한다.

다른 개발자 분들도 나랑 비슷한 요구사항이나 Django RestFramework에서 "권한"에 관련된 것들을 관리해야한다면 permissions 기능을 이용해보는 것을 추천드린다!

profile
열공모드

0개의 댓글