Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 41 additions & 1 deletion backend/contributions/permissions.py
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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
Expand All @@ -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())
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
60 changes: 60 additions & 0 deletions backend/contributions/tests/test_steward_permissions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
21 changes: 7 additions & 14 deletions backend/contributions/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -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):
Expand Down
Loading