Help us understand the problem. What is going on with this article?

[Python] Djangoで行レベルのアクセス制限をかける

More than 1 year has passed since last update.

この記事について

Djangoのパッケージdjango-guardiandjango-rulesを使って、行レベルのアクセス制限を実現する方法を紹介します。

Djangoのプロジェクトには最初からユーザー管理とアクセス制御を行うためのアプリケーションが付属しています。しかしこれで可能なのはテーブルレベルで特定の操作(作成・更新・参照 等)を行えるかどうかの制限であり、行レベルでのアクセス制限をかけるには独自に作りこみが必要です。

その手間を解消するため、行レベルのアクセス制限を実現するためのパッケージがいくつか公開されています。その中からよく使われるものを2つとりあげ、それを組み合わせて幅広い要件のアクセス制限に対応する方法を解説します。

★「行レベル」と表現した方が一般的に通じるようなのでこのタイトルにしました。これ以降の説明ではDBの行のことを「オブジェクト」、テーブルのことを「モデル」と表現します。

参考

パッケージの概要

Djangoでオブジェクトレベルのアクセス制限を実現するパッケージは以下の2つが有名です。django-guardianの方が歴史があり普及しています。

django-guardian

特徴

  • 標準のアクセス制限機能を拡張し、モデルレベルのアクセス制御と同じ手法でオブジェクトレベルのアクセス制限の行う。アクセス制限の設定はデータベースの専用テーブルに保存する。
  • アクセス権の付与はコードでAPIを使って行う他、サイト管理アプリ画面から行うことができる。
  • 「ユーザーがアクセスできる全てのオブジェクトを取得」「オブジェクトにアクセスできる全てのユーザーを取得」という便利なAPIが整備されており、アプリケーションの記述を簡易にできる。

django-rules

公式(github):https://github.com/dfunckt/django-rules

特徴

  • 専用モジュール(rules.py)にコーディングでルールを定義する。
  • 定義したルールをアプリケーション内の任意の場所で判定式として利用できる。ビューに対してはデコレーター(クラスベースビューにはMixin)で適用する。
  • 任意のコードを使えるので、ユーザー権限以外に時間や処理に無関係のデータの状態、ファイルの有無などを判定に使うことができる。

django-guardianとdjango-rulesの併用

django-guardianは広く利用されていて洗練されたパッケージですが、権限をデータベースで管理するため、権限の付与処理がコード内に分散してしまい一元的な管理が困難です。また、ルールを変更する場合、データベース上で既存の権限データの修正を大量に行う必要があります。複雑なアクセスルールが必要なアプリケーションには、ちょっと使いづらい仕様です。

そこでコーディングベースでアクセスルールを定義できるdjango-rulesを併用します。
django-rulesは自在にルールを記述し一元的な管理が可能ですが、一時的な権限の付与などの動的なルールには対応していません。この部分をdjango-guardianで補助することで、多様なアクセスルールに対応することができます。以降の記事で併用時の導入方法を説明します。

django-guardian&django-rulesの導入方法

サンプルコード

以降の説明はすでに何かしらのアプリケーションが作成されていることを前提とし、そこにアクセス権限の判定を追加する際の作業となります。

ここでは以前のエントリで作成したアプリケーションに手を加え、アクセス権限のあるユーザーのみデータの編集ができるようにします。

完成品をGithubにおきました。git clone後に以下のコマンドを入力することで動作確認できます。ユーザーは各自追加してください。

※Windows用(Macでは適切に読み替えてください。)

python -m venv env
env\Scripts\activate
pip install -r requirements.txt
manage.py migrate
manage.py createsuperuser 
manage.py runserver

手順1.パッケージのインストール

pipコマンドでパッケージをインストールします。

pip install django-guardian rules

django-rulesのパッケージ名は「rules」です。django-rulesとすると開発放棄された同名パッケージをインストールしようとするので注意してください

手順2.設定ファイル更新

INSTALLED_APPSを変更し、AUTHENTICATION_BACKENDSを追加します。

settings.py
INSTALLED_APPS = [
    
    'guardian',
    'rules.apps.AutodiscoverRulesConfig',
    
]



AUTHENTICATION_BACKENDS = (
    'rules.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend', 
    'guardian.backends.ObjectPermissionBackend',
)

※rules.apps.AutodiscoverRulesConfigはdjango-rulesのオプションで、アプリケーション内のrules.pyを自動的に読み込む設定です。

参考:https://github.com/dfunckt/django-rules#best-practices

手順3.DBマイグレーション

マイグレーションを実行してdjango-guardian が使うテーブルを作成します。

manage.py migrate

実行後、以下のテーブルが作成されます。

  • オブジェクト別のユーザー権限を管理
    • guardian_userobjectpermission
  • オブジェクト別のグループ権限を管理
    • guardian_groupobjectpermission

手順4.admin.pyの更新

django-guardian用のアクセス権付与機能をサイト管理アプリで使えるようにします。

アプリケーションのadmin.pyを開き、継承クラスをadmin.ModelAdminからGuardedModelAdminに変更します。

admin.py
from django.contrib import admin
from guardian.admin import GuardedModelAdmin

# Register your models here.
from .models import Item


@admin.register(Item)
class ItemAdmin(GuardedModelAdmin): #修正

    class Meta:
        verbose_name = 'アイテム'
        verbose_name_plural = 'アイテム'

ここで実際にサイト管理アプリで確認します。オブジェクトの変更画面にアクセスすると、上部に新しいボタンが表示されています。ここより個別の権限を追加することができます。
image.png

手順5.rules.pyの編集

ルールの定義方法の大まかな説明です。詳細は公式を見てください。
公式:https://github.com/dfunckt/django-rules#using-rules

今回は編集ルールとして、「オブジェクトの作成者、または標準の権限管理機能で編集権を与えられた者、またはdjango-guardianよりオブジェクトの編集権を与えられた者にオブジェクトの編集権がある」というルールを作成してみたいと思います。

1.ファイルを作る

まずアプリケーションのディレクトリにファイル rules.py を作ります。django-rules用のルール定義モジュールとなります。このファイルの読み込みは、settings.pyで指定したrules.apps.AutodiscoverRulesConfigで行われます。

2.Django の権限チェックの仕組み

まずDjangoの権限チェックの仕組みについて簡単に説明します。詳しい内容は公式サイトの以下のページを見てください。

Djangoの権限チェックにはUserオブジェクトのhas_permメソッドが使われます。
has_permを実行すると、AUTHENTICATION_BACKENDS に定義されたモジュールのhas_permが定義順に実行されます。ここで一つでも判定がTureになれば権限ありとされます。

has_perm では第一引数に権限名、第二引数に対象オブジェクトを指定します。権限名は「<アプリケーション名>.<操作名>_<モデル名>」というフォーマットです。モデルの新規マイグレーション時に自動的に「作成(add)、更新(change)、削除(delete)という3つの権限が作成されます。データベースのauth_permissionsで管理されるので、実際に見た方が理解が早いです。ちなみにモデル定義内のMETA要素を使って独自の権限を定義することもできます。

この仕組みを使って、標準の権限チェックとdjango-guardianの権限チェックを任意に呼び出すことができます。

  • 標準の権限チェックを使う

引数を一つだけ指定すると標準の権限チェックで判定されます。
※ django-guardianでは、引数1つの場合は常にFalseとなる

   user.has_perm('app.change_item')
  • django-guardianの権限チェックを使う

第二引数に対象オブジェクトを指定するとdjango-guardianで判定されます。
※ 引数2つの場合、標準のチェックは常にFalseとなる

   user.has_perm('app.change_item', item)

これらのチェックと独自のコーディングを組み合わせてrules.pyを作成します。

3.predicate を作る

predicate とは bool値を返す関数という意味です。django-rules のルールを構成する最小要素です。bool値を返す関数を定義しデコレータ @rules.predicate を付与します。

今回作成するルールは汎用クラスビュー用のMixinのPermissionRequiredMixin内で実行されます。PermissionRequiredMixin内ではログインユーザーのhas_permが実行されるので、第一引数にユーザーオブジェクト、第二引数に対象オブジェクトがわたされす。それを前提として predicate を作成します。

rules.py
@rules.predicate
def is_item_owner(user, item):
    return item.created_by == user`

ここではオブジェクトの作成者か否かの判定をしています。判定で引数を使わない場合はfn(user)、fn()という記述でも問題ありません。

加えて「2.Django の権限チェックの仕組み」で説明したように標準チェックとdjango-guardianの権限チェック用の predicate を作成します。

rules.py
# 標準の権限チェック
@rules.predicate
def has_model_level_permission(user):
    return user.has_perm('app.change_item')

# django-guardianの権限チェック
@rules.predicate
def has_object_level_permission(user, item):
    return user.has_perm('app.change_item', item)

django-rulesにはユーザーの有効判定(is_active)や所属グループの判定(is_group_member)を行う組み込みpredicateが用意されています。必要に応じて使ってください。

参考:https://github.com/dfunckt/django-rules#predefined-predicates

4.辞書オブジェクトに登録する。

定義した predicate をrulesの辞書オブジェクトに登録します。

rules.py
import rules
import guardian

@rules.predicate
def is_item_owner(user, item):
    return item.created_by == user

@rules.predicate
def has_model_level_permission(user):
    return user.has_perm('app.change_item')

@rules.predicate
def has_object_level_permission(user, item):
    return user.has_perm('app.change_item', item)

rules.add_perm('app.rules_change_item', is_item_owner |
                                    has_model_level_permission |
                                    has_object_level_permission )

今回はクラスベース汎用ビューにPermissionRequiredMixinを使ってルールを適用させるので、rules.add_permを使います。別にrules.add_ruleというメソッドがあり、こちらはコード内の任意の場所でrules.rule_testメソッドを呼び出して判定処理を行うためのものです。

第一引数に設定している権限名app.rules_change_itemは独自に決めたものです。標準の権限名のフォーマットと異なるフォーマットを使うのがいいでしょう。

Predicateは演算子で結合してOr条件を作っています。演算子についてはdjango-rulesの公式ドキュメントを参照してください。

参考:https://github.com/dfunckt/django-rules#combining-predicates

手順6.ビューにルールを適用する

クラスベース汎用ビューへのアクセスに制限を適用します。
適用は簡単で、PermissionRequiredMixinを継承し、permission_required にrules.pyで作成した権限名を指定するだけです。権限のないユーザーがアクセスすると403ページが表示されます。

views.py
from rules.contrib.views import PermissionRequiredMixin

class ItemUpdateView(PermissionRequiredMixin, LoginRequiredMixin, UpdateView):
    model = Item
    form_class = ItemForm
    success_url = reverse_lazy('index')
    permission_required = 'app.rules_change_item'

関数ビューへの指定は以下のようにデコレータを使います。

views.py
from rules.contrib.views import permission_required, objectgetter
from posts.models import Post

@permission_required('posts.change_post', fn=objectgetter(Post, 'post_id'))
def post_update(request, post_id):
    # ...

参考:https://github.com/dfunckt/django-rules#using-the-function-based-view-decorator

手順7.テンプレートでルールを使う

ルールの判定はテンプレート内でも使用することができます。
django-rulesのテンプレートタグを読込み、コード内と同様の感覚で使うことができます。

item_filter.html
{% load rules %}
…
{% has_perm 'app.rules_change_item' user item as rules_change_item %}
<a class="btn btn-secondary {{ rules_change_item|yesno:',disabled'}}" href="{% url 'update' item.pk %}" >編集</a>

以下のように権限のない機能へのリンクがDisabledとなります。

image.png

django-guardian の API

django-guardianには権限の付与や、所有する権限の一覧、オブジェクトに権限を持つユーザーの一覧を表示するAPIが用意されています。全て公式サイトに説明があるので参照してください。

https://django-guardian.readthedocs.io/en/stable/api/index.html

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away