1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Django Rest FrameworkでPermissionを実装しよう!

Last updated at Posted at 2022-11-27

概要

今回はログインしたユーザのロール別でPermissionを実装する方法について解説していきます

はじめに

以下のファイルに必要な記述を記載していきます

  • settings.py
  • models.py
  • fixtures.json
  • serializers.py
  • permissons.py
  • views.py
  • urls.py

また、Swaggerを使って実際にPermissionが機能しているか検証するので興味がある方はこちらの記事を見て設定してみてください

Permissionの設定

settings.py

認証できたユーザ別にPermissionを付与するのでsettings.pyに以下を記載します

settings.py
REST_FRAMEWORK = {
    "DEFAULT_PERMISSION_CLASSES": [
        'rest_framework.permissions.IsAuthenticated'
    ],
}

# カスタムユーザの設定
AUTH_USER_MODEL = 'application.User'

models.py

今回はカスタムユーザを作成して認証します
また、superuserを使っての認証も行うのでUserのModelにis_superuserのfieldを追加します
カスタムユーザの作成方法について詳しく知りたい方は以下の記事を参照してください

models.py
import uuid
from django.db import models
from django.core.validators import RegexValidator
from django.contrib.auth.models import AbstractUser
from django.contrib.auth.validators import UnicodeUsernameValidator


class User(AbstractUser):
    username_validator = UnicodeUsernameValidator()

    class Role(models.IntegerChoices):
        MANAGEMENT = 0
        GENERAL = 1
        PART_TIME = 2

    first_name = None
    last_name = None
    date_joined = None
    groups = None
    id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
    employee_number = models.CharField(
        unique=True,
        validators=[RegexValidator(r'^[0-9]{8}$')],
        max_length=8,
        verbose_name="社員番号",
    )
    username = models.CharField(
        max_length=150,
        unique=True,
        validators=[username_validator],
    )
    email = models.EmailField(max_length=254, unique=True)
    role = models.PositiveIntegerField(choices=Role.choices, default=Role.PART_TIME)
    is_superuser = models.BooleanField(default=False)
    created_at = models.DateTimeField(auto_now_add=True)
    updated_at = models.DateTimeField(auto_now=True)

    USERNAME_FIELD = "employee_number"
    REQUIRED_FIELDS = ["email","username"]

    class Meta:
        ordering = ["employee_number"]
        db_table = "User"

    def __str__(self):
        return self.username

fixtures.json

fixtureを使うとカスタムユーザのデータを楽に入れることができます
パスワードが暗号化されていますが全てtestです
fixtureの使い方について詳しく知りたい方は以下の記事を参照してください

fixtures.json
[
    {
        "model": "relationships.User",
        "pk": 1,
        "fields": {
            "employee_number": "00000001",
            "username":"test01",
            "password":"pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email":"test01@example.com",
            "role":0,
            "is_superuser":0,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z"
        }
    },
    {
        "model": "relationships.User",
        "pk": 2,
        "fields": {
            "employee_number": "00000002",
            "username":"test02",
            "password":"pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email":"test02@example.com",
            "role":1,
            "is_superuser":0,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z"
        }
    },
    {
        "model": "relationships.User",
        "pk": 3,
        "fields": {
            "employee_number": "00000003",
            "username":"test03",
            "password":"pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email":"test03@example.com",
            "role":2,
            "is_superuser":0,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z"
        }
    },
    {
        "model": "relationships.User",
        "pk": 4,
        "fields": {
            "employee_number": "00000004",
            "username":"test04",
            "password":"pbkdf2_sha256$390000$KF4YHJxvWjSODaXdxLBg6S$U5XDh8mR77kMMUtlRcBZS/bkaxdpjNR/P4zyy25g3/I=",
            "email":"test04@example.com",
            "role":0,
            "is_superuser":1,
            "created_at": "2022-07-28T00:31:09.732Z",
            "updated_at": "2022-07-28T00:31:09.732Z"
        }
    },
]

自分で暗号化(ハッシュ化)されたパスワードを作成するには?

Djangoのshellを使います
make_password関数の引数に任意のパスワードを入れると作成できます

# poetry run python manage.py shell
Python 3.10.9 (main, Dec 21 2022, 18:59:22) [GCC 10.2.1 20210110] on linux
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> from django.contrib.auth.hashers import make_password
>>> password = make_password("password")
>>> print(password)
pbkdf2_sha256$390000$K8JpO5A4iDOSSxZVpu8VBC$qJPNKobtP7+2HCUv8KUJ7GvpU3ZMiY/FlHOUjYSPf1g=

serializers.py

今回は簡単なUserとLoginのエンドポイントを実装します

serializers.py
from django.core.validators import RegexValidator
from rest_framework import serializers

from .models import User


class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = "id","employee_number","username","email","role","is_superuser"
        read_only_fields = ["created_at","updated_at","created_by","updated_by"]


class LoginSerializer(serializers.ModelSerializer):
    employee_number = serializers.CharField(
        max_length=8,
        min_length=8,
        validators=[RegexValidator(r"^[0-9]{8}$")],
    )

    class Meta:
        model = User
        fields = ["employee_number","password"]

permissions.py

views.pyを作成する前に権限用のファイル(permissions.py)を作成します
今回は

  • superuser(スーパーユーザ)
  • management(管理ユーザ)
  • general(一般ユーザ)
  • part_time(アルバイト)

の4つのロール別のPermissionを作成します
権限の構成は以下の表の通りにします

role list retrieve create update partial_update destroy
superuser
management -
general - - -
part_time - - - -

このようにsuperuserの順で権限が強く、例えばgeneral(一般ユーザ)ができることはmanagement(管理ユーザ)とsuperuserは全てできるようになっています
それをpermissions.pyにコードとして落とし込んでいきます

permissions.py
"""
権限用のモジュール
"""
from rest_framework.permissions import BasePermission
from .models import User


class IsPartTimeUser(BasePermission):
    def has_permission(self, request, view):
        """アルバイトユーザかどうか判定

        Args:
            request: リクエスト
            view: ビュー

        Returns:
            アルバイトユーザならTrue
            それ以外はFalse
        """
        if request.user.is_superuser:
            return True

        if request.user.is_authenticated:
            # アルバイトユーザーにできて一般ユーザーと管理ユーザーにできないことはないので管理ユーザーもTrue
            if request.user.role in [
                User.Role.PART_TIME,
                User.Role.GENERAL,
                User.Role.MANAGEMENT,
            ]:
                return True
        return False


class IsGeneralUser(BasePermission):
    def has_permission(self, request, view):
        """一般ユーザかどうか判定

        Args:
            request: リクエスト
            view: ビュー

        Returns:
            一般ユーザならTrue
            それ以外はFalse
        """
        if request.user.is_superuser:
            return True

        if request.user.is_authenticated:
            # 一般ユーザー、管理ユーザーともにTrueになるよう設定
            if request.user.role in [
                User.Role.GENERAL,
                User.Role.MANAGEMENT,
            ]:
                return True
        return False


class IsManagementUser(BasePermission):
    def has_permission(self, request, view):
        """管理ユーザかどうか判定

        Args:
            request: リクエスト
            view: ビュー

        Returns:
            管理ユーザならTrue
            それ以外はFalse
        """
        if request.user.is_superuser:
            return True

        if request.user.is_authenticated:
            if request.user.role == User.Role.MANAGEMENT:
                return True
        return False


class IsSuperUser(BasePermission):
    def has_permission(self, request, view):
        """スーパーユーザかどうか判定

        Args:
            request: リクエスト
            view: ビュー

        Returns:
            スーパーユーザならTrue
            それ以外はFalse
        """
        return request.user.is_superuser

views.py

permissions.pyで作成したクラスとget_permissionsメソッドを使ってログインしたユーザ別に権限を付与していきます
ログイン機能の作成方法について詳しく知りたい方は以下の記事を参照してください

views.py
from django.contrib.auth import authenticate, login, logout
from django.http import HttpResponse, JsonResponse
from rest_framework import status
from rest_framework.decorators import action
from rest_framework.permissions import AllowAny, IsAuthenticated
from rest_framework.viewsets import ModelViewSet
from rest_framework.viewsets import ViewSet

from .models import User
from .permissions import IsGeneralUser, IsManagementUser, IsPartTimeUser, IsSuperUser
from .serializers import LoginSerializer, UserSerilaizer


class UserViewSet(ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerilaizer

    # get_permissionsメソッドを使えば前述の表に従って権限を付与できる
    def get_permissions(self):
        if self.action in ["update", "partial_update"]:
            permission_classes = [IsManagementUser]
        elif self.action == "create":
            permission_classes = [IsGeneralUser]
        elif self.action == "destroy":
            permission_classes = [IsSuperUser]
        elif self.action in ["list", "retrieve"]:
            permission_classes = [IsPartTimeUser]
        else:
            permission_classes = [IsAuthenticated]
        return [permission() for permission in permission_classes]


class LoginViewSet(ViewSet):
    serializer_class = LoginSerializer
    permission_classes = [AllowAny]

    @action(detail=False, methods=["POST"])
    def login(self, request):
        """ユーザのログイン"""
        serializer = LoginSerializer(data=request.data)
        serializer.is_valid(raise_exception=True)
        employee_number = serializer.validated_data.get("employee_number")
        password = serializer.validated_data.get("password")
        user = authenticate(employee_number=employee_number, password=password)
        if not user:
            return JsonResponse(
                data={"msg": "社員番号またはパスワードが間違っています"},
                status=status.HTTP_400_BAD_REQUEST,
            )
        else:
            login(request, user)
            return JsonResponse(data={"role": user.Role(user.role).name})

    @action(methods=["POST"], detail=False)
    def logout(self, request):
        logout(request)
        return HttpResponse()

urls.py

アプリケーションのurls.pyは以下のようになります

urls.py
from django.urls import path, include
from rest_framework_nested import routers

from application.views import UserViewSet, LoginViewSet

router = routers.DefaultRouter()
router.register(r"users", UserViewSet, basename="user")
router.register(r"", LoginViewSet, basename="login")

urlpatterns = [
    path(r"", include(router.urls)),
]

実際に権限別の挙動を確認してみよう!

まずはログインせずにエンドポイントを操作すると以下のように

{
  "detail": "認証情報が含まれていません。"
}

と表示されるのでログイン認証に関してはうまく実装できていることが確認できます
スクリーンショット 2022-11-27 10.22.12.png

今回はfixtureで作成したアルバイトユーザでログインしてみましょう
スクリーンショット 2022-11-27 10.26.00.png

今回はログインに成功できたらroleを返すようにしているので成功です
スクリーンショット 2022-11-27 10.30.25.png

ログインした後にもう一度userのGETを行うと成功です
スクリーンショット 2022-11-27 10.39.37.png

あたらめてもう一度表を確認します

role list retrieve create update partial_update destroy
superuser
management -
general - - -
part_time - - - -

今回はアルバイトユーザはlistとretrieve(GET、詳細GET)しかできません
実際はcreate(POST)してみると、以下のように権限がなくてできないことが確認できます
スクリーンショット 2022-11-27 10.41.35.png
スクリーンショット 2022-11-27 10.43.20.png

以上のようにロール別で権限を変えることができることが確認できました!

記事の紹介

以下の記事も書きましたのでよかったら読んでみてください

参考

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?