From 398bb63925a6ebe5a8b8bad08c6e447eb5123ddb Mon Sep 17 00:00:00 2001 From: JoaquinBN Date: Fri, 17 Jul 2026 17:05:19 +0200 Subject: [PATCH] Grant steward superusers all permissions (#942) --- backend/contributions/permissions.py | 42 ++++++++++++- .../tests/test_steward_permissions.py | 60 +++++++++++++++++++ backend/contributions/views.py | 21 +++---- 3 files changed, 108 insertions(+), 15 deletions(-) diff --git a/backend/contributions/permissions.py b/backend/contributions/permissions.py index eeedcc1d..1f678999 100644 --- a/backend/contributions/permissions.py +++ b/backend/contributions/permissions.py @@ -1,6 +1,16 @@ from rest_framework import permissions +def is_steward_superuser(user): + """Return whether a steward should receive every steward permission.""" + return bool( + user + and user.is_authenticated + and getattr(user, 'is_superuser', False) + and hasattr(user, 'steward') + ) + + class IsSteward(permissions.BasePermission): """ Custom permission to only allow stewards to access certain views. @@ -22,6 +32,8 @@ def steward_has_permission(user, contribution_type_id, action): Check if a steward has a specific action permission on a contribution type. Returns False if user is not a steward or doesn't have the permission. """ + if is_steward_superuser(user): + return True if not hasattr(user, 'steward'): return False from stewards.models import StewardPermission @@ -38,10 +50,38 @@ def steward_permitted_type_ids(user, actions=None): If actions is provided, only return types where the steward has at least one of those actions. If actions is None, return types where the steward has any permission. """ + if is_steward_superuser(user): + from contributions.models import ContributionType + return list(ContributionType.objects.values_list('id', flat=True)) if not hasattr(user, 'steward'): return [] from stewards.models import StewardPermission qs = StewardPermission.objects.filter(steward=user.steward) if actions: qs = qs.filter(action__in=actions) - return list(qs.values_list('contribution_type_id', flat=True).distinct()) \ No newline at end of file + return list(qs.values_list('contribution_type_id', flat=True).distinct()) + + +def steward_permission_map(user): + """Return the effective per-type steward permissions for a user.""" + from stewards.models import StewardPermission + + if is_steward_superuser(user): + from contributions.models import ContributionType + + actions = [choice[0] for choice in StewardPermission.ACTION_CHOICES] + return { + str(contribution_type_id): list(actions) + for contribution_type_id in ContributionType.objects.values_list('id', flat=True) + } + + if not hasattr(user, 'steward'): + return {} + + result = {} + permissions_qs = StewardPermission.objects.filter( + steward=user.steward, + ).values_list('contribution_type_id', 'action') + for contribution_type_id, action in permissions_qs: + result.setdefault(str(contribution_type_id), []).append(action) + return result diff --git a/backend/contributions/tests/test_steward_permissions.py b/backend/contributions/tests/test_steward_permissions.py index c5088220..44f15aa8 100644 --- a/backend/contributions/tests/test_steward_permissions.py +++ b/backend/contributions/tests/test_steward_permissions.py @@ -162,6 +162,66 @@ def test_steward_can_access_steward_endpoints(self): self.assertEqual(response.status_code, status.HTTP_200_OK) self.assertEqual(response.data['pending_count'], 1) + def test_steward_superuser_has_all_permissions_without_permission_rows(self): + """A steward marked as a superuser receives every effective permission.""" + admin_steward = Steward.objects.create(user=self.admin_user) + self.assertFalse( + StewardPermission.objects.filter(steward=admin_steward).exists() + ) + + self.client.force_authenticate(user=self.admin_user) + + list_response = self.client.get('/api/v1/steward-submissions/') + self.assertEqual(list_response.status_code, status.HTTP_200_OK) + self.assertEqual(len(list_response.data['results']), 1) + + permissions_response = self.client.get( + '/api/v1/steward-submissions/my-permissions/' + ) + self.assertEqual(permissions_response.status_code, status.HTTP_200_OK) + expected_actions = { + 'propose', + 'accept', + 'reject', + 'request_more_info', + } + self.assertTrue( + {str(self.contribution_type.id), str(self.other_contribution_type.id)} + <= set(permissions_response.data) + ) + for actions in permissions_response.data.values(): + self.assertEqual(set(actions), expected_actions) + + me_response = self.client.get('/api/v1/users/me/') + self.assertEqual(me_response.status_code, status.HTTP_200_OK) + self.assertIsNotNone(me_response.data['steward']) + + def test_steward_superuser_can_review_without_permission_rows(self): + Steward.objects.create(user=self.admin_user) + self.client.force_authenticate(user=self.admin_user) + + response = self.client.post( + f'/api/v1/steward-submissions/{self.submission.id}/review/', + { + 'action': 'accept', + 'points': 50, + 'contribution_type': self.contribution_type.id, + }, + format='json', + ) + + self.assertEqual(response.status_code, status.HTTP_200_OK, response.data) + self.submission.refresh_from_db() + self.assertEqual(self.submission.state, 'accepted') + self.assertEqual(self.submission.reviewed_by, self.admin_user) + + def test_non_steward_superuser_cannot_access_steward_endpoints(self): + self.client.force_authenticate(user=self.admin_user) + + response = self.client.get('/api/v1/steward-submissions/') + + self.assertEqual(response.status_code, status.HTTP_403_FORBIDDEN) + def test_steward_can_change_pending_submission_type_without_reviewing(self): """Stewards can save a new type before making a review decision.""" StewardPermission.objects.create( diff --git a/backend/contributions/views.py b/backend/contributions/views.py index a7e5b1ea..3c4d3326 100644 --- a/backend/contributions/views.py +++ b/backend/contributions/views.py @@ -52,7 +52,12 @@ StartupRequestListSerializer, StartupRequestDetailSerializer, FeaturedContentSerializer, AlertSerializer, ContributionDiscordXPStateSerializer) -from .permissions import IsSteward, steward_has_permission, steward_permitted_type_ids +from .permissions import ( + IsSteward, + steward_has_permission, + steward_permission_map, + steward_permitted_type_ids, +) from .lifecycle_filters import SubmissionLifecycleFilterMixin from .proposal_filters import ProposalReviewStatusFilterMixin from .project_milestones import ( @@ -3314,19 +3319,7 @@ def bulk_reject(self, request): @action(detail=False, methods=['get'], url_path='my-permissions') def my_permissions(self, request): """Get current steward's permissions map: { contribution_type_id: [actions] }.""" - from stewards.models import StewardPermission - if not hasattr(request.user, 'steward'): - return Response({}) - - perms = StewardPermission.objects.filter( - steward=request.user.steward - ).values_list('contribution_type_id', 'action') - - result = {} - for ct_id, action_name in perms: - result.setdefault(str(ct_id), []).append(action_name) - - return Response(result) + return Response(steward_permission_map(request.user)) @action(detail=False, methods=['get'], url_path='templates') def templates(self, request):