Commit 3108693b authored by Tanner Prestegard's avatar Tanner Prestegard Committed by GraceDB

Add web view for enabling/disabling pipelines

A web view has been created which allows EM advocates to disable
or enable pipeline submissions. This is to prevent misbehaving
pipelines from submitting bad information. The mechanism works by
preventing events from being submitted to a given pipeline, not
by revoking certificates or removing user account permissions.
parent 8e0492c8
......@@ -13,7 +13,7 @@ from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ValidationError
from django.db import IntegrityError
from django.http import HttpResponse, HttpResponseForbidden, \
HttpResponseNotFound, HttpResponseServerError
HttpResponseNotFound, HttpResponseServerError, HttpResponseBadRequest
from django.http.request import QueryDict
from django.utils.functional import wraps
......@@ -469,6 +469,15 @@ class EventList(InheritPermissionsAPIView):
if not user_has_perm(request.user, "populate", pipeline):
return HttpResponseForbidden("You don't have permission on this pipeline.")
# Get search since we won't block MDC event submissions even if the
# pipeline is disabled
search_name = request.data.get('search', None)
if not pipeline.enabled and search_name != 'MDC':
err_msg = ('The {0} pipeline has been temporarily disabled by '
'an EM advocate due to suspected misbehavior.').format(
pipeline.name)
return HttpResponseBadRequest(err_msg)
# The following looks a bit funny but it is actually necessary. The
# django form expects a dict containing the POST data as the first
# arg, and a dict containing the FILE data as the second. In the
......
......@@ -253,6 +253,10 @@ class SignoffGroupsAndUsersSetup(TestCase):
content_type__app_label='superevents',
codename='do_adv_signoff')
g.permissions.add(*p)
p = Permission.objects.filter(
content_type__app_label='events',
codename='manage_pipeline')
g.permissions.add(*p)
# Also add user to internal group
internal_group.user_set.add(user)
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-04-02 20:19
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('events', '0032_create_imbh_search'),
]
operations = [
migrations.CreateModel(
name='PipelineLog',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created', models.DateTimeField(auto_now_add=True)),
('action', models.CharField(choices=[(b'D', b'disable'), (b'E', b'enable')], max_length=10)),
('creator', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
],
),
migrations.AlterModelOptions(
name='pipeline',
options={'permissions': (('manage_pipeline', 'Can enable or disable pipeline'),)},
),
migrations.AddField(
model_name='pipeline',
name='enabled',
field=models.BooleanField(default=True),
),
migrations.AddField(
model_name='pipelinelog',
name='pipeline',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='events.Pipeline'),
),
]
......@@ -69,10 +69,30 @@ class Group(models.Model):
class Pipeline(models.Model):
name = models.CharField(max_length=100)
enabled = models.BooleanField(default=True)
# XXX Need any additional fields? Like a librarian email? Or perhaps even fk?
class Meta:
permissions = (
('manage_pipeline', 'Can enable or disable pipeline'),
)
def __unicode__(self):
return self.name
class PipelineLog(models.Model):
PIPELINE_LOG_ACTION_DISABLE = 'D'
PIPELINE_LOG_ACTION_ENABLE = 'E'
PIPELINE_LOG_ACTION_CHOICES = (
(PIPELINE_LOG_ACTION_DISABLE, 'disable'),
(PIPELINE_LOG_ACTION_ENABLE, 'enable'),
)
creator = models.ForeignKey(UserModel)
pipeline = models.ForeignKey(Pipeline)
created = models.DateTimeField(auto_now_add=True)
action = models.CharField(max_length=10,
choices=PIPELINE_LOG_ACTION_CHOICES)
class Search(models.Model):
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
......
......@@ -45,6 +45,14 @@ urlpatterns = [
url(r'^(?P<graceid>[GEHMT]\d+)/emobservation/(?P<num>([\d]*|preview))$',
views.emobservation_entry, name="emobservation_entry"),
# Manage pipelines
url(r'^pipelines/manage/$', views.PipelineManageView.as_view(),
name='manage-pipelines'),
url(r'^pipelines/(?P<pk>\d+)/enable/$', views.PipelineEnableView.as_view(),
name='enable-pipeline'),
url(r'^pipelines/(?P<pk>\d+)/disable/$',
views.PipelineDisableView.as_view(), name='disable-pipeline'),
# Legacy URLs -------------------------------------------------------------
# Event detail
url(r'^view/(?P<graceid>[GEHMT]\d+)', views.view, name="legacyview"),
......
from datetime import timedelta
from django.core.exceptions import PermissionDenied
from django.http import HttpResponse
from django.http import HttpResponseRedirect, HttpResponseNotFound, HttpResponseBadRequest, Http404
from django.http import HttpResponseForbidden, HttpResponseServerError
from django.template import RequestContext
from django.urls import reverse
from django.urls import reverse, reverse_lazy
from django.shortcuts import render
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views.generic import ListView
from django.views.generic.edit import UpdateView
from core.file_utils import get_file_list
from core.http import check_and_serve_file
from .models import Event, Group, EventLog, Label, Tag, Pipeline, Search, GrbEvent
from .models import EMGroup, Signoff
from .models import EMGroup, Signoff, PipelineLog
from .forms import CreateEventForm, SignoffForm
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import User, Permission
from django.contrib.auth.models import Group as AuthGroup
from django.contrib.contenttypes.models import ContentType
from .permission_utils import filter_events_for_user, user_has_perm
from .permission_utils import internal_user_required, is_external, \
check_external_file_access
from .permission_utils import is_external, check_external_file_access
from guardian.models import GroupObjectPermission
from ligoauth.decorators import internal_user_required
from .view_logic import _createEventFromForm
from .view_logic import get_performance_info
......@@ -170,6 +176,15 @@ def _create(request):
if not user_has_perm(request.user, "populate", pipeline):
return HttpResponseForbidden("You do not have permission to submit events to this pipeline.")
# Get search since we won't block MDC event submissions even if
# the pipeline is disabled
search_name = request.POST.get('search', None)
if not pipeline.enabled and search_name != 'MDC':
err_msg = ('The {0} pipeline has been temporarily disabled by '
'an EM advocate due to suspected misbehavior.').format(
pipeline.name)
return HttpResponseBadRequest(err_msg)
form = CreateEventForm(request.POST, request.FILES)
if form.is_valid():
# Alert is issued in this function
......@@ -958,3 +973,94 @@ def modify_signoff(request, event):
# Finished. Redirect back to the event.
return HttpResponseRedirect(reverse("view", args=[event.graceid]))
# Managing pipeline submissions -----------------------------------------------
PIPELINE_LIST = ['gstlal', 'pycbc', 'MBTAOnline', 'CWB', 'oLIB', 'spiir']
PIPELINE_LOG_ACTION_DICT = dict(PipelineLog.PIPELINE_LOG_ACTION_CHOICES)
@method_decorator(internal_user_required(raise_exception=True),
name='dispatch')
class PipelineManageView(ListView):
model = Pipeline
template_name = 'gracedb/manage_pipelines.html'
log_number = 10
def get_queryset(self):
qs = Pipeline.objects.filter(name__in=PIPELINE_LIST).order_by('name')
return qs
def get_context_data(self, **kwargs):
context = super(PipelineManageView, self).get_context_data(**kwargs)
# Get number of events created in a few different time periods for
# each pipeline and last submission time
n_events_dict = {}
submission_dict = {}
now = timezone.now()
dts = [now-timedelta(minutes=1), now-timedelta(minutes=10),
now-timedelta(minutes=60), now-timedelta(days=1)]
for p in self.object_list:
n_events_dict[p.name] = [p.event_set.filter(created__gt=dt)
.exclude(group__name='Test').exclude(search__name='MDC')
.count() for dt in dts]
last_event = p.event_set.exclude(group__name='Test').exclude(
search__name='MDC').order_by('-pk').first()
submission_dict[p.name] = getattr(last_event, 'created', None)
context['n_events_dict'] = n_events_dict
context['submission_dict'] = submission_dict
# Get list of pipeline logs
context['logs'] = PipelineLog.objects.order_by('-created')[
:self.log_number]
log_message_template = '{pipeline} {action}d by {user} at {dt}'
context['log_messages'] = [log_message_template.format(
pipeline=log.pipeline.name, user=log.creator.get_full_name(),
dt=log.created.strftime('%H:%M:%S %Z on %B %e, %Y'),
action=PIPELINE_LOG_ACTION_DICT[log.action])
for log in context['logs']]
# Determine whether user can enable/disable pipelines
context['user_can_manage'] = self.request.user.has_perm(
'events.manage_pipeline')
return context
@method_decorator(permission_required('events.manage_pipeline',
raise_exception=True), name='dispatch')
class PipelineEnableView(UpdateView):
"""Enable a pipeline"""
success_url = reverse_lazy('manage-pipelines')
def get_queryset(self):
qs = Pipeline.objects.filter(name__in=PIPELINE_LIST)
return qs
def get(self, request, *args, **kwargs):
return self.post(request, *args, **kwargs)
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if not self.object.enabled:
self.object.enabled = True
self.object.save(update_fields=['enabled'])
PipelineLog.objects.create(creator=request.user,
pipeline=self.object,
action=PipelineLog.PIPELINE_LOG_ACTION_ENABLE)
return HttpResponseRedirect(self.get_success_url())
@method_decorator(permission_required('events.manage_pipeline',
raise_exception=True), name='dispatch')
class PipelineDisableView(PipelineEnableView):
"""Disable a pipeline"""
def post(self, request, *args, **kwargs):
self.object = self.get_object()
if self.object.enabled:
self.object.enabled = False
self.object.save(update_fields=['enabled'])
PipelineLog.objects.create(creator=request.user,
pipeline=self.object,
action=PipelineLog.PIPELINE_LOG_ACTION_DISABLE)
return HttpResponseRedirect(self.get_success_url())
......@@ -4,11 +4,14 @@ def LigoAuthContext(request):
user_is_internal = False
user_is_lvem = False
user_is_advocate = False # user is an EM advocate
if request.user:
if request.user.groups.filter(name=settings.LVC_GROUP).exists():
user_is_internal = True
if request.user.groups.filter(name=settings.LVEM_GROUP).exists():
user_is_lvem = True
if request.user.groups.filter(name=settings.EM_ADVOCATE_GROUP).exists():
user_is_advocate = True
return {'user': request.user, 'user_is_internal': user_is_internal,
'user_is_lvem': user_is_lvem}
'user_is_lvem': user_is_lvem, 'user_is_advocate': user_is_advocate}
# -*- coding: utf-8 -*-
# Generated by Django 1.11.18 on 2019-04-02 21:21
from __future__ import unicode_literals
from django.conf import settings
from django.db import migrations
from django.contrib.auth.management import create_permissions
GROUP_NAME = settings.EM_ADVOCATE_GROUP
PERMISSION_KWARGS = {
'content_type__app_label': 'events',
'codename': 'manage_pipeline',
}
# We have to run this to force the permissions to actually be created.
# Otherwise they are created by a post-migrate signal and it's not possible
# to run all of the migrations in a single command
def create_perms(apps, schema_editor):
for app_config in apps.get_app_configs():
app_config.models_module = True
create_permissions(app_config, apps=apps, verbosity=0)
app_config.models_module = None
def add_perm(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
Permission = apps.get_model('auth', 'Permission')
# Get permission
perm = Permission.objects.get(**PERMISSION_KWARGS)
# Get group
group = Group.objects.get(name=GROUP_NAME)
# Add permission to group
perm.group_set.add(group)
def remove_perm(apps, schema_editor):
Group = apps.get_model('auth', 'Group')
Permission = apps.get_model('auth', 'Permission')
# Get permission
perm = Permission.objects.get(**PERMISSION_KWARGS)
# Get group
group = Group.objects.get(name=GROUP_NAME)
# Remove permission from group
perm.group_set.remove(group)
class Migration(migrations.Migration):
dependencies = [
('auth', '0022_populate_raven_users_group'),
('events', '0033_pipelinelog_and_pipeline_enabled'),
]
operations = [
migrations.RunPython(create_perms, migrations.RunPython.noop),
migrations.RunPython(add_perm, remove_perm),
]
{% extends "base.html" %}
{% load util_tags %}
{% block title %}Manage pipelines{% endblock %}
{% block heading %}Enable/disable analysis pipelines{% endblock %}
{% block pageid %}pipelines{% endblock %}
{% load static %}
{% block headcontents %}
<link rel="stylesheet" href="{% static "css/bootstrap_buttons.css" %}"></script>
{{ block.super }}
{% endblock %}
{% block content %}
<div style="padding-bottom: 20px;">
<p>This page provides a brief summary of a pipeline's recent activity and provides the ability for EM advocates to disable (or enable) a pipeline's ability to submit events.
When a pipeline is disabled, non-Test, non-MDC events cannot be submitted for it.
This functionality is intended to mitigate the effects of a pipeline that is misbehaving and submitting erroneous event data.</p>
<p>All users: if you suspect any problems with a pipeline, you should contact one of the EM advocates who is currently on duty. The EM advocate schedule for O3 is <a href="https://ldas-jobs.ligo.caltech.edu/~emfollow/followup-advocate-guide/roster.html#shift-calendar">here</a>.</p>
<p>EM advocates: after you disable or enable a pipeline, you should contact the <a href="mailto:emfollow@ligo.org">low-latency group</a> to describe the action you have taken and the reasons for it.</p>
</div>
{% if user_can_manage %}
<div align="center" style="padding-bottom: 20px;">
<div style="background-color: red; color: white; display: inline-block; padding: 10px; border-radius: 5px;">
<p align="center" style="font-weight: bold; font-size: 1.8rem; margin: 0;">WARNING: this page is live and any actions you take here will have consequences. Please be careful!</p>
</div>
</div>
{% endif %}
<div style="padding-bottom: 40px;">
<table class="bootstrap-dark" align="center">
<caption>Pipeline status</caption>
<tr>
<th></th>
<th></th>
<th style="text-align: center;" colspan="5">Recent submissions</th>
<th></th>
</tr>
<tr>
<th>Pipeline</th>
<th class="center">Current status</th>
<th class="center">Time of last submission (UTC)</th>
<th class="center">Last minute</th>
<th class="center">Last 10 minutes</th>
<th class="center">Last hour</th>
<th class="center">Last day</th>
<th class="center">Action</th>
</tr>
{% for pipeline in pipeline_list %}
<tr>
<td>{{ pipeline.name }}</td>
<td class="center">
{% if pipeline.enabled %}
<span style="color: green; font-weight: bold;">Enabled</span>
{% else %}
<span style="color: red; font-weight: bold;">Disabled</span>
{% endif %}
</td>
<td class="center">{{ submission_dict|get_item:pipeline.name }}</td>
{% with n_events_dict|get_item:pipeline.name as n_events_list %}
<td class="center">{{ n_events_list.0 }}</td>
<td class="center">{{ n_events_list.1 }}</td>
<td class="center">{{ n_events_list.2 }}</td>
<td class="center">{{ n_events_list.3 }}</td>
{% endwith %}
<td class="center">
{% if pipeline.enabled %}
{% if user_can_manage %}
<a class="btn btn-danger" href="{% url "disable-pipeline" pipeline.pk %}">Disable</a>
{% else %}
<button class="btn btn-danger" disabled>Disable</button>
{% endif %}
{% else %}
{% if user_can_manage %}
<a class="btn btn-success" href="{% url "enable-pipeline" pipeline.pk %}">Enable</a>
{% else %}
<button class="btn btn-success" disabled>Enable</button>
{% endif %}
{% endif %}
</td>
</tr>
{% endfor %}
</table>
</div>
{% if log_messages %}
<div style="padding-bottom: 10px;">
<table class="bootstrap-dark" align="center">
<caption>Recent activity</caption>
<tr>
<th>#</th>
<th>Comment</th>
{% for log in log_messages %}
<tr>
<td>{{ forloop.revcounter }}
<td>{{ log }}</td>
</tr>
{% endfor %}
</table>
</div>
{% endif %}
{% endblock %}
......@@ -13,6 +13,9 @@
{% elif user_is_lvem %}
<li id="nav-password"><a href="{% url "manage-password" %}">Manage Password</a></li>
{% endif %}
{% if user_is_advocate %}
<li id="nav-pipelines"><a href="{% url "manage-pipelines" %}">Pipelines</a></li>
{% endif %}
<li id="nav-docs"><a href="{% url "home" %}documentation/">Documentation</a></li>
{% if user %}
{% if user.is_superuser %}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment