Skip to content
Open
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
4 changes: 4 additions & 0 deletions .env_sample
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ DJANGO_SETTINGS_MODULE=settings.develop
ALLOWED_HOSTS=localhost,example.com
SUBMISSIONS_API_URL=http://django:8000/api
MAX_EXECUTION_TIME_LIMIT=600 # time limit for the default queue (in seconds)
# Optional: if set, only members of this group can create competitions.
# If empty, behavior falls back to the current default.
# If set to a non-existent group, competition creation is denied.
COMPETITION_CREATOR_GROUP=

# Local domain definition
DOMAIN_NAME=localhost:80
Expand Down
39 changes: 39 additions & 0 deletions src/apps/api/permissions.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,47 @@
from django.conf import settings
from django.contrib.auth.models import Group
from rest_framework import permissions

from profiles.models import Membership


def _get_competition_creator_group_name():
return getattr(settings, "COMPETITION_CREATOR_GROUP", "").strip()


def is_creator_group_missing():
group_name = _get_competition_creator_group_name()

return bool(group_name) and (not Group.objects.filter(name=group_name).exists())


def user_can_create_competition(user):
if not user or not user.is_authenticated:
return False

group_name = _get_competition_creator_group_name()
if not group_name:
return True

# if configured group is missing, deny creation.
if not Group.objects.filter(name=group_name).exists():
return False

if user.is_superuser:
return True

return user.groups.filter(name=group_name).exists()


class IsCompetitionCreator(permissions.BasePermission):
def has_permission(self, request, view):
if is_creator_group_missing():
self.message = (
"Competition creation is disabled: configured COMPETITION_CREATOR_GROUP does not exist."
)
return user_can_create_competition(request.user)


class IsOrganizerOrCollaborator(permissions.BasePermission):
def has_object_permission(self, request, view, obj):
is_collab = request.user in obj.collaborators.all() if hasattr(obj, 'collaborators') else False
Expand Down
25 changes: 25 additions & 0 deletions src/apps/api/tests/test_competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,40 @@
from zipfile import ZipFile
from io import StringIO, BytesIO
from unittest import mock
from django.contrib.auth.models import Group
from django.test import override_settings
from django.urls import reverse
from rest_framework.test import APITestCase

from api.permissions import user_can_create_competition
from api.serializers.competitions import CompetitionSerializer
from competitions.models import CompetitionParticipant, Submission, Competition
from factories import UserFactory, CompetitionFactory, CompetitionParticipantFactory, PhaseFactory, LeaderboardFactory, \
ColumnFactory, SubmissionFactory, SubmissionScoreFactory, TaskFactory


class CompetitionCreatePermissionTests(APITestCase):
@override_settings(COMPETITION_CREATOR_GROUP='')
def test_defaults_to_legacy_behavior_when_group_setting_is_empty(self):
user = UserFactory()
assert user_can_create_competition(user) is True

@override_settings(COMPETITION_CREATOR_GROUP='competition_creators')
def test_denies_when_group_setting_points_to_missing_group(self):
user = UserFactory()
assert user_can_create_competition(user) is False

@override_settings(COMPETITION_CREATOR_GROUP='competition_creators')
def test_requires_membership_when_group_exists(self):
group = Group.objects.create(name='competition_creators')
member = UserFactory()
non_member = UserFactory()
member.groups.add(group)

assert user_can_create_competition(member) is True
assert user_can_create_competition(non_member) is False


class CompetitionTests(APITestCase):
def setUp(self):
self.creator = UserFactory(username='creator', password='creator')
Expand Down
39 changes: 39 additions & 0 deletions src/apps/api/tests/test_datasets.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
from django.urls import reverse
from faker import Factory
from django.contrib.auth.models import Group
from django.test import override_settings
from django.test import TestCase
from rest_framework.test import APITestCase
from datasets.models import Data
from competitions.models import CompetitionCreationTaskStatus
from factories import (
UserFactory,
DataFactory,
Expand Down Expand Up @@ -212,6 +215,42 @@ def test_download_nonexistent_dataset(self):
self.assertEqual(response.status_code, 404)


class DatasetUploadCompletedPermissionTests(APITestCase):
def setUp(self):
self.user = UserFactory()
self.client.force_login(self.user)
self.dataset = DataFactory(created_by=self.user, type=Data.COMPETITION_BUNDLE)
self.url = f"/api/datasets/completed/{self.dataset.key}/"

@override_settings(COMPETITION_CREATOR_GROUP='competition_creators')
@patch("competitions.tasks.unpack_competition.apply_async")
def test_upload_completed_forbidden_for_non_group_member_when_group_exists(self, mock_apply_async):
Group.objects.create(name='competition_creators')

response = self.client.put(self.url)

self.assertEqual(response.status_code, 403)
self.assertEqual(
response.data["detail"],
"You do not have permission to create competitions"
)
self.assertEqual(CompetitionCreationTaskStatus.objects.count(), 0)
mock_apply_async.assert_not_called()

@override_settings(COMPETITION_CREATOR_GROUP='competition_creators')
@patch("competitions.tasks.unpack_competition.apply_async")
def test_upload_completed_forbidden_when_group_is_missing(self, mock_apply_async):
response = self.client.put(self.url)

self.assertEqual(response.status_code, 403)
self.assertEqual(
response.data["detail"],
"Competition creation is disabled: configured COMPETITION_CREATOR_GROUP does not exist."
)
self.assertEqual(CompetitionCreationTaskStatus.objects.count(), 0)
mock_apply_async.assert_not_called()


class DatasetCreateTests(APITestCase):
def setUp(self):
self.user = UserFactory(username='creator', password='creator')
Expand Down
4 changes: 2 additions & 2 deletions src/apps/api/views/competitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@
from competitions.utils import get_popular_competitions, get_recent_competitions
from leaderboards.models import Leaderboard
from utils.data import make_url_sassy
from api.permissions import IsOrganizerOrCollaborator
from api.permissions import IsOrganizerOrCollaborator, IsCompetitionCreator
from django.db import transaction
from django.conf import settings

Expand Down Expand Up @@ -186,7 +186,7 @@ def get_permissions(self):
if self.action in ['update', 'partial_update', 'destroy']:
self.permission_classes = [IsOrganizerOrCollaborator]
elif self.action in ['create']:
self.permission_classes = [IsAuthenticated]
self.permission_classes = [IsCompetitionCreator]
elif self.action in ['retrieve', 'list']:
self.permission_classes = [AllowAny]
return [permission() for permission in self.permission_classes]
Expand Down
10 changes: 10 additions & 0 deletions src/apps/api/views/datasets.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,13 @@
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework import status
from rest_framework.decorators import api_view, action
from rest_framework.exceptions import PermissionDenied
from rest_framework.filters import SearchFilter
from rest_framework.response import Response
from rest_framework.viewsets import ModelViewSet
from rest_framework.permissions import AllowAny

from api.permissions import user_can_create_competition, is_creator_group_missing
from api.pagination import BasicPagination, LargePagination
from api.serializers import datasets as serializers
from datasets.models import Data
Expand Down Expand Up @@ -266,6 +268,14 @@ def upload_completed(request, key):
dataset.save()

if dataset.type == Data.COMPETITION_BUNDLE:
if is_creator_group_missing():
raise PermissionDenied(
"Competition creation is disabled: configured COMPETITION_CREATOR_GROUP does not exist."
)

if not user_can_create_competition(request.user):
raise PermissionDenied("You do not have permission to create competitions")

# Doing a local import here to avoid circular imports
from competitions.tasks import unpack_competition

Expand Down
1 change: 1 addition & 0 deletions src/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -161,6 +161,7 @@
# User Models
AUTH_USER_MODEL = 'profiles.User'
SOCIAL_AUTH_USER_MODEL = 'profiles.User'
COMPETITION_CREATOR_GROUP = os.environ.get('COMPETITION_CREATOR_GROUP', '').strip()

# =============================================================================
# Debugging
Expand Down
4 changes: 2 additions & 2 deletions src/static/riot/competitions/management.tag
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@
<div class="ui center aligned grid">
<div class="fourteen wide column">
<h1 style="float: left; display: inline-block;">Benchmark Management</h1>
<a class="ui right floated green button" href="{ URLS.COMPETITION_UPLOAD }">
<a if="{ CODALAB.state.user.can_create_competition }" class="ui right floated green button" href="{ URLS.COMPETITION_UPLOAD }">
<i class="upload icon"></i> Upload
</a>
<a class="ui right floated green button" href="{ URLS.COMPETITION_ADD }">
<a if="{ CODALAB.state.user.can_create_competition }" class="ui right floated green button" href="{ URLS.COMPETITION_ADD }">
<i class="add square icon"></i> Create
</a>
</div>
Expand Down
2 changes: 1 addition & 1 deletion src/static/riot/competitions/public-list.tag
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
<!-- Title -->
<div class="page-header">
<h1 class="page-title">Public Benchmarks and Competitions</h1>
<div class="action-buttons">
<div class="action-buttons" if="{ CODALAB.state.user.can_create_competition }">
<a class="create-btn" href="{ URLS.COMPETITION_ADD }">
<i class="bi bi-plus-square-fill me-1"></i> Create
</a>
Expand Down
4 changes: 3 additions & 1 deletion src/utils/context_processors.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import json
import os
from django.conf import settings
from api.permissions import user_can_create_competition

BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Set the absolute path for the version file
Expand All @@ -19,10 +20,11 @@ def common_settings(request):
"bio": request.user.bio,
"is_staff": request.user.is_staff,
"is_superuser": request.user.is_superuser,
"can_create_competition": user_can_create_competition(request.user),
"logged_in": request.user.is_authenticated,
}
else:
user_json_data = {"logged_in": False}
user_json_data = {"logged_in": False, "can_create_competition": False}

# Read version information from the version.json file
version_info = {}
Expand Down