Adminで行レベル権限管理

2008年07月31日(木) 01:14 この記事をクリップ!

さてさて、私がDjangoを触り出したのは2005/07/25だったと記憶しているのではや3年が過ぎました。

当時はオープンソース化されたばかりで、リリースバージョンもありませんでした。

世の中はRailsに沸きscaffoldという仕組みに魅せられていたようです。そんななか、Rubyのendに耐えられず、Pythonのインデントには耐えられた私は、DjangoのAdminという次世代の仕組みに心を踊らせていました。

しかし、DjangoのAdminは地方新聞社のウェブサイトを迅速に更新する仕組みとして構築されていたことから、業務としての必要最小限、対象のコンテンツタイプというくくりでしか権限の管理がありませんでした。
つまり、ブログのエントリを編集できるユーザは誰が登録したエントリであっても編集できてしまうのでした。私はAdminに行レベルの権限管理が備わっていれば…とずっと思いつづけてきました。
もちろん行レベルの権限管理の実現が完全にできなかったということではありませんでしたが、ThreadLocalと呼ぶスレッドレベルのグローバル変数を用いる方法しかなかったため、気に入らなかったのです。


時は過ぎ、django projectはついにVersion1.0をリリースしようと動いています。予定がずれなければ2008/09/02に1.0がリリースされます。
時間がかかったのは、開発者たちが完璧主義者であることもありますが、Adminのリファクタリングが大きなものであったことがあります。
今までのAdminは対象のコンテンツタイプ定義にAdminでの設定も記述していました。新しくなったAdminはコンテンツタイプ定義とは別にAdmin用の定義を行うように変更されました。
私はなんでもかんでもルースカップリングというのは好きではありません。おそらく、普段の業務で数千個のファイルで構成された複数のアプリケーションで構成されたJavaプロジェクト(もちろん設定ファイルの数は…)と戦っているからでしょう。私がDjangoを好きな理由の一つは、特定の領域に関するコードはまとまって存在する、という現実的な設計でした。
それでもなお、今回のAdminの変更は歓迎すべきものです。多くのフックポイントが定義され、柔軟にAdminの制御が行えるようになっています。そして、多くのフックポイントがあるがゆえに、コンテンツタイプと定義を分けるという選択はリーズナブルです。


Djangoの新しいAdminで行レベルの権限管理を行えるようになったのか、楽しみに仕組みを見てみました。どうやら行レベルの権限管理ができそうに見えます。まだ、完全に理解をしていないので作りがおかしいかもしれませんが、簡単に行レベルの権限管理を機能追加できるクラスを作成しましてみました。djangoの1.0αで動作の確認をしています。仕事ではないので、テストはありません :)


from django.contrib import admin
from django.db.models import Q
from django.forms.widgets import Select

class RowLevelAdmin(admin.ModelAdmin):
    
    def get_user_field_name(self):
        raise NotImplementedError
    
    def has_add_permission(self, request, obj=None):
        self._author = request.user
        if not obj:
            return True
        return (request.user.is_superuser
                or ('%s' % (request.user.id,) == request.POST.get(self.get_user_field_name(), None)))
    
    def has_change_permission(self, request, obj=None):
        self._author = request.user
        if not obj:
            return True
        return (request.user.is_superuser
                or ((request.POST.get(self.get_user_field_name(), None) == None
                    or '%s' % (request.user.id,) == request.POST.get(self.get_user_field_name(), None))
                    and request.user == getattr(obj, self.get_user_field_name())
                    and super(RowLevelAdmin, self).has_change_permission(request, obj)))
    
    def has_delete_permission(self, request, obj=None):
        self._author = request.user
        if not obj:
            return True
        return (request.user.is_superuser
                or ((request.POST.get(self.get_user_field_name(), None) == None
                    or '%s' % (request.user.id,) == request.POST.get(self.get_user_field_name(), None))
                    and request.user == getattr(obj, self.get_user_field_name())
                    and super(RowLevelAdmin, self).has_delete_permission(request, obj)))
    
    def queryset(self, request):
        default_queryset = super(RowLevelAdmin, self).queryset(request)
        if not request.user.is_superuser:
            kwarg = {self.get_user_field_name():request.user}
            return default_queryset.filter(**kwarg)
        return default_queryset
    
    def formfield_for_dbfield(self, db_field, **kwargs):
        field = super(RowLevelAdmin, self).formfield_for_dbfield(db_field, **kwargs)
        if not self._author.is_superuser:
            if db_field.name == self.get_user_field_name():
                field.widget = Select(choices=(('%d' % self._author.id, unicode(self._author)),))
        return field


行レベルの権限管理を行いたい場合には、このクラスを継承してAdminModelを作成します。
継承したクラスでは、get_user_field_nameメソッドを上書きし、django.contrib.auth.models.UserをFKにしているフィールドの名前を指定します。すると、次のような権限のチェックを行うようになります。

  1. 一覧には、ログインしたユーザがFKのデータとして登録されているデータのみ表示されます

  2. 登録時、UserをFKとするフィールドのセレクトにログインしたユーザのみが表示されます。また、UserをFKとするフィールドの値とログインしているユーザのIDが一致しない場合には権限エラーが発生します

  3. 変更時、登録時と同様な権限チェックとともに、現在登録されているデータのUserをFKとするフィールドの値とログインしているユーザのIDが一致しない場合には権限エラーが発生します

  4. 削除時、現在登録されているデータのUserをFKとするフィールドの値とログインしているユーザのIDが一致しない場合には権限エラーが発生します


ただし、ログインユーザがスーパー管理者権限を保持している場合には通常どおりに動作します。


次のように利用します。Memoが行レベルで権限管理できます。

#models.py
from django.db import models
from django.contrib.auth.models import User

class Memo(models.Model):
    note = models.CharField(max_length=100)
    writer = models.ForeignKey(User)

#admin.py
from models import Memo

class MemoAdmin(RowLevelAdmin):
    list_display = ('note',)

    def get_user_field_name(self):
        return 'writer'

admin.site.register(Memo, MemoAdmin)


djangoが正式に1.0になったら、もう少しましにしてPyPIに登録します。


 
ponybadge

Powered by

Feedbacks

Tags

Calendar