From 66dfd2913708b7b679b8b32cde6e2a1c7e01903c Mon Sep 17 00:00:00 2001 From: Tanner Prestegard <tanner.prestegard@ligo.org> Date: Wed, 5 Sep 2018 11:29:09 -0500 Subject: [PATCH] Adding permissions resource to superevents API Adding an endpoint to the superevents API for listing, retrieving, and modifying superevent access permissions. Includes views, a serializer, permissions, urls, etc. --- gracedb/api/v1/superevents/permissions.py | 26 +++ gracedb/api/v1/superevents/serializers.py | 23 ++- .../api/v1/superevents/tests/test_access.py | 184 +++++++++++++++++- gracedb/api/v1/superevents/url_templates.py | 6 +- gracedb/api/v1/superevents/urls.py | 14 ++ gracedb/api/v1/superevents/views.py | 66 ++++++- 6 files changed, 311 insertions(+), 8 deletions(-) diff --git a/gracedb/api/v1/superevents/permissions.py b/gracedb/api/v1/superevents/permissions.py index 022926300..c10418f83 100644 --- a/gracedb/api/v1/superevents/permissions.py +++ b/gracedb/api/v1/superevents/permissions.py @@ -342,6 +342,32 @@ class SupereventVOEventModelPermissions(permissions.DjangoModelPermissions): message = 'You do not have permission to create VOEvents.' +class SupereventGroupObjectPermissionPermissions( + FunctionalModelPermissions): + allowed_methods = ['OPTIONS', 'HEAD', 'GET', 'POST'] + + def get_get_permissions(self, request): + required_permissions = [ + 'superevents.view_supereventgroupobjectpermission', + ] + self.message = 'You are not allowed to view superevent permissions.' + return required_permissions + + def get_post_permissions(self, request): + # Get action from request data + action = request.data.get('action', None) + + required_permissions = [] + if (action == 'expose'): + required_permissions.append('superevents.expose_superevent') + self.message = 'You are not allowed to expose superevents.' + elif (action == 'hide'): + required_permissions.append('superevents.hide_superevent') + self.message = 'You are not allowed to hide superevents.' + + return required_permissions + + class SupereventSignoffModelPermissions(FunctionalModelPermissions): allowed_methods = ['OPTIONS', 'HEAD', 'GET', 'POST', 'PATCH', 'DELETE'] diff --git a/gracedb/api/v1/superevents/serializers.py b/gracedb/api/v1/superevents/serializers.py index 1c66747aa..d228c6d3a 100644 --- a/gracedb/api/v1/superevents/serializers.py +++ b/gracedb/api/v1/superevents/serializers.py @@ -5,6 +5,7 @@ import os from django.conf import settings from django.contrib.auth import get_user_model +from django.contrib.auth.models import Group as AuthGroup from django.utils.translation import ugettext_lazy as _ from rest_framework import serializers, validators @@ -12,7 +13,7 @@ from rest_framework.exceptions import ValidationError from events.models import Event, Label, Tag, EMGroup from superevents.models import Superevent, Labelling, Log, VOEvent, \ - EMObservation, EMFootprint, Signoff + EMObservation, EMFootprint, Signoff, SupereventGroupObjectPermission from .settings import SUPEREVENT_LOOKUP_URL_KWARG from ..fields import ParentObjectDefault, CommaSeparatedOrListField, \ ChoiceDisplayField @@ -777,3 +778,23 @@ class SupereventSignoffSerializer(serializers.ModelSerializer): instance = update_signoff(instance, updater, status, comment, add_log_message = True, issue_alert=True) return instance + + +class SupereventGroupObjectPermissionSerializer(serializers.ModelSerializer): + """ + NOTE: this is actually a Group serializer, but the purpose is to show + a list of GroupObjectPermissions for this group-superevent pair. + """ + permissions = serializers.SerializerMethodField(read_only=True) + action = serializers.ChoiceField(write_only=True, + choices=['expose', 'hide']) + superevent = serializers.HiddenField(write_only=True, + default=ParentObjectDefault(context_key='superevent')) + + class Meta: + model = AuthGroup + fields = ['name', 'permissions', 'action', 'superevent'] + + def get_permissions(self, obj): + return [sgop.permission.codename for sgop in + obj.supereventgroupobjectpermission_set.all()] diff --git a/gracedb/api/v1/superevents/tests/test_access.py b/gracedb/api/v1/superevents/tests/test_access.py index 45937bc35..7677b27da 100644 --- a/gracedb/api/v1/superevents/tests/test_access.py +++ b/gracedb/api/v1/superevents/tests/test_access.py @@ -18,7 +18,7 @@ from core.tests.utils import GraceDbTestBase, \ from events.models import Label, Tag, EMGroup from superevents.models import Superevent, Labelling, Log, VOEvent, \ EMObservation, Signoff -from superevents.utils import create_log +from superevents.utils import create_log, expose_superevent from .mixins import SupereventCreateMixin from ...settings import API_VERSION @@ -42,8 +42,9 @@ class SupereventSetup(GraceDbTestBase, SupereventCreateMixin): cls.internal_superevent = cls.create_superevent(cls.internal_user) cls.lvem_superevent = cls.create_superevent(cls.internal_user) - # Expose one to LV-EM and assign relevant permissions - expose_event_or_superevent_to_lvem(cls.lvem_superevent) + # Expose one to the LV-EM and assign relevant permissions + expose_superevent(cls.lvem_superevent, cls.internal_user, + add_log_message=False, issue_alert=False) class TestSupereventListGet(SupereventSetup, GraceDbApiTestBase): @@ -2565,6 +2566,183 @@ class TestSupereventFileDetail(SupereventSetup, GraceDbApiTestBase): # TODO +class TestSupereventGroupObjectPermissionList(SupereventSetup, + GraceDbApiTestBase): + + def test_internal_user_get_permissions(self): + """Internal user can view permissions list for all superevents""" + # Internal + url = v_reverse('superevents:superevent-permission-list', + args=[self.internal_superevent.superevent_id]) + response = self.request_as_user(url, "GET", self.internal_user) + # Check response and data + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data['results'], []) + + # Exposed + url = v_reverse('superevents:superevent-permission-list', + args=[self.lvem_superevent.superevent_id]) + response = self.request_as_user(url, "GET", self.internal_user) + # Check response and data + data = response.data['results'] + groups = [p['name'] for p in data] + self.assertEqual(response.status_code, 200) + self.assertEqual(len(data), 2) + self.assertIn(settings.PUBLIC_GROUP, groups) + self.assertIn(settings.LVEM_OBSERVERS_GROUP, groups) + + def test_lvem_user_get_permissions(self): + """LV-EM user can't get permission list""" + # Internal + url = v_reverse('superevents:superevent-permission-list', + args=[self.internal_superevent.superevent_id]) + response = self.request_as_user(url, "GET", self.lvem_user) + # Check response and data + self.assertEqual(response.status_code, 404) + + # Exposed + url = v_reverse('superevents:superevent-permission-list', + args=[self.lvem_superevent.superevent_id]) + response = self.request_as_user(url, "GET", self.lvem_user) + # Check response and data + self.assertEqual(response.status_code, 403) + + def test_public_user_get_permissions(self): + """Public user can't get permission list""" + # Internal + url = v_reverse('superevents:superevent-permission-list', + args=[self.internal_superevent.superevent_id]) + response = self.request_as_user(url, "GET") + # Check response and data + self.assertEqual(response.status_code, 403) + # TODO: this will be 404 in the future + + # Exposed: TODO + + +class TestSupereventGroupObjectPermissionDetail(SupereventSetup, + GraceDbApiTestBase): + + def test_internal_user_get_permissions_detail(self): + """Internal user can view all permissions detail for all superevents""" + for s in Superevent.objects.all(): + for gop in s.supereventgroupobjectpermission_set.all(): + url = v_reverse('superevents:superevent-permission-detail', + args=[s.superevent_id, gop.group.name]) + response = self.request_as_user(url, "GET", self.internal_user) + # Check response and data + self.assertEqual(response.status_code, 200) + + def test_lvem_user_get_permissions_detail(self): + """LV-EM user can't get permission details""" + # Internal + url = v_reverse('superevents:superevent-permission-detail', + args=[self.internal_superevent.superevent_id, + settings.LVEM_OBSERVERS_GROUP]) + response = self.request_as_user(url, "GET", self.lvem_user) + # Check response and data + self.assertEqual(response.status_code, 404) + + # Exposed + url = v_reverse('superevents:superevent-permission-detail', + args=[self.lvem_superevent.superevent_id, + settings.LVEM_OBSERVERS_GROUP]) + response = self.request_as_user(url, "GET", self.lvem_user) + # Check response and data + self.assertEqual(response.status_code, 403) + + def test_public_user_get_permissions(self): + """Public user can't get permission details""" + # Internal + url = v_reverse('superevents:superevent-permission-detail', + args=[self.internal_superevent.superevent_id, + settings.PUBLIC_GROUP]) + response = self.request_as_user(url, "GET") + # Check response and data + self.assertEqual(response.status_code, 403) + # TODO: this will be 404 in the future + + # Exposed: TODO + + +class TestSupereventGroupObjectPermissionModify(SupereventSetup, + AccessManagersGroupAndUserSetup, GraceDbApiTestBase): + + def test_internal_user_expose_internal_superevent(self): + """Internal user can't modify permissions to expose superevent""" + url = v_reverse('superevents:superevent-permission-modify', + args=[self.internal_superevent.superevent_id]) + response = self.request_as_user(url, "POST", self.internal_user, + data={'action': 'expose'}) + # Check response + self.assertEqual(response.status_code, 403) + self.assertIn('not allowed to expose superevents', + response.data['detail']) + + def test_internal_user_hide_exposed_superevent(self): + """Internal user can't modify permissions to hide superevent""" + url = v_reverse('superevents:superevent-permission-modify', + args=[self.lvem_superevent.superevent_id]) + response = self.request_as_user(url, "POST", self.internal_user, + data={'action': 'hide'}) + # Check response + self.assertEqual(response.status_code, 403) + self.assertIn('not allowed to hide superevents', + response.data['detail']) + + def test_access_manager_expose_internal_superevent(self): + """Access manager can modify permissions to expose superevent""" + url = v_reverse('superevents:superevent-permission-modify', + args=[self.internal_superevent.superevent_id]) + response = self.request_as_user(url, "POST", self.am_user, + data={'action': 'expose'}) + # Check response + groups = [p['name'] for p in response.data] + self.assertEqual(response.status_code, 200) + self.assertEqual(len(response.data), 2) + self.assertIn(settings.PUBLIC_GROUP, groups) + self.assertIn(settings.LVEM_OBSERVERS_GROUP, groups) + + def test_access_manager_hide_exposed_superevent(self): + """Access manager can modify permissions to hide superevent""" + url = v_reverse('superevents:superevent-permission-modify', + args=[self.lvem_superevent.superevent_id]) + response = self.request_as_user(url, "POST", self.am_user, + data={'action': 'hide'}) + # Check response + self.assertEqual(response.status_code, 200) + self.assertEqual(response.data, []) + + def test_lvem_user_permissions_modify(self): + """LV-EM user can't modify permissions""" + # Internal - expose + url = v_reverse('superevents:superevent-permission-modify', + args=[self.internal_superevent.superevent_id]) + response = self.request_as_user(url, "POST", self.lvem_user, + data={'action': 'expose'}) + # Check response and data + self.assertEqual(response.status_code, 404) + + # Exposed + url = v_reverse('superevents:superevent-permission-modify', + args=[self.lvem_superevent.superevent_id]) + response = self.request_as_user(url, "POST", self.lvem_user, + data={'action': 'hide'}) + # Check response and data + self.assertEqual(response.status_code, 403) + + def test_public_user_get_permissions(self): + # Internal + url = v_reverse('superevents:superevent-permission-modify', + args=[self.internal_superevent.superevent_id]) + response = self.request_as_user(url, "GET") + # Check response and data + self.assertEqual(response.status_code, 403) + # TODO: this will be 404 in the future + + # Exposed: TODO + + class TestSupereventSignoffList(SupereventSetup, GraceDbApiTestBase): @classmethod diff --git a/gracedb/api/v1/superevents/url_templates.py b/gracedb/api/v1/superevents/url_templates.py index 36e46babc..49433e0bb 100644 --- a/gracedb/api/v1/superevents/url_templates.py +++ b/gracedb/api/v1/superevents/url_templates.py @@ -3,7 +3,8 @@ from __future__ import absolute_import from .views import SupereventViewSet, SupereventEventViewSet, \ SupereventLabelViewSet, SupereventLogViewSet, SupereventLogTagViewSet, \ SupereventFileViewSet, SupereventVOEventViewSet, \ - SupereventEMObservationViewSet, SupereventSignoffViewSet + SupereventEMObservationViewSet, SupereventGroupObjectPermissionViewSet, \ + SupereventSignoffViewSet from ...utils import api_reverse # Placeholder parameters for getting URLs with reverse @@ -18,6 +19,7 @@ PH = { SupereventEMObservationViewSet.lookup_url_kwarg: '5555', # EMObservation # number (N) SupereventSignoffViewSet.lookup_url_kwarg: 'TYPE_INST', # type + instrument + SupereventGroupObjectPermissionViewSet.lookup_url_kwarg: 'GROUP_NAME', } @@ -50,6 +52,8 @@ def construct_url_templates(request=None): 'superevent-signoff-list': [], 'superevent-signoff-detail': [ PH[SupereventSignoffViewSet.lookup_url_kwarg]], + 'superevent-permission-list': [], + 'superevent-permission-modify': [], } # Dict of URL templates: diff --git a/gracedb/api/v1/superevents/urls.py b/gracedb/api/v1/superevents/urls.py index fdd539341..5e06e9169 100644 --- a/gracedb/api/v1/superevents/urls.py +++ b/gracedb/api/v1/superevents/urls.py @@ -93,6 +93,20 @@ suburlpatterns = [ SupereventSignoffViewSet.as_view({'get': 'retrieve', 'patch': 'partial_update', 'delete': 'destroy'}), name='superevent-signoff-detail'), + + # Permissions list and creation + url(r'permissions/$', SupereventGroupObjectPermissionViewSet.as_view( + {'get': 'list'}), name='superevent-permission-list'), + # Permissions modification (expose/hide superevent). Has to come before + # permissions detail, otherwise .+ wildcard will match it first. + url(r'^permissions/modify/$', + SupereventGroupObjectPermissionViewSet.as_view({'post': 'modify'}), + name='superevent-permission-modify'), + # Permissions detail + url(r'permissions/(?P<{lookup_url_kwarg}>.+)/$'.format(lookup_url_kwarg= + SupereventGroupObjectPermissionViewSet.lookup_url_kwarg), + SupereventGroupObjectPermissionViewSet.as_view({'get': 'retrieve'}), + name='superevent-permission-detail'), ] # Full urlpatterns diff --git a/gracedb/api/v1/superevents/views.py b/gracedb/api/v1/superevents/views.py index 38f1d67b9..8b6a6b4ba 100644 --- a/gracedb/api/v1/superevents/views.py +++ b/gracedb/api/v1/superevents/views.py @@ -6,6 +6,7 @@ import os from django.http import HttpResponse from django.db.models import QuerySet from django.shortcuts import get_object_or_404 +from django.contrib.auth.models import Group as AuthGroup from guardian.shortcuts import get_objects_for_user from rest_framework import mixins, parsers, permissions, serializers, status, \ @@ -23,7 +24,7 @@ from superevents.models import Superevent, Log, Signoff from superevents.utils import remove_tag_from_log, \ remove_event_from_superevent, remove_label_from_superevent, \ confirm_superevent_as_gw, get_superevent_by_date_id_or_404, \ - delete_signoff + expose_superevent, hide_superevent, delete_signoff from .filters import SupereventSearchFilter, SupereventOrderingFilter from .paginators import CustomSupereventPagination from .permissions import SupereventModelPermissions, \ @@ -32,12 +33,13 @@ from .permissions import SupereventModelPermissions, \ SupereventLogTagModelPermissions, SupereventLogTagObjectPermissions, \ SupereventVOEventModelPermissions, ParentSupereventAnnotatePermissions, \ SupereventSignoffModelPermissions, SupereventSignoffTypeModelPermissions, \ - SupereventSignoffTypeObjectPermissions + SupereventSignoffTypeObjectPermissions, \ + SupereventGroupObjectPermissionPermissions from .serializers import SupereventSerializer, SupereventUpdateSerializer, \ SupereventEventSerializer, SupereventLabelSerializer, \ SupereventLogSerializer, SupereventLogTagSerializer, \ SupereventVOEventSerializer, SupereventEMObservationSerializer, \ - SupereventSignoffSerializer + SupereventSignoffSerializer, SupereventGroupObjectPermissionSerializer from .settings import SUPEREVENT_LOOKUP_URL_KWARG, SUPEREVENT_LOOKUP_REGEX from .viewsets import SupereventNestedViewSet from ..filters import DjangoObjectAndGlobalPermissionsFilter @@ -410,3 +412,61 @@ class SupereventSignoffViewSet(viewsets.ModelViewSet, def perform_destroy(self, instance): delete_signoff(instance, self.request.user, add_log_message=True, issue_alert=True) + + +class SupereventGroupObjectPermissionViewSet(viewsets.ModelViewSet, + SafeCreateMixin, + SafeDestroyMixin, + SupereventNestedViewSet): + """ + View for object permissions associated with exposing/hiding + a superevent to/from LV-EM users or the public. + """ + serializer_class = SupereventGroupObjectPermissionSerializer + permission_classes = (permissions.IsAuthenticatedOrReadOnly, + SupereventGroupObjectPermissionPermissions,) + lookup_url_kwarg = 'name' + lookup_field = 'name' + + def get_queryset(self): + superevent = self.get_parent_object() + + # Get GOPs attached to parent superevent + gops = superevent.supereventgroupobjectpermission_set.all() + + # Determine groups for these GOPs and return queryset from that + gop_group_pks = gops.values_list('group', flat=True).distinct() + queryset = AuthGroup.objects.filter(pk__in=gop_group_pks) + return queryset + + @action(methods=['post'], detail=False) + def modify(self, request, superevent_id): + """ + Expose or hide a superevent by creating or deleting + GroupObjectPermissions + """ + + # Get superevent + superevent = self.get_parent_object() + + # Get action from data + action = request.data.get('action', None) + + # Validation + if action not in ['expose', 'hide']: + return Response('action must be \'expose\' or \'hide\'', + status=status.HTTP_400_BAD_REQUEST) + + # We make exposing and hiding a superevent idempotent + # so as to prevent possible errors due to multi-user + # race conditions + if action == 'expose' and not superevent.is_exposed: + expose_superevent(superevent, request.user, add_log_message=True, + issue_alert=True) + elif action == 'hide' and superevent.is_exposed: + hide_superevent(superevent, request.user, add_log_message=True, + issue_alert=True) + + # Return list of permissions + serializer = self.get_serializer(self.get_queryset(), many=True) + return Response(serializer.data, status=status.HTTP_200_OK) -- GitLab