Django 객체 레벨 권한 설계 — Owner와 팀 기반 접근 제어
IsAuthenticated만으로는 "내 일정을 남이 수정하는 것"을 막을 수 없다. created_by 패턴으로 소유권을 확립하고, 팀 기반 상위 권한을 추가하는 실전 권한 시스템 설계를 단계별로 설명한다.
Seobway · · 14분
왜 IsAuthenticated만으로는 부족한가
# 이 코드의 구멍은?
class TaskViewSet(ModelViewSet):
permission_classes = [IsAuthenticated]
queryset = Task.objects.all()
serializer_class = TaskSerializer
로그인한 사용자라면 누구든 모든 Task를 수정·삭제할 수 있다.IsAuthenticated는 "로그인했는가"만 확인하지, "이 데이터의 주인인가"를 확인하지 않는다.
%% desc: Django DRF 권한 체계 — 3개 레이어
flowchart TD
A["요청 도착"]
B["Authentication\n인증: 이 사람이 누구인가?\nJWT·Session·Token"]
C["Permission (View 레벨)\nhas_permission()\n이 뷰에 접근할 수 있는가?\nIsAuthenticated 등"]
D["Object Permission (Object 레벨)\nhas_object_permission()\n이 특정 객체를 다룰 수 있는가?\n→ 여기가 핵심"]
E["비즈니스 로직 실행"]
A --> B
B -->|"인증 실패"| ERR1["401 Unauthorized"]
B --> C
C -->|"권한 없음"| ERR2["403 Forbidden"]
C --> D
D -->|"객체 권한 없음"| ERR2
D --> E
Step 1: 모델에 소유자 필드 추가
누가 만들었는지 모르면 소유권 검사가 불가능하다. created_by를 먼저 추가한다.[1]
# models.py
from django.db import models
from django.contrib.auth import get_user_model
User = get_user_model()
class Task(models.Model):
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
category = models.ForeignKey('Category', on_delete=models.SET_NULL, null=True)
due_date = models.DateField(null=True, blank=True)
# ── 소유권 필드 ───────────────────────────────────────────────────
created_by = models.ForeignKey(
User,
on_delete=models.SET_NULL,
null=True, # 기존 데이터 호환: null 허용
blank=True,
related_name='created_tasks',
verbose_name='작성자',
)
created_at = models.DateTimeField(auto_now_add=True)
updated_at = models.DateTimeField(auto_now=True)
class Meta:
ordering = ['-created_at']
null=True가 필요한 이유: 기존에 데이터가 있는 테이블에 ForeignKey를 추가하면 기존 행에 넣을 값이 없다.
null=True를 지정하지 않으면makemigrations시 기본값을 요구하고, 잘못된 값을 입력하면 DB 무결성이 깨진다.
마이그레이션 생성
python manage.py makemigrations tasks --name task_auditing_fields
python manage.py migrate
생성되는 0003_task_auditing_fields.py:
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('tasks', '0002_previous_migration'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AddField(
model_name='task',
name='created_by',
field=models.ForeignKey(
blank=True,
null=True,
on_delete=django.db.models.deletion.SET_NULL,
related_name='created_tasks',
to=settings.AUTH_USER_MODEL,
verbose_name='작성자',
),
),
]
Step 2: 커스텀 Permission 클래스 작성
DRF의 BasePermission을 상속한다.[2]
# permissions.py
from rest_framework.permissions import BasePermission, SAFE_METHODS
# 승인된 팀 코드 목록
PRIVILEGED_TEAMS = {'MGMT', 'SALES'} # 경영지원, 기술영업
class IsOwnerOrPrivilegedTeam(BasePermission):
"""
접근 허용 조건 (OR 관계):
1. 이 객체를 만든 사람 (Owner)
2. MGMT / SALES 팀 소속 (상위 권한 보유 팀)
"""
def has_permission(self, request, view):
"""뷰 접근 자체: 로그인만 확인"""
return request.user and request.user.is_authenticated
def has_object_permission(self, request, view, obj):
"""객체 수준 권한: 읽기는 허용, 쓰기는 조건 확인"""
# 읽기(GET, HEAD, OPTIONS)는 항상 허용
if request.method in SAFE_METHODS:
return True
# 조건 1: 소유자 본인
if obj.created_by == request.user:
return True
# 조건 2: 상위 권한 팀 (User 모델에 team/department 필드가 있다고 가정)
user_team = getattr(request.user, 'team', None)
if user_team and user_team.upper() in PRIVILEGED_TEAMS:
return True
return False
팀 정보를 어디서 가져오는가
# User 모델 확장 예시 (Profile 또는 AbstractUser)
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
team = models.CharField(
max_length=20,
choices=[
('MGMT', '경영지원'),
('SALES', '기술영업'),
('DEV', '개발'),
('OPS', '운영'),
],
default='DEV',
)
# Permission에서 접근
user_team = getattr(request.user.profile, 'team', None)
Step 3: ViewSet에 권한 적용
# views.py
from rest_framework import viewsets, status
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from .models import Task
from .serializers import TaskModelSerializer
from .permissions import IsOwnerOrPrivilegedTeam
class TaskViewSet(viewsets.ModelViewSet):
serializer_class = TaskModelSerializer
# ── 권한: 로그인 + 객체 레벨 권한 ────────────────────────────────
permission_classes = [IsOwnerOrPrivilegedTeam]
def get_queryset(self):
"""
일반 사용자: 본인이 만든 태스크만 조회
MGMT/SALES: 전체 조회
"""
user = self.request.user
team = getattr(getattr(user, 'profile', None), 'team', '')
if team.upper() in {'MGMT', 'SALES'}:
return Task.objects.all().select_related('created_by', 'category')
return Task.objects.filter(
created_by=user
).select_related('created_by', 'category')
def perform_create(self, serializer):
"""생성 시 자동으로 created_by 주입"""
serializer.save(created_by=self.request.user)
def get_permissions(self):
"""
액션별 권한 클래스를 다르게 설정할 수 있다.
create는 로그인만 확인, update/delete는 객체 권한 필요
"""
if self.action == 'create':
return [IsAuthenticated()]
return [IsOwnerOrPrivilegedTeam()]
권한 매트릭스
%% desc: 역할별 Task CRUD 권한 매트릭스
flowchart LR
subgraph OWNER["소유자 (Owner)"]
O1["본인 Task 조회 ✅"]
O2["본인 Task 수정 ✅"]
O3["본인 Task 삭제 ✅"]
O4["타인 Task 조회 ❌"]
O5["타인 Task 수정 ❌"]
end
subgraph MGMT_SALES["MGMT / SALES 팀"]
M1["전체 Task 조회 ✅"]
M2["전체 Task 수정 ✅"]
M3["전체 Task 삭제 ✅"]
end
subgraph OTHERS["일반 사용자"]
G1["본인 Task 조회 ✅"]
G2["타인 Task 수정 ❌ → 403"]
end
| 액션 | 본인(Owner) | MGMT/SALES | 타팀 일반 사용자 |
|---|---|---|---|
| List (GET) | 본인 것만 | 전체 | 본인 것만 |
| Retrieve (GET) | ✅ | ✅ | ❌ 403 |
| Create (POST) | ✅ | ✅ | ✅ |
| Update (PUT/PATCH) | ✅ | ✅ | ❌ 403 |
| Destroy (DELETE) | ✅ | ✅ | ❌ 403 |
Step 4: Serializer에서 created_by 읽기 전용 처리
class TaskModelSerializer(serializers.ModelSerializer):
# 소유자 정보: 읽기 전용으로 노출
created_by = serializers.SerializerMethodField()
class Meta:
model = Task
fields = ['id', 'title', 'category', 'due_date', 'created_by', ...]
read_only_fields = ['id', 'created_by']
def get_created_by(self, obj):
if obj.created_by:
return {
'id': obj.created_by.id,
'username': obj.created_by.username,
'team': getattr(getattr(obj.created_by, 'profile', None), 'team', None),
}
return None
다중 날짜 일정 지원
오늘 구현한 다른 기능: 달력에서 여러 날짜를 선택해 일정 범위를 지정하는 기능.
# 모델에 start_date, end_date 추가
class Task(models.Model):
# 기존 필드...
start_date = models.DateField(null=True, blank=True)
end_date = models.DateField(null=True, blank=True)
# Serializer: 날짜 범위 교차 검증
def validate(self, data):
start = data.get('start_date')
end = data.get('end_date')
if start and end and start > end:
raise serializers.ValidationError({
'end_date': "종료일은 시작일 이후여야 합니다."
})
return data # ✅ 반드시 return
// Frontend: ScheduleCalendar.tsx — 2번 클릭으로 범위 지정
const [rangeStart, setRangeStart] = useState<Date | null>(null)
const handleDateClick = (date: Date) => {
if (!rangeStart) {
setRangeStart(date) // 첫 번째 클릭: 시작일 설정
} else {
const start = rangeStart < date ? rangeStart : date
const end = rangeStart < date ? date : rangeStart
openNewScheduleModal({ start_date: start, end_date: end })
setRangeStart(null) // 선택 완료 후 초기화
}
}
권한 테스트 작성
from rest_framework.test import APITestCase
from rest_framework import status
from django.contrib.auth import get_user_model
from .models import Task
User = get_user_model()
class TaskPermissionTest(APITestCase):
def setUp(self):
self.owner = User.objects.create_user('owner', password='pass')
self.other = User.objects.create_user('other', password='pass')
self.mgmt_user = User.objects.create_user('mgmt', password='pass')
# mgmt_user.profile.team = 'MGMT' 설정 가정
self.task = Task.objects.create(
title='Test Task',
created_by=self.owner
)
def test_owner_can_update(self):
self.client.force_authenticate(user=self.owner)
res = self.client.patch(f'/api/tasks/{self.task.id}/', {'title': 'Updated'})
self.assertEqual(res.status_code, status.HTTP_200_OK)
def test_other_user_cannot_update(self):
self.client.force_authenticate(user=self.other)
res = self.client.patch(f'/api/tasks/{self.task.id}/', {'title': 'Hacked'})
self.assertEqual(res.status_code, status.HTTP_403_FORBIDDEN)
def test_mgmt_can_update_any(self):
self.client.force_authenticate(user=self.mgmt_user)
res = self.client.patch(f'/api/tasks/{self.task.id}/', {'title': 'Managed'})
self.assertEqual(res.status_code, status.HTTP_200_OK)
참고
- [1] Django ForeignKey — 공식 문서
- [2] DRF Custom Permissions — 공식 문서
- [3] DRF Object Level Permissions — 공식 문서
관련 글
- Django
- DRF
- Permissions
- ObjectPermission
- RBAC
- Authorization
- 권한시스템