Django REST framework（DRF）公式チュートリアル④ Authentication & Permissions

# Tutorial 4: Authentication & Permissions

### はじめに
本記事は、Django REST framework の公式チュートリアル[Tutorial 4: Authentication & Permissions](https://www.django-rest-framework.org/tutorial/4-authentication-and-permissions/)の内容をもとに、和訳および補足解説を行ったものです。

基本的にはチュートリアルの翻訳を軸に構成しており、`✏️補足`の箇所では、各コードセクションの補足や内部処理の解説を備忘録的に記載しています。

他のチュートリアル記事もあわせて読みたい方は、以下のまとめ記事からご覧いただけます👇  
[DRF（Django REST framework）公式チュートリアルを日本語でまとめてみた【全6回】](https://qiita.com/yutty-kov/items/1850f3e865ec1a50a7bb)

それでは、本チュートリアルを始めましょう。
- - -

現状、このAPIには、誰がコードスニペットを編集または削除できるかについての制限がありません。
以下のことを確実にするため、もう少し高度な動作を実装したいと考えています：

・コードスニペットは常に作成者と紐づいている。
・スニペットは認証済みのユーザーのみ作成できる。
・スニペットの作成者だけが、それを更新・削除できる。
・認証されていないリクエストには、読み取り専用のアクセス権が与えられる。

### Modelへの情報の追加

<font color="#d14">`Snippet`</font> モデルクラスにいくつか変更を加えます。
まずは、いくつかのフィールドを追加してみましょう。
そのうちの1つは、コードスニペットを作成したユーザーを表すためのフィールドです。
もう1つは、ハイライトされたHTML形式のコードを保存するためのフィールドです。

<font color="#d14">`models.py`</font> にある <font color="#d14">`Snippet`</font> モデルに、以下の2つのフィールドを追加してください。

```python:snippets/models.py
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()
```

Modelを保存する際には、<font color="#d14">`pygments`</font> というコードのハイライトライブラリを使って、`highlighted` フィールドに値を設定するようにする必要もあります。

そのために、いくつか追加のインポートが必要になります：

```python:snippets/models.py
from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight
```

そして、これでModelクラスに <font color="#d14">`.save()`</font> メソッドを追加します。

```python:snippets/models.py
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のテスト用に、いくつかのユーザーを作成しておくとよいでしょう。
最も手早い方法は、<font color="#d14">`createsuperuser`</font> コマンドを使うことです。
```
python manage.py createsuperuser
```

## ✏️補足
`Model`クラスの `save()` メソッドは、オブジェクトをデータベースに保存する際に呼び出されます。
このメソッドをオーバーライドすることで、保存処理の前後に任意の処理を追加できます。
以下では、`highlighted` フィールドにハイライト済みの HTML を生成して保存するために、`save()` メソッドをオーバーライドしています。
※各コード行の説明は、コメントとして記述しています（ハイライト済みの HTML を生成する処理の解説です）

```python:snippets/models.py
# 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 の作成は簡単です。<font color="#d14">`serializers.py`</font> に以下を追加しましょう:
```python: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']
```
<font color="#d14">`snippets`</font> は `User` モデルにおけるリバースリレーションであるため、<font color="#d14">`ModelSerializer`</font> クラスを使ってもデフォルトでは含まれません。そのため、明示的にフィールドを追加する必要がありました。

<font color="#d14">`views.py`</font> にいくつかの View も追加しましょう。
ユーザーの表現（ユーザー情報のAPI）には読み取り専用の View だけを使いたいので、<font color="#d14">`ListAPIView`</font> と <font color="#d14">`RetrieveAPIView`</font> というジェネリッククラスベースビューを使用します。

```python:snippets/views.py
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
```

<font color="#d14">`UserSerializer`</font> クラスも忘れずにインポートしてください。

```python:snippets/views.py
from snippets.serializers import UserSerializer
```

最後に、それらのビューをAPIに追加する必要があります。URL設定からそれらを参照することで実現できます。次のコードを <font color="#d14">`snippets/urls.py`</font> の `urlpatterns` に追加してください。

```python:snippets/urls.py
path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),
```

## ✏️補足
>snippets は User モデルにおけるリバースリレーションであるため、ModelSerializer クラスを使ってもデフォルトでは含まれません。そのため、明示的にフィールドを追加する必要がありました。

これはどういうことかというと、
Userモデル と Snippetモデルは、Snippetモデル側の次の定義によって関連付けられています：

```python:
owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
```

しかし、Userモデル自身には Snippet との関係を示すフィールドを直接定義していないため、`ModelSerializer` クラスを使っただけでは自動的に `snippets` フィールドは含まれません。

```python:
# 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 を取得・表示できるようにします：

```python:snippets/serializers.py
# Serializerクラス
class UserSerializer(serializers.ModelSerializer):
    snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

    class Meta:
        model = User
        fields = ['id', 'username', 'snippets']
```
<br>

また、`PrimaryKeyRelatedField()` メソッドの仕様は、
第1引数の `many=True` によって「複数の Snippet をリストとして取得する」ことを示し、
第2引数の `queryset` に関連付ける Snippet モデルの全オブジェクトを指定します。

この設定により、そのユーザーと紐づく Snippet オブジェクトだけがフィールドとして取得・表示されるようになります。

```python:
                                        # リスト指定で複数のSnippetを取得   # Snippet全体から、ユーザーに紐づくものが対象となる
snippets = serializers.PrimaryKeyRelatedField(many=True, queryset=Snippet.objects.all())

```
- - -

### SnippetをUserと関連付ける
現時点では、コードスニペットを作成しても、それを作成したユーザーとスニペットインスタンスを関連付ける方法がありません。
ユーザーはシリアライズされたデータの一部として送信されるのではなく、リクエストのプロパティとして扱われます。

この問題に対処するため、スニペットビューで <font color="#d14">`.perform_create()`</font> メソッドをオーバーライドします。このメソッドを使うことで、インスタンスの保存方法をカスタマイズでき、リクエストやURLに暗黙的に含まれる情報を処理することができます。

<font color="#d14">`SnippetList`</font> ビュークラスに、次のメソッドを追加してください：
```python:snippets/views.py
def perform_create(self, serializer):
    serializer.save(owner=self.request.user)
```

Serializer の <font color="#d14">`create()`</font> メソッドには、リクエストからの検証済みデータ（ `validated data` ）に加えて、追加の <font color="#d14">`owner`</font> フィールドも渡されるようになります。

## Serializerを更新する
Snippetがそれを作成したユーザーと関連付けられるようになったので、それを反映するために <font color="#d14">`SnippetSerializer`</font> を更新しましょう。
<font color="#d14">`serializers.py`</font> のSerializer定義に、次のフィールドを追加してください：
```python:snippets/serializers.py
owner = serializers.ReadOnlyField(source='owner.username')
```

**注意**：<font color="#d14">`Meta`</font> クラスの `fields` リストに <font color="#d14">`owner`</font> を追加するのも忘れないでください。

このフィールドは、なかなか興味深いことをしています。
<font color="#d14">`source`</font> 引数は、どの属性を使ってフィールドの値を埋めるかを指定するもので、シリアライズされるインスタンスの任意の属性を参照できます。
また、上記のように「ドット記法（`owner.username` のような形式）」を使うこともでき、その場合は Django のテンプレート言語と同様に、指定された属性を順にたどって値を取得します。

今回追加したフィールドは、<font color="#d14">`CharField`</font> や <font color="#d14">`BooleanField`</font> のような型付きフィールドとは異なり、型指定のない <font color="#d14">`ReadOnlyField`</font> クラスです。
<font color="#d14">`ReadOnlyField`</font> は常に読み取り専用であり、シリアライズ時には使われますが、デシリアライズ時（リクエストからModelを更新するとき）には使われません。

この用途には、<font color="#d14">`CharField(read_only=True)`</font> を使うこともできます。

## ✏️補足
>ユーザーはシリアライズされたデータの一部として送信されるのではなく、リクエストのプロパティとして扱われます。

これはどういうことかというと、例えば Snippet 作成用の POST リクエストがクライアントから送信されたとしても、下記のようにリクエストボディにはユーザー情報（例："user": 1 ）が含まれません。そのため、そのままでは作成される Snippet インスタンスがどのユーザーに紐づいているのか判断できないということです。

```
POST /snippets/
{
    "title": "Hello World",
    "code": "print('Hello, world!')"
}
```

しかし、ログインしているユーザー情報は認証情報（ `request.user` ）としてセットされてサーバー側に送られるため、以下のように、リクエストからの検証済みデータ（`validated_data`）に加えて `request.user` も渡しています。

```python:
def perform_create(self, serializer):
    serializer.save(owner=self.request.user)
```

また、`perform_create()` は、`serializer.save()` メソッドを呼び出すためのフックメソッドで、通常ではデフォルトの動作として自動で実行されます。
ただし、検証済みデータ（`validated_data`）以外にも情報を渡したい場合、任意の値を追加する処理を加えてオーバーライドします。
例えば以下のように、第１引数にフィールド名（ `owner` ）を指定し、その値として `self.request.user` （ログイン中のユーザー情報）を渡すことで、作成される Snippetインスタンスにユーザー情報を紐づけることができます。

```python:
def perform_create(self, serializer):
    serializer.save(owner=self.request.user)
```

- - -
### Viewに必要なパーミッションを追加する
コードスニペットがユーザーと関連付けられたので、認証されたユーザーだけがコードスニペットを作成・更新・削除できるようにしたいです。

Django REST framework には、特定の View へのアクセスを制限するための パーミッションクラス がいくつか用意されています。
今回使いたいのは <font color="#d14">`IsAuthenticatedOrReadOnly`</font> です。
これを使うことで、認証されたリクエストは読み書きのアクセスを持ち、認証されていないリクエストは読み取り専用アクセスのみになります。

まずは、`views.py` モジュールに以下のインポートを追加してください：

```python:snippets/views.py
from rest_framework import permissions
```

そして、以下のプロパティを <font color="#d14">`SnippetList`</font> と <font color="#d14">`SnippetDetail`</font> の両方の Viewクラスに追加してください。

```python:snippets/views.py
permission_classes = [permissions.IsAuthenticatedOrReadOnly]
```
<br>

## ✏️補足
`permisson_classes` は、Viewクラスに定義するアクセス制御用のクラス変数です。
この設定でアクセスを拒否する仕組みは以下になります。

1. リクエストがくる
　例）ユーザーが `POST /snippets/` を送信します。
2. Viewに設定された `permission_classes` をチェック
　例）`permission_classes = [xxx]` のように設定されたクラスのメソッドが呼び出されます。
3. 許可判定を行う
　メソッド内で `request.method` などを確認し、許可するかどうかを（`True` / `False`）で判断します。
4. 許可されなければ `403 Forbidden` を返す
　`False` が返ると、Viewの処理には進まず、自動的にアクセスが拒否されます。

- - -

### ブラウズ可能なAPIにログイン機能を追加する
現在、ブラウザでブラウズ可能なAPIにアクセスしても、新しいコードスニペットを作成できません。これを行うには、ユーザーとしてログインできる必要があります。

ブラウズ可能なAPIで使用するためのログインビューは、プロジェクトレベルの <font color="#d14">`urls.py`</font> ファイルを編集することで追加できます。

まず、ファイルの先頭に以下のインポートを追加してください：

```python:snippets/urls.py
from django.urls import path, include
```

そして、ファイルの末尾に、ログインおよびログアウトビューを含めるためのパターンを追加します：

```python:snippets/urls.py
urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]
```

<font color="#d14">`api-auth/`</font> の部分は、実際には任意のURLに変更できます。

ブラウザを再度開いてページを更新すると、画面右上に「Log in」リンクが表示されます。先ほど作成したユーザーのいずれかでログインすれば、再びコードスニペットを作成できるようになります。

コードスニペットをいくつか作成したら、`/users/` エンドポイントにアクセスしてみてください。すると、それぞれのユーザーの `snippets` フィールドに、そのユーザーに関連付けられたスニペットIDのリストが表示されていることが分かるはずです。
<br>

## ✏️補足
以下のコードで、ブラウザ可能なAPIにログインビューを追加できる理由としては、**`rest_framework.urls` にログイン・ログアウト用のViewとテンプレートが含まれているから** です。

```python:snippets/urls.py
urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]
```
<br>

これを `include()` することで、次のようなURLが使えて、ログイン・ログアウトビューを実装することができます：

- `/api-auth/login/` でログイン画面が表示される
![259908DB-3C52-4E32-84CA-9A8F6899AFE3_4_5005_c.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593085/cd62769b-19c4-4a94-a140-e625d28f41e6.jpeg)
<br>

- `/api-auth/logout/` でログアウトできる
![80113807-5625-40E0-BBF1-7349847449E0.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593085/364a2508-a670-4a56-beb2-a397af377bb9.jpeg)

<br>

ちなみに、`rest_framework.urls` を追加した場合にはトップページの赤枠の箇所にログイン画面へのリンクが生成されます。

![2A6ABE7F-A2FC-45FA-9D05-B6A6167375D6.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/593085/a0a1cb16-1c63-4742-8ce2-3c9c9075c0df.jpeg)


- - -

### オブジェクトレベルのパーミッション
全てのコードスニペットを誰でも閲覧できるようにしたいですが、同時にその Snippet を作成したユーザーだけが更新や削除できるようにしたいです。

そのためには、カスタムパーミッションを作成する必要があります。

`snippets` アプリ内に、新しいファイル <font color="#d14">`permissions.py`</font> を作成してください。

```python: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
```

これで、<font color="#d14">`SnippetDetail`</font> Viewクラスの <font color="#d14">`permission_classes`</font> プロパティを編集することで、作成したカスタムパーミッションをスニペットインスタンスのエンドポイントに追加できるようになりました：

```python:snippets/views.py
permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]
```

また、<font color="#d14">`IsOwnerOrReadOnly`</font> クラスをインポートするのを忘れずに行ってください：

```python:snippets/permissions.py
from snippets.permissions import IsOwnerOrReadOnly
```

これで、ブラウザを再度開いて Snippet の個別エンドポイントにアクセスすると、DELETE や PUT の操作は、その Snippet を作成したユーザーとしてログインしている場合にのみ表示されるようになります。
<br>

### APIでの認証
現在、APIにパーミッションが設定されているため、Snippet を編集したい場合はリクエストに認証が必要になります。
まだ<font color="#d14">認証クラス</font>を設定していないため、現在はデフォルトの設定が適用されており、<font color="#d14">`SessionAuthentication`</font> と <font color="#d14">`BasicAuthentication`</font> が使用されています。

ウェブブラウザを通じて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"
}
```
<br>

### まとめ
これで、Web API に対してかなり細かく制御されたパーミッションを設定できるようになり、システムのユーザーや、彼らが作成したコードスニペット用のエンドポイントも整いました。

[<font color="#d14">チュートリアルのパート5</font>](https://qiita.com/yutty-kov/items/2ffd0099157246ae1f64)では、ハイライト表示された Snippet用の HTML エンドポイントを作成することで全体をまとめ、システム内の関係性にハイパーリンクを使用することで API の一貫性を高める方法について学びます。