본문 바로가기

Python/Django

[Django REST framework] 4. Authentication & Permissions

https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/

 

4 - Authentication and permissions - Django REST framework

Currently our API doesn't have any restrictions on who can edit or delete code snippets. We'd like to have some more advanced behavior in order to make sure that: Code snippets are always associated with a creator. Only authenticated users may create snipp

www.django-rest-framework.org

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를 만들어야하기에 ListAPIViewRetrieveAPIView 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/

login이 안된 상태
user01로 로그인했을 때
user01로 code snippet을 만들었을 때
user02로 login했을 때, user01이 만든 snippets는 read-only

API로 인증하기

우리는 authenticated class에 대해선 아무것도 setup하지 않았지만,

defaults로 SessionAuthenticationBasicAuthentication이 적용되어있다.

 

그래서 브라우저 말고 프로그래밍적으로 API와 interact하기 위해서는, authentication credentials를 명시해야한다:

> http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"