From d9557ba4d22bd7d1c9c91d8db734093e9f1a3079 Mon Sep 17 00:00:00 2001 From: Tanner Prestegard <tanner.prestegard@ligo.org> Date: Thu, 11 Oct 2018 14:39:45 -0500 Subject: [PATCH] Enforce protected labels Enforce label protection through the API for applying labels, removing labels, and creating events and superevents with labels attached. We also don't allow users to reapply a signoff request label when a signoff status label is already applied (e.g., can't apply ADVREQ when ADVNO already exists). --- gracedb/api/v1/events/views.py | 4 +- gracedb/api/v1/superevents/serializers.py | 48 ++++++++++++++++++++++- gracedb/api/v1/superevents/views.py | 16 ++++++-- gracedb/events/forms.py | 18 +++++++++ gracedb/events/models.py | 12 ++++++ gracedb/events/view_logic.py | 23 +++++++++-- gracedb/events/views.py | 18 +++++---- 7 files changed, 121 insertions(+), 18 deletions(-) diff --git a/gracedb/api/v1/events/views.py b/gracedb/api/v1/events/views.py index 1a04d2dcb..17b0d037f 100644 --- a/gracedb/api/v1/events/views.py +++ b/gracedb/api/v1/events/views.py @@ -703,7 +703,7 @@ class EventLabel(APIView): def put(self, request, event, label): try: rv, label_created = create_label(event, request, label) - except ValueError, e: + except (ValueError, Label.ProtectedLabelError) as e: return Response(e.message, status=status.HTTP_400_BAD_REQUEST) @@ -717,7 +717,7 @@ class EventLabel(APIView): def delete(self, request, event, label): try: rv = delete_label(event, request, label) - except ValueError, e: + except (ValueError, Label.ProtectedLabelError) as e: return Response(e.message, status=status.HTTP_400_BAD_REQUEST) diff --git a/gracedb/api/v1/superevents/serializers.py b/gracedb/api/v1/superevents/serializers.py index 87571cb9a..f85e53e9c 100644 --- a/gracedb/api/v1/superevents/serializers.py +++ b/gracedb/api/v1/superevents/serializers.py @@ -34,7 +34,10 @@ class SupereventSerializer(serializers.ModelSerializer): 'Superevent'), 'category_mismatch': _('Event {graceid} is of type \'{e_category}\', ' 'and cannot be assigned to a superevent of ' - 'type \'{s_category}\''), + 'type \'{s_category}\''), + 'protected_label': _('The following label(s) are managed by an' + 'automated process and cannot be manually added: ' + '{labels}'), } # Fields @@ -71,6 +74,7 @@ class SupereventSerializer(serializers.ModelSerializer): preferred_event = data.get('preferred_event') events = data.get('events') category = data.get('category') + labels = data.get('labels') category_display = \ dict(Superevent.SUPEREVENT_CATEGORY_CHOICES)[category] @@ -97,6 +101,13 @@ class SupereventSerializer(serializers.ModelSerializer): e_category=ev.get_event_category(), s_category=category_display) + # Can't add protected labels to a superevent + if labels: + protected_labels = [l.name for l in labels if l.protected] + if protected_labels: + self.fail('protected_label', + labels=', '.join(protected_labels)) + return data def create(self, validated_data): @@ -251,6 +262,13 @@ class SupereventEventSerializer(serializers.ModelSerializer): class SupereventLabelSerializer(serializers.ModelSerializer): + default_error_messages = { + 'protected_label': _('The label \'{label}\' is managed by an automated' + ' process and cannot be manually added'), + 'bad_signoff_request_label': _('The \'{label}\' label cannot be ' + 'applied to request a signoff because ' + 'a related signoff already exists.'), + } # Read only fields self = serializers.SerializerMethodField(read_only=True) created = serializers.DateTimeField(format=settings.GRACE_STRFTIME_FORMAT, @@ -259,7 +277,7 @@ class SupereventLabelSerializer(serializers.ModelSerializer): read_only=True) # Read/write name = serializers.SlugRelatedField(source='label', slug_field='name', - queryset=Label.objects.all()) + queryset=Label.objects.all(), required=True) # Write only fields (submitter used to set creator for created instance) submitter = serializers.HiddenField(write_only=True, default=serializers.CurrentUserDefault()) @@ -276,6 +294,32 @@ class SupereventLabelSerializer(serializers.ModelSerializer): obj.superevent.superevent_id, obj.label.name], request=self.context.get('request', None)) + def validate(self, data): + data = super(SupereventLabelSerializer, self).validate(data) + label = data.get('label') + superevent = data.get('superevent') + + # Don't allow protected labels to be applied + if label.protected: + self.fail('protected_label', label=label.name) + + # If a label exists for a signoff status, users shouldn't be + # able to reapply the related "request signoff" label. Example: + # ADVOK is already applied since an advocate signoff with 'OK' + # status exists. So users shouldn't be able to apply 'ADVREQ' in + # this case. + req_labels = { + 'ADVREQ': ['ADVNO', 'ADVOK'], + 'H1OPS': ['H1NO', 'H1OK'], + 'L1OPS': ['L1NO', 'L1OK'], + 'V1OPS': ['V1NO', 'V1OK'], + } + if (label.name in req_labels and superevent.labels.filter( + name__in=req_labels[label.name]).exists()): + self.fail('bad_signoff_request_label', label=label.name) + + return data + def create(self, validated_data): # Function-level import to prevent circular import in alerts from superevents.utils import add_label_to_superevent diff --git a/gracedb/api/v1/superevents/views.py b/gracedb/api/v1/superevents/views.py index 1c357aa9e..ca4b2faba 100644 --- a/gracedb/api/v1/superevents/views.py +++ b/gracedb/api/v1/superevents/views.py @@ -43,7 +43,7 @@ from .serializers import SupereventSerializer, SupereventUpdateSerializer, \ from .settings import SUPEREVENT_LOOKUP_URL_KWARG, SUPEREVENT_LOOKUP_REGEX from .viewsets import SupereventNestedViewSet from ..filters import DjangoObjectAndGlobalPermissionsFilter -from ..mixins import SafeCreateMixin, SafeDestroyMixin +from ..mixins import SafeCreateMixin, SafeDestroyMixin, ValidateDestroyMixin from ..paginators import BasePaginationFactory, CustomLabelPagination, \ CustomLogTagPagination from ...utils import api_reverse @@ -118,7 +118,6 @@ class SupereventEventViewSet(SafeDestroyMixin, destroy_error_classes = (Superevent.PreferredEventRemovalError,) destroy_error_response_status = status.HTTP_400_BAD_REQUEST list_view_order_by = ('pk',) - # TODO: do we need to filter events by user? def get_object(self): queryset = self.filter_queryset(self.get_queryset()) @@ -137,7 +136,8 @@ class SupereventEventViewSet(SafeDestroyMixin, add_event_log=True, issue_alert=True) -class SupereventLabelViewSet(SupereventNestedViewSet): +class SupereventLabelViewSet(ValidateDestroyMixin, + SupereventNestedViewSet): """Superevent labels""" serializer_class = SupereventLabelSerializer pagination_class = CustomLabelPagination @@ -147,6 +147,16 @@ class SupereventLabelViewSet(SupereventNestedViewSet): lookup_field = 'label__name' list_view_order_by = ('label__name',) + def validate_destroy(self, request, instance): + # Don't allow removal of protected labels + if instance.label.protected: + err_msg = ('The label \'{label}\' is managed by an automated ' + 'process and cannot be removed manually').format( + label=instance.label.name) + return False, err_msg + else: + return True, None + def perform_destroy(self, instance): remove_label_from_superevent(instance, self.request.user, add_log_message=True, issue_alert=True) diff --git a/gracedb/events/forms.py b/gracedb/events/forms.py index 5fdb51036..fc70f22ef 100644 --- a/gracedb/events/forms.py +++ b/gracedb/events/forms.py @@ -1,3 +1,5 @@ +import logging + from django import forms from django.utils.safestring import mark_safe from django.utils.html import escape @@ -11,6 +13,9 @@ from .fields import GraceQueryField from .query import parseQuery, filter_for_labels from pyparsing import ParseException +# Set up logger +logger = logging.getLogger(__name__) + htmlEntityStar = "★" htmlEntityRightPointingHand = "☞" htmlEntitySkullAndCrossbones = "☠" @@ -39,6 +44,19 @@ class CreateEventForm(forms.Form): # don't specify this parameter. offline = forms.BooleanField(required=False) + def clean(self): + cleaned_data = super(CreateEventForm, self).clean() + + # Don't allow protected labels + labels = cleaned_data.get('labels') + protected_labels = labels.filter(protected=True) + if protected_labels.exists(): + raise forms.ValidationError({'labels': 'The following label(s) are' + ' managed automatically and cannot be manually applied: {0}' + .format(', '.join([l.name for l in protected_labels]))}) + + return cleaned_data + class EventSearchForm(forms.Form): graceidStart = forms.CharField(required=False) diff --git a/gracedb/events/models.py b/gracedb/events/models.py index 000c450e3..e4dcb6012 100644 --- a/gracedb/events/models.py +++ b/gracedb/events/models.py @@ -98,8 +98,20 @@ class Label(models.Model): def __unicode__(self): return self.name + class ProtectedLabelError(Exception): + # To be raised when an attempt is made to apply or remove a + # protected label to/from an event or superevent + pass + + class RelatedSignoffExistsError(Exception): + # To be raised when an attempt is made to apply a "signoff request" + # label (like ADVREQ, H1OPS, etc.) when a signoff of that type already + # exists (example: an advocate signoff exists and ADVOK or ADVNO is + # applied, but a user tries to apply 'ADVREQ') + pass DEFAULT_PIPELINE_ID = 1 + class Event(models.Model): objects = InheritanceManager() # Queries can return subclasses, if available. diff --git a/gracedb/events/view_logic.py b/gracedb/events/view_logic.py index ef45e92ac..f19d461c6 100644 --- a/gracedb/events/view_logic.py +++ b/gracedb/events/view_logic.py @@ -188,15 +188,23 @@ def _createEventFromForm(request, form): event = None return event, warnings -def create_label(event, request, labelName, doAlert=True, doXMPP=True): + +def create_label(event, request, labelName, can_add_protected=False, + doAlert=True, doXMPP=True): + creator = request.user - event_url = request.build_absolute_uri(reverse('view', args=[event.graceid()])) d = {} try: label = Label.objects.filter(name=labelName)[0] except IndexError: raise ValueError("No such Label '%s'" % labelName) + # Check if label is protected + if label.protected and not can_add_protected: + err_msg = ('The \'{label}\' label is managed as part of an automated ' + 'process and cannot be applied manually').format(label=label.name) + raise Label.ProtectedLabelError(err_msg) + # Don't add a label more than once. # track whether label is actually created so as to # send the correct HTTP response code @@ -231,11 +239,12 @@ def create_label(event, request, labelName, doAlert=True, doXMPP=True): # and label_created bool return json.dumps(d), label_created -def delete_label(event, request, labelName, doXMPP=True): +def delete_label(event, request, labelName, can_remove_protected=False, + doXMPP=True): + # This function deletes a label. It starts out a lot like the create # label function. First get user and event info: creator = request.user - event_url = request.build_absolute_uri(reverse('view', args=[event.graceid()])) d = {} # First,throw out an error if the label doesn't exist in the list of available @@ -245,6 +254,12 @@ def delete_label(event, request, labelName, doXMPP=True): except IndexError: raise ValueError("No such Label '%s'" % labelName) + # Check if label is protected + if label.protected and not can_remove_protected: + err_msg = ('The \'{label}\' label is managed as part of an automated ' + 'process and cannot be removed manually').format(label=label.name) + raise Label.ProtectedLabelError(err_msg) + # Next, check if the label is in the list of labels for the event. Throw out an # error if it isn't. There might be a more elegant way of doing this. if label not in event.labels.all(): diff --git a/gracedb/events/views.py b/gracedb/events/views.py index 0781639d5..1894d9420 100644 --- a/gracedb/events/views.py +++ b/gracedb/events/views.py @@ -902,11 +902,13 @@ def modify_signoff(request, event): # Remove the request label. for l in event.labelling_set.all(): if l.label.name == req_label: - delete_label(event, request, req_label) + delete_label(event, request, req_label, + can_remove_protected=False) # Create a new label. label_name = label_stem + status - create_label(event, request, label_name, doAlert=False, doXMPP=False) + create_label(event, request, label_name, can_add_protected=True, + doAlert=False, doXMPP=False) # Create a log message msg = "%s signoff certified status as %s" % (signoff_type, status) @@ -950,10 +952,11 @@ def modify_signoff(request, event): label_name = label_stem + signoff.status existing_label = event.labelling_set.get( label__name=label_name).label.name - delete_label(event, request, existing_label) + delete_label(event, request, existing_label, + can_remove_protected=True) # also restore the label - create_label(event, request, req_label) + create_label(event, request, req_label, can_add_protected=False) # Create a log message msg = "deleted %s signoff status" % signoff_type @@ -981,11 +984,12 @@ def modify_signoff(request, event): label_name = label_stem + signoff.status existing_label = event.labelling_set.get( label__name=label_name).label.name - delete_label(event, request, existing_label) + delete_label(event, request, existing_label, + can_remove_protected=True) # Create a new label. label_name = label_stem + status - create_label(event, request, label_name, doAlert=False, - doXMPP=False) + create_label(event, request, label_name, + can_add_protected=True, doAlert=False, doXMPP=False) # update the values signoff.status = status -- GitLab