Tutorial 4: Authentication & Permissions
はじめに
本記事は、Django REST framework の公式チュートリアルTutorial 4: Authentication & Permissionsの内容をもとに、和訳および補足解説を行ったものです。
基本的にはチュートリアルの翻訳を軸に構成しており、✏️補足
の箇所では、各コードセクションの補足や内部処理の解説を備忘録的に記載しています。
他のチュートリアル記事もあわせて読みたい方は、以下のまとめ記事からご覧いただけます👇
DRF(Django REST framework)公式チュートリアルを日本語でまとめてみた【全6回】
それでは、本チュートリアルを始めましょう。
現状、このAPIには、誰がコードスニペットを編集または削除できるかについての制限がありません。
以下のことを確実にするため、もう少し高度な動作を実装したいと考えています:
・コードスニペットは常に作成者と紐づいている。
・スニペットは認証済みのユーザーのみ作成できる。
・スニペットの作成者だけが、それを更新・削除できる。
・認証されていないリクエストには、読み取り専用のアクセス権が与えられる。
Modelへの情報の追加
Snippet
モデルクラスにいくつか変更を加えます。
まずは、いくつかのフィールドを追加してみましょう。
そのうちの1つは、コードスニペットを作成したユーザーを表すためのフィールドです。
もう1つは、ハイライトされたHTML形式のコードを保存するためのフィールドです。
models.py
にある Snippet
モデルに、以下の2つのフィールドを追加してください。
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
Modelを保存する際には、pygments
というコードのハイライトライブラリを使って、highlighted
フィールドに値を設定するようにする必要もあります。
そのために、いくつか追加のインポートが必要になります:
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
そして、これでModelクラスに .save()
メソッドを追加します。
def save(self, *args, **kwargs):
"""
pygments ライブラリを使用して、コードスニペットのハイライトされた HTML 表現を作成します。
"""
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().save(*args, **kwargs)
全ての作業が完了したら、データベースのテーブルを更新する必要があります。
通常であればそのためにマイグレーションを作成しますが、このチュートリアルでは、データベースを削除して最初からやり直すことにしましょう。
rm -f db.sqlite3
rm -r snippets/migrations
python manage.py makemigrations snippets
python manage.py migrate
APIのテスト用に、いくつかのユーザーを作成しておくとよいでしょう。
最も手早い方法は、createsuperuser
コマンドを使うことです。
python manage.py createsuperuser
✏️補足
Model
クラスの save()
メソッドは、オブジェクトをデータベースに保存する際に呼び出されます。
このメソッドをオーバーライドすることで、保存処理の前後に任意の処理を追加できます。
以下では、highlighted
フィールドにハイライト済みの HTML を生成して保存するために、save()
メソッドをオーバーライドしています。
※各コード行の説明は、コメントとして記述しています(ハイライト済みの HTML を生成する処理の解説です)
# ModelオブジェクトをDBに保存する処理を行うメソッド。
# 保存処理をカスタマイズしたいときにはオーバーライドをおこなう。
def save(self, *args, **kwargs):
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, # 全体を含む HTML ドキュメントとして出力する
**options # タイトルがあれば、それを含める
)
# self.code を指定した言語(lexer)とフォーマット(HTML)でハイライトし、self.highlighted に保存する
self.highlighted = highlight(self.code, lexer, formatter)
# 親クラスの save() を呼び出して、通常の保存処理を実行
super().save(*args, **kwargs)
Userモデルにエンドポイントを追加する
これで操作できるユーザーが用意できたので、これらのユーザーを API上で確認できるようにしておくとよいでしょう。
新しい Serializer の作成は簡単です。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']
snippets
は User
モデルにおけるリバースリレーションであるため、ModelSerializer
クラスを使ってもデフォルトでは含まれません。そのため、明示的にフィールドを追加する必要がありました。
views.py
にいくつかの View も追加しましょう。
ユーザーの表現(ユーザー情報のAPI)には読み取り専用の View だけを使いたいので、ListAPIView
と RetrieveAPIView
というジェネリッククラスベースビューを使用します。
from django.contrib.auth.models import User
class UserList(generics.ListAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
class UserDetail(generics.RetrieveAPIView):
queryset = User.objects.all()
serializer_class = UserSerializer
UserSerializer
クラスも忘れずにインポートしてください。
from snippets.serializers import UserSerializer
最後に、それらのビューをAPIに追加する必要があります。URL設定からそれらを参照することで実現できます。次のコードを snippets/urls.py
の urlpatterns
に追加してください。
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
✏️補足
snippets は User モデルにおけるリバースリレーションであるため、ModelSerializer クラスを使ってもデフォルトでは含まれません。そのため、明示的にフィールドを追加する必要がありました。
これはどういうことかというと、
Userモデル と Snippetモデルは、Snippetモデル側の次の定義によって関連付けられています:
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
しかし、Userモデル自身には Snippet との関係を示すフィールドを直接定義していないため、ModelSerializer
クラスを使っただけでは自動的に snippets
フィールドは含まれません。
# Model定義の一部抜粋
class Snippet(models.Model):
...
# Snippetとのリレーションを明示的に定義している
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
class User(AbstractUser):
# Snippetとのリレーションを明示的に定義しているわけではない
username = models.CharField(...)
first_name = models.CharField(max_length=150, blank=True)
...
このように、Snippet側からのリバースリレーションによって user.snippets
のようなアクセスは可能になりますが、
それを Serializer上で表現するには明示的にフィールドとして定義する必要がある というわけです。
そのため、以下のように PrimaryKeyRelatedField を使って、User に紐づくすべての Snippet を取得・表示できるようにします:
# Serializerクラス
class UserSerializer(serializers.ModelSerializer):
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
class Meta:
model = User
fields = ['id', 'username', 'snippets']
また、PrimaryKeyRelatedField()
メソッドの仕様は、
第1引数の many=True
によって「複数の Snippet をリストとして取得する」ことを示し、
第2引数の queryset
に関連付ける Snippet モデルの全オブジェクトを指定します。
この設定により、そのユーザーと紐づく Snippet オブジェクトだけがフィールドとして取得・表示されるようになります。
# リスト指定で複数のSnippetを取得 # Snippet全体から、ユーザーに紐づくものが対象となる
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())
SnippetをUserと関連付ける
現時点では、コードスニペットを作成しても、それを作成したユーザーとスニペットインスタンスを関連付ける方法がありません。
ユーザーはシリアライズされたデータの一部として送信されるのではなく、リクエストのプロパティとして扱われます。
この問題に対処するため、スニペットビューで .perform_create()
メソッドをオーバーライドします。このメソッドを使うことで、インスタンスの保存方法をカスタマイズでき、リクエストやURLに暗黙的に含まれる情報を処理することができます。
SnippetList
ビュークラスに、次のメソッドを追加してください:
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
Serializer の create()
メソッドには、リクエストからの検証済みデータ( validated data
)に加えて、追加の owner
フィールドも渡されるようになります。
Serializerを更新する
Snippetがそれを作成したユーザーと関連付けられるようになったので、それを反映するために SnippetSerializer
を更新しましょう。
serializers.py
のSerializer定義に、次のフィールドを追加してください:
owner = serializers.ReadOnlyField(source='owner.username')
注意:Meta
クラスの fields
リストに owner
を追加するのも忘れないでください。
このフィールドは、なかなか興味深いことをしています。
source
引数は、どの属性を使ってフィールドの値を埋めるかを指定するもので、シリアライズされるインスタンスの任意の属性を参照できます。
また、上記のように「ドット記法(owner.username
のような形式)」を使うこともでき、その場合は Django のテンプレート言語と同様に、指定された属性を順にたどって値を取得します。
今回追加したフィールドは、CharField
や BooleanField
のような型付きフィールドとは異なり、型指定のない ReadOnlyField
クラスです。
ReadOnlyField
は常に読み取り専用であり、シリアライズ時には使われますが、デシリアライズ時(リクエストからModelを更新するとき)には使われません。
この用途には、CharField(read_only=True)
を使うこともできます。
✏️補足
ユーザーはシリアライズされたデータの一部として送信されるのではなく、リクエストのプロパティとして扱われます。
これはどういうことかというと、例えば Snippet 作成用の POST リクエストがクライアントから送信されたとしても、下記のようにリクエストボディにはユーザー情報(例:"user": 1 )が含まれません。そのため、そのままでは作成される Snippet インスタンスがどのユーザーに紐づいているのか判断できないということです。
POST /snippets/
{
"title": "Hello World",
"code": "print('Hello, world!')"
}
しかし、ログインしているユーザー情報は認証情報( request.user
)としてセットされてサーバー側に送られるため、以下のように、リクエストからの検証済みデータ(validated_data
)に加えて request.user
も渡しています。
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
また、perform_create()
は、serializer.save()
メソッドを呼び出すためのフックメソッドで、通常ではデフォルトの動作として自動で実行されます。
ただし、検証済みデータ(validated_data
)以外にも情報を渡したい場合、任意の値を追加する処理を加えてオーバーライドします。
例えば以下のように、第1引数にフィールド名( owner
)を指定し、その値として self.request.user
(ログイン中のユーザー情報)を渡すことで、作成される Snippetインスタンスにユーザー情報を紐づけることができます。
def perform_create(self, serializer):
serializer.save(owner=self.request.user)
Viewに必要なパーミッションを追加する
コードスニペットがユーザーと関連付けられたので、認証されたユーザーだけがコードスニペットを作成・更新・削除できるようにしたいです。
Django REST framework には、特定の View へのアクセスを制限するための パーミッションクラス がいくつか用意されています。
今回使いたいのは IsAuthenticatedOrReadOnly
です。
これを使うことで、認証されたリクエストは読み書きのアクセスを持ち、認証されていないリクエストは読み取り専用アクセスのみになります。
まずは、views.py
モジュールに以下のインポートを追加してください:
from rest_framework import permissions
そして、以下のプロパティを SnippetList
と SnippetDetail
の両方の Viewクラスに追加してください。
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
✏️補足
permisson_classes
は、Viewクラスに定義するアクセス制御用のクラス変数です。
この設定でアクセスを拒否する仕組みは以下になります。
- リクエストがくる
例)ユーザーがPOST /snippets/
を送信します。 - Viewに設定された
permission_classes
をチェック
例)permission_classes = [xxx]
のように設定されたクラスのメソッドが呼び出されます。 - 許可判定を行う
メソッド内でrequest.method
などを確認し、許可するかどうかを(True
/False
)で判断します。 - 許可されなければ
403 Forbidden
を返す
False
が返ると、Viewの処理には進まず、自動的にアクセスが拒否されます。
ブラウズ可能なAPIにログイン機能を追加する
現在、ブラウザでブラウズ可能なAPIにアクセスしても、新しいコードスニペットを作成できません。これを行うには、ユーザーとしてログインできる必要があります。
ブラウズ可能なAPIで使用するためのログインビューは、プロジェクトレベルの urls.py
ファイルを編集することで追加できます。
まず、ファイルの先頭に以下のインポートを追加してください:
from django.urls import path, include
そして、ファイルの末尾に、ログインおよびログアウトビューを含めるためのパターンを追加します:
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]
api-auth/
の部分は、実際には任意のURLに変更できます。
ブラウザを再度開いてページを更新すると、画面右上に「Log in」リンクが表示されます。先ほど作成したユーザーのいずれかでログインすれば、再びコードスニペットを作成できるようになります。
コードスニペットをいくつか作成したら、/users/
エンドポイントにアクセスしてみてください。すると、それぞれのユーザーの snippets
フィールドに、そのユーザーに関連付けられたスニペットIDのリストが表示されていることが分かるはずです。
✏️補足
以下のコードで、ブラウザ可能なAPIにログインビューを追加できる理由としては、rest_framework.urls
にログイン・ログアウト用のViewとテンプレートが含まれているから です。
urlpatterns += [
path('api-auth/', include('rest_framework.urls')),
]
これを include()
することで、次のようなURLが使えて、ログイン・ログアウトビューを実装することができます:
ちなみに、rest_framework.urls
を追加した場合にはトップページの赤枠の箇所にログイン画面へのリンクが生成されます。
オブジェクトレベルのパーミッション
全てのコードスニペットを誰でも閲覧できるようにしたいですが、同時にその Snippet を作成したユーザーだけが更新や削除できるようにしたいです。
そのためには、カスタムパーミッションを作成する必要があります。
snippets
アプリ内に、新しいファイル permissions.py
を作成してください。
from rest_framework import permissions
class IsOwnerOrReadOnly(permissions.BasePermission):
"""
オブジェクトの所有者のみが編集できるようにするカスタムパーミッション。
"""
def has_object_permission(self, request, view, obj):
# 読み取りパーミッションはすべてのリクエストに許可されます。
# したがって、GET、HEAD、OPTIONS リクエストは常に許可されます。
if request.method in permissions.SAFE_METHODS:
return True
# 書き込みパーミッションはスニペットの所有者にのみ許可されます。
return obj.owner == request.user
これで、SnippetDetail
Viewクラスの permission_classes
プロパティを編集することで、作成したカスタムパーミッションをスニペットインスタンスのエンドポイントに追加できるようになりました:
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
IsOwnerOrReadOnly]
また、IsOwnerOrReadOnly
クラスをインポートするのを忘れずに行ってください:
from snippets.permissions import IsOwnerOrReadOnly
これで、ブラウザを再度開いて Snippet の個別エンドポイントにアクセスすると、DELETE や PUT の操作は、その Snippet を作成したユーザーとしてログインしている場合にのみ表示されるようになります。
APIでの認証
現在、APIにパーミッションが設定されているため、Snippet を編集したい場合はリクエストに認証が必要になります。
まだ認証クラスを設定していないため、現在はデフォルトの設定が適用されており、SessionAuthentication
と BasicAuthentication
が使用されています。
ウェブブラウザを通じてAPIとやり取りする場合は、ログインすることでブラウザのセッションがリクエストに必要な認証情報を提供してくれます。
一方で、プログラムなどからAPIとやり取りする場合は、各リクエストに対して明示的に認証情報を付与する必要があります。
認証せずに Snippet を作成しようとすると、エラーが発生します:
http POST http://127.0.0.1:8000/snippets/ code="print(123)"
{
"detail": "Authentication credentials were not provided."
}
先ほど作成したユーザーのユーザー名とパスワードを含めることで、リクエストを成功させることができます。
http -a admin:password123 POST http://127.0.0.1:8000/snippets/ code="print(789)"
{
"id": 1,
"owner": "admin",
"title": "foo",
"code": "print(789)",
"linenos": false,
"language": "python",
"style": "friendly"
}
まとめ
これで、Web API に対してかなり細かく制御されたパーミッションを設定できるようになり、システムのユーザーや、彼らが作成したコードスニペット用のエンドポイントも整いました。
チュートリアルのパート5では、ハイライト表示された Snippet用の HTML エンドポイントを作成することで全体をまとめ、システム内の関係性にハイパーリンクを使用することで API の一貫性を高める方法について学びます。