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 = "&#9733;"
 htmlEntityRightPointingHand = "&#9758;"
 htmlEntitySkullAndCrossbones = "&#9760;"
@@ -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