TL;DR
- Djangoの権限管理ライブラリである、django-rulesはインスタンス同士で権限管理できる
- django-rulesの機能として、Djangoに内蔵されている権限と認可機能との統合がある
- この統合機能により
has_permを使うとインスタンス同士の比較だけでなくデータべースのレコードのチェック行われてしまう - インスタンス同士の比較に留めたい場合は
rules.test_ruleを使おう
django-rulesとは
Djangoで権限管理をしたい場合、Djangoに内蔵されている権限と認可機能を利用したいと考えると思います。
しかし、このデフォルトの機能は権限をデータベースのレコードに保存するため、取り回しが面倒です。
Ruby on Railsでよく使われるPunditやPHPのLaravelにある認可のしくみ のようにデータべースのレコードでなく、インスタンス同士で比較する形をDjangoでも実現したい場合に便利なのがdjango-rulesです。
django-rulesに関しては以下の記事が参考になります。
- Djangoで組織とユーザーの権限の管理をやってみる #devio2022 | DevelopersIO
- django-rulesを使って、オブジェクトレベルの認可判定をViewとテンプレートでそれぞれ実装してみた - メモ的な思考的な
なお、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の権限と認可機能を透過的に利用することができます。
これは便利な一方で、意図しないデータべースのアクセス増加を引き起こす場合があります。
具体的には :
has_permメソッドが呼ばれると、Djangoは設定された認証バックエンドを順番に試しますrules.permissions.ObjectPermissionBackendが処理を試みます
該当するルールがない場合や、結果がFalseの場合、次の
django.contrib.auth.backends.ModelBackendに処理が移りますModelBackendはauth_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_perm はdjango-rulesのRuleSet内の権限チェックで完結すると思い、Django Templateで呼び出したところ急激にデータべースの負荷があがったという失態を犯してしまったのでした…
この記事が他の方に役立てば幸いです。