Djangoでパーミッションまわりを調べていたところ、Django1.9より
- AccessMixin
- LoginRequiredMixin
- PermissionRequiredMixin
- UserPassesTestMixin
が追加されたということを知りました。
- Django 1.9で追加されたメジャー機能の紹介 - 僕とコードとブルーハワイ
- Django 1.9 release notes - Permission mixins for class-based views | Django documentation | Django
そこで、リリースノートにPermission mixinsとして書かれている、LoginRequiredMixin、PermissionRequiredMixin、UserPassesTestMixinを使ったアクセス制御を試してみました。
なお、AccessMixinは上記3つの親クラスっぽいので、今回扱いません。
django/mixins.py - django/django
目次
- 環境
- 出てくる用語
- 試す内容
- 準備
- ログイン/ログアウトの実装
- ログインしている場合、内容を表示
- ログインし、かつ、条件を満たすユーザの場合、内容を表示
- パーミッションを持つユーザやグループの場合、内容を表示
- ログイン状態やパーミッションにより表示内容を制限
- ソースコード
環境
- Windows 10
- Python 3.5.1
- Django 1.9.2
出てくる用語
用語の内容については、以下が参考になりました。
- Django Authenticationのパーミッション、グループに関するメモ - blog.ryotarai.info
- [django] Djangoメモ書き permisson - at_yasuの日記もといメモ書き
その中でも気になったものをメモしておきます。
パーミッション (Permissions)
以下の役割を持つもので、設定先は各Modelとなります。
あるユーザが特定のタスクを実行してよいかどうかを決める、バイナリ (yes/no) のフラグ
Django でのユーザ認証 - 概要 — Django 1.4 documentationBinary (yes/no) flags designating whether a user may perform a certain task.
User authentication in Django - Overview | Django documentation | Django
デフォルトのパーミッションとして、各Modelにadd
、change
、delete
があります。
Using the Django authentication system - Default permissions | Django documentation | Django
また、必要に応じて、自分で各Modelにパーミッションを設定できます。
Customizing authentication in Django - Custom permissions | Django documentation | Django
パーミッションはModelで定義すると、Permissionモデルへも反映されます。
django.contrib.auth - Permission model | Django documentation | Django
グループ (Groups)
以下の役割を持つものです。
複数のユーザに対してラベル付したり、認証を設定したりするための一般的な方法です。
Django でのユーザ認証 - 概要 — Django 1.4 documentationA generic way of applying labels and permissions to more than one user.
User authentication in Django - Overview | Django documentation | Django
設定先はGroupモデルになります。
django.contrib.auth - Group model | Django documentation | Django
試す内容
今回は
- ログインしている場合に表示
LoginRequiredMixin
viewを使う
- ログインし、かつ、条件を満たすユーザの場合に表示
UserPassesTestMixin
viewを使う
- パーミッションがあるユーザの場合に表示
PermissionRequiredMixin
viewを使う
- ログイン状態やパーミッションにより表示内容を制限
user
やperms
のオブジェクトをテンプレートで使う- Permission mixins関係ない...
を試してみます。
参考までに、GitHubにもソースコード全体を上げてあります。
thinkAmi-sandbox/Django_permissions_sample - Python
準備
コマンドプロンプトでの実行
今回、ユーザ登録でDjango shell + IPythonを行うため、Djangoの他にIPythonも入れます。
IPythonの使い方 - Qiita
d:\Sandbox\django_permissions_sample>virtualenv -p c:\python35-32\python.exe env Running virtualenv with interpreter c:\python35-32\python.exe d:\Sandbox\django_permissions_sample>env\Scripts\activate (env) d:\Sandbox\django_permissions_sample>pip install ipython (env) d:\Sandbox\django_permissions_sample>pip install django (env) d:\Sandbox\django_permissions_sample>django-admin startproject myproject . (env) d:\Sandbox\django_permissions_sample>python manage.py startapp myapp # スーパーユーザーを作成するために、事前にmigrateしておく # migrateしない場合、"jango.db.utils.OperationalError: no such table: auth_user" というエラー発生 (env) d:\Sandbox\django_permissions_sample>python manage.py migrate # スーパーユーザーの作成 (env) d:\Sandbox\django_permissions_sample>python manage.py createsuperuser --username=admin --email=admin@example.com # パスワードも"admin"としたところ、エラーで作成できず Password: Password (again): This password is too short. It must contain at least 8 characters. This password is too common. # パスワードは、"admin-password"とした Password: Password (again): Superuser created successfully.
settings.pyへの設定
以下の2つを行います。
INSTALL_APPS
に、django.contrib.auth
とdjango.contrib.contenttypes
が存在することを確認INSTALL_APPS
に、myapp
アプリを追加
myproject/settings.py
INSTALLED_APPS = [ ... 'django.contrib.auth', 'django.contrib.contenttypes', ... 'myapp', ]
Modelまわり
パーミッションはModelのMetaオプションで定義します。
Customizing authentication in Django - custom-permissions | Django documentation | Django
そのため、今回のModelは以下の内容で作成します。
can_view
というパーミッションを用意- 表示内容の制限を確認しやすくするため、3つのフィールドを用意
myapp/models.py
from django.db import models class Article(models.Model): public_content = models.CharField('public', max_length=255) private_content = models.CharField('private', max_length=255) permission_content = models.CharField('private', max_length=255) # Metaオプションにて、パーミッション設定 class Meta: permissions = ( ("can_view", "Can see content"), )
また、Model用のfixtureも作成します。
myapp/fixtures/initial_data.json
[ { "model": "myapp.Article", "pk": 1, "fields": { "public_content": "for all users", "private_content": "for logged-in users", "permission_content": "for permission users/groups" } } ]
最後に、マイグレーションを行い、fixtureをModelへロードします。
(env) d:\Sandbox\django_permissions_sample>python manage.py makemigrations (env) d:\Sandbox\django_permissions_sample>python manage.py migrate (env) d:\Sandbox\django_permissions_sample>python manage.py loaddata initial_data
ユーザやグループの作成と、パーミッションの割り当て
今回作成するユーザやグループは以下の通りです。
種類 | 名前 | パーミッション | 所属するグループ |
---|---|---|---|
ユーザ | no-perm | - | - |
ユーザ | perm | can_view |
- |
ユーザ | group | - | viewable_users |
グループ | viewable_users | can_view |
- |
以下を参考に、今回はDjango shell + IPythonで作業を行います。
まずは、Django shell + IPython を起動します。
(env) d:\Sandbox\django_permissions_sample>python manage.py shell -i ipython In [1]: (この位置でカーソルが点滅)
続いて、以下の内容をDjango shellへコピー&ペーストします。
from django.contrib.auth.models import User, Permission, Group # パーミッションなしのユーザ User.objects.create_user('no-perm', 'no-perm@example.com', 'no-permpassword') # この時点で、"no-perm"ユーザが"auth_user"テーブルに保存されている # パーミッションありのユーザ perm = User.objects.create_user('perm', 'perm@example.com', 'permpassword') permission = Permission.objects.get(codename='can_view') perm.user_permissions.add(permission) # この時点で、パーミッションが"auth_user_user_permissions"テーブルに保存されている # グループに対してパーミッションがあるユーザ # まずグループを作る group = Group.objects.create(name='viewable_users') # グループにパーミッションを割当 group.permissions.add(permission) # この時点で、"auth_group_permissions"テーブルに保存されている # 続いてユーザを作る group_user = User.objects.create_user('group', 'group@example.com', 'grouppassword') # ユーザをグループに割り当て group_user.groups.add(group) # この時点で、"auth_user_groups"テーブルに保存されている self.stdout.write(self.style.SUCCESS('Complete'))
ペーストすると、group_user.groups.add(group)
の後ろにカーソルがあると思います。Enter
キーを押すことで、ユーザやグループの作成とパーミッションの割り当てが終わります。
最後に、Django shellを終わらせるために、Ctrl + Z
の後にEnter
キーを押すと
Do you really want to exit ([y]/n)?
と表示されます。デフォルトでy
が選択されているのを確認し、Enter
キーを押します。
この結果、各テーブルは以下の内容となりました。
auth_user
auth_group
auth_user_groups
auth_permission
auth_user_user_permissions
auth_group_permissions
なお、ユーザやグループの作成やパーミッションの割り当てについては、以下が参考になりました。
ログイン/ログアウトの実装
以前の方法で実装します。
Djangoで、Djangoアプリ単体でのユーザ作成・変更・認証・パスワード変更・パスワードリセットを試してみた - メモ的な思考的な
ログインしている場合、内容を表示
実装
LoginRequiredMixin
をViewに組み込みます。
Using the Django authentication system - The LoginRequired mixin | Django documentation | Django
未ログイン時の動作は以下のいずれかになります。今回は後者にします。
- ログインページヘのリダイレクト
- クラスメンバ
raise_exception
に何も設定しないか、Falseを設定
- クラスメンバ
- 403ページの表示
- クラスメンバ
raise_exception
にTrueを設定
- クラスメンバ
また、ログインページのURLの設定先は以下のいずれかになります。今回は後者にします。
- クラスメンバの
login_url
に設定 - settings.pyの
LOGIN_URL
参考:Using the Django authentication system - AccessMixin - login_url | Django documentation | Django
この結果、Viewは以下の通りとなります。
myapp/views.py
# 未ログインの場合、403ページを表示する版 class LoginRequiredWith403View(LoginRequiredMixin, TemplateView): template_name = 'myapp/login_required.html' raise_exception = True
他に、
- myapp/urls.py
- myapp/templates/myapp/login_required.html
を作成します。
また、現在ログイン中のユーザを403ページに表示させたいため、自作の403ページファイル(myapp/templates/403.html
)も作成しておきます。
Built-in Views - The 403 (HTTP Forbidden) view | Django documentation | Django
動作確認
ログインしていない場合
ログインしている場合
ログインし、かつ、条件を満たすユーザの場合、内容を表示
実装
UserPassesTestMixin
をViewに組み込みます。
Using the Django authentication system - class UserPassesTestMixin | Django documentation | Django
条件はtest_func()
メソッドに実装します。戻り値がTrue
の場合はアクセス可能、False
の場合はアクセス不可となります。
今回はUserモデルのemailがpermで始まるユーザのみアクセス可能にします。なお、ログインしていない時のユーザAnonymousUser
はemail属性を持っていないことに注意します。
django.contrib.auth - AnonymousUser object | Django documentation | Django
この結果、Viewは以下の通りとなります。
myapp/views.py
class LimitedUserRequiredWith403View(UserPassesTestMixin, TemplateView): template_name = 'myapp/limited_user_required.html' raise_exception = True def test_func(self): if not hasattr(self.request.user, 'email'): return False return self.request.user.email.startswith('perm')
他に、
- myapp/urls.py
- myapp/templates/myapp/limited_user_required.html
も作成します。403ページは先ほど作成したのを流用します。
動作確認
ログインしているが、メールアドレスが条件を満たさない場合
ログインしており、メールアドレスも条件を満たす場合
パーミッションを持つユーザやグループの場合、内容を表示
実装
PermissionRequiredMixin
をViewに組み込みます。
Using the Django authentication system - The PermissionRequiredMixin mixin | Django documentation | Django
アクセス可能なパーミッションを、クラスメンバpermission_required
に指定します。形式は以下の通りです。
- パーミッションは、
<app label>.<Permissionクラスのcodename>
という形で指定 - 指定する中身は、文字列かタプル
この結果、Viewは以下の通りとなります。
myapp/views.py
class PermissionRequiredWith403View(PermissionRequiredMixin, TemplateView): template_name = 'myapp/permission_required.html' permission_required = ('myapp.can_view',) raise_exception=True
他に、
- myapp/urls.py
- myapp/templates/myapp/permission_required.html
も作成します。403ページは先ほど作成したのを流用します。
動作確認
パーミッション無しのユーザの場合
パーミッションがあるユーザの場合
パーミッションがあるグループに所属しているユーザの場合
ログイン状態やパーミッションにより表示内容を制限
表示内容の制限はテンプレートで行います。制限方法は以下となります。
- ログインしているか:
user.is_authenticated
- パーミッションがあるか:
perms.myapp.can_view
myapp/templates/myapp/article.html
<div id="main"> <p>Public content: <strong>{{ object.public_content }}</strong></p> <p>Private content: {% if user.is_authenticated %} <strong>{{ object.private_content }}</strong> {% else %} <strong>!! Login required !!</strong> {% endif %} </p> <p>Permission content: {% if perms.myapp.can_view %} <strong>{{ object.permission_content }}</strong> {% else %} <strong>!! Permission required !!</strong> {% endif %} </p> </div>
他に、
- myapp/views.py
- myapp/urls.py
を作成します。
確認
ログインしていない場合
ログインしているがパーミッションがない場合
ログインしており、かつ、パーミッションがある場合
ソースコード
GitHubに上げました。
thinkAmi-sandbox/Django_permissions_sample - Python
なお、ユーザ・グループの登録とパーミッションの割り当てについては、Djangoのコマンドとしても動作するように myapp/management/commands/create_users.py
ファイルとして作成しました。Django shellで使う場合には、そこから必要な部分を抜き出せばいいかと思います。
また、Djangoのコマンドで同じユーザを登録するのを避けるため、以下を行いました。
- ユーザーの存在チェックを、
get()
を使って行う
他に、上記では403ページを表示するパターンのみを書きましたが、GitHubのソースコードではログインページヘのリダイレクト版も実装してあります。