From 59d8d73c883830f9bbb7cbd7ca1b8aac288f9405 Mon Sep 17 00:00:00 2001 From: imperosol Date: Sun, 9 Nov 2025 21:19:15 +0100 Subject: [PATCH] exclude hidden users from ajax search --- com/views.py | 3 ++- core/api.py | 6 +++-- core/management/commands/populate.py | 3 ++- core/models.py | 9 +++++++ core/tests/test_user.py | 38 +++++++++++++++++++++++++++- core/views/user.py | 4 +-- 6 files changed, 55 insertions(+), 8 deletions(-) diff --git a/com/views.py b/com/views.py index 72b4cc41..92d74652 100644 --- a/com/views.py +++ b/com/views.py @@ -240,7 +240,8 @@ class NewsListView(TemplateView): if not self.request.user.has_perm("core.view_user"): return [] return itertools.groupby( - User.objects.filter( + User.objects.viewable_by(self.request.user) + .filter( date_of_birth__month=localdate().month, date_of_birth__day=localdate().day, is_viewable=True, diff --git a/core/api.py b/core/api.py index af4daff5..ee451d3c 100644 --- a/core/api.py +++ b/core/api.py @@ -74,7 +74,7 @@ class MailingListController(ControllerBase): class UserController(ControllerBase): @route.get("", response=list[UserProfileSchema], permissions=[CanAccessLookup]) def fetch_profiles(self, pks: Query[set[int]]): - return User.objects.filter(pk__in=pks) + return User.objects.viewable_by(self.context.request.user).filter(pk__in=pks) @route.get("/{int:user_id}", response=UserSchema, permissions=[CanView]) def fetch_user(self, user_id: int): @@ -90,7 +90,9 @@ class UserController(ControllerBase): @paginate(PageNumberPaginationExtra, page_size=20) def search_users(self, filters: Query[UserFilterSchema]): return filters.filter( - User.objects.order_by(F("last_login").desc(nulls_last=True)) + User.objects.viewable_by(self.context.request.user).order_by( + F("last_login").desc(nulls_last=True) + ) ) diff --git a/core/management/commands/populate.py b/core/management/commands/populate.py index cd0087e7..60c282aa 100644 --- a/core/management/commands/populate.py +++ b/core/management/commands/populate.py @@ -150,7 +150,8 @@ class Command(BaseCommand): Weekmail().save() - # Here we add a lot of test datas, that are not necessary for the Sith, but that provide a basic development environment + # Here we add a lot of test datas, that are not necessary for the Sith, + # but that provide a basic development environment self.now = timezone.now().replace(hour=12, second=0) skia = User.objects.create_user( diff --git a/core/models.py b/core/models.py index 3c53419b..fd105148 100644 --- a/core/models.py +++ b/core/models.py @@ -180,6 +180,15 @@ class UserQuerySet(models.QuerySet): Q(Exists(subscriptions)) | Q(Exists(refills)) | Q(Exists(purchases)) ) + def viewable_by(self, user: User) -> Self: + if user.has_perm("core.view_hidden_user"): + return self + if user.has_perm("core.view_user"): + return self.filter(is_viewable=True) + if user.is_anonymous: + return self.none() + return self.filter(id=user.id) + class CustomUserManager(UserManager.from_queryset(UserQuerySet)): # see https://docs.djangoproject.com/fr/stable/topics/migrations/#model-managers diff --git a/core/tests/test_user.py b/core/tests/test_user.py index 8be9bf5f..e69b1515 100644 --- a/core/tests/test_user.py +++ b/core/tests/test_user.py @@ -3,6 +3,7 @@ from datetime import timedelta import pytest from django.conf import settings from django.contrib import auth +from django.contrib.auth.models import Permission from django.core.management import call_command from django.test import Client, RequestFactory, TestCase from django.urls import reverse @@ -18,7 +19,7 @@ from core.baker_recipes import ( subscriber_user, very_old_subscriber_user, ) -from core.models import Group, User +from core.models import AnonymousUser, Group, User from core.views import UserTabsMixin from counter.baker_recipes import sale_recipe from counter.models import Counter, Customer, Refilling, Selling @@ -368,3 +369,38 @@ class TestRedirectMe: def test_promo_has_logo(promo): user = baker.make(User, promo=promo) assert user.promo_has_logo() + + +@pytest.mark.django_db +class TestUserQuerySetViewableBy: + @pytest.fixture + def users(self) -> list[User]: + return [ + baker.make(User), + subscriber_user.make(), + subscriber_user.make(is_viewable=False), + ] + + def test_admin_user(self, users: list[User]): + user = baker.make( + User, + user_permissions=[Permission.objects.get(codename="view_hidden_user")], + ) + viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user) + assert set(viewable) == set(users) + + @pytest.mark.parametrize( + "user_factory", [old_subscriber_user.make, subscriber_user.make] + ) + def test_subscriber(self, users: list[User], user_factory): + user = user_factory() + viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user) + assert set(viewable) == {users[0], users[1]} + + @pytest.mark.parametrize( + "user_factory", [lambda: baker.make(User), lambda: AnonymousUser()] + ) + def test_not_subscriber(self, users: list[User], user_factory): + user = user_factory() + viewable = User.objects.filter(id__in=[u.id for u in users]).viewable_by(user) + assert not viewable.exists() diff --git a/core/views/user.py b/core/views/user.py index ba7beaa9..4b9c32cb 100644 --- a/core/views/user.py +++ b/core/views/user.py @@ -103,9 +103,7 @@ def password_root_change(request, user_id): """Allows a root user to change someone's password.""" if not request.user.is_root: raise PermissionDenied - user = User.objects.filter(id=user_id).first() - if not user: - raise Http404("User not found") + user = get_object_or_404(User, id=user_id) if request.method == "POST": form = views.SetPasswordForm(user=user, data=request.POST) if form.is_valid():