지금까지는 settings.py에서 모든 권한을 허용함으로써 전체 프로젝트에 대한 권한을 설정했는데,
이젠 개별 프로젝트 별로 다른 권한을 부여할 것이다.
권한에는 총 4가지 종류가 있다.
- AllowAny
- IsAuthenticated
- IsAdminUser
- IsAuthenticatedOrReadOnly
해당 api에 접근하려는 모든 User는 로그인 혹은 DB인증같은 과정을 수반해야 권한을 부여받을 수 있게된다.
이번 프로젝트의 경우에는 가장 마지막에 권한 인증이 되었다면 읽기 전용의 권한을 부여할 것이다.
관리자 화면에서 수동으로 Users를 하나 생성해주자.
그럼 해당 계정의 권한을 설정하는 Permissions tab으로 오면 이런 화면이 나온다.
만약 유저 권한으로 'blog|post|Can view Post'를 부여한다면 로그인 시, 포스트를 볼 수 있게 된다.
문제는 유저가 1억명이 있다고 가정하자. 그럼 이런 과정을 하고 싶을까?? 난 하기 싫어..
그래서 처음부터 유저를 생성하면 자동으로 정해진 권한을 부여해야만 한다.
일단 그전에 User permissions 항목 위에 Group이 있는데, 이 그룹에 권한을 던져두고
신규 유저들을 해당 그룹에 추가시켜버리면 알아서 권한이 부여되도록 만들고 싶다.
User말고 이번엔 그룹을 추가시켜주자.
권한으로는 포스트를 추가하고 볼 수 있게 설정해줄 것이다.
다시 아까 만든 유저 객체로 돌아와서 생성된 user를 api 그룹에 추가시키면
해당 그룹에 부여된 권한이 자동으로 상속된다.
이제 이걸 blog_api의 views.py에서 설정해보도록 하자.
Django REST Framwork 홈페이지에서는 장고 모델의 권한이 쿼리셋이 있는 보기에만 적용되어야 한다고 한다.
추가적으로 이 권한에 내장된 다른 액세스 권한들을 잠재적으로 가질 수 있게되는 것을 주의하자.
페이지에는 어떻게 권한을 부여할 수 있는가?? 확인해보기 위해서 하나 테스트를 해보자.
views.py에 IsAdminUser을 import해주고 Permission_classes를 추가해주고
settings.py에서는 IsAuthenticatedOrReadOnly로 REST 전체 권한을 수정해주자.
그리고 관리자 계정을 Logou한 이후에 api 페이지를 확인해보면 다음과 같은 화면이 나타난다.
이전까지만 해도 잘 보이던 정보들이 403 에러가 뜨면서 데이터를 보여주지 않는데,
현재 내게 관리자 권한이 없기 때문에 해당 화면을 확인할 수 없게 되는 것.
근데 settings.py에서 IsAuthenticatedOrReadOnly 권한을 줘버리면 수정이나 업데이트는 안 되지만,
api 내용을 확인할 수는 있게 된다. 이것이 바로 우리가 바라는 페이지와 사용자에 대한 권한이다.
이제 관리자 계정이 아니라 일반 유저 계정으로 확인해보자.
이 부분의 시작부분은 앞서 시작파트에서 다룬 적이 있었다.
urls.py에 api-auth를 추가하면 DRF에서 지원하는 로그인 기능을 구현할 수 있게 된다.
실제로 리액트에서 해당 경로를 사용하여 계정을 만들거나 하지는 않지만
api를 만들면서 시뮬레이트 할 때마다 리액트랑 연동해서 만들 수는 없으니 일단 이걸 써먹을 것.
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
path('api-auth/', include('rest_framework.urls', namespace='rest_framework')),
path('', include('blog.urls', namespace='blog')),
path('api/', include('blog_api.urls', namespace='blog_api')),
]
myproject.urls.py에 api-auth/ 경로를 추가하자.
from rest_framework import generics
from blog.models import Post
from .serializers import PostSerializer
from rest_framework.permissions import DjangoModelPermissions
# 게시물 목록 뷰가 될 첫 번째 뷰 빌드 (본질적 우리에게 보여줄 항목)
# 어지간한 건 알아서 다 해주는데, 정상 작동을 위해 몇 가지 정보를 우리가 좀 알려줄 필요가 있음
class PostList(generics.ListCreateAPIView):
Permission_classes = [ DjangoModelPermissions ]
queryset = Post.objects.all() # 해당 데이터를 여기서 사용하겠다.
serializer_class = PostSerializer # 직렬 변환기 (프론트에서 사용할 수 있는 형식으로 모두 변환)
# 게시물 세부 정보 뷰. (검색과 삭제)
class PostDetail(generics.RetrieveDestroyAPIView):
queryset = Post.objects.all()
serializer_class = PostSerializer
blog_api.views.py에 있는 permission_classes에는 IsAdminUser 대신에
장고 모델에서 지원해주는 권한인 DjangoModelPermissions를 부여한다.
그 다음 관리자 계정을 로그아웃하고 user 계정으로 로그인하면 api 내용을 확인하면
현재 데이터를 읽을 수 있는 권한이 있으므로 api가 화면에 보이는 것을 알 수 있다.
이 아래로 꽤 많이 썼는데, 갑자기 사진 올리다가 에러떠서 새로고침 했더니 다 날아갔다...ㅠㅠㅠ
이제 권한과 http 요청에 대해서 확인해보자.
- View → GET
- Delete → DELETE
- Change → PUT PATCH
- Add → POST
우측에 있는 기능들이 어디로부터 권한을 참조하는지를 기억해야 한다.
왜냐하면, 요청과 권한이 일치해야 우리가 원하는 목적에 부합하는 기능들을 구현할 수 있기 때문이다.
이제 Comtom Permission을 구현해보도록 하자.
게시물 모델에는 author라는 필드가 있는데, 이것과 잘 연결되는 권한을 구현해보자.
무슨 뜻이냐면, 해당 게시물을 작성한 User라면 게시물의 삭제, 수정이 가능해야 한다는 뜻이다.
해당 게시물을 편집, 삭제하려는 사람 또는 사용자에게 어떻게 권한을 주어야 할까?
DRF 홈페이지에 따르면 costom permissions을 빌드하기 위해서는 하나 혹은 두 가지 모두를 구현해야 한다.
만약 SAFE_METHODS('GET', 'OPTIONS', 'HEAD')에 포함된 요청이라면 True를 리턴시켜
유저에게 read-only 권한을 부여한다.
여기서 매개변수로써 view와 obj도 받아야 한다!
blog_api.views.api
from rest_framework import generics
from blog.models import Post
from .serializers import PostSerializer
from rest_framework.permissions import SAFE_METHODS, BasePermission, DjangoModelPermissions
class PostUserWritePermission(BasePermission): # 사용자 쓰기 권한
# 게시물에 대한 권한이 있는 User가 잠재적으로 작성, 편집, 삭제할 수 있는 유일한 객체이다.
message = 'Editing posts is restricted to the author only.'
def has_object_permission(self, request, view, obj): # 객체와 뷰도 받는다.
if request.method in SAFE_METHODS: # 모든 정보가 이 함수 내에서 액세스할 수 있도록 한다.
# http method인 get put 등을 확인하여 안전한 메소드인지 확인한다.
return True # user는 ReadOnly 권한을 부여받을 수 있게된다. 쓰기 권한이 있는지는 obj를 리턴시켜서 작성자가 일치하는지 확인한다.
return obj.author == request.user # PostList에서 user에 대한 정보를 가지고 있기 때문에 로그인 시에 일치 여부 판단이 가능해진다.
# DB에 분명히 author 필드가 존재하므로 일치시키면 된다.
# 게시물 목록 뷰가 될 첫 번째 뷰 빌드 (본질적 우리에게 보여줄 항목)
# 어지간한 건 알아서 다 해주는데, 정상 작동을 위해 몇 가지 정보를 우리가 좀 알려줄 필요가 있음
class PostList(generics.ListCreateAPIView):
permission_classes = [ DjangoModelPermissions ]
queryset = Post.postobjects.all() # 해당 데이터를 여기서 사용하겠다.
serializer_class = PostSerializer # 직렬 변환기 (프론트에서 사용할 수 있는 형식으로 모두 변환)
# 게시물 세부 정보 뷰. (검색과 삭제)
class PostDetail(generics.RetrieveDestroyAPIView, PostUserWritePermission):
permission_classes = [ PostUserWritePermission ]
queryset = Post.objects.all()
serializer_class = PostSerializer
PostUserWritePermission 클래스는 사용자에게 읽기 혹은 쓰기 권한을 주기 위한 객체다.
http 요청이 import한 SAFE_METHODS에 포함된 명령 중에 포함되는 경우는 Read-Only 권한을 부여하면 된다.
그게 아닐 경우 게시물의 author 필드의 정보를 불러와 요청한 유저와 일치한다면 쓰기 권한을 부여한다.
관리자 화면에서 아무런 Post를 작성하고 저자에 일반 계정, status에 Published를 정하고 저장한다.
우리가 만든 목적에 부합하기 위해서는 user1으로 로그인하여 api/3 경로에 접근하면
읽기와 쓰기 권한이 부여되어야 한다.
하지만 GET요청은 무사히 수행했으나, 쓰기 권한은 아직 부여되지 않을 것 같다.
그 이유는 generics에서 Destroy와 APIView만 받았기 때문이다. Update를 추가해주자.
그러면 드디어 user1이 작성한 게시물을 update할 수 있는 form이 나타나게 된다.
만약 자신이 권한을 부여받지 못한 게시글로 이동할 경우에는 수정 및 삭제가 불가능해진다.
가장 처음에 전체 페이지에 대한 권한을 IsAuthenticatedOrReadOnly로 부여한 이유는 바로
정상적으로 권한을 부여받지 않은 유저 외에는 게시물에 대한 수정 및 삭제 권한을 차단시키기 위함이었다.
지금까지 구현한 것이 정상적으로 돌아가는지 테스트 해보자.
test.py에 테스트 케이스를 작성한 후에 bash에 해당 명령어를 입력해주어야 한다.
pip install coverage
coverage run --omit='*/venv/*' manage.py test
blog_api.tests.py
from urllib import response
from django.test import TestCase
from django.urls import reverse
from rest_framework import status
from rest_framework.test import APITestCase
from blog.models import Post, Category
from django.contrib.auth.models import User
from rest_framework.test import APIClient
class PostTests(APITestCase):
def test_view_posts(self):
"""
Ensure we can view all objects
"""
url = reverse('blog_api:listcreate')
response = self.client.get(url, format='json') # url을 가져오고 정보를 표시할 수 있는지 확인
self.assertEqual(response.status_code, status.HTTP_200_OK)
def test_create_post(self):
"""
Ensure we can create a new Post object and view object.
"""
self.test_category = Category.objects.create(name='django')
self.testuser1 = User.objects.create_superuser (
username='test_user1', password='123456789')
# self.testuser1.is_staff = True
self.client.login(username=self.testuser1.username,
password='123456789')
data = {"title":"new", "author":1,
"excerpt":"new", "content":"new"}
url = reverse('blog_api:listcreate')
response = self.client.post(url, data, format='json')
self.assertEqual(response.status_code, status.HTTP_201_CREATED)
def test_post_update(self):
client = APIClient()
self.test_category = Category.objects.create(name='django')
self.testuser1 = User.objects.create_user (
username='test_user1', password='123456789')
self.testuser2 = User.objects.create_user (
username='test_user2', password='123456789')
test_post = Post.objects.create(
category_id=1,
title='Post Title',
excerpt='Post Excerpt',
content='Post Content',
slug='post-title',
author_id=1,
status='published',
)
client.login(username=self.testuser2.username,
password='123456789')
url = reverse(('blog_api:detailcreate'), kwargs={'pk': 1})
# 1번 게시물을 가져와서 현재 로그인된 계정으로 PUT 요청을 했을 때, 받아들여지는가?
response = client.put(
url, {
"title": "New",
"author": 1,
"excerpt": "New",
"content": "New",
"status": "published"
}, format='json')
print(response.data)
self.assertEqual(response.status_code, status.HTTP_200_OK)
돌려봤더니 403 에러가 떴다고 한다. 403에러가 뭔지 구글에 찾아보면 이런 요청이 금지되어 있다고 한다.
무슨 소린가 하면 사진의 위쪽에 ErrorDetail에서 잘 설명해주고 있다.
"현 게시물을 작성자에게만 권한이 제한되어 있습니다"
왜냐하면 현재 테스트 케이스로 만든 게시물은 testuser1의 것인데,
로그인은 testuser2로 한 채로 put 요청을 실행했기 때문이다.
테스트 케이스에서 user1로 로그인해보고 다시 실행해보자.
이번에는 무사히 실행된다. ㅎㅎ
여기까지 했다면 DRF의 권한 부여까지 할 수 있게 된 것이다.
다음에는 토큰과 JWT에 대해서 다루어보자.