チュートリアル4:認証とパーミッション

現在、APIにはコードスニペットの編集や削除に関する制限がありません。より高度な動作を追加して、以下を確実にします。

  • コードスニペットは常に作成者と関連付けられています。
  • 認証されたユーザーのみがスニペットを作成できます。
  • スニペットの作成者のみがスニペットを更新または削除できます。
  • 認証されていないリクエストは、完全な読み取り専用アクセス権限を持つ必要があります。

モデルへの情報の追加

Snippetモデルクラスにいくつかの変更を加えます。まず、いくつかのフィールドを追加します。これらのフィールドの1つは、コードスニペットを作成したユーザーを表すために使用されます。もう1つのフィールドは、コードの強調表示されたHTML表現を格納するために使用されます。

models.pySnippetモデルに次の2つのフィールドを追加します。

owner = models.ForeignKey('auth.User', related_name='snippets', on_delete=models.CASCADE)
highlighted = models.TextField()

モデルを保存する際に、pygmentsコード強調表示ライブラリを使用して強調表示フィールドに入力する必要があります。

追加のインポートが必要です

from pygments.lexers import get_lexer_by_name
from pygments.formatters.html import HtmlFormatter
from pygments import highlight

これで、モデルクラスに.save()メソッドを追加できます

def save(self, *args, **kwargs):
    """
    Use the `pygments` library to create a highlighted HTML
    representation of the code snippet.
    """
    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

ユーザーモデルのエンドポイントの追加

使用するユーザーができたので、APIにそれらのユーザーの表現を追加しましょう。新しいシリアライザーの作成は簡単です。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にいくつかのビューを追加します。ユーザー表現には読み取り専用のビューのみを使用したいので、ListAPIViewRetrieveAPIViewジェネリッククラスベースビューを使用します。

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

最後に、URLconfから参照することで、APIにこれらのビューを追加する必要があります。snippets/urls.pyのパターンに以下を追加します。

path('users/', views.UserList.as_view()),
path('users/<int:pk>/', views.UserDetail.as_view()),

スニペットとユーザーの関連付け

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

これに対処する方法は、スニペットビューで.perform_create()メソッドをオーバーライドすることです。これにより、インスタンスの保存方法を変更し、受信リクエストまたは要求されたURLに暗黙的に含まれる情報を処理できます。

SnippetListビュークラスに、次のメソッドを追加します

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

シリアライザーのcreate()メソッドには、リクエストからの検証済みデータに加えて、追加の'owner'フィールドが渡されるようになります。

シリアライザーの更新

スニペットが作成したユーザーと関連付けられるようになったので、それを反映するようにSnippetSerializerを更新しましょう。serializers.pyのシリアライザー定義に次のフィールドを追加します

owner = serializers.ReadOnlyField(source='owner.username')

**注:** 内部Metaクラスのフィールドのリストにも'owner',を追加してください。

このフィールドは非常に興味深いことを行っています。source引数は、フィールドの入力に使用される属性を制御し、シリアライズされたインスタンスの任意の属性を指すことができます。上記の点線表記を使用することもできます。この場合、Djangoのテンプレート言語で使用されているのと同様に、指定された属性をトラバースします。

追加したフィールドは、CharFieldBooleanFieldなどの一部の型付きフィールドとは対照的に、型なしのReadOnlyFieldクラスです。型なしのReadOnlyFieldは常に読み取り専用であり、シリアライズされた表現に使用されますが、逆シリアライズ時にモデルインスタンスを更新するためには使用されません。ここではCharField(read_only=True)を使用することもできます。

ビューへの必須パーミッションの追加

コードスニペットがユーザーと関連付けられるようになったので、認証されたユーザーのみがコードスニペットの作成、更新、削除ができるようにする必要があります。

REST frameworkには、特定のビューにアクセスできるユーザーを制限するために使用できる多くのパーミッションクラスが含まれています。この場合、探しているのはIsAuthenticatedOrReadOnlyです。これにより、認証されたリクエストには読み取り/書き込みアクセス権が与えられ、認証されていないリクエストには読み取り専用アクセス権が与えられます。

まず、ビューモジュールに次のインポートを追加します

from rest_framework import permissions

次に、SnippetListビュークラスとSnippetDetailビュークラスの**両方**に次のプロパティを追加します。

permission_classes = [permissions.IsAuthenticatedOrReadOnly]

ブラウザブルAPIへのログインの追加

ブラウザを開いて、現時点でのブラウザブルAPIに移動すると、新しいコードスニペットを作成できなくなっていることがわかります。そのためには、ユーザーとしてログインできる必要があります。

プロジェクトレベルのurls.pyファイルでURLconfを編集することにより、ブラウザブルAPIで使用するためのログインビューを追加できます。

ファイルの先頭に次のインポートを追加します

from django.urls import path, include

ファイルの最後に、ブラウザブルAPIのログインビューとログアウトビューを含めるためのパターンを追加します。

urlpatterns += [
    path('api-auth/', include('rest_framework.urls')),
]

パターンの'api-auth/'の部分は、実際には任意のURLを使用できます。

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

いくつかのコードスニペットを作成したら、'/users/'エンドポイントに移動し、各ユーザーの'snippets'フィールドに、各ユーザーに関連付けられているスニペットIDのリストが含まれていることに注意してください。

オブジェクトレベルのパーミッション

実際には、すべてのコードスニペットを誰でも表示できるようにしたいと考えていますが、コードスニペットを作成したユーザーのみがコードスニペットを更新または削除できるようにする必要があります。

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

snippetsアプリで、新しいファイルpermissions.pyを作成します

from rest_framework import permissions


class IsOwnerOrReadOnly(permissions.BasePermission):
    """
    Custom permission to only allow owners of an object to edit it.
    """

    def has_object_permission(self, request, view, obj):
        # Read permissions are allowed to any request,
        # so we'll always allow GET, HEAD or OPTIONS requests.
        if request.method in permissions.SAFE_METHODS:
            return True

        # Write permissions are only allowed to the owner of the snippet.
        return obj.owner == request.user

これで、SnippetDetailビュークラスのpermission_classesプロパティを編集することにより、スニペットインスタンスエンドポイントにそのカスタムパーミッションを追加できます

permission_classes = [permissions.IsAuthenticatedOrReadOnly,
                      IsOwnerOrReadOnly]

IsOwnerOrReadOnlyクラスもインポートしてください。

from snippets.permissions import IsOwnerOrReadOnly

ブラウザを再度開くと、「削除」と「PUT」アクションは、コードスニペットを作成したユーザーと同じユーザーとしてログインした場合にのみ、スニペットインスタンスエンドポイントに表示されることがわかります。

APIへの認証

APIにパーミッションのセットが設定されたので、スニペットを編集する場合は、APIへのリクエストを認証する必要があります。 認証クラス を設定していないため、現在、デフォルトでSessionAuthenticationBasicAuthenticationが適用されています。

Webブラウザを介してAPIとやり取りする場合、ログインでき、ブラウザセッションによってリクエストに必要な認証が提供されます。

プログラムによってAPIとやり取りする場合は、各リクエストで認証資格を明示的に提供する必要があります。

認証せずにスニペットを作成しようとすると、エラーが発生します

http POST http://127.0.0.1:8000/snippets/ code="print(123)"

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

前に作成したユーザーの1つのユーザー名とパスワードを含めることで、正常なリクエストを行うことができます。

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では、強調表示されたスニペットのHTMLエンドポイントを作成し、システム内のリレーションシップにハイパーリンクを使用することにより、APIの一貫性を向上させる方法について説明します。