[drf | agiliq] Access Control

Hyeseong·2021년 2월 20일
0

Access Control

API에 대한 액세스 제어를 추가하고 API를 추가하여 사용자를 만들고 인증합니다.

현재 우리 API는 완전히 open상태인데요. 누구나 모든 것을 만들고 액세스하고 삭제할 수 있습니다. 이러한 액세스 제어를 추가하려고합니다.

  • 유저는 인증을 해야만 poll or list of polls에 접근 가능.
  • 인증된 유저만 poll create 가능
  • 인증된 유저만 choice create 가능
  • 해당 유저가 생성한 poll에 대해서만 choice생성 가능
  • poll을 만든 인증된 유저가 delete 가능
  • 인증된 유저만 vote가능 그리고 다른 사람의 poll에 튜표 가능

access control을 가능하게 하기 위해서 API 2개를 추가할게요.

  • /users/ 엔드포인트 - 유저 생성 API
  • /login/ 엔드포인트 - 유저를 확인하고 토큰 발급

Creating a user

유저 시리얼라이저를 추가할텐데요. creating기능을 허용해줍니다.

# ...
from django.contrib.auth.models import User

# ...
class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'password')
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(
            email=validated_data['email'],
            username=validated_data['username']
        )
        user.set_password(validated_data['password'])
        user.save()
        return user

User 인스턴스를 저장하는 ModelSerializer 메소드인 create()를 오버라이딩 할게요.
hash한 비밀번호로 저장하게 하기위해서 user.set_password()를 사용하기도 했어요.
extra_kwargs = { 'password': { 'write_only': True}}를 사용하여 해당 응답으로 비밀번호를 찾지는 않게 할게요.

유저 생성을 위해서 UserSerializer를 뷰에 더하도록 할게요. 그리고 뷰에서 구현되면 urls.py에 연결하도록 하겠습니다.

# ...
from .serializers import PollSerializer, ChoiceSerializer, VoteSerializer, UserSerializer

# ...
class UserCreate(generics.CreateAPIView):
    serializer_class = UserSerializer

# in urls.py
# ...
from .apiviews import PollViewSet, ChoiceList, CreateVote, UserCreate


urlpatterns = [
    # ...
    path("users/", UserCreate.as_view(), name="user_create"),
]

/users/에 json형식으로 작성한 API를 테스팅 할수 있어요.

{
    "username": "nate.silver",
    "email": "nate.silver@example.com",
    "password": "FiveThirtyEight"
}

응답을 아래와 같이 주게 되요.

{
    "username": "nate.silver",
    "email": "nate.silver@example.com"
}

동일한 데이터를 다시 입력하게되면 아래와 같이 오류를 뱉어네요.

{
    "username": [
        "A user with that username already exists."
    ]
}

Authentication scheme setup

Django Rest Framework를 사용하면 DEFAULT_AUTHENTICATION_CLASSES를 사용하여 모든 뷰에 적용되는 기본 인증 체계를 설정할 수 있습니다.
토큰 인증을 사용 할건데요. settings.py에서 추가하도록 할게요.

REST_FRAMEWORK = {
    'DEFAULT_AUTHENTICATION_CLASSES': (
        'rest_framework.authentication.TokenAuthentication',
        'rest_framework.authentication.SessionAuthentication',
    )
}

또한 rest_framework.authtoken앱 을 활성화해야 하므로 INSTALLED_APPSsettings.py에서 업데이트 하십시오.

INSTALLED_APPS = (
    ...
    'rest_framework.authtoken'
)

python manage.py migrate # 테이블 생성도 하겠습니다. 방금 입력한 앱의 테이블을 만들어 주는거에요. 토큰 값을 저장하게되요.

REST_FRAMEWORK = {
    # ...
    'DEFAULT_PERMISSION_CLASSES': (
        'rest_framework.permissions.IsAuthenticated',
    )
}

또한 UserCreate 뷰에 예외를 부여하는 하기 위해 전역 설정이 아래와 같이 필요해요.

class UserCreate(generics.CreateAPIView):
    authentication_classes = ()
    permission_classes = ()
    serializer_class = UserSerializer

UserCreate뷰에 global authentication scheme 시키지 않기 위해 제외하려면 authentication_classes = ()permission_classes = ()를 클래스 변수로 정의해줘야해요.

UserCreate 뷰에서 사용자가 생성 될 때 토큰이 생성되도록하고 싶으므로 UserSerializer를 업데이트합니다.

from rest_framework.authtoken.models import Token

class UserSerializer(serializers.ModelSerializer):

    class Meta:
        model = User
        fields = ('username', 'email', 'password')
        extra_kwargs = {'password': {'write_only': True}}

    def create(self, validated_data):
        user = User(
            email=validated_data['email'],
            username=validated_data['username']
        )
        user.set_password(validated_data['password'])
        user.save()
        Token.objects.create(user=user)
        return user

The login API

settings.pyrest_framework.authentication.TokenAuthentication을 추가 했으므로. Authorization: Token c2a84953f47288ac1943a3f389a6034e395ad940헤더에 넣어줘야해요. 를 auhenticate로 설정해야합니다.

사용자가 사용자 이름과 비밀번호를 제공하고 토큰을 되 찾을 수있는 API가 필요합니다. - 로그인 API
이 API를 사용하여 토큰을 저장하지 않기 때문에 serializer를 추가하지 않을 것입니다.

# in apiviews.py
# ...
from django.contrib.auth import authenticate

class LoginView(APIView):
    permission_classes = ()

    def post(self, request,):
        username = request.data.get("username")
        password = request.data.get("password")
        user = authenticate(username=username, password=password)
        if user:
            return Response({"token": user.auth_token.key})
        else:
            return Response({"error": "Wrong Credentials"}, status=status.HTTP_400_BAD_REQUEST)


# in urls.py
# ...

from .apiviews import PollViewSet, ChoiceList, CreateVote, UserCreate, LoginView



urlpatterns = [
    path("login/", LoginView.as_view(), name="login"),
    # ...
]

참고로 /user/ 엔드포인트를 지금 당장 사용할수 없습니다. 회원가입을 통해서 토큰 생성을 해야하는데 아직 토큰을 발급 받은 유저가 없조?
만약 로그인을 시도한다고 해도 "User has no auth_token" 오류가 발생해요.
/user/엔드 포인트를 이용해서 회원가입시 토큰이 발생된 유저를 만들도록 할게요.

httpie를 통해서 POST요청을 보내게 되면 아래와 같아요.

❯ http POST http://localhost:8000/login/ 'username=korean' 'password=11111111'
HTTP/1.1 200 OK
Allow: POST, OPTIONS
Content-Length: 52
Content-Type: application/json
Date: Sat, 20 Feb 2021 15:36:48 GMT
Referrer-Policy: same-origin
Server: WSGIServer/0.2 CPython/3.9.1
Vary: Cookie
X-Content-Type-Options: nosniff
X-Frame-Options: DENY

{
    "token": "d8306c4de9cb05c3c6da1f5332a965be3b9ac7b1"
}

올바르게 POST요청을 보내게 된다면 아래와 같은 토큰 값을 받게되요.

{
    "token": "c300998d0e2d1b8b4ed9215589df4497de12000c"
}

다른 방법으로 간다면 obtain_auth_token메서드를 이용할 수 있어요.


urlpatterns = [
	...
    path("api-token-auth/", views.obtain_auth_token, name="login"),
]

아래와 같이 httpie를 이용해서 접근해보세요. 그럼 토큰값을 반환 받게 되요.

http post http://127.0.0.1:8000/api-token-auth/ username=vitor password=123

Fine grained access control

/polls/ 엔드포인트에 헤더없이 API에 액세스 해보십시오 . 아래와 같은 http 상태 코드로 오류가 발생 합니다. HTTP 401 Unauthorized

{
    "detail": "Authentication credentials were not provided."
}

결국 HTTP헤더에는 해당 Authorization: Token <your token> 형식을 갖춘 키와 값이 있어야해요.

  • 인증 된 사용자는 자신이 만든 투표에 대해서만 선택 항목을 만들 수 있습니다.
  • 인증 된 사용자는 자신이 만든 설문 조사 만 삭제할 수 있습니다.

PollViewSet.destroy and ChoiceList.post를 오버라이딩하여 재정의할게요.
하나는 뷰셋이고 나머지는 APIView조!?

# ...
from rest_framework.exceptions import PermissionDenied


class PollViewSet(viewsets.ModelViewSet):
    # ...

    def destroy(self, request, *args, **kwargs):
        poll = Poll.objects.get(pk=self.kwargs["pk"])
        if not request.user == poll.created_by:
            raise PermissionDenied("You can not delete this poll.")
        return super().destroy(request, *args, **kwargs)


class ChoiceList(generics.ListCreateAPIView):
    # ...

    def post(self, request, *args, **kwargs):
        poll = Poll.objects.get(pk=self.kwargs["pk"])
        if not request.user == poll.created_by:
            raise PermissionDenied("You can not create choice for this poll.")
        return super().post(request, *args, **kwargs)

둘다 request.user를 확인하게되요. 그리고 PermissionDenied에러를 발생시킬 수 있게 코딩도 되어있어요.

다른 유저의 poll을 DELETE메서드를 이용해서 확인할수 있어요. 정상적으로 되었다면 HTTP 403 Forbiden을 응답으로 뱉어내야해요.

{
    "detail": "You can not delete this poll."
}

Next steps

다음 장에서는 API 및 직렬 변환기에 대한 테스트 추가를 살펴볼 것입니다. 또한 flake8CI 환경에서 테스트 를 사용 하고 실행 하는 방법도 살펴 봅니다 .

profile
어제보다 오늘 그리고 오늘 보다 내일...

0개의 댓글