django-rules:has_permの罠とその解決策

TL;DR

django-rulesとは

Djangoで権限管理をしたい場合、Djangoに内蔵されている権限と認可機能を利用したいと考えると思います。

しかし、このデフォルトの機能は権限をデータベースのレコードに保存するため、取り回しが面倒です。

Ruby on Railsでよく使われるPunditPHPのLaravelにある認可のしくみ のようにデータべースのレコードでなく、インスタンス同士で比較する形をDjangoでも実現したい場合に便利なのがdjango-rulesです。

django-rulesに関しては以下の記事が参考になります。

なお、Djangoの認証認可については id:hirokiky さんのPyconjp 2017のスライド もわかりやすいです。(最終的に自作していてすごい)

django-rulesの has_perm の問題点

さて、ここから本題です。 django-rulesにはインスタンス同士の比較だけでなく、前述したDjango内蔵している権限と認可機能とも統合できるメソッドがあります。それが、 has_perm です。

このhas_permというメソッド名、Djangoが持っているメソッドと同名です。

django-rulesのREADMEにあるように以下の設定をすることで

# settings.py
AUTHENTICATION_BACKENDS = (
    'rules.permissions.ObjectPermissionBackend',
    'django.contrib.auth.backends.ModelBackend',
)

django-rulesで書いたルールとDjangoの権限と認可機能を透過的に利用することができます。

これは便利な一方で、意図しないデータべースのアクセス増加を引き起こす場合があります。

具体的には :

  1. has_permメソッドが呼ばれると、Djangoは設定された認証バックエンドを順番に試します

  2. rules.permissions.ObjectPermissionBackendが処理を試みます

  3. 該当するルールがない場合や、結果がFalseの場合、次のdjango.contrib.auth.backends.ModelBackendに処理が移ります

  4. ModelBackendauth_permission テーブルをチェックするためのクエリを発行します

AUTHENTICATION_BACKENDSの処理を行っているコードはこちら(2025-03-27現在のLTSである4.2のもの)

特にdjango-rulesが提供するカスタムテンプレートタグ has_permをテンプレート内で利用すると、テンプレートのレンダリング中にデータべースへのアクセスが発生する原因になります。

解決策: test_rule を使う

django-rulesが提供した範囲内で権限チェックを行えるようRuleSet.test_rule というメソッドを提供しています。これを使うことで、Djangoの認証バックエンド連鎖を経由せず、django-rulesで定義したルールだけをチェックできます。

このtest_ruleを呼び出すためのカスタムテンプレートタグ test_ruleも用意 されているので利用することができます。

おわりに

てっきり has_permdjango-rulesのRuleSet内の権限チェックで完結すると思い、Django Templateで呼び出したところ急激にデータべースの負荷があがったという失態を犯してしまったのでした…

この記事が他の方に役立てば幸いです。