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


##この記事について

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

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

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

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

## 参考
- [プロダクト開発してわかったDjangoの深〜いパーミッション管理の話 @ PyconJP2017](
https://www.slideshare.net/hirokiky/django-pyconjp2017)

- [Can django-guardian and django-rules be used together?](https://stackoverflow.com/questions/37306677/can-django-guardian-and-django-rules-be-used-together)


##パッケージの概要

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

### django-guardian

- 公式：https://github.com/django-guardian/django-guardian

**特徴**

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

### 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の導入方法

### サンプルコード

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

ここでは[以前のエントリ](https://qiita.com/okoppe8/items/54eb105c9c94c0960f14)で作成したアプリケーションに手を加え、アクセス権限のあるユーザーのみデータの編集ができるようにします。

完成品を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
```

### 手順１．パッケージのインストール

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

```
pip install django-guardian rules
```

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

### 手順２．設定ファイル更新

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

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

###手順３．ＤＢマイグレーション

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

```
manage.py migrate
```

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

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


### 手順４．admin.pyの更新

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

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

```py: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](https://qiita-image-store.s3.amazonaws.com/0/217094/b8ea1c8f-e0c2-e3d5-128f-feaa5fd92ee1.png)

### 手順５．rules.pyの編集

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

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

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

#### ２．Django の権限チェックの仕組み

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

- [公式：Djangoの認証システムを使用する](https://docs.djangoproject.com/ja/2.0/topics/auth/default/)

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

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

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

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

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

```py
   user.has_perm('app.change_item')
```

- django-guardianの権限チェックを使う

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

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

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

#### ３．predicate を作る

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

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


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

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

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

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

#### ４．辞書オブジェクトに登録する。

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

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

### 手順６．ビューにルールを適用する

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


```py: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'

```

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

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

### 手順７．テンプレートでルールを使う

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

```html: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](https://qiita-image-store.s3.amazonaws.com/0/217094/bb77978f-7af2-2517-c1aa-3c6f4011dc42.png)

## django-guardian の API

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

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