アクセス制御の基本概念
アクセス制御とは、ソフトウェアシステムにおける各種リソースへのアクセスと操作を管理する仕組みです。システム内のデータベース、メモリ、ストレージ内のデータはすべてリソースとして扱われます。
リソースは静的な存在であり、適切なアクションを通じてアクセス・操作する必要があります。権限管理の本質は、データリソースへのアクセスアクションを制御することにあります。
アクションは以下の2種類に分類できます:
- データリソース操作:データベースやファイルのデータに直接アクセス・操作する
- ビジネスロジック実行:データリソースを介してビジネスプロセスを実行する
RBACモデルの設計
RBAC(Role-Based Access Control)はロールベースのアクセス制御モデルです。基本的な関係性は以下の通りです:
- 一人のユーザーは複数のロールを持てる
- 一つのロールは複数のユーザーに割り当て可能
- 一つのロールは複数の権限を持てる
- 一つの権限は複数のロールに割り当て可能
from django.db import models
class AccessRight(models.Model):
"""
アクセス権限モデル
"""
name = models.CharField('権限名', max_length=64)
pattern = models.CharField('URLパターン(正規表現)', max_length=256)
def __str__(self):
return self.name
class UserGroup(models.Model):
"""
ユーザーグループ(ロール)モデル
"""
group_name = models.CharField('グループ名', max_length=64)
access_rights = models.ManyToManyField(
AccessRight,
verbose_name='割り当てられた権限',
blank=True
)
def __str__(self):
return self.group_name
class Employee(models.Model):
"""
従業員(ユーザー)モデル
"""
username = models.CharField('ユーザー名', max_length=64)
passwd = models.CharField('パスワード', max_length=128)
email = models.EmailField('メールアドレス', max_length=100)
groups = models.ManyToManyField(
UserGroup,
verbose_name='所属グループ',
blank=True
)
def __str__(self):
return self.username
権限注入とセッション管理
ユーザーが正常にログインした後、権限情報をセッションに保存し、各リクエスト時に権限検証を行います。
def authenticate_user(request):
if request.method == "POST":
username = request.POST.get("username")
password = request.POST.get("password")
employee = Employee.objects.filter(
username=username,
passwd=password
).first()
if employee:
# セッションにユーザーIDを保存
request.session["employee_id"] = employee.pk
# 権限情報をセッションに注入
setup_permission_session(employee, request)
return HttpResponse("ログイン成功")
return render(request, "auth/login.html")
def setup_permission_session(employee, request):
# ユーザーの全権限を取得
rights = employee.groups.all().values(
"access_rights__pattern"
).distinct()
permission_patterns = []
for right in rights:
permission_patterns.append(right["access_rights__pattern"])
request.session["permission_patterns"] = permission_patterns
ミドルウェアによる権限検証
ミドルウェアを実装して、全リクエストに対して一貫した権限検証を行います。特定のURL(ログイン、管理画面など)はホワイトリストに追加します。
import re
from django.utils.deprecation import MiddlewareMixin
from django.shortcuts import HttpResponse, redirect
class AuthorizationMiddleware(MiddlewareMixin):
def process_request(self, request):
current_route = request.path_info
# ホワイトリストチェック
whitelist_patterns = [
r"/auth/login/",
r"/auth/register/",
r"/admin/.*",
r"/static/.*"
]
for pattern in whitelist_patterns:
if re.match(pattern, current_route):
return None
# ログイン状態確認
if not request.session.get("employee_id"):
return redirect("/auth/login/")
# 権限検証
permission_patterns = request.session.get(
"permission_patterns",
[]
)
has_permission = False
for pattern in permission_patterns:
full_pattern = f"^{pattern}$"
if re.match(full_pattern, current_route):
has_permission = True
break
if not has_permission:
return HttpResponse("アクセス権限がありません")
return None
動的UI制御
テンプレートレベルでの権限制御を実装し、ユーザーの権限に応じてUI要素を動的に表示・非表示します。
def employee_list_view(request):
employees = Employee.objects.all()
permission_patterns = request.session.get("permission_patterns")
context = {
'employees': employees,
'permissions': permission_patterns
}
return render(request, "employees/list.html", context)
{% if '/employees/create/' in permissions %}
<a href="/employees/create/" class="btn btn-primary">
新規作成
</a>
{% endif %}
権限グループの拡張
権限にアクションタイプ(作成、削除、更新など)とグループ分け機能を追加します。
class AccessRight(models.Model):
"""
拡張権限モデル
"""
name = models.CharField('権限名', max_length=64)
pattern = models.CharField('URLパターン', max_length=256)
action_type = models.CharField(
'アクションタイプ',
max_length=32,
default="",
help_text="create, delete, update, view"
)
right_group = models.ForeignKey(
'RightGroup',
on_delete=models.CASCADE,
null=True,
blank=True,
verbose_name='権限グループ'
)
def __str__(self):
return self.name
class RightGroup(models.Model):
"""
権限グループモデル
"""
group_name = models.CharField('グループ名', max_length=64)
def __str__(self):
return self.group_name
拡張セッション管理
def setup_permission_session(employee, request):
# 権限情報をグループ化して取得
rights = employee.groups.all().values(
"access_rights__pattern",
"access_rights__action_type",
"access_rights__right_group_id"
).distinct()
permission_structure = {}
for right in rights:
group_id = right.get('access_rights__right_group_id')
if group_id not in permission_structure:
permission_structure[group_id] = {
'patterns': [right["access_rights__pattern"]],
'actions': [right["access_rights__action_type"]]
}
else:
permission_structure[group_id]['patterns'].append(
right["access_rights__pattern"]
)
permission_structure[group_id]['actions'].append(
right["access_rights__action_type"]
)
request.session['permission_structure'] = permission_structure
権限ヘルパークラス
class PermissionHelper:
def __init__(self, allowed_actions):
self.allowed_actions = allowed_actions
def can_create(self):
return "create" in self.allowed_actions
def can_delete(self):
return "delete" in self.allowed_actions
def can_update(self):
return "update" in self.allowed_actions
def can_view(self):
return "view" in self.allowed_actions
def employee_list_view(request):
employees = Employee.objects.all()
# 現在のアクション権限を取得
current_actions = getattr(request, 'current_actions', [])
permission_helper = PermissionHelper(current_actions)
context = {
'employees': employees,
'perm': permission_helper
}
return render(request, "employees/list.html", context)
動的メニュー表示
ユーザーの権限に基づいて動的にナビゲーションメニューを生成します。
def setup_permission_session(employee, request):
# ... 既存の権限設定 ...
# メニュー表示用の権限を取得
menu_rights = employee.groups.all().values(
"access_rights__pattern",
"access_rights__right_group__group_name"
).filter(access_rights__action_type="view").distinct()
menu_data = []
for right in menu_rights:
menu_data.append({
'url': right["access_rights__pattern"],
'name': right["access_rights__right_group__group_name"]
})
request.session["menu_data"] = menu_data
カスタムテンプレートタグの実装:
from django import template
register = template.Library()
@register.inclusion_tag("navigation/menu.html")
def render_navigation_menu(request):
menu_items = request.session.get("menu_data", [])
return {"menu_items": menu_items}
メニューテンプレート (navigation/menu.html):
<nav class="main-navigation">
{% for item in menu_items %}
<a href="{{ item.url }}" class="nav-item">
{{ item.name }}
</a>
{% endfor %}
</nav>
ベーステンプレートでの使用:
<header>
{% render_navigation_menu request %}
</header>