https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/
API에 대한 접근을 통제해보도록 하자:
- snippets는 만든사람과 연관되어 있다.
- 허가된 users만이 snippets를 create할 수 있다.
- 해당 snippets을 만든 사람만이 이를 update/delete 할 수 있다.
- 허가되지 않은 requests는 full read-only access가 가능하다.
model에 information 추가하기
Snippet model calss에 field를 추가하자:
1. snippet creator을 식별하기 위한 필드
2. highlighted HTML representation of code
# snippets/models.py에 Snippet class에 추가하자
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
그리고 model이 저장되면, pygments(code highlighting library)를 사용하여 highlighted field를 채워야한다.
다음 라이브러리를 import하고, Snippet class에 .save() 메소드를 추가하자
그러면 최종적으로 models.py의 코드는 다음과 같아야 할 것이다.
from django.db import models
from pygments.lexers import get_all_lexers, get_lexer_by_name
from pygments.styles import get_all_styles
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
LEXERS = [item for item in get_all_lexers() if item[1]]
LANGUAGE_CHOICES = sorted([(item[1][0], item[0]) for item in LEXERS])
STYLE_CHOICES = sorted([(item, item) for item in get_all_styles()])
class Snippet(models.Model):
created = models.DateTimeField(auto_now_add=True)
title = models.CharField(max_length=100, blank=True, default='')
code = models.TextField()
linenos = models.BooleanField(default=False)
language = models.CharField(choices=LANGUAGE_CHOICES, default='python', max_length=100)
style = models.CharField(choices=STYLE_CHOICES, default='friendly', max_length=100)
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
objects = models.Manager() # because of vscode problem in handling models.objects
class Meta:
ordering = ['created']
def save(self, *args, **kwargs):
"""
Use the `pygments` library to create a highlighted HTML
representation of the code Snippet
"""
lexer = get_lexer_by_name(self.language)
linenos = 'table' if self.linenos else False
options = {'title': self.title} if self.title else {}
formatter = HtmlFormatter(style=self.style, linenos=linenos, full=True, **options)
self.highlighted = highlight(self.code, lexer, formatter)
super(Snippet, self).save(*args, **kwargs)
models를 수정했기에, database table을 업데이트해야한다.
원래는 database migration을 하지만,
이 튜토리얼에서는 db를 지우고 다시 만들어보자.
> rm -f db.sqlite3
> rm -r snippets/migrations
> python manage.py makemigrations snippets
> python manage.py migrate
API를 테스트하기 위해 users 몇개를 만들어놓자
> python manage.py createsuperuser
User models에 endpoints 추가하기
앞서 만든 users의 representation을 API에 추가하자.
새로운 serializer을 만들자
# snippets/serializers.py
from django.contrib.auth.models import User
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'snippets']
* serializer의 relation에 대해 더 공부하려면: https://www.django-rest-framework.org/api-guide/relations/
view를 추가하자
user representations를 위한 read-only view를 만들어야하기에 ListAPIView와 RetrieveAPIView generic class-based views를 사용할 것이다. (SnippetList나 SnippetDetail은 create/update/destroy 가능했는데 반해)
# snippets/views.py
from snippets.models import Snippet
from snippets.serializers import SnippetSerializer, UserSerializer
from rest_framework import generics
from django.contrib.auth.models import User
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
URL conf에서 참조하게 하자
# snippets/urls.py
from django.urls import path
from snippets import views
from rest_framework.urlpatterns import format_suffix_patterns
urlpatterns = [
path('snippets/', views.SnippetList.as_view()),
path('snippets/<int:pk>/', views.SnippetDetail.as_view()),
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
]
urlpatterns = format_suffix_patterns(urlpatterns) # format suffix
Snippets를 User와 연관시키기
code snippet을 create하면, snippet instance만으로는 이를 user과 associate할 방법이 없다.
이를 위해 snippet views에 .perform_create() 메소드를 overriding하는 것이다.
1. instance save 관리되는 방식을 변경시킬 수 있게 하고
2. incoming request나 requested URL에 암시된 information을 다룰 수 있게 한다
SnippetList view class에 메소드를 추가하자
# snippets/views.py
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
serializer 업데이트하기
Snippets를 User와 associate시킨걸
SnippetSerializer을 업데이트해서 반영시켜야 한다.
# snippets/serializers.py
class SnippetSerializer(serializers.ModelSerializer):
owner = serializers.ReadOnlyField(source='owner.username') # owner field 추가
class Meta:
model = Snippet
fields = ['id', 'owner', 'title', 'code', 'linenos', 'language', 'style'] # owner field 추가
* source 인자는 어떤 속성이 field를 채울것인지 컨트롤하고, serialized instance에 어떤 속성도 가리킬 수 있다.
* ReadOnlyField class는 serialized representations에 사용되지만, model instances가 deserialized되었을 때 이를 업데이트하는데 사용될 수는 없다. CharField(read_only=True) 로 사용해도 된다.
Views에 required permission 추가하기
required permission: only authenticated users are able to create/update/delete code snippets
IsAuthenticatedOrReadOnly:
- authenticated requests get read-wite access
- unauthenticated requests get read-only access
# snippets/views.py
from rest_framework import permissions # permission library
class SnippetList(generics.ListCreateAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly] # add permission
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly] # add permission
Browsable API에 login 추가하기
user로 login하는 기능을 추가해보자
browsable API에서 사용할 수 있는 login view는 URLconf를 수정하면 된다.(project-level의 urls.py)
# tutorial/urls.py
from django.urls import path, include
urlpatterns = [
path('', include('snippets.urls')),
]
# user login
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]
Object level permissions
code snippets는 모두가 볼 수 있어야하는 반면, 오직 그 snippet code를 만든 user만 이를 update/delete할 수 있어야 한다.
이를 위해선, custom permission을 만들어야 한다.
* 앞에 required permissions은 로그인한 인증된 user을 말하는 것이지, 이것과는 다른 맥락이다.
snippets/permissions.py파일을 만들고 다음 코드를 넣자
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
Custom permission to only allow owners of an object to edit it.
"""
def has_object_permission(self, request, view, obj):
# Read permissions are allowed to any request,
# so we'll always allow GET, HEAD or OPTIONS requests.
if request.method in permissions.SAFE_METHODS:
return True
# Write permissions are only allowed to the owner of the snippets
return obj.owner == request.user # 같은면 True, 다르면 False 반환
그리고 이 custom permissions를 snippet instance endpoint에 추가해야 한다.
SnippetDetail view class에 permission_classes property를 추가하자
# snippets/views.py
from snippets.permissions import IsOwnerOrReadOnly # custom permission
class SnippetDetail(generics.RetrieveUpdateDestroyAPIView):
queryset = Snippet.objects.all()
serializer_class = SnippetSerializer
permission_classes = [permissions.IsAuthenticatedOrReadOnly, IsOwnerOrReadOnly] # add permission
테스트해보자
> python manage.py runserver
http://127.0.0.1:8000/snippets/
API로 인증하기
우리는 authenticated class에 대해선 아무것도 setup하지 않았지만,
defaults로 SessionAuthentication과 BasicAuthentication이 적용되어있다.
그래서 브라우저 말고 프로그래밍적으로 API와 interact하기 위해서는, authentication credentials를 명시해야한다:
> http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"
'Python > Django' 카테고리의 다른 글
[Django REST framework] 6. ViewSets & Routers (0) | 2020.04.12 |
---|---|
[Django REST framework] 5. Relationships & Hyperlinked APIs (0) | 2020.04.11 |
[Django REST framework] 3. Class-based Views (0) | 2020.04.10 |
[Django REST framework] 2. Requests and Responses (0) | 2020.04.10 |
[Django REST framework] 1. Serialization (0) | 2020.04.08 |