앱 및 데이터 접근 권한 관리 방법 정리

개요

Django 앱에서 권한 관리를 구현하는 방법은 크게

  1. Django가 제공하는 기본 권한 시스템
  2. Django REST Framework에 내장된 Permission 클래스
  3. Object-level 권한을 다루는 타사 라이브러리
  4. RBAC 라이브러리
  5. 커스텀 인증 백엔드

등으로 구분할 수 있다. 이 앱에서는 Django가 제공하는 기본 권한 시스템을 주로 사용한다.

  • User, Group, Permission 모델
  • 인증/권한 데코레이터
  • CBV의 믹스인(Mixin)

1. User/Role/Membership을 이용한 다중 조직 기반 RBAC 구조

조직(Organization)과 회원(Membership) 모델을 통해, 하나의 사용자(User)가 여러 조직에서 다른 역할(Role)과 권한(Permission)을 갖도록 구현

주요 포인트

  1. Organization & Membership
  • Organization: 회사나 그룹 등의 단체를 표현.
  • Membership:
    • 사용자(User)와 조직(Organization) 사이의 관계.
    • membership_type(main/sub)으로 사용자-조직 관계 유형을 구분.
    • roles에 여러 Role을 할당.
    • additional_permissions / excluded_permissions로 권한을 추가하거나 제외.
  1. Role
  • Django의 기본 Permission 모델(django.contrib.auth.models.Permission)을 ManyToMany로 연결한 Role 모델.
  • 예를 들어, “관리자(Role)”에 추가/수정/삭제 권한을 묶어두고, 사용자에게 “관리자(Role)”만 할당할 수 있음.
  1. User
  • Django의 AbstractUser를 상속받아 커스텀 유저 모델.
  • 회원가입 시 UserProfile 자동 생성 (시그널 사용).
  • 여러 Membership을 가질 수 있음 → 여러 조직에서 다른 역할/권한 가능.
  1. 권한 체크 로직 (permission_service.py)
  • user_has_org_permission(user, org, perm_codename) 함수:
    1. 주어진 사용자와 조직에 해당하는 Membership을 찾음.
    2. Membership에 묶인 모든 Role의 권한들을 합집합으로 모음.
    3. additional_permissions를 합집합에 추가.
    4. excluded_permissions를 합집합에서 제거.
    5. 최종적으로 (앱이름, 권한코드) 형태가 합집합에 있으면 True, 아니면 False.

주요 모델 구조

예: accounts/models.py

class Organization(models.Model):
    name = models.CharField(max_length=255, unique=True)
    # 기타 필드들...
 
class Role(models.Model):
    name = models.CharField(max_length=50, unique=True)
    permissions = models.ManyToManyField(Permission, blank=True)
    # 여러 권한(Permission)을 묶어놓은 "역할(Role)" 개념
 
class Membership(models.Model):
    user = models.ForeignKey(User, on_delete=models.CASCADE)
    organization = models.ForeignKey(Organization, on_delete=models.CASCADE)
    roles = models.ManyToManyField(Role, blank=True)
    additional_permissions = models.ManyToManyField(Permission, blank=True)
    excluded_permissions = models.ManyToManyField(Permission, blank=True)
    # + membership_type 등 기타 필드들...
    
    # clean()에서 membership_type 비즈니스 로직 검사
    # save()에서 clean() 호출

권한 체크 로직

예: accounts/services/permission_service.py

def user_has_org_permission(user, org, perm_codename):
    """
    user가 org 조직에서 perm_codename (예: 'bookkeeping.delete_journalentry')
    권한이 있는지 확인하는 함수
    """
    if not user.is_authenticated:
        return False
 
    from accounts.models import Membership
    membership = Membership.objects.filter(
        user=user, 
        organization=org
    ).prefetch_related(
        'roles__permissions', 
        'additional_permissions', 
        'excluded_permissions'
    ).first()
 
    if not membership:
        return False
 
    # 1. Role에 연결된 모든 권한(permissions) 합집합
    all_permissions = set()
    for role in membership.roles.all():
        for p in role.permissions.all():
            all_permissions.add((p.content_type.app_label, p.codename))
 
    # 2. 추가 권한 적용
    for p in membership.additional_permissions.all():
        all_permissions.add((p.content_type.app_label, p.codename))
 
    # 3. 제외 권한 제거
    for p in membership.excluded_permissions.all():
        all_permissions.discard((p.content_type.app_label, p.codename))
 
    # 최종적으로 perm_codename("app_label.codename")이 있는지 확인
    app_label, code = perm_codename.split('.', 1)
    return (app_label, code) in all_permissions

실제 뷰에서 사용 예시

예: erp/bookkeeping/views/journal/crud_views.py

조직별 권한이 필요한 뷰에서 user_has_org_permission 함수를 호출하여 접근을 제어합니다.

from django.views import View
from django.http import HttpResponseForbidden
from accounts.services.permission_service import user_has_org_permission
 
class UpdateJournalEntriesView(LoginRequiredMixin, OrgAwareMixin, View):
    def get_required_permission(self, transaction):
        return "bookkeeping.add_journalentry"
 
    def get(self, request, transaction_id=None):
        # 1) self.active_org (OrgAwareMixin에서 가져옴)
        # 2) 특정 권한 체크
        required_perm = self.get_required_permission(None)
        if not user_has_org_permission(request.user, self.active_org, required_perm):
            return HttpResponseForbidden("You do not have permission to edit journal entries.")
 
        # 권한이 있으면 로직 진행...
        # ...

2. LoginRequiredMixin, OrgAwareMixin를 이용한 권한 관리

이 부분은 Django가 기본 제공하는 LoginRequiredMixin과, 프로젝트에서 커스텀한 OrgAwareMixin를 이용해 간단하게 구현하는 방법을 정리합니다.

LoginRequiredMixin

개념

  • Django 내장 믹스인으로, 로그인이 안 된 사용자의 접근을 자동으로 차단하고, 로그인 페이지로 리다이렉트합니다.
  • 클래스 기반 뷰(예: View, ListView, DetailView 등)에서 상속받아 사용합니다.

예시 코드

from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
 
class MySecureView(LoginRequiredMixin, View):
    def get(self, request, *args, **kwargs):
        # 로그인된 사용자만 접근 가능
        return HttpResponse("Hello, secured world!")
  • 사용자가 로그인이 안 되어 있다면, 설정된 LOGIN_URL 또는 Django 기본 로그인 페이지로 리다이렉트됩니다.
  • 장점: 인증되지 않은 사용자 접근을 막는 기본 로직을 매번 작성할 필요가 없음.

OrgAwareMixin

개념

  • 프로젝트에서 별도로 구현한 믹스인으로, 세션(active_org_id)에서 현재 활성화된 조직(Organization)을 가져오고, 만약 없다면 조직 설정 페이지로 리다이렉트합니다.
  • “현재 어떤 조직에서 작업 중인가?”를 자동으로 파악할 수 있어, 뷰 내부에서 self.active_org를 통해 손쉽게 접근할 수 있습니다.

예시 코드

# erp/bookkeeping/views/mixins.py
from django.shortcuts import redirect
from accounts.models import Organization
 
class OrgAwareMixin:
    def get_active_org(self):
        request = getattr(self, 'request', None)
        if not request:
            return None
        active_org_id = request.session.get('active_org_id', None)
        if active_org_id:
            return Organization.objects.filter(id=active_org_id).first()
        return None
 
    def dispatch(self, request, *args, **kwargs):
        self.active_org = self.get_active_org()
        if not self.active_org:
            # active_org_id가 없는 경우, 조직 설정 페이지로 이동
            return redirect('accounts:set_active_org')
        return super().dispatch(request, *args, **kwargs)
  • dispatch() 메서드를 오버라이딩하여, 뷰가 실행되기 전에 self.active_org를 세팅하고, 없을 시 특정 페이지로 이동시킵니다.
  • 따라서 이 믹스인을 상속받은 뷰에서는 self.active_org가 항상 존재한다고 가정하고 사용할 수 있습니다.

실제 사용 예시

from django.views import View
from django.contrib.auth.mixins import LoginRequiredMixin
from .mixins import OrgAwareMixin
 
class SomeOrgRequiredView(LoginRequiredMixin, OrgAwareMixin, View):
    def get(self, request, *args, **kwargs):
        # 1) 로그인 미들웨어 -> 로그인 안 되어 있으면 로그인 페이지
        # 2) OrgAwareMixin -> active_org가 없으면 'set_active_org' 페이지
        # 최종적으로, self.active_org에 접근 가능
        return HttpResponse(f"Active Org is: {self.active_org.name}")
  • 이 뷰를 접근하는 흐름:

    1. LoginRequiredMixin: 로그인 안 되어 있다면 로그인 페이지로 이동.
    2. OrgAwareMixin: active_org_id 세션 키가 없으면 set_active_org로 이동.
    3. 통과하면, self.active_org가 셋팅된 상태로 get() 메서드가 실행됨.