読者です 読者をやめる 読者になる 読者になる

Django1.9で追加されたPermission mixinsを使って、パーミッションなどのアクセス制御を試してみた

Django Python

Djangoパーミッションまわりを調べていたところ、Django1.9より

  • AccessMixin
  • LoginRequiredMixin
  • PermissionRequiredMixin
  • UserPassesTestMixin

が追加されたということを知りました。

 
そこで、リリースノートにPermission mixinsとして書かれている、LoginRequiredMixin、PermissionRequiredMixin、UserPassesTestMixinを使ったアクセス制御を試してみました。

なお、AccessMixinは上記3つの親クラスっぽいので、今回扱いません。
django/mixins.py - django/django

 

環境

 

出てくる用語

用語の内容については、以下が参考になりました。

その中でも気になったものをメモしておきます。

 

パーミッション (Permissions)

以下の役割を持つもので、設定先は各Modelとなります。

あるユーザが特定のタスクを実行してよいかどうかを決める、バイナリ (yes/no) のフラグ
Django でのユーザ認証 - 概要 — Django 1.4 documentation

Binary (yes/no) flags designating whether a user may perform a certain task.
User authentication in Django - Overview | Django documentation | Django

 
デフォルトのパーミッションとして、各Modelにaddchangedeleteがあります。
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 documentation

A 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を使う
  • ログイン状態やパーミッションにより表示内容を制限
    • userpermsのオブジェクトをテンプレートで使う
    • 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つを行います。

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

f:id:thinkAmi:20160202231256p:plain

 

auth_group

f:id:thinkAmi:20160202231303p:plain

 

auth_user_groups

f:id:thinkAmi:20160202231311p:plain

 

auth_permission

f:id:thinkAmi:20160202231318p:plain

 

auth_user_user_permissions

f:id:thinkAmi:20160202231325p:plain

 

auth_group_permissions

f:id:thinkAmi:20160202231334p:plain

 
なお、ユーザやグループの作成やパーミッションの割り当てについては、以下が参考になりました。

 

ログイン/ログアウトの実装

以前の方法で実装します。
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

 

動作確認
ログインしていない場合

f:id:thinkAmi:20160202222221p:plain

 

ログインしている場合

f:id:thinkAmi:20160202222230p:plain

 

ログインし、かつ、条件を満たすユーザの場合、内容を表示

実装

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ページは先ほど作成したのを流用します。

 

動作確認
ログインしているが、メールアドレスが条件を満たさない場合

f:id:thinkAmi:20160202222424p:plain

 

ログインしており、メールアドレスも条件を満たす場合

f:id:thinkAmi:20160202222431p:plain

 

パーミッションを持つユーザやグループの場合、内容を表示

実装

PermissionRequiredMixinをViewに組み込みます。
Using the Django authentication system - The PermissionRequiredMixin mixin | Django documentation | Django

アクセス可能なパーミッションを、クラスメンバpermission_requiredに指定します。形式は以下の通りです。

この結果、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ページは先ほど作成したのを流用します。

 

動作確認
パーミッション無しのユーザの場合

f:id:thinkAmi:20160202222641p:plain

 

パーミッションがあるユーザの場合

f:id:thinkAmi:20160202222650p:plain

 

パーミッションがあるグループに所属しているユーザの場合

f:id:thinkAmi:20160202222657p:plain

 

ログイン状態やパーミッションにより表示内容を制限

表示内容の制限はテンプレートで行います。制限方法は以下となります。

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

を作成します。

 

確認
ログインしていない場合

f:id:thinkAmi:20160202222938p:plain

 

ログインしているがパーミッションがない場合

f:id:thinkAmi:20160202222945p:plain

 

ログインしており、かつ、パーミッションがある場合

f:id:thinkAmi:20160202222957p:plain

 

ソースコード

GitHubに上げました。
thinkAmi-sandbox/Django_permissions_sample - Python

 
なお、ユーザ・グループの登録とパーミッションの割り当てについては、Djangoのコマンドとしても動作するように myapp/management/commands/create_users.py ファイルとして作成しました。Django shellで使う場合には、そこから必要な部分を抜き出せばいいかと思います。

また、Djangoのコマンドで同じユーザを登録するのを避けるため、以下を行いました。

 
他に、上記では403ページを表示するパターンのみを書きましたが、GitHubソースコードではログインページヘのリダイレクト版も実装してあります。