From f5da80ce3f0d522688c796aac552889ca0b7fbc3 Mon Sep 17 00:00:00 2001 From: lizerk Date: Mon, 10 Aug 2026 11:38:44 +0300 Subject: [PATCH] fix(tasks): require board membership for admins --- backend/tasks/access.py | 5 --- backend/tasks/realtime.py | 8 ---- backend/tests/api/v1/tasks/test_tasks_api.py | 44 ++++++++++++++++---- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/backend/tasks/access.py b/backend/tasks/access.py index 50a76c8b..1f35f47e 100644 --- a/backend/tasks/access.py +++ b/backend/tasks/access.py @@ -34,9 +34,6 @@ def get_user_department_ids(user) -> list[int]: def task_board_access_q(user): - if getattr(user, "is_staff", False) or getattr(user, "is_superuser", False): - return Q() - department_ids = get_user_department_ids(user) q = Q(created_by=user) non_private_q = ~Q(access_scope=TaskBoardAccessScope.PRIVATE) @@ -63,8 +60,6 @@ def task_board_group_access_q(user): def user_can_access_task_board(user, board: TaskBoard | None) -> bool: if not user or not user.is_authenticated or board is None: return False - if user.is_staff or user.is_superuser: - return True if board.created_by_id == user.id: return True if board.access_scope == TaskBoardAccessScope.PRIVATE: diff --git a/backend/tasks/realtime.py b/backend/tasks/realtime.py index 7a84a966..06a3187b 100644 --- a/backend/tasks/realtime.py +++ b/backend/tasks/realtime.py @@ -5,7 +5,6 @@ from asgiref.sync import async_to_sync from channels.layers import get_channel_layer from django.contrib.auth import get_user_model from django.db import transaction -from django.db.models import Q from employees.models import Department, EmployeeDepartment, RoleAssignment @@ -19,7 +18,6 @@ def get_task_board_recipient_ids(board: TaskBoard) -> set[int]: return set() User = get_user_model() - admin_q = Q(is_staff=True) | Q(is_superuser=True) is_private = board.access_scope == TaskBoardAccessScope.PRIVATE if ( @@ -64,12 +62,6 @@ def get_task_board_recipient_ids(board: TaskBoard) -> set[int]: ).values_list("employee_id", flat=True) ) - recipient_ids.update( - User.objects.filter(is_active=True) - .filter(admin_q) - .values_list("id", flat=True) - ) - return set( User.objects.filter( id__in={user_id for user_id in recipient_ids if user_id}, diff --git a/backend/tests/api/v1/tasks/test_tasks_api.py b/backend/tests/api/v1/tasks/test_tasks_api.py index 322013a5..c3c7eae8 100644 --- a/backend/tests/api/v1/tasks/test_tasks_api.py +++ b/backend/tests/api/v1/tasks/test_tasks_api.py @@ -2428,7 +2428,7 @@ def test_create_board_accepts_members_and_departments(api_client, user): assert TaskColumn.objects.filter(board_id=response.data["id"]).count() == 4 -def test_private_board_is_available_only_to_owner_and_admin(api_client): +def test_private_board_is_available_only_to_owner(api_client): owner = make_user("task-private-owner@example.com", "+79994440901") selected_member = make_user("task-private-member@example.com", "+79994440902") department_member = make_user("task-private-dept@example.com", "+79994440903") @@ -2462,9 +2462,9 @@ def test_private_board_is_available_only_to_owner_and_admin(api_client): assert create_response.data["members"] == [] assert create_response.data["departments"] == [] board = TaskBoard.objects.get(id=create_response.data["id"]) - assert get_task_board_recipient_ids(board) == {owner.id, admin.id} + assert get_task_board_recipient_ids(board) == {owner.id} - for hidden_user in (selected_member, department_member, outsider): + for hidden_user in (selected_member, department_member, outsider, admin): api_client.force_authenticate(user=hidden_user) list_response = api_client.get(reverse("api:v1:tasks:task-board-list")) assert list_response.status_code == status.HTTP_200_OK @@ -2474,11 +2474,39 @@ def test_private_board_is_available_only_to_owner_and_admin(api_client): ) assert detail_response.status_code == status.HTTP_404_NOT_FOUND - api_client.force_authenticate(user=admin) - admin_response = api_client.get( - reverse("api:v1:tasks:task-board-detail", kwargs={"pk": board.id}) + +def test_admin_needs_membership_to_access_restricted_board(api_client): + owner = make_user("task-restricted-owner@example.com", "+79994440911") + admin = make_user( + "task-restricted-admin@example.com", + "+79994440912", + is_staff=True, + is_superuser=True, ) - assert admin_response.status_code == status.HTTP_200_OK + board = TaskBoard.objects.create( + name="Выборочная доска", + created_by=owner, + access_scope="restricted", + ) + detail_url = reverse( + "api:v1:tasks:task-board-detail", + kwargs={"pk": board.id}, + ) + api_client.force_authenticate(user=admin) + + list_response = api_client.get(reverse("api:v1:tasks:task-board-list")) + assert list_response.status_code == status.HTTP_200_OK + assert board.id not in {item["id"] for item in list_response.data["results"]} + assert api_client.get(detail_url).status_code == status.HTTP_404_NOT_FOUND + assert ( + api_client.patch(detail_url, {"name": "Чужое изменение"}, format="json").status_code + == status.HTTP_404_NOT_FOUND + ) + assert api_client.delete(detail_url).status_code == status.HTTP_404_NOT_FOUND + + board.members.add(admin) + + assert api_client.get(detail_url).status_code == status.HTTP_200_OK def test_board_member_can_access_private_board(api_client): @@ -2590,7 +2618,7 @@ def test_task_board_realtime_recipients_match_board_access(): assert member.id in recipients assert department_user.id in recipients assert role_user.id in recipients - assert admin.id in recipients + assert admin.id not in recipients assert outsider.id not in recipients assert inactive_member.id not in recipients