Adminで行レベル権限管理

2008/07/31 01:14

※ 商品のリンクをクリックして何かを購入すると私に少額の報酬が入ることがあります【広告表示】

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

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

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

しかし、DjangoのAdminは地方新聞社のウェブサイトを迅速に更新する仕組みとして構築されていたことから、業務としての必要最小限、対象のコンテンツタイプというくくりでしか権限の管理がありませんでした。

つまり、ブログのエントリを編集できるユーザは誰が登録したエントリであっても編集できてしまうのでした。私はAdminに行レベルの権限管理が備わっていれば…とずっと思いつづけてきました。

もちろん行レベルの権限管理の実現が完全にできなかったということではありませんでしたが、ThreadLocalと呼ぶスレッドレベルのグローバル変数を用いる方法しかなかったため、気に入らなかったのです。

時は過ぎ、djangoprojectはついに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にしているフィールドの名前を指定します。すると、次のような権限のチェックを行うようになります。

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

次のように利用します。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に登録します。

Prev Entry

Next Entry