From e05021c8c623bb3a4b69691ed3525c687492b2a4 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 23 Jul 2015 17:17:18 +0100 Subject: [PATCH 1/3] Guard against erronous direct .queryset evaluation in CBVs. --- rest_framework/views.py | 12 ++++++++++++ tests/test_generics.py | 16 ++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/rest_framework/views.py b/rest_framework/views.py index a709c2f6b..9c9c8e19a 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -7,6 +7,7 @@ import inspect import warnings from django.core.exceptions import PermissionDenied +from django.db import models from django.http import Http404 from django.utils import six from django.utils.encoding import smart_text @@ -118,8 +119,19 @@ class APIView(View): This allows us to discover information about the view when we do URL reverse lookups. Used for breadcrumb generation. """ + if isinstance(getattr(cls, 'queryset', None), models.QuerySet): + def force_evaluation(): + raise AssertionError( + 'Do not evaluate the `.queryset` attribute directly, ' + 'as the result will be cached and reused between requests. ' + 'Use `.all()` or call `.get_queryset()` instead.' + ) + + cls.queryset._fetch_all = force_evaluation + view = super(APIView, cls).as_view(**initkwargs) view.cls = cls + # Note: session based authentication is explicitly CSRF validated, # all other authentication is CSRF exempt. return csrf_exempt(view) diff --git a/tests/test_generics.py b/tests/test_generics.py index 219a83a5d..5db0b6f71 100644 --- a/tests/test_generics.py +++ b/tests/test_generics.py @@ -1,12 +1,14 @@ from __future__ import unicode_literals import django +import pytest from django.db import models from django.shortcuts import get_object_or_404 from django.test import TestCase from django.utils import six from rest_framework import generics, renderers, serializers, status +from rest_framework.response import Response from rest_framework.test import APIRequestFactory from tests.models import ( BasicModel, ForeignKeySource, ForeignKeyTarget, RESTFrameworkModel @@ -527,3 +529,17 @@ class TestFilterBackendAppliedToViews(TestCase): response = view(request).render() self.assertContains(response, 'field_b') self.assertNotContains(response, 'field_a') + + +class TestGuardedQueryset(TestCase): + def test_guarded_queryset(self): + class QuerysetAccessError(generics.ListAPIView): + queryset = BasicModel.objects.all() + + def get(self, request): + return Response(list(self.queryset)) + + view = QuerysetAccessError.as_view() + request = factory.get('/') + with pytest.raises(AssertionError): + view(request).render() From c0e3e670ca74e35d4d214419a355c4ac4c96d4ee Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Thu, 23 Jul 2015 17:43:49 +0100 Subject: [PATCH 2/3] Fix Django compat for Queryset import --- rest_framework/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index 9c9c8e19a..debcb07a2 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -119,7 +119,7 @@ class APIView(View): This allows us to discover information about the view when we do URL reverse lookups. Used for breadcrumb generation. """ - if isinstance(getattr(cls, 'queryset', None), models.QuerySet): + if isinstance(getattr(cls, 'queryset', None), models.query.QuerySet): def force_evaluation(): raise AssertionError( 'Do not evaluate the `.queryset` attribute directly, ' From 108dfafa44284bc2ad14558882acb5a62c370da0 Mon Sep 17 00:00:00 2001 From: Tom Christie Date: Fri, 24 Jul 2015 09:02:16 +0100 Subject: [PATCH 3/3] Fix Django 1.5/1.4 compat issue --- rest_framework/views.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rest_framework/views.py b/rest_framework/views.py index debcb07a2..8141a6550 100644 --- a/rest_framework/views.py +++ b/rest_framework/views.py @@ -126,8 +126,8 @@ class APIView(View): 'as the result will be cached and reused between requests. ' 'Use `.all()` or call `.get_queryset()` instead.' ) - cls.queryset._fetch_all = force_evaluation + cls.queryset._result_iter = force_evaluation # Django <= 1.5 view = super(APIView, cls).as_view(**initkwargs) view.cls = cls