Djangoによるロールベースアクセス制御システムの実装

アクセス制御の基本概念

アクセス制御とは、ソフトウェアシステムにおける各種リソースへのアクセスと操作を管理する仕組みです。システム内のデータベース、メモリ、ストレージ内のデータはすべてリソースとして扱われます。

リソースは静的な存在であり、適切なアクションを通じてアクセス・操作する必要があります。権限管理の本質は、データリソースへのアクセスアクションを制御することにあります。

アクションは以下の2種類に分類できます:

  1. データリソース操作:データベースやファイルのデータに直接アクセス・操作する
  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>

タグ: Django rbac アクセス制御 ミドルウェア パーミッション

8月31日 10:34 投稿