【Django】Auth機能とMessageフレームワークを使ってユーザーごとにアクセス権限を設ける方法

# 概要
* Djangoにて、ユーザー（グループ）ごとにページへのアクセス制限が設定し、アクセス権がない場合はフラッシュメッセージ（アラートみたいなポップアップ）を表示する実装を行いました。
    * デフォルトで実装されているauth機能と、メッセージフレームワークを利用します。
* 本記事ではその実装方法を記載します。

## 前提
* ディレクトリ構造は以下の通りです(関係ないものは省略)。

```
app
├── app
│   ├── settings.py
│   └── etc...
├── home
│   ├── views.py
│   ├── urls.py
│   ├── etc...
│   └── templates/pages
│       ├── base.html
│       └── home.html
└── sample
│   ├── views.py
│   ├── urls.py
│   └── templates/sample
│       ├── base.html
│       └── sample.html
└── sample2 etc...
```

* `home.html`には、`sample`や`sample2`のボタンがあり、それをクリックするとそれぞれの`html`に飛べる状態になっています。今回のアクセス制限では、ボタンを押してもユーザーが該当のグループに属していない場合はホーム画面にリダイレクトされる実装を行います。
* URLの直打ちでもルートにリダイレクトする設定になっています。

# 実装内容
①Auth機能でGroup作成＆User追加
②`settings.py`の編集
③`urls.py`の編集
④`views.py`の編集
⑤`html`テンプレートの編集

## ①Auth機能でGroup作成＆User追加
* Djangoの管理者アカウントでログインします。
* Admin画面から「Group」を選択すると、以下の画面に遷移されるので、任意の名前でグループを作成します。今回は「writers」とします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/ed2d6386-a183-7184-ec9b-828355f2531a.png)

* 次に、「Users」を選択して、任意のユーザー「hoge」を作成後、先ほどのグループに追加してあげます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/8f3cea73-5ef2-33fe-611b-8eae9a0eaece.png)

* 追加が終わったら、SAVEで保存します。
* これで、「writers」グループにユーザー「hoge」を追加することができました。

## ②`settings.py`の編集
* まず、`'django.contrib.messages'`や`'django.contrib.messages.middleware.MessageMiddleware'`が記載されていることを確認します（デフォルトで記載されているはず）。

```app/app/settings.py

INSTALLED_APPS = [
    # 他のアプリケーション
    'django.contrib.messages',
]

MIDDLEWARE = [
    # 他のミドルウェア
    'django.contrib.messages.middleware.MessageMiddleware'
]


MESSAGE_STORAGE = 'django.contrib.messages.storage.session.SessionStorage'
```

* また、`MESSAGE_STORAGE`を追記します。これにより、メッセージはセッションストレージを使用して保存されるようになります。

## ③`urls.py`の編集

```app/sample/urls.py
from django.urls import path
from . import views

urlpatterns = [
   path('', views.restricted_for_writers, name="sample"),
]
```

* ルートURLに対して`views.restricted_for_writers`ビューを関連付けます。`restricted_for_writers`関数は、次の`views.py`で定義します。

## ④`views.py`の編集

```app/sample/views.py
from django.shortcuts import render
from django.shortcuts import redirect
from django.contrib import messages

# Loginユーザーのみアクセス可
@login_required
def index(request):
    context = list_param(None)
    return render(request, 'sample/sample.html', context)

# 該当のグループに属している場合のみアクセス可
def for_groups_only(*group_names):
    def decorator(view_func):
        def wrapper(request, *args, **kwargs):
            user_groups = request.user.groups.values_list('name', flat=True)
            if any(group_name in user_groups for group_name in group_names):
                return view_func(request, *args, **kwargs)
            else:
                messages.warning(request, 'You do not have permission')
                return redirect('/')
        return wrapper
    return decorator

# このビューは 'writers' または 'checkers' グループに属するユーザーのみがアクセスできる
@for_groups_only('writers', 'checkers')
def restricted_for_writers(request):
    return render(request, 'sample/sample.html')

```

* `for_groups_only`デコレータは、指定されたグループに属するユーザーのみがアクセスできるようにするためのデコレータです。
* 「writers」はDjango Authで作成したグループ名です。

## ⑤`html`テンプレートの編集

* `base.html`テンプレート内には、メッセージフレームワークで表示されるメッセージを画面上部に表示するためのコードを記載します。
* 今回は以下のサイトよりコピーさせていただきました（Bootstrap4で表示）。
    * [Djangoで通知メッセージを利用する（メッセージフレームワーク）](https://dot-blog.jp/news/django-messages-frame-work/)

```python:app/home/templates/pages/base.html
# 略

<body>

{% if messages %}
<ul class="messages_ul">
  {% for message in messages %}
    <li class="alert{% if message.tags %} alert-{{ message.tags }}{% endif %}" role="alert">{{ message }}</li>
  {% endfor %}
</ul>
{% endif %}

# 略
```

## 完成。いざ検証！
* 作成したユーザー「hoge」でログインしている状態で、アクセス権限のないURLへ飛ぶボタンをクリックすると、ホーム画面に戻って以下のメッセージがページ上部に表示されることを確認できました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2710373/20d48eb9-acac-83cf-587a-80211b8f885a.png)

* 今回は`warning`を指定していますが、他にも以下のような項目があります。

```
debug
info
success
warning
error
```

* ここのメッセージ内容や色は任意に変更ができます（詳しくはCSSなど）。Bootstrapの利用方法など、以下にも記載あるのでご参考ください。
    * [【Django】通知メッセージを表示する方法（メッセージフレームワーク）](https://office54.net/python/django/display-message-framework)
* 今回の実装方法では、URLの直打ちでも強制的にリダイレクトができます。実際に打ち込んでみると、ちゃんとホーム画面に遷移されました。

## 備考
* ちなみに、`user_passes_test`デコレータを使った実装方法もあります。ただし、この場合、リダイレクト先として`login_url='/'`と指定しているので、アクセス権限がない場合はルートURL`('/')`に飛んでしまい、メッセージの表示ができませんでした。関数の中に`if not...`と記載してメッセージのコードを記載しても、リダイレクトが優先されてしまうようです。メッセージが不要な場合は以下の方が簡潔に実装できます。(このやり方で、メッセージも表示できる方法をご存知の方いましたらぜひ教えてください)

```views.py
from django.shortcuts import render
from django.contrib.auth.decorators import user_passes_test

@user_passes_test(lambda u: u.groups.filter(name='writers').exists(), login_url='/')
def restricted_for_writers(request):
    return render(request, 'sample/sample.html')
```


* 以上、いろいろやってみた結果、Auth機能とMessageフレームワークを使ったアクセス権限を設定する方法を実現できました。
* 今回の実装方法では、ユーザーによってはアクセスできないボタンがホーム画面に出る状態です。ユーザーごとにボタンの表示が変わる方がセキュリティ的には好ましいと思うので、別途その実装もしてみようと思います。
    * →23.05.18に投稿：[【Django】Auth機能とカスタムテンプレートタグによってユーザーごとにアクセス可能なボタンを表示・非表示する方法](https://qiita.com/Ryo-0131/items/56080d504d823f5d87ef)
* また、「アクセス権限がありません」というページを準備して、「ホーム画面に戻る」みたいなボタンを下部に設置するのも親切かもしれませんね。