概要
今回はログインしたユーザのロール別でPermissionを実装する方法について解説していきます
はじめに
以下のファイルに必要な記述を記載していきます
- settings.py
- models.py
- fixtures.json
- serializers.py
- permissons.py
- views.py
- urls.py
また、Swaggerを使って実際にPermissionが機能しているか検証するので興味がある方はこちらの記事を見て設定してみてください
Permissionの設定
settings.py
認証できたユーザ別にPermissionを付与するのでsettings.pyに以下を記載します
REST_FRAMEWORK = {
"DEFAULT_PERMISSION_CLASSES": [
'rest_framework.permissions.IsAuthenticated'
],
}
# カスタムユーザの設定
AUTH_USER_MODEL = 'application.User'
models.py
今回はカスタムユーザを作成して認証します
また、superuserを使っての認証も行うのでUserのModelにis_superuserのfieldを追加します
カスタムユーザの作成方法について詳しく知りたい方は以下の記事を参照してください
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の使い方について詳しく知りたい方は以下の記事を参照してください
[
{
"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のエンドポイントを実装します
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にコードとして落とし込んでいきます
"""
権限用のモジュール
"""
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
メソッドを使ってログインしたユーザ別に権限を付与していきます
ログイン機能の作成方法について詳しく知りたい方は以下の記事を参照してください
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は以下のようになります
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": "認証情報が含まれていません。"
}
と表示されるのでログイン認証に関してはうまく実装できていることが確認できます
今回はfixtureで作成したアルバイトユーザでログインしてみましょう
今回はログインに成功できたらroleを返すようにしているので成功です
あたらめてもう一度表を確認します
role | list | retrieve | create | update | partial_update | destroy |
---|---|---|---|---|---|---|
superuser | ○ | ○ | ○ | ○ | ○ | ○ |
management | ○ | ○ | ○ | ○ | ○ | - |
general | ○ | ○ | ○ | - | - | - |
part_time | ○ | ○ | - | - | - | - |
今回はアルバイトユーザはlistとretrieve(GET、詳細GET)しかできません
実際はcreate(POST)してみると、以下のように権限がなくてできないことが確認できます
以上のようにロール別で権限を変えることができることが確認できました!
記事の紹介
以下の記事も書きましたのでよかったら読んでみてください
参考