DRF(Django REST framework)에서 권한 관리하기

antoliny0919
2024625
현재 필자의 웹사이트에는 관리자를 제외한 두 명의 유저가 존재한다.
no-img
그리고 안톨리니와 버터링은 각각 자신이 작성한 포스트도 하나 가지고있다.
no-img
현재 버터링은 자신이 만든 포스트가 맘에 들지 않아서 삭제하려고 한다.
포스트를 삭제하기 위해서는 서버에 HTTP Request DELETE 메서드로 '/api/posts/{삭제할 포스트 id}/'경로로 요청하기만 하면 된다.
버터링의 포스트 id값은 위 그림에서 볼 수 있듯이 '4'이므로 '/api/posts/4/' 경로로 요청하면 된다는걸 알 수 있지만
버터링이 실수로 5를 입력하여 자신이 소유한 포스트가 아닌 안톨리니의 포스트를 삭제하려고 하면 어떻게 될까?
no-img
버터링이 '/api/posts/5/'경로로 요청을 보냈고 Response Status Code가 204인걸 보면 자신이 소유하지않은 포스트는 삭제되면 안되지만 전체 포스트를 다시 확인했을때 안톨리니의 포스트가 존재하지 않는걸로 보아 실수로 잘못보낸 삭제 요청이 받아들여져 클라이언트의 의도와는 달리 동작해버린걸 확인할 수 있다.
no-img
현재 필자의 웹사이트는 위 사례에서 봤듯이 관리자가 아닌 일반 유저가 자신이 소유하지 않은 포스트를 삭제할 수 있는 치명적인 버그가 존재한다.
만약 수 많은 유저들이 존재하는 웹사이트에서 버터링은 고의가 아니였지만 해당 버그를 악용하는 유저가 존재한다고 상상하면 아마 그 웹 사이트는 데이터 백업을 하지 않았다면 문을 닫아야  될지도 모른다.
결론은 위와 같은 버그는 권한이 설정되어 있지 않아서 발생한 문제이다.
필자의 웹사이트 소스코드를 수정하여 권한과 관련된 문제가 발생하지 않도록 해보겠다.
 
DRF는 권한을 언제 확인할까?
 

dispatch()

 
일단 필자의 소스코드를 수정하기 전에 DRF가 권한을 확인하는 시점에 대해서 알아보겠다.
DRF에서 사용자가 정의한 View로직이 호출될때 APIView의 dispatch메서드에 의하여 호출되는데
python
COPY

    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)
            # ------------------------------------------------
            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)
            # -------------------------------------------------
        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
"# --------"내부 코드가 사용자가 정의한 View로직이 호출되는 구간이다.
만약 필자가 아래와 같이 TestView를 작성했다고 가정했을때
python
COPY


# views.py

class TestView(APIView):
  
  def get(self, request):
    posts = Test.objects.all()
    serializer = TestSerializer(posts, many=True)
    
    return Response(serializer.data, status=status.HTTP_200_OK)

# urls.py

urlpatterns = [
  path('test/', views.TestView.as_view(), name='test-view'),
]
client가 'test/'경로로 GET요청을 보낸다면 결론적으로 위 get메서드 객체가 Dispatch메서드 "#----"내부 마지막에 있는 handler변수에 할당되어 호출되는 식이다.
handler(request, *args, **kwargs)가 사실상 TestView의 get(request, *args, **kwargs)이라고 보면된다.
 
필자의 웹사이트에서 발생한 버그는 권한과 관련된 문제였다.
그렇다면 일단 client의 권한을 파악하기 위해서는 request를 요청한 client가 누구인지부터 알아야 해당 유저가 어떤 권한이 있는지 파악할 수 있다.
 

Authentication

 
항상 권한을 확인하기 전에 해결되어야 하는 부분은 인증부분이다.
위에서도 설명했듯이 요청을 보낸 클라이언트가 누구인지 알아야 해당 클라이언트의 권한을 확인할 수 있기 때문이다.
필자의 웹사이트는 인증 클래스로 DRF에서 제공하는 Token인증을 사용했다.
python
COPY

class PostView(APIView):
  
  authentication_classes = [TokenAuthentication]
  ...
그렇기때문에 클라이언트가 Request를 요청할때 Request Header에 Authorization 필드를 추가하여 요청하면 서버에서
Request를 요청한 클라이언트가 누구인지 정확히 파악할 수 있다.
테스트를 위해 authentication_classes로 TokenAuthentication을 가졌고 응답으로 request._request.user --> request한 클라이언트의 username을 반환하는 로직을 작성했다.
python
COPY


class AuthenticationTestView(APIView):
  authentication_classes = [TokenAuthentication]
  
  def get(self, request):
    
    response_data = json.dumps({"request_user": str(request._request.user)})
    return Response(response_data, status=status.HTTP_200_OK)
버터링이 위와같은 View에 요청할때 HTTP Header에 본인의 토큰값을 Authorization키값에 담아서 전달할 것이고
응답으로 본인의 username인 "butterring"이 반환될것이라고 예상할 수 있다.
no-img
실제로 curl을 통해 버터링의 토큰값을 담아 요청해보면 request._request.user의 값으로 버터링의 username인 "butterring"이 반환되는걸 확인할 수 있다.
이렇게 인증을 구현함으로써 이제 request를 보낸 클라이언트가 누구인지 정확히 파악할 수 있는 단계가 되었다.
즉 이번 포스트의 주요내용인 권한과 관련된 코드를 작성할 준비가 다되었다는 의미이다.
인증을 구현하는 방법?
인증을 구현하는 방법에 대해서는 필자가 작성한 인증과 관련된 글(DRF는 어떻게 인증을 구현했는가?)을 참고하기 바란다.
 
DRF에서 Permission이 수행되는 과정
 

Permission상황

 
버터링과 안톨리니는 같은 등급(일반)의 유저이지만
만약 필자의 웹사이트에서 포스트를 삭제하는 기능은 어드민 유저에게만 허락된다고 가정해보자
권한이 없는 안톨리니가 포스트를 삭제하는 request를 보낸 상황이다.
python
COPY


class PostView(APIView):
  
  authentication_classes = [TokenAuthentication]

  ...
  def delete(self, request, id):
    post = get_object_or_404(Post, id=id)
    post.delete()
    return Response(status=status.HTTP_204_NO_CONTENT)
버터링은 권한이 없기 때문에 위 delete메서드에 도달하기 전에 403 상태코드를 담은 response를 반환해야 할 것이다.
일단 필자가 이전에 사용자가 작성한 View는 APIView의 dispatch메서드에서 '#----'블럭 내부에서 호출된다고 했었다.
python
COPY

    def dispatch(self, request, *args, **kwargs):
        """
        `.dispatch()` is pretty much the same as Django's regular dispatch,
        but with extra hooks for startup, finalize, and exception handling.
        """
        self.args = args
        self.kwargs = kwargs
        request = self.initialize_request(request, *args, **kwargs)
        self.request = request
        self.headers = self.default_response_headers  # deprecate?

        try:
            self.initial(request, *args, **kwargs)
            # ------------------------------------------------
            # Get the appropriate handler method
            if request.method.lower() in self.http_method_names:
                handler = getattr(self, request.method.lower(),
                                  self.http_method_not_allowed)
            else:
                handler = self.http_method_not_allowed

            response = handler(request, *args, **kwargs)
            # -------------------------------------------------
        except Exception as exc:
            response = self.handle_exception(exc)

        self.response = self.finalize_response(request, response, *args, **kwargs)
        return self.response
그렇기 때문에 해당 블럭에 도달하기전에 권한이 존재하지 않다는 응답을 반환해야 할것이다.
'#-----'블럭 바로 위에 self.initial(request, *args, **kwargs)라는 코드가 있고 initial메서드를 호출하는 부분으로 initial메서드를 확인해보면
python
COPY


    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)
마지막부분에 self.perform_authentication(request)self.check_permissions라는 코드가 있다.
 

check_permissions()

 
참고로 self.perform_authentication은 메서드명 그대로 인증을 이행하는 메서드로 실행되면 인증과 관련된 과정이 진행되며 해당 시점부터 request를 전송한 클라이언트가 누구인지 정확히 알고 있는 상태이다.
그래서 self.perform_authentication이 진행되면 버터링이라는 유저가 요청을 보냈다는걸 서버입장에서 알고 다음코드인 self.check_permissions를 통해 요청을 보낸 유저가 요청을 이행할 수 있는 권한이 존재하는 클라이언트인지 검증하는 과정을 거친다.
python
COPY

    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)
                )
check_permissions메서드는 이름 그대로 권한을 체크하는 부분이다.
코드를 보면 가장 먼저 self.get_permissions()메서드를 호출하는데
self.get_permissions메서드는 메서드명 그대로 권한 클래스들을 가져오는 메서드이다.
python
COPY

class APIView(View):
    ...
    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]
    
    

권한클래스 가져오기

 
get_permissions메서드는 self.permission_classes값을 사용하는데 APIView에서 permission_classes의 값은 api_settings.DEFAULT_PERMISSION_CLASSES이다.
참고로 api_settings는 사용자의 프로젝트 settings.py에 따로 정의되어 있다면 해당 값을 가져오고 만약 정의되어 있지 않다면 rest_framework가 제공하는 settings.py 기본값을 따르게 된다.
간단하게 필자의 settings.py에 아래와 같은 코드가 추가되었다면
python
COPY

REST_FRAMEWORK = {
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.IsAuthenticated',
    ]
}
APIView의 permission_classes는 IsAuthenticated클래스가 되고 만약 사용자가 정의한 로직에 permission_classes를 오버라이딩 하지 않았다고 가정하면 사용자가 정의한 모든 로직에 IsAuthenticated클래스가 permission_classes로 사용된다.
그렇기 때문에 모든 로직에 기본적으로 적용하고 싶은 권한이 있다 라는 상황이라면 사용자의 settings.py에 위와같은 형태로 자신이 원하는 권한 클래스를 지정하면 된다.
사용자의 settings.py에도 'DEFAULT_PERMISSION_CLASSES'를 재정의 하지 않고 호출된 로직에서도 permission_classes를 오버라이딩 하지 않은 상황에서는 앞서 설명했듯이 rest_framework가 제공하는 settings.py 기본값을 따르게 된다.
python
COPY

DEFAULTS = {
    # Base API policies
    ...
    'DEFAULT_PERMISSION_CLASSES': [
        'rest_framework.permissions.AllowAny',
    ],
    ...
settings.py의 permission_classes기본값으로 AllowAny클래스를 사용하는데 클래스명 그대로 어떠한 유저든 허용한다는 권한 클래스이다.(권한검증을 하지 않는다와 같다.)
no-img
필자는 포스트를 삭제하는 기능을 어드민에게만 허용하려고 한다.
필자는 포스트와 관련된 View에 permission_classes로 어드민만 권한을 허용하는 IsAdminUser클래스를 추가하는식으로 문제를 해결하였다.
python
COPY


class PostView(APIView):
  
  authentication_classes = [TokenAuthentication]
  permission_classes = [IsAdminUser]

  ...
  def delete(self, request, id):
    post = get_object_or_404(Post, id=id)
    post.delete()
    return Response(status=status.HTTP_204_NO_CONTENT)
참고로 이전에봤던 권한 클래스 IsAuthenticated, AllowAny, IsAdminUser 전부 다 DRF에서 제공하는 권한 클래스이다.
필자의 PostView가 사용하는 IsAdminUser클래스를 예시로 has_permission메서드의 반환값은 request.user --> 요청을 보낸 유저가 is_staff필드값(request.user.is_staff)이 True이면 True를 반환하게 된다.(권한 허용)
python
COPY

class IsAdminUser(BasePermission):
    """
    Allows access only to admin users.
    """

    def has_permission(self, request, view):
        return bool(request.user and request.user.is_staff)
이제 어드민 유저가 아닌 버터링이 포스트 삭제를 시도하면
no-img
403 Forbidden 상태코드가 반환되는걸 확인할 수 있다.
그렇다면 어드민 유저로 포스트 삭제를 시도하면 정상적으로 삭제가 될까?
이번에는 어드민 유저인 antoliny0000의 토큰값을 담아 시도해봤다.
no-img
204 No Content 상태코드가 반환되는걸 보아 필자가 정의한 PostView의 delete메서드가 잘 동작한걸 알 수 있다.
하지만 한가지 문제점이 발생했다.
python
COPY

class PostView(APIView):
  
  authentication_classes = [TokenAuthentication]
  permission_classes = [IsAdminUser]
  
  def get(self, request):
    posts = Post.objects.all()
    serializer = PostSerializer(posts, many=True)
    print(request._request.user)
    
    return Response(serializer.data, status=status.HTTP_200_OK)
  
  def post(self, request):
    serializer = PostSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save()
    return Response(serializer.data, status=status.HTTP_201_CREATED)
    
  def delete(self, request, id):
    post = get_object_or_404(Post, id=id)
    post.delete()
    return Response(status=status.HTTP_204_NO_CONTENT)
필자는 delete메서드에만 IsAdminUser권한을 적용하려는 의도였는데
get, post또한 동일한 과정으로 호출되기 때문에 get, post에도 IsAdminUser권한이 적용되어 버렸다.
no-img
토큰을 전달하지 않은 상태인 인증되지 않은 유저(Anonymous User)로 GET을 요청했을때 401 Unauthorized 상태코드를 반환하는걸 확인할 수 있다.(401을 반환했지만 사실상 권한문제이다.)
어떻게 하면 메서드마다 다른 권한을 적용할 수 있을까?
함수형 View에서는 메서드별로 정의하기 때문에 문제없겠지만 클래스형태의 View는 permission_classes를 클래스변수로 정의해야하기 때문에 이러한 문제점이 발생하는데 DRF는 이런 문제를 굉장히 신기함과 동시에 대단하게 해결했다.
간단하게 결론부터 보면
python
COPY

class ReadOnly(BasePermission):
  def has_permission(self, request, view):
    return request.method in SAFE_METHODS

class PostIsOkey(BasePermission):
  def has_permission(self, request, view):
    if request.method.lower() == 'post':
      return bool(request.user and request.user.is_authenticated)

class PostView(APIView):
  
  authentication_classes = [TokenAuthentication]
  permission_classes = [IsAdminUser|ReadOnly]
  
  def get(self, request):
    posts = Post.objects.all()
    serializer = PostSerializer(posts, many=True)
    print(request._request.user)
    
    return Response(serializer.data, status=status.HTTP_200_OK)
  
  def post(self, request):
    serializer = PostSerializer(data=request.data)
    serializer.is_valid(raise_exception=True)
    serializer.save()
    return Response(serializer.data, status=status.HTTP_201_CREATED)
    
  def delete(self, request, id):
    post = get_object_or_404(Post, id=id)
    post.delete()
    return Response(status=status.HTTP_204_NO_CONTENT)
필자는 ReadOnly라는 권한을 생성했고 해당 권한은 SAFE_METHOD --> 'GET', 'HEAD', 'OPTION"으로 오는 요청은 허용하는 권한이다.
그리고 permission_classes에 IsAdminUser와 ReadOnly에 Or연산을 사용했는데
여기서 DRF는 메타클래스를 사용하여 BasePermission을 상속받은 권한 클래스들끼리 비트 연산시 새로운 클래스를 반환하는 것으로 문제를 해결했다.
필자의 예시 PostView에서는 직관적으로 IsAdminUser, ReadOnly 권한 검사시 둘 중 하나라도 True이면 권한이 허용되는 형태라고 보면 된다.
no-img
이전과 다르게 다시 인증되지 않은 유저로 '/api/posts/' GET 요청을 보냈을때 200 상태코드가 반환되는걸 확인할 수 있다.
IsAdminUser권한은 False를 반환하겠지만 ReadOnly권한은 True를 반환하기 때문에 위와 같은 인증되지 않은 유저의 요청에도 권한이 최종적으로 허용되었다.
다시 get_permissions메서드로 돌아가면
python
COPY

class APIView(View):
    ...
    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]
get_permissions메서드는 permission_classes의 값인 권한 클래스들을 반복하여 각 객체를 생성한다.
만약 permission_classes의 값이 [IsAdminUser, AllowAny, ReadOnly]라면 get_permissions의 반환값은
[IsAdminUser(), AllowAny(), ReadOnly()]가 된다.
python
COPY

    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)
                )
get_permissions를 호출하는 check_permissions메서드에서 다시 get_permissions메서드의 반환값을 반복하여 각 권한 클래스 객체들의 has_permission메서드를 호출한다.
has_permission메서드는 이전에도 잠깐 등장했는데 권한 클래스의 권한을 검증하는 메서드라고 보면된다.
만약 has_permission이 하나라도 실패하게 된다면 permission_denied라는 메서드를 호출하는데
python
COPY

    def permission_denied(self, request, message=None, code=None):
        """
        If request is not permitted, determine what kind of exception to raise.
        """
        if request.authenticators and not request.successful_authenticator:
            raise exceptions.NotAuthenticated()
        raise exceptions.PermissionDenied(detail=message, code=code)
permission_denied메서드는 요청했던 유저가 인증하지 않은 유저라면 NotAuthenticated 401 상태코드를 반환하고 인증된 유저이면 PermissionDenied 403상태코드를 반환한다.
이런 동작때문에 이전에 IsAdminUser가 적용된 코드에서 인증하지 않은 유저로 GET요청을 보냈을때 403상태코드가 아닌 401상태코드가 반환된 것이다.
지금까지 필자는 포스트를 삭제하는 기능을 어드민 유저에게만 허락되게 했다.
하지만 아직 해결되지 않은 부분이 또 존재한다.
필자가 이번 포스트의 처음 부분에 안톨리니와 버터링을 통해 한가지 상황을 제시했는데
버터링이 자신이 소유하지 않은 안톨리니의 포스트를 삭제하는 상황이었다.
요청자가 소유하지 않은 객체를 삭제하려고 할때 어떻게 해야할까?
 
객체 소유권 검사
 

get_object()

 
삭제할 포스트를 가져오는 부분은 사용자가 생성한 View에서 이루어진다.
python
COPY

class PostView(APIView):
  
  authentication_classes = [TokenAuthentication]
  permission_classes = [IsAdminUser|ReadOnly]
  ...
    
  def delete(self, request, id):
    post = get_object_or_404(Post, id=id)
    post.delete()
    return Response(status=status.HTTP_204_NO_CONTENT)
필자의 PostView클래스의 delete메서드를 보면 get_object_or_404함수로 삭제할 포스트를 db로부터 가져오고
post.delete()메서드를 통해 해당 포스트를 삭제한다.
그렇다면 get_object_or_404함수와 post.delete()함수 사이에 삭제할 포스트가 요청자의 소유인지 확인하면 되지 않을까?
python
COPY

class GenericAPIView(views.APIView):
    ...
    def get_object(self):
        """
        Returns the object the view is displaying.

        You may want to override this if you need to provide non-standard
        queryset lookups.  Eg if objects are referenced using multiple
        keyword arguments in the url conf.
        """
        queryset = self.filter_queryset(self.get_queryset())

        # Perform the lookup filtering.
        lookup_url_kwarg = self.lookup_url_kwarg or self.lookup_field

        assert lookup_url_kwarg in self.kwargs, (
            'Expected view %s to be called with a URL keyword argument '
            'named "%s". Fix your URL conf, or set the `.lookup_field` '
            'attribute on the view correctly.' %
            (self.__class__.__name__, lookup_url_kwarg)
        )

        filter_kwargs = {self.lookup_field: self.kwargs[lookup_url_kwarg]}
        obj = get_object_or_404(queryset, **filter_kwargs)

        # May raise a permission denied
        self.check_object_permissions(self.request, obj)

        return obj
DRF에서 제공하는 GenericAPIView에 get_object라는 메서드가 있다.
get_object메서드는 메서드명 그대로 단일 객체를 가져오는 메서드로 마지막 부분을 보면 get_object_or_404함수를 통해 단일 객체를 가져오고 check_object_permissions라는 메서드를 호출하는데 check_object_permissions메서드가 바로 객체 소유권을 검사하는 메서드이다.
 

check_object_permissions()

 
check_object_permissions메서드는 이전에 봤던 check_permission과 거의 동일하다.
python
COPY

    def check_object_permissions(self, request, obj):
        """
        Check if the request should be permitted for a given object.
        Raises an appropriate exception if the request is not permitted.
        """
        for permission in self.get_permissions():
            if not permission.has_object_permission(request, self, obj):
                self.permission_denied(
                    request,
                    message=getattr(permission, 'message', None),
                    code=getattr(permission, 'code', None)
                )
차이점이 딱 두 가지 존재하는데 check_permission은 Permission클래스 객체의 has_permission메서드를 호출했지만
check_object_permissions메서드는 has_object_permission이라는 메서드를 호출하고 파라미터로 obj가 추가된 형태이다.
그렇다면 객체 소유권을 검사하기 위해서는 권한 클래스에 has_object_permission메서드를 구현하고 has_object_permission을 trigger해줄 check_object_permissions메서드를 호출하기만 하면 된다.
필자는 인증된 유저가 포스트를 삭제할 수 있으며 포스트 삭제는 본인 소유의것만 가능하도록 커스텀 권한 IsAuthenticatedAndOwner를 생성했다.
python
COPY

class IsAuthenticatedAndOwner(IsAuthenticated):
  
  def has_object_permission(self, request, view, obj):
    return request.user == obj.author
IsAuthenticatedAndOwner권한 클래스는 IsAuthenticated권한을 상속받으면서 has_permission메서드는 인증된 유저만 허용되는 IsAuthenticated권한클래스의 has_permission메서드를 사용한다.
그리고 has_object_permission메서드는 따로 정의해줬는데 파라미터로 받은 obj는 Post클래스 객체에 해당하며 Post클래스 객체의 author필드와(ForeignKey User) 요청한 유저의 username이 같다면 권한이 허용되도록 했다
python
COPY

class PostView( APIView):
  
  authentication_classes = [TokenAuthentication]
  permission_classes = [IsAuthenticatedAndOwner|ReadOnly]
  ...
  def delete(self, request, id):
    post = get_object_or_404(Post, id=id)
    self.check_object_permissions(request, post)
    post.delete()
    return Response(status=status.HTTP_204_NO_CONTENT)
PostView delete메서드에도 권한 클래스의 has_object_permission을 호출해줄 self.check_object_permissions(request, post)가 추가됐다.
현재 필자의 프로젝트에는 두 개의 포스트가 존재하는데
no-img
이제 버터링이 자신의 소유가 아닌 안톨리니의 포스트를 삭제하려고 요청하면 해당 요청은 권한에 의해 거절될까?
no-img
버터링이 '/api/posts/8/'로 안톨리니의 포스트를 삭제하려고 했지만 403 Forbidden 상태코드가 반환되어 삭제에 실패한걸 확인할 수 있다.
위와 같이 동작할 수 있는 이유는 필자가 정의한 권한 클래스인 IsAuthenticatedAndOwner의 has_object_permission을 통과하지 못했기 때문이다.(False반환 --> self.permission_denied호출)
이번에는 버터링이 본인의 포스트인 '/api/posts/9/'를 삭제하려고 요청하면
no-img
204상태코드와 함께 본인의 포스트이기때문에 has_object_permission메서드를 통과하여 포스트 삭제에 성공했다.
no-img
지금까지 DRF에서 Permission이 진행되는 과정과 Permission을 어떻게 구현하는가에 대해서 알아봤다.
필자가 사용한 대부분의 Permission클래스는 DRF에서 제공해주는 permissions.py에 존재하는 클래스들 이고
필자의 상황에 맞게 커스텀한 권한 클래스는 ReadOnly와 IsAuthenticatedAndOwner로 BasePermission클래스를 상속한 뒤 has_permission이나 has_object_permission을 구현하면 된다.
자세한 내용은 DRF Permission공식문서DRF소스코드를 참고하기 바란다.