BaseFilterSet resolved referenced SavedFilters without a visibility check, so a private (shared=False) filter owned by one user could have its parameters applied to another user's request. Restrict resolution to shared or owned filters via restrict_to_shared(), matching the visibility enforced on the UI, REST, and GraphQL SavedFilter surfaces.
This commit is contained in:
parent
8aa7b3b2c8
commit
b3c27b951d
|
|
@ -2,6 +2,7 @@ import json
|
|||
from copy import deepcopy
|
||||
|
||||
import django_filters
|
||||
from django.contrib.auth.models import AnonymousUser
|
||||
from django.contrib.contenttypes.models import ContentType
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
|
|
@ -118,7 +119,11 @@ class BaseFilterSet(django_filters.FilterSet):
|
|||
except (ValueError, TypeError):
|
||||
pass
|
||||
|
||||
saved_filters = SavedFilter.objects.filter(
|
||||
# Only apply SavedFilters the requesting user is permitted to see (#22790). Fall back to
|
||||
# anonymous visibility (shared filters only) when no request is available.
|
||||
request = kwargs.get('request')
|
||||
user = request.user if request else AnonymousUser()
|
||||
saved_filters = SavedFilter.objects.restrict_to_shared(user).filter(
|
||||
Q(slug__in=data.pop('filter', [])) |
|
||||
Q(pk__in=filter_ids)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import django_filters
|
||||
from django.conf import settings
|
||||
from django.contrib.auth import get_user_model
|
||||
from django.db import models
|
||||
from django.http import QueryDict
|
||||
from django.test import TestCase
|
||||
from django.test import RequestFactory, TestCase
|
||||
from mptt.fields import TreeForeignKey
|
||||
from taggit.managers import TaggableManager
|
||||
|
||||
|
|
@ -38,6 +39,8 @@ from utilities.filters import (
|
|||
)
|
||||
from wireless.choices import WirelessRoleChoices
|
||||
|
||||
User = get_user_model()
|
||||
|
||||
|
||||
class TreeNodeMultipleChoiceFilterTestCase(TestCase):
|
||||
|
||||
|
|
@ -761,22 +764,80 @@ class SavedFilterApplicationTestCase(TestCase):
|
|||
)
|
||||
cls.saved_filter.object_types.set([ObjectType.objects.get_for_model(Site)])
|
||||
|
||||
def setUp(self):
|
||||
self.request = RequestFactory().get('/')
|
||||
self.request.user = User.objects.create_user('testuser')
|
||||
|
||||
def test_filter_id_valid(self):
|
||||
# A referenced SavedFilter's parameters are applied to the queryset
|
||||
data = QueryDict(f'filter_id={self.saved_filter.pk}')
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all()).qs.count(), 1)
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all(), request=self.request).qs.count(), 1)
|
||||
|
||||
def test_filter_id_nonexistent(self):
|
||||
# A non-existent (but valid integer) filter_id is ignored
|
||||
data = QueryDict('filter_id=999999')
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all()).qs.count(), 2)
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all(), request=self.request).qs.count(), 2)
|
||||
|
||||
def test_filter_id_non_integer(self):
|
||||
# A non-integer filter_id is ignored rather than raising a ValueError (#22568)
|
||||
data = QueryDict('filter_id=abc')
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all()).qs.count(), 2)
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all(), request=self.request).qs.count(), 2)
|
||||
|
||||
def test_filter_slug(self):
|
||||
# A referenced SavedFilter may also be applied by slug
|
||||
data = QueryDict('filter=active-sites')
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all(), request=self.request).qs.count(), 1)
|
||||
|
||||
def test_private_filter_not_applied_for_other_user(self):
|
||||
# A private SavedFilter owned by another user must not be applied (#22790)
|
||||
owner = User.objects.create_user('owner')
|
||||
private_filter = SavedFilter.objects.create(
|
||||
name='Private',
|
||||
slug='private',
|
||||
user=owner,
|
||||
shared=False,
|
||||
parameters={'status': [SiteStatusChoices.STATUS_ACTIVE]},
|
||||
)
|
||||
private_filter.object_types.set([ObjectType.objects.get_for_model(Site)])
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.user = User.objects.create_user('other')
|
||||
data = QueryDict('filter=private')
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all(), request=request).qs.count(), 2)
|
||||
|
||||
def test_private_filter_applied_for_owner(self):
|
||||
# The owner of a private SavedFilter can still apply it (#22790)
|
||||
owner = User.objects.create_user('owner')
|
||||
private_filter = SavedFilter.objects.create(
|
||||
name='Private',
|
||||
slug='private',
|
||||
user=owner,
|
||||
shared=False,
|
||||
parameters={'status': [SiteStatusChoices.STATUS_ACTIVE]},
|
||||
)
|
||||
private_filter.object_types.set([ObjectType.objects.get_for_model(Site)])
|
||||
|
||||
request = RequestFactory().get('/')
|
||||
request.user = owner
|
||||
data = QueryDict('filter=private')
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all(), request=request).qs.count(), 1)
|
||||
|
||||
def test_shared_filter_applied_without_request(self):
|
||||
# Without a request, a shared SavedFilter is still applied (anonymous visibility)
|
||||
data = QueryDict('filter=active-sites')
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all()).qs.count(), 1)
|
||||
|
||||
def test_private_filter_not_applied_without_request(self):
|
||||
# Without a request, a private SavedFilter is not applied (#22790)
|
||||
owner = User.objects.create_user('owner')
|
||||
private_filter = SavedFilter.objects.create(
|
||||
name='Private',
|
||||
slug='private',
|
||||
user=owner,
|
||||
shared=False,
|
||||
parameters={'status': [SiteStatusChoices.STATUS_ACTIVE]},
|
||||
)
|
||||
private_filter.object_types.set([ObjectType.objects.get_for_model(Site)])
|
||||
|
||||
data = QueryDict('filter=private')
|
||||
self.assertEqual(SiteFilterSet(data, Site.objects.all()).qs.count(), 2)
|
||||
|
|
|
|||
Loading…
Reference in New Issue