🐛 Типичные ошибки — Урок 42
⚡ Топ-3 ошибки урока
- has_object_permission не вызывается для list — защита списка через
get_queryset(), не через permissions - owner не read_only в сериализаторе — клиент может подменить владельца; добавьте в
read_only_fields - Забыли добавить IsAuthenticated —
has_object_permissionполучаетAnonymousUser, что ломает сравнениеobj.owner == request.user
Ошибка 1: has_object_permission не защищает list-эндпоинт
Самая частая ошибка: разработчик добавляет
IsOwnerOrReadOnly в permission_classes ViewSet и думает, что список книг тоже защищён. Это не так.
# НЕПРАВИЛЬНО: has_object_permission НЕ вызывается при GET /books/
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [IsOwnerOrReadOnly]
# Любой может увидеть список всех книг!
# ПРАВИЛЬНО: фильтрация в get_queryset()
class BookViewSet(viewsets.ModelViewSet):
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
def get_queryset(self):
# Для list — только книги текущего пользователя
return Book.objects.filter(owner=self.request.user)
Ошибка 2: owner не помечен как read_only
# НЕПРАВИЛЬНО: owner можно подменить в запросе
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
# Нет read_only_fields!
# ПРАВИЛЬНО
class BookSerializer(serializers.ModelSerializer):
class Meta:
model = Book
fields = '__all__'
read_only_fields = ['owner'] # клиент не может изменить owner
Ошибка 3: Отсутствие IsAuthenticated при кастомном permission
# НЕПРАВИЛЬНО: анонимный пользователь вызовет AttributeError
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.owner == request.user # request.user = AnonymousUser!
class BookView(RetrieveUpdateDestroyAPIView):
permission_classes = [IsOwnerOrReadOnly] # нет IsAuthenticated!
# ПРАВИЛЬНО: сначала проверяем аутентификацию
class BookView(RetrieveUpdateDestroyAPIView):
permission_classes = [IsAuthenticated, IsOwnerOrReadOnly]
Ошибка 4: Неправильное место проверки in has_permission vs has_object_permission
# НЕПРАВИЛЬНО: проверка владельца в has_permission (нет доступа к объекту)
class IsOwner(BasePermission):
def has_permission(self, request, view):
# obj недоступен здесь!
return request.user == view.get_object().owner # рекурсия или N+1 запросов
# ПРАВИЛЬНО: проверка владельца в has_object_permission
class IsOwnerOrReadOnly(BasePermission):
def has_object_permission(self, request, view, obj):
if request.method in SAFE_METHODS:
return True
return obj.owner == request.user # obj здесь доступен
Ошибка 5: DjangoModelPermissions без queryset
# НЕПРАВИЛЬНО: DjangoModelPermissions требует queryset для определения модели
class BookView(APIView):
permission_classes = [DjangoModelPermissions]
# AssertionError: Cannot use DjangoModelPermissions without a queryset
# ПРАВИЛЬНО: добавьте queryset или get_queryset
class BookViewSet(viewsets.ModelViewSet):
queryset = Book.objects.all() # обязательно
permission_classes = [DjangoModelPermissions]
Ошибка 6: Миграция поля owner без null=True
# СИТУАЦИЯ: добавляем owner в существующую модель
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books')
# Без null=True Django требует default для существующих строк
# ВАРИАНТ 1: добавить null=True (если OK иметь книги без владельца)
owner = models.ForeignKey(User, on_delete=models.CASCADE, related_name='books', null=True, blank=True)
# ВАРИАНТ 2: в диалоге миграции выбрать "Provide a one-off default"
# и указать id существующего пользователя (например, 1)
# ВАРИАНТ 3: null=True временно, потом убрать после заполнения
# Шаг 1: добавить null=True, мигрировать
# Шаг 2: заполнить owner для всех объектов
# Шаг 3: убрать null=True, мигрировать снова
Ошибка 7: Сравнение owner через id вместо объекта
# НЕПРАВИЛЬНО: сравнение id (работает, но неявно)
return obj.owner_id == request.user.id
# ДОПУСТИМО, но лучше использовать объект
return obj.owner == request.user
# Django сравнивает по pk (id) автоматически при сравнении экземпляров моделей