...
 
Commits (23)
......@@ -58,8 +58,6 @@ before_script:
libxml2-dev
swig
${PYTHON}-pip
# install voeventlib for python2
- if [[ "${PYTHON_MAJOR}" -eq 2 ]]; then apt-get -o dir::cache::archives="${APT_CACHE_DIR}" install -yqq python-voeventlib; fi
# install everything else from pip
- ${PYTHON} -m pip install -r requirements.txt
# create logs path required for tests
......
......@@ -28,7 +28,6 @@ RUN apt-get update && \
python2.7-dev \
python-libxml2 \
python-pip \
python-voeventlib \
procps \
shibboleth \
supervisor \
......
......@@ -21,6 +21,10 @@ def get_from_env(envvar, default_value=None, fail_if_not_found=True):
'Could not get environment variable {0}'.format(envvar))
return value
# Maintenance mode
MAINTENANCE_MODE = False
MAINTENANCE_MODE_MESSAGE = None
# Version ---------------------------------------------------------------------
PROJECT_VERSION = '2.6.3'
......@@ -307,6 +311,7 @@ AUTHENTICATION_BACKENDS = [
# List of middleware classes to use.
MIDDLEWARE = [
'core.middleware.maintenance.MaintenanceModeMiddleware',
'events.middleware.PerformanceMiddleware',
'core.middleware.accept.AcceptMiddleware',
'core.middleware.api.ClientVersionMiddleware',
......
......@@ -61,6 +61,18 @@ TWILIO_AUTH_TOKEN = os.environ.get('DJANGO_TWILIO_AUTH_TOKEN', None)
if TWILIO_AUTH_TOKEN is None:
raise ImproperlyConfigured('Could not get Twilio auth token from envvars.')
# Get maintenance mode settings from environment
maintenance_mode = get_from_env(
'DJANGO_MAINTENANCE_MODE_ACTIVE',
default_value=False,
fail_if_not_found=False
)
if (isinstance(maintenance_mode, str) and
maintenance_mode.lower() in ['true', 't']):
MAINTENANCE_MODE = True
MAINTENANCE_MODE_MESSAGE = \
get_from_env('DJANGO_MAINTENANCE_MODE_MESSAGE', fail_if_not_found=False)
# Get email settings from environment
EMAIL_BACKEND = 'django_ses.SESBackend'
AWS_SES_ACCESS_KEY_ID = get_from_env('AWS_SES_ACCESS_KEY_ID')
......
......@@ -33,6 +33,7 @@ ALLOWED_HOSTS += ['testserver']
# Home page stuff
INSTANCE_TITLE = 'GraceDB Playground'
INSTANCE_INFO = """
<h3>Playground instance</h3>
<p>
This GraceDB instance is designed for users to develop and test their own
applications. It mimics the production instance in all but the following ways:
......
......@@ -2,7 +2,7 @@ from django.conf import settings
from django.conf.urls import url, include
from django.contrib import admin
from django.contrib.auth.views import logout
from django.contrib.auth.views import LogoutView
from django.views.generic import TemplateView
# Import feeds
......@@ -10,7 +10,9 @@ import core.views
from events.feeds import EventFeed, feedview
import events.reports
import events.views
from ligoauth.views import pre_login, post_login, shib_logout, manage_password
from ligoauth.views import (
manage_password, ShibLoginView, ShibPostLoginView
)
import search.views
# Django admin auto-discover
......@@ -49,9 +51,9 @@ urlpatterns = [
url(r'^search/$', search.views.search, name="mainsearch"),
# Authentication
url(r'^login/$', pre_login, name='login'),
url(r'^post-login/$', post_login, name='post-login'),
url(r'^logout/$', shib_logout, name='logout'),
url(r'^login/$', ShibLoginView.as_view(), name='login'),
url(r'^post-login/$', ShibPostLoginView.as_view(), name='post-login'),
url(r'^logout/$', LogoutView.as_view(), name='logout'),
# Password management
url('^manage-password/$', manage_password, name='manage-password'),
......
import logging
from rest_framework import permissions
# Set up logger
logger = logging.getLogger(__name__)
class CanUpdateGrbEvent(permissions.BasePermission):
def has_permission(self, request, view):
return request.user.has_perm('events.t90_grbevent')
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
from django.db.models import Q
from django.urls import reverse
from rest_framework.test import APIRequestFactory as rf
from events.models import GrbEvent
from ..views import GrbEventPatchView
from ...settings import API_VERSION
def v_reverse(viewname, *args, **kwargs):
"""Easily customizable versioned API reverse for testing"""
viewname = 'api:{version}:'.format(version=API_VERSION) + viewname
return reverse(viewname, *args, **kwargs)
def test_access(mock_internal_user):
# Create a mock event
mock_event = mock.MagicMock(spec=GrbEvent, graceid='E1234')
mock_event.search.name = 'GRB'
mock_event.redshift = 3
# Get URL and set up request
url = v_reverse("events:update-grbevent", args=[mock_event.graceid])
data = {'redshift': 2}
request = rf().patch(url, data=data)
request.user = mock_internal_user
view = GrbEventPatchView.as_view()
with mock.patch('api.v1.events.views.EventAlertIssuer'), \
mock.patch('api.v1.events.views.Event.getByGraceid') as mock_get_event, \
mock.patch('api.v1.events.views.eventToDict'):
mock_get_event.return_value = mock_event
response = view(request, mock_event.graceid)
response.render()
assert mock_get_event.call_args[0] == (mock_event.graceid,)
......@@ -9,6 +9,8 @@ urlpatterns = [
url(r'^$', EventList.as_view(), name='event-list'),
url(r'^(?P<graceid>[GEHMT]\d+)$', EventDetail.as_view(),
name='event-detail'),
url(r'^(?P<graceid>[GEHMT]\d+)/update-grbevent/$',
GrbEventPatchView.as_view(), name='update-grbevent'),
# Event Log Resources
# events/{graceid}/logs/[{logid}]
......
......@@ -26,7 +26,9 @@ from glue.ligolw.ligolw import LIGOLWContentHandler
from glue.ligolw.lsctables import use_in
from guardian.models import GroupObjectPermission
from rest_framework import authentication, parsers, serializers, status
from rest_framework.permissions import IsAuthenticated, BasePermission, SAFE_METHODS
from rest_framework.exceptions import ValidationError as DrfValidationError
from rest_framework.permissions import IsAuthenticated, BasePermission, \
SAFE_METHODS
from rest_framework.renderers import BaseRenderer, JSONRenderer, \
BrowsableAPIRenderer
from rest_framework.response import Response
......@@ -40,7 +42,7 @@ from core.vfile import VersionedFile
from events.buildVOEvent import buildVOEvent, VOEventBuilderException
from events.forms import CreateEventForm
from events.models import Event, Group, Search, Pipeline, EventLog, Tag, \
Label, EMGroup, EMBBEventLog, EMSPECTRUM, VOEvent
Label, Labelling, EMGroup, EMBBEventLog, EMSPECTRUM, VOEvent, GrbEvent
from events.permission_utils import user_has_perm, filter_events_for_user, \
is_external, check_external_file_access
from events.translator import handle_uploaded_data
......@@ -53,6 +55,7 @@ from events.view_utils import eventToDict, eventLogToDict, labelToDict, \
from search.forms import SimpleSearchForm
from search.query.events import parseQuery, ParseException
from superevents.models import Superevent
from .permissions import CanUpdateGrbEvent
from .throttling import EventCreationThrottle, AnnotationThrottle
from ..mixins import InheritDefaultPermissionsMixin
from ...utils import api_reverse
......@@ -82,7 +85,7 @@ class IsAuthorizedForEvent(BasePermission):
# "Unsafe methods" require change permissions on the event.
# Note that DELETE is only implemented for event-log-tag
# relationships.
elif request.method in ['PUT','POST','DELETE']:
elif request.method in ['PUT', 'PATCH', 'POST', 'DELETE']:
shortname = 'change'
else:
return False
......@@ -637,6 +640,81 @@ class EventDetail(InheritPermissionsAPIView):
return Response(status=status.HTTP_202_ACCEPTED)
# New class *only* for updating GRB event properties
class GrbEventPatchView(InheritPermissionsAPIView):
permission_classes = (IsAuthenticated, IsAuthorizedForEvent,
CanUpdateGrbEvent)
updatable_attributes = ['t90', 'redshift', 'designation', 'ra', 'dec',
'error_radius']
def process_data(self, data):
return {k: (float(v) if k != 'designation' else v)
for k, v in data.items()}
def get_attributes_to_update(self, grbevent, data):
attrib_to_update = [k for k in data if (k in self.updatable_attributes
and getattr(grbevent, k, None) != data[k])]
# If none, raise an error
if not attrib_to_update:
raise DrfValidationError('Request would not modify the GRB event')
return {k: data[k] for k in attrib_to_update}
def generate_log_message(self, grbevent, update_dict):
# Templates
comment = "Updated GRB event parameters: {msg}"
param_template = "{name}: {old} -> {new}"
# Message strings for updated parameters
update_list = [
param_template.format(
name=k,
old=getattr(grbevent, k),
new=update_dict[k]
)
for k in update_dict
]
return comment.format(msg=", ".join(update_list))
@event_and_auth_required
def patch(self, request, grbevent):
# grbevent here should be a GrbEvent due to the way
# event_and_auth_required works
# Make sure this is a GRB event
if (grbevent.search is None
or (grbevent.search and grbevent.search.name != 'GRB')
or not isinstance(grbevent, GrbEvent)):
msg = ("Cannot update GRB event parameters for non-GRB event "
"{gid}").format(gid=grbevent.graceid)
return Response(msg, status=status.HTTP_400_BAD_REQUEST)
# Process data - should be all floats except designation
data = self.process_data(request.data)
# Get attributes to update and their values
update_dict = self.get_attributes_to_update(grbevent, data)
# Generate log message before updating event
update_message = self.generate_log_message(grbevent, update_dict)
# Update the event
for attribute in update_dict:
setattr(grbevent, attribute, update_dict[attribute])
grbevent.save()
# Save log message
grbevent.eventlog_set.create(comment=update_message,
issuer=request.user)
# Send LVAlert
EventAlertIssuer(grbevent, alert_type='update').issue_alerts()
return Response(eventToDict(grbevent, request=request))
#==================================================================
# Neighbors
......@@ -730,11 +808,13 @@ class EventLabel(InheritPermissionsAPIView):
@event_and_auth_required
def delete(self, request, event, label):
try:
rv = delete_label(event, request, label)
except Labelling.DoesNotExist as e:
return Response(e.message, status=status.HTTP_404_NOT_FOUND)
except (ValueError, Label.ProtectedLabelError) as e:
return Response(e.message,
status=status.HTTP_400_BAD_REQUEST)
return Response(e.message, status=status.HTTP_400_BAD_REQUEST)
return Response(status=status.HTTP_204_NO_CONTENT)
......
......@@ -103,6 +103,10 @@ class GracedbRoot(APIView):
signofflist = api_reverse("events:signoff-list", args=["G1200"], request=request)
signofflist = signofflist.replace("G1200", "{graceid}")
update_grbevent = api_reverse("events:update-grbevent", args=["G1200"],
request=request)
update_grbevent = update_grbevent.replace("G1200", "{graceid}")
# XXX Need a template for the tag list?
templates = {
......@@ -119,6 +123,7 @@ class GracedbRoot(APIView):
"tag-template" : tag,
"taglist-template" : taglist,
"signoff-list-template": signofflist,
"update-grbevent-template": update_grbevent,
}
# Get superevent templates
......
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.conf import settings
......@@ -5,6 +9,9 @@ from django.contrib.auth.models import (
Group, Permission, AnonymousUser,
)
from django_mock_queries.query import MockSet
# Groups ----------------------------------------------------------------------
@pytest.mark.django_db
@pytest.fixture
......@@ -89,6 +96,7 @@ def em_advocate_user(django_user_model, internal_group, em_advocates_group):
return user
# User lists ------------------------------------------------------------------
@pytest.fixture(params=['internal_user', 'public_user'])
def standard_user(request):
......@@ -97,3 +105,43 @@ def standard_user(request):
internal user, public user (LV-EM user to come?)
"""
return request.getfixturevalue(request.param)
# Attempts at mocking things away
## Mock groups ------------------------
@pytest.fixture
def mock_internal_group():
group = mock.MagicMock(spec=Group)
group.permissions = MockSet()
group.name = settings.LVC_GROUP
# Create mock permissions
perm_data = [
{'content_type.app.label': 'superevents', 'codename': 'add_labelling'},
{'content_type.app.label': 'superevents', 'codename': 'delete_labelling'},
{'content_type.app.label': 'superevents', 'codename': 'tag_log'},
{'content_type.app.label': 'superevents', 'codename': 'untag_log'},
{'content_type.app.label': 'superevents', 'codename': 'view_log'},
{'content_type.app.label': 'superevents', 'codename': 'add_test_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'change_test_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'confirm_gw_test_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'annotate_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'view_superevent'},
{'content_type.app.label': 'superevents', 'codename': 'add_voevent'},
{'content_type.app.label': 'superevents', 'codename': 'view_supereventgroupobjectpermission'},
{'content_type.app.label': 'superevents', 'codename': 'view_signoff'},
]
for perm in perm_data:
p = mock.MagicMock(spec=Permission, **perm)
group.permissions.add(p)
return group
## Mock user objects ------------------
@pytest.fixture
def mock_internal_user(django_user_model, mock_internal_group):
user = mock.MagicMock(spec=django_user_model)
user.groups = MockSet()
user.groups.add(mock_internal_group)
return user
from functools import wraps
def ignore_maintenance_mode(view):
@wraps(view)
def inner(request, *args, **kwargs):
return view(request, *args, **kwargs)
inner.__dict__['ignore_maintenance_mode'] = True
return inner
from django.conf import settings
from django.http import HttpResponse
from django.shortcuts import render
from django.urls import resolve
import logging
# Set up logger
logger = logging.getLogger(__name__)
class MaintenanceModeMiddleware(object):
accept_header_name = 'HTTP_ACCEPT'
default_message = 'The site is temporarily down for maintenance.'
def __init__(self, get_response):
self.get_response = get_response
def __call__(self, request):
# Process request -----------------------------------------------------
if settings.MAINTENANCE_MODE is True:
# Check if the view specifies to ignore maintenance mode
ignore_maintenance = \
self.check_for_ignore_maintenance_mode(request)
if not ignore_maintenance:
# Get message to display
maintenance_message = self.get_message()
accept_header = request.META.get(self.accept_header_name, None)
if accept_header and 'text/html' in accept_header:
# Attempt to handle browsers
context = {'message': maintenance_message}
return render(request, 'maintenance.html', context=context,
status=503)
else:
# Anything else (likely client API requests)
return HttpResponse(maintenance_message, status=503)
# Otherwise, get response and return with no further processing -------
response = self.get_response(request)
return response
@staticmethod
def check_for_ignore_maintenance_mode(request):
resolver_match = resolve(request.path)
view_func = resolver_match.func
return view_func.__dict__.get('ignore_maintenance_mode', False)
def get_message(self):
message = settings.MAINTENANCE_MODE_MESSAGE
if message is None:
message = self.default_message
return message
import logging
from django.http import HttpResponse
from django.views.generic.edit import FormView
import logging
from .decorators import ignore_maintenance_mode
# Set up logger
logger = logging.getLogger(__name__)
@ignore_maintenance_mode
def heartbeat(request):
# Do something (?) and return 200 response
return HttpResponse()
......
......@@ -3,7 +3,7 @@ import logging
from django import forms
from django.utils.safestring import mark_safe
from django.utils.html import escape
from .models import Event, Group, Label
from .models import Event, Group, Label, GrbEvent
from .models import Pipeline, Search, Signoff
from django.contrib.auth.models import User
from django.core.exceptions import FieldError
......@@ -49,3 +49,10 @@ class SignoffForm(ModelForm):
class Meta:
model = Signoff
fields = [ 'status', 'comment' ]
class GrbEventUpdateForm(ModelForm):
class Meta:
model = GrbEvent
fields = ['ra', 'dec', 'error_radius', 't90', 'redshift',
'designation']
from django.db import models
# Custom managers for the Pipeline model --------------------------------------
class ProductionPipelineManager(models.Manager):
"""Pipelines which are production search pipelines"""
def get_queryset(self):
return super(ProductionPipelineManager, self).get_queryset().filter(
pipeline_type=self.model.PIPELINE_TYPE_SEARCH_PRODUCTION
)
class ExternalPipelineManager(models.Manager):
"""Pipelines which correspond to external experiments"""
def get_queryset(self):
return super(ExternalPipelineManager, self).get_queryset().filter(
pipeline_type=self.model.PIPELINE_TYPE_EXTERNAL
)
......@@ -89,19 +89,34 @@ def populate_values(voevent, event_or_superevent):
# Parse parameters
for parameter in PARAMETER_MAPPINGS:
result = None
for path in PARAMETER_MAPPINGS[parameter]:
result = None
try:
result = root.find(path)
except SyntaxError as e:
pass
else:
# If result is not None, we've found something, so let's break
if result is not None:
break
if result is None:
# not found, likely due to an old VOEvent schema
continue
# Special parameter parsing
if parameter in ['hardware_inj', 'internal', 'open_alert']:
value = bool(int(result.attrib['value']))
# Better bool processing
try:
value = bool(int(result.attrib['value']))
except ValueError:
value = result.attrib['value']
if value.lower() in ['f', '0', 'false']:
value = False
elif value.lower() in ['t', '1', 'true']:
value = True
else:
raise ValueError("Can't process value {0}".format(value))
elif parameter == 'skymap_filename':
value = result.attrib['value'].split('/')[-1]
elif parameter == 'skymap_type':
......@@ -118,9 +133,10 @@ def populate_values(voevent, event_or_superevent):
for parameter, str_list in DESCRIPTION_PARAMETER_MAPPINGS.items():
for s in str_list:
if any([s in dp for dp in desc_text]):
setattr(voevent, parameter, True)
value = True
else:
setattr(voevent, parameter, False)
value = False
setattr(voevent, parameter, value)
# Save VOEvent
voevent.save()
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-10 19:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('events', '0037_delete_approval_model'),
]
operations = [
migrations.AddField(
model_name='pipeline',
name='pipeline_type',
field=models.CharField(choices=[(b'E', b'external'), (b'O', b'other'), (b'SO', b'non-production search'), (b'SP', b'production search')], default='O', max_length=2),
preserve_default=False,
),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-10 19:34
from __future__ import unicode_literals
from django.db import migrations
from events.models import Pipeline as pipeline_model
PIPELINES = [
('CWB', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('MBTAOnline', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('gstlal', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('oLIB', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('pycbc', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('spiir', pipeline_model.PIPELINE_TYPE_SEARCH_PRODUCTION),
('CWB2G', pipeline_model.PIPELINE_TYPE_SEARCH_OTHER),
('Ringdown', pipeline_model.PIPELINE_TYPE_SEARCH_OTHER),
('X', pipeline_model.PIPELINE_TYPE_SEARCH_OTHER),
('HardwareInjection', pipeline_model.PIPELINE_TYPE_OTHER),
('Omega', pipeline_model.PIPELINE_TYPE_OTHER),
('Q', pipeline_model.PIPELINE_TYPE_OTHER),
('Fermi', pipeline_model.PIPELINE_TYPE_EXTERNAL),
('SNEWS', pipeline_model.PIPELINE_TYPE_EXTERNAL),
('Swift', pipeline_model.PIPELINE_TYPE_EXTERNAL),
]
DEFAULT_PIPELINE_TYPE = pipeline_model.PIPELINE_TYPE_OTHER
def update_pipeline_types(apps, schema_editor):
Pipeline = apps.get_model('events', 'Pipeline')
for p_tuple in PIPELINES:
p = Pipeline.objects.get(name=p_tuple[0])
p.pipeline_type = p_tuple[1]
p.save(update_fields=['pipeline_type'])
def revert_pipeline_types(apps, schema_editor):
Pipeline = apps.get_model('events', 'Pipeline')
for p_tuple in PIPELINES:
p = Pipeline.objects.get(name=p_tuple[0])
p.pipeline_type = DEFAULT_PIPELINE_TYPE
p.save(update_fields=['pipeline_type'])
class Migration(migrations.Migration):
dependencies = [
('events', '0038_pipeline_pipeline_type'),
]
operations = [
migrations.RunPython(update_pipeline_types, revert_pipeline_types),
]
......@@ -37,6 +37,9 @@ from cStringIO import StringIO
from hashlib import sha1
import shutil
from .managers import ProductionPipelineManager, ExternalPipelineManager
UserModel = get_user_model()
SERVER_TZ = pytz.timezone(settings.TIME_ZONE)
......@@ -65,14 +68,35 @@ class Group(models.Model):
def __unicode__(self):
return self.name
class Pipeline(models.Model):
PIPELINE_TYPE_EXTERNAL = 'E'
PIPELINE_TYPE_OTHER = 'O'
PIPELINE_TYPE_SEARCH_OTHER = 'SO'
PIPELINE_TYPE_SEARCH_PRODUCTION = 'SP'
PIPELINE_TYPE_CHOICES = (
(PIPELINE_TYPE_EXTERNAL, 'external'),
(PIPELINE_TYPE_OTHER, 'other'),
(PIPELINE_TYPE_SEARCH_OTHER, 'non-production search'),
(PIPELINE_TYPE_SEARCH_PRODUCTION, 'production search'),
)
name = models.CharField(max_length=100)
# Are submissions allowed for this pipeline?
enabled = models.BooleanField(default=True)
# XXX Need any additional fields? Like a librarian email? Or perhaps even fk?
# Pipeline type
pipeline_type = models.CharField(max_length=2,
choices=PIPELINE_TYPE_CHOICES)
# Add custom managers; must manually define 'objects' as well
objects = models.Manager()
production_objects = ProductionPipelineManager()
external_objects = ExternalPipelineManager()
class Meta:
permissions = (
('manage_pipeline', 'Can enable or disable pipeline'),
)
def __unicode__(self):
return self.name
......
......@@ -473,76 +473,6 @@ class TestEventNeighborsView(EventSetup, GraceDbTestBase):
self.assertEqual(response.status_code, 403)
class TestEventModifyT90(EventSetup, GraceDbTestBase):
@classmethod
def setUpTestData(cls):
super(TestEventModifyT90, cls).setUpTestData()
# Create a grb event
ext_group = Group.objects.create(name='External')
fermi_pipeline = Pipeline.objects.create(name='Fermi')
grb_search = Search.objects.create(name='GRB')
cls.grb_event = GrbEvent.objects.create(group=ext_group,
pipeline=fermi_pipeline, search=grb_search,
submitter=cls.internal_user)
ctype = ContentType.objects.get_for_model(GrbEvent)
perm = Permission.objects.create(codename='view_grbevent',
content_type=ctype)
assign_default_event_perms(cls.grb_event)
@classmethod
def setUpClass(cls):
super(TestEventModifyT90, cls).setUpClass()
cls.t90_data = {
'redshift': 2,
}
def test_basic_internal_user_t90(self):
"""Basic internal user can't t90 GRB events"""
url = reverse('modify_t90', args=[self.grb_event.graceid])
response = self.request_as_user(url, "POST", self.internal_user,
data=self.t90_data)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.content,
"You aren't authorized to modify GRB attributes.")
def test_privileged_internal_user_t90(self):
"""Privileged internal user can t90 GRB events"""
# Create permission and give to user
ctype = ContentType.objects.get_for_model(GrbEvent)
perm = Permission.objects.create(codename='t90_grbevent',
content_type=ctype)
perm.user_set.add(self.internal_user)
# Make request and check response
url = reverse('modify_t90', args=[self.grb_event.graceid])
response = self.request_as_user(url, "POST", self.internal_user,
data=self.t90_data)
# 302 response code means success since we were redirected to the
# event page
self.assertEqual(response.status_code, 302)
self.assertEqual(response.url, reverse('view',
args=[self.grb_event.graceid]))
self.grb_event.refresh_from_db()
self.assertEqual(self.grb_event.redshift, self.t90_data['redshift'])
def test_lvem_user_t90(self):
"""LV-EM user can't t90 GRB events"""
url = reverse('modify_t90', args=[self.grb_event.graceid])
response = self.request_as_user(url, "POST", self.lvem_user,
data=self.t90_data)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.templates[0].name, '403.html')
def test_public_user_t90(self):
"""Public user can't t90 GRB events"""
url = reverse('modify_t90', args=[self.grb_event.graceid])
response = self.request_as_user(url, "POST", data=self.t90_data)
self.assertEqual(response.status_code, 403)
self.assertEqual(response.templates[0].name, '403.html')
class TestEventModifyPermissions(EventSetup, GraceDbTestBase):
@classmethod
......
try:
from unittest import mock
except ImportError: # python < 3
import mock
import pytest
from django.urls import reverse
......@@ -52,6 +48,8 @@ def test_pipeline_change_views(view, standard_user, client):
# Create a pipeline
p, _ = Pipeline.objects.get_or_create(name='fake_pipeline')
p.pipeline_type = Pipeline.PIPELINE_TYPE_SEARCH_PRODUCTION
p.save(update_fields=['pipeline_type'])
# NOTE: get() is wired to post() in the view
response = client.get(reverse(view, args=[p.pk]))
......@@ -69,11 +67,10 @@ def test_pipeline_change_views_as_advocate(view, em_advocate_user, client):
# Create a pipeline
p, _ = Pipeline.objects.get_or_create(name='fake_pipeline')
p.pipeline_type = Pipeline.PIPELINE_TYPE_SEARCH_PRODUCTION
p.save(update_fields=['pipeline_type'])
# NOTE: get() is wired to post() in the view
with mock.patch('events.views.PIPELINE_LIST', new_callable=list) \
as mock_pipeline_list:
mock_pipeline_list.append(p.name)
response = client.get(reverse(view, args=[p.pk]))
response = client.get(reverse(view, args=[p.pk]))
assert response.status_code == 302
......@@ -26,9 +26,6 @@ urlpatterns = [
'(,(?P<delta2>[-+]?\d+)\)?)?/$'), views.neighbors, name="neighbors"),
# Form processing ---------------------------------------------------------
# Modify t90
url(r'^(?P<graceid>[GEHMT]\d+)/t90/$', views.modify_t90,
name="modify_t90"),
# Modify permissions
url(r'^(?P<graceid>[GEHMT]\d+)/perms/$', views.modify_permissions,
name="modify_permissions"),
......
......@@ -240,9 +240,9 @@ def delete_label(event, request, labelName, can_remove_protected=False,
# 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():
d['warning'] = "No label '%s' associated with event %s" % (labelName, event.graceid)
raise ValueError("No label '%s' associated with event %s" % (labelName, event.graceid))
if not event.labelling_set.filter(label__name=labelName).exists():
d['warning'] = "No label '%s' associated with event %s" % (labelName, event.graceid)
raise Labelling.DoesNotExist("No label '%s' associated with event %s" % (labelName, event.graceid))
else:
this_label = Labelling.objects.get(
event = event,
......
......@@ -16,7 +16,7 @@ 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, PipelineLog
from .forms import CreateEventForm, SignoffForm
from .forms import CreateEventForm, SignoffForm, GrbEventUpdateForm
from django.contrib.auth.decorators import permission_required
from django.contrib.auth.models import User, Permission
......@@ -369,8 +369,15 @@ def view(request, event):
# given permission to access an event. We want it to be the observers group.
context['lvem_group_name'] = settings.LVEM_OBSERVERS_GROUP
# GRB event update permissions/form
if event.pipeline.name in settings.GRB_PIPELINES:
context['can_modify_t90'] = request.user.has_perm('events.t90_grbevent')
context['can_update_grbevent'] = request.user.has_perm(
'events.t90_grbevent')
# If the user has permission, add the form
if context['can_update_grbevent']:
context['update_grbevent_form'] = \
GrbEventUpdateForm(instance=event)
# Is the user an external user? (I.e., not part of the LVC?) The template
# needs to know that in order to decide what pieces of information to show.
......@@ -747,41 +754,6 @@ def emobservation_entry(request, event, num=None):
else:
return HttpResponseBadRequest("This URL only supports POST.")
#
# Despite the name, this view function will handle updates to all of the
# hand-entered GRB data, including t90, redshift, and event designation.
#
@event_and_auth_required
def modify_t90(request, event):
if not request.method=='POST':
msg = 'This URL only allows POST.'
return HttpResponseBadRequest(msg)
if not isinstance(event, GrbEvent):
msg = 'This method only works on GrbEvent objects.'
return HttpResponseBadRequest(msg)
if not request.user.has_perm('events.t90_grbevent'):
msg = "You aren't authorized to modify GRB attributes."
return HttpResponseForbidden(msg)
designation = request.POST.get('designation', None)
redshift = request.POST.get('redshift', None)
t90 = request.POST.get('t90', None)
if not (t90 or designation or redshift):
msg = 'This method requires one of: designation, redshift, or t90 in POST.'
return HttpResponseBadRequest(msg)
if t90:
event.t90 = t90
elif redshift:
event.redshift = redshift
elif designation:
event.designation = designation
event.save()
# Finished. Redirect back to the event.
return HttpResponseRedirect(reverse("view", args=[event.graceid]))
def get_signoff_type(stype):
for t in Signoff.SIGNOFF_TYPE_CHOICES:
......@@ -975,7 +947,6 @@ def modify_signoff(request, event):
# 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),
......@@ -986,8 +957,7 @@ class PipelineManageView(ListView):
log_number = 10
def get_queryset(self):
qs = Pipeline.objects.filter(name__in=PIPELINE_LIST).order_by('name')
return qs
return Pipeline.production_objects.order_by('name')
def get_context_data(self, **kwargs):
context = super(PipelineManageView, self).get_context_data(**kwargs)
......@@ -1033,8 +1003,7 @@ class PipelineEnableView(UpdateView):
success_url = reverse_lazy('manage-pipelines')
def get_queryset(self):
qs = Pipeline.objects.filter(name__in=PIPELINE_LIST)
return qs
return Pipeline.production_objects.all()
def get(self, request, *args, **kwargs):
return self.post(request, *args, **kwargs)
......
......@@ -5,10 +5,12 @@ from .utils import is_internal
def LigoAuthContext(request):
user = None
user_is_internal = False
user_is_lvem = False
user_is_advocate = False # user is an EM advocate
if request.user:
if hasattr(request, 'user'):
user = request.user
if is_internal(request.user):
user_is_internal = True
if request.user.groups.filter(name=settings.LVEM_GROUP).exists():
......@@ -16,5 +18,5 @@ def LigoAuthContext(request):
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,
return {'user': user, 'user_is_internal': user_is_internal,
'user_is_lvem': user_is_lvem, 'user_is_advocate': user_is_advocate}
......@@ -64,7 +64,7 @@ LOCALUSERS = [
],
},
{
'username': 'excesspower-processor ',
'username': 'excesspower-processor',
'last_name': 'Excess Power Processor',
'email': 'pankow@gravity.phys.uwm.edu',
'x509certs': [
......
......@@ -4,6 +4,11 @@ from __future__ import unicode_literals
from django.db import migrations
# Previously, this was taken from settings.LVC_GROUP, but that value has
# changed. So we have to hard-code it for past migrations
LVC_GROUP = 'Communities:LSCVirgoLIGOGroupMembers'
OLD = {
'username': 'gerrit.kuehn@LIGO.ORG',
'cert': '/DC=org/DC=ligo/O=LIGO/OU=Services/CN=ext-alert/geo-ws4.geo.rt.aei.uni-hannover.de',
......@@ -21,6 +26,7 @@ NEW_CERT = '/DC=org/DC=cilogon/C=US/O=LIGO/OU=Robots/CN=geo-ws4.geo.rt.aei.uni-h
def add_account_and_update_certs(apps, schema_editor):
RobotUser = apps.get_model('ligoauth', 'RobotUser')
X509Cert = apps.get_model('ligoauth', 'X509Cert')
Group = apps.get_model('auth', 'Group')
# Delete old_cert
old_cert = X509Cert.objects.filter(subject=OLD['cert'])
......@@ -34,6 +40,10 @@ def add_account_and_update_certs(apps, schema_editor):
# Create new certificate
user.x509cert_set.create(subject=NEW_CERT)
# Add user to internal group
group = Group.objects.get(name=LVC_GROUP)
group.user_set.add(user)
def remove_account_and_revert_certs(apps, schema_editor):
User = apps.get_model('auth', 'User')
......
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-08-06 18:06
from __future__ import unicode_literals
from django.db import migrations
ACCOUNT = {
'name': 'emfollow',
'new_cert': '/DC=org/DC=cilogon/C=US/O=LIGO/OU=Robots/CN=emfollow-playground.ligo.caltech.edu/CN=emfollow-playground/CN=Leo Singer/CN=UID:leo.singer.robot'
}
def add_cert(apps, schema_editor):
User = apps.get_model('auth', 'User')
# Get user
user = User.objects.get(username=ACCOUNT['name'])
# Create new certificate
user.x509cert_set.create(subject=ACCOUNT['new_cert'])
def delete_cert(apps, schema_editor):
User = apps.get_model('auth', 'User')
# Get user
user = User.objects.get(username=ACCOUNT['name'])
# Delete new certificate
cert = user.x509cert_set.get(subject=ACCOUNT['new_cert'])
cert.delete()
class Migration(migrations.Migration):
dependencies = [
('ligoauth', '0046_delete_robotuser_model'),
]
operations = [
migrations.RunPython(add_cert, delete_cert),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-08 20:30
from __future__ import unicode_literals
from django.db import migrations
# NOTE: membership in the 'executives' group has been managed by a script in a
# separate repo in the past, so this migration is the starting point for
# going away from that workflow. Also, the executives group will eventually
# be replaced by the 'access_managers' group, but until we update the
# permissions structure in the events app, we have to maintain it.
EXECUTIVES = [
'patrick.brady@LIGO.ORG',
'tanner.prestegard@LIGO.ORG',
'alexander.pace@LIGO.ORG',
]
def update_membership(apps, schema_editor):
User = apps.get_model('auth', 'User')
AuthGroup = apps.get_model('ligoauth', 'AuthGroup')
# Get executives group
execs = AuthGroup.objects.get(name='executives')
# Clear membership
execs.user_set.clear()
# Add users
for username in EXECUTIVES:
user, _ = User.objects.get_or_create(username=username)
execs.user_set.add(user)
class Migration(migrations.Migration):
dependencies = [
('ligoauth', '0046_delete_robotuser_model'),
]
operations = [
migrations.RunPython(update_membership, migrations.RunPython.noop),
]
# -*- coding: utf-8 -*-
# Generated by Django 1.11.20 on 2019-07-08 20:37
from __future__ import unicode_literals
from django.db import migrations
ACCESS_MANAGERS = [
'alexander.pace@LIGO.ORG',
'brian.oreilly@LIGO.ORG',
'emfollow',
'erik.katsavounidis@LIGO.ORG'
'keita.kawabe@LIGO.ORG',
'leo.singer@LIGO.ORG',
'patrick.brady@LIGO.ORG',
'peter.shawhan@LIGO.ORG',
'sarah.antier@LIGO.ORG',
'shaon.ghosh@LIGO.ORG',
'tanner.prestegard@LIGO.ORG',
]
def update_membership(apps, schema_editor):
User = apps.get_model('auth', 'User')
AuthGroup = apps.get_model('ligoauth', 'AuthGroup')
# Get access_managers group
group = AuthGroup.objects.get(name='access_managers')
# Clear membership
group.user_set.clear()
# Add users
for username in ACCESS_MANAGERS:
user, _ = User.objects.get_or_create(username=username)
group.user_set.add(user)
class Migration(migrations.Migration):
dependencies = [
('ligoauth', '0047_update_executives_membership'),
]
operations = [
migrations.RunPython(update_membership, migrations.RunPython.noop),
]
......@@ -2,12 +2,18 @@ import logging
from django.conf import settings
from django.contrib.auth import (
logout, get_user_model, update_session_auth_hash,
get_user_model, update_session_auth_hash, REDIRECT_FIELD_NAME
)
from django.http import HttpResponseRedirect, HttpResponseForbidden
from django.contrib.auth.views import SuccessURLAllowedHostsMixin
from django.http import HttpResponseRedirect
from django.shortcuts import resolve_url, render
from django.urls import reverse
from django.utils import timezone
from django.utils.decorators import method_decorator
from django.utils.http import urlencode, is_safe_url
from django.views.decorators.cache import never_cache
from django.views.decorators.debug import sensitive_post_parameters
from django.views.generic.base import RedirectView
from .decorators import lvem_observers_only
......@@ -19,81 +25,76 @@ UserModel = get_user_model()
logger = logging.getLogger(__name__)
ORIGINAL_PAGE_KEY = 'login_from_page'
# Three steps in login process:
# 1. Pre-login view where we try to cache the page that the user was just on
# and redirect to the Shibboleth SSO page for login through an IdP
# 1. Pre-login view where we set up all of the necessary redirects, starting
# with the Shibboleth SSO page for login through an IdP.
# 2. Login through IdP, redirect to post-login view.
# 3. Post-login view, where Apache puts the user's attributes into the
# session. Our Django middleware and auth backends consume the attributes
# and use them to log into a user account in the database. The user is
# then redirected to the original page where they logged in from.
def pre_login(request):
"""
Sends user to settings.SHIB_LOGIN_URL (Shibboleth login) and sets up a
redirect target to the actual login page where we parse the shib session
attributes. Saves the current page (where the login button was clicked
from) in the session so that our login page can then redirect back to
the original page.
If original URL is not found, redirect to the home page
"""
# Set target for SSO page to redirect to
shib_target = reverse('post-login')
# Get original url (page where the login button was clicked).
# First try to get referer header. If not available, try to get the 'next
# query string parameter (that's how the Django login_required
# handles it)
original_url = request.META.get('HTTP_REFERER', None)
if original_url is None:
original_url = request.GET.get('next',
resolve_url(settings.LOGIN_REDIRECT_URL))
# Store original url in session
request.session[ORIGINAL_PAGE_KEY] = original_url
# Set up url for shibboleth login with redirect target
full_login_url = "{base}?target={target}".format(
base=settings.SHIB_LOGIN_URL, target=shib_target)
# Redirect to the shibboleth login
return HttpResponseRedirect(full_login_url)
def post_login(request):
"""
pre_login should redirect to the URL which corresponds to this view.
Apache should be configured to put the Shibboleth session information into
the request headers at this view's URL.
The middleware should handle attribute extraction and logging in. So all
we need to do here is redirect to the original page (where the user clicked
the login button). If we can't seem to find that information, then just
redirect to the home page.
"""
original_url = request.session.get(ORIGINAL_PAGE_KEY,
resolve_url(settings.LOGIN_REDIRECT_URL))
# Redirect to the original url
return HttpResponseRedirect(original_url)
def shib_logout(request):
# Call Django logout function
logout(request)
# Get original url where the logout button was pressed from
original_url = request.META.get('HTTP_REFERER',
resolve_url(settings.LOGOUT_REDIRECT_URL))
return HttpResponseRedirect(original_url)
# request headers. Our Django middleware and auth backends consume the
# attributes and use them to log into a user account in the database. The
# user is then redirected to the original page where they logged in from.
@method_decorator(sensitive_post_parameters(), name='dispatch')
@method_decorator(never_cache, name='dispatch')
class ShibLoginView(SuccessURLAllowedHostsMixin, RedirectView):
redirect_authenticated_user = True
redirect_field_name = REDIRECT_FIELD_NAME
post_login_view_name = 'post-login'
def dispatch(self, request, *args, **kwargs):
if (self.redirect_authenticated_user
and self.request.user.is_authenticated):
redirect_to = self.get_success_url()
if redirect_to == self.request.path:
raise ValueError(
"Redirection loop for authenticated user detected. Check "
"that your LOGIN_REDIRECT_URL doesn't point to a login "
"page."
)
return HttpResponseRedirect(redirect_to)
return super(ShibLoginView, self).dispatch(request, *args, **kwargs)
def get_success_url(self):
"""User is already logged in."""
url = self.request.POST.get(
self.redirect_field_name,
self.request.GET.get(
self.redirect_field_name,
resolve_url(settings.LOGIN_REDIRECT_URL)
)
)
return url
def get_redirect_url(self, *args, **kwargs):
# Where the user should finally be redirected to
original_url = self.get_success_url()
# Target to pass to the shibboleth SSO page as a URL param
shib_target = "{url}?{params}".format(
url=reverse(self.post_login_view_name),
params=urlencode({self.redirect_field_name: original_url})
)
# Full URL to redirect to right now
full_login_url = "{base}?{params}".format(
base=settings.SHIB_LOGIN_URL,
params=urlencode({'target': shib_target})
)
return full_login_url
@method_decorator(sensitive_post_parameters(), name='dispatch')
@method_decorator(never_cache, name='dispatch')
class ShibPostLoginView(SuccessURLAllowedHostsMixin, RedirectView):
redirect_field_name = REDIRECT_FIELD_NAME
def get_redirect_url(self):
redirect_to = self.request.GET.get(self.redirect_field_name, '')
url_is_safe = is_safe_url(
url=redirect_to,
allowed_hosts=self.get_success_url_allowed_hosts(),
require_https=self.request.is_secure(),
)
return redirect_to if url_is_safe else ''
@lvem_observers_only(superuser_allowed=True)
......
......@@ -17,11 +17,12 @@ def add_users(apps, schema_editor):
# Get group
pg = Group.objects.get(name=GROUP_NAME)
# Get users
users = User.objects.filter(username__in=USERS)
# Users might not exist yet since they are populated from the LDAP
for username in USERS:
user, _ = User.objects.get_or_create(username=username)
# Add users
pg.user_set.add(*users)
# Add user to group
pg.user_set.add(user)
def remove_users(apps, schema_editor):
......
......@@ -37,6 +37,12 @@ def parse_superevent_id(name, toks, filter_prefix=None):
if (toks.prefix == Superevent.GW_ID_PREFIX):
toks.suffix = toks.suffix.upper()
# Allow flexible suffix capitalization
if (toks.prefix == Superevent.GW_ID_PREFIX):
toks.suffix = toks.suffix.upper()
else:
toks.suffix = toks.suffix.lower()
# Combine into full ID and get lookup kwargs
s_id = toks.preprefix + toks.prefix + toks.date + toks.suffix
f_kwargs = Superevent.get_filter_kwargs_for_date_id_lookup(s_id)
......
......@@ -43,6 +43,10 @@ SUPEREVENT_QUERY_TEST_DATA = [
("", DEFAULT_Q),
("id: S190509bc",
Q(**Superevent.get_filter_kwargs_for_date_id_lookup("S190509bc"))),
("id: Tgw190331eBz",
Q(**Superevent.get_filter_kwargs_for_date_id_lookup("TGW190331EBZ"))),
("id: ms190331BCdE",
Q(**Superevent.get_filter_kwargs_for_date_id_lookup("MS190331bcde"))),
("superevent_id: S190509bc",
Q(**Superevent.get_filter_kwargs_for_date_id_lookup("S190509bc"))),
("S190509bc",
......
......@@ -86,12 +86,17 @@ def populate_values(voevent, event_or_superevent):
# Parse parameters
for parameter in PARAMETER_MAPPINGS:
result = None
for path in PARAMETER_MAPPINGS[parameter]:
result = None
try:
result = root.find(path)
except SyntaxError as e:
pass
else:
# If result is not None, we've found something, so let's break
if result is not None:
break
if result is None:
continue
......@@ -114,9 +119,10 @@ def populate_values(voevent, event_or_superevent):
for parameter, str_list in DESCRIPTION_PARAMETER_MAPPINGS.items():
for s in str_list:
if any([s in dp for dp in desc_text]):
setattr(voevent, parameter, True)
value = True
else:
setattr(voevent, parameter, False)
value = False
setattr(voevent, parameter, value)
# Save VOEvent
voevent.save()
......
......@@ -12,6 +12,7 @@
<script src="{% static "moment/moment.js" %}"></script>
<script src="{% static "moment-timezone/builds/moment-timezone-with-data.min.js" %}"></script>
<script src="{% static "dojo/dojo.js" %}" data-dojo-config="async: true"></script>
<script src="{% static "jquery/dist/jquery.min.js" %}"></script>
<!-- Styles for dgrid -->
<!-- <link rel="stylesheet" href="{% static "dgrid/css/dgrid.css" %}" /> -->
<!-- Styles for the editor components -->
......
......@@ -90,19 +90,18 @@
</tbody>
</table>
{% if can_modify_t90 %}
{% if can_update_grbevent %}
<div class="content-area">
<form action="{% url "modify_t90" object.graceid %}" method="post">
<input type="text" name="t90" value="{{ object.t90 }}">
<input type="submit" value="Update T90" class="t90ButtonClass">
</form>
<form action="{% url "modify_t90" object.graceid %}" method="post">
<input type="text" name="redshift" value="{{ object.redshift }}">
<input type="submit" value="Update redshift" class="t90ButtonClass">
</form>
<form action="{% url "modify_t90" object.graceid %}" method="post">
<input type="text" name="designation" value="{{ object.designation }}">
<input type="submit" value="Update designation" class="t90ButtonClass">
<h3>Update GRB Event</h3>
<form id="update_grbevent_form" action="{% url "legacy_apiweb:default:events:update-grbevent" object.graceid %}">
<table>
{{ update_grbevent_form.as_table }}
<tr>
<td></td>
<td><input type="submit" value="Update" disabled></td>
</tr>
</table>
</form>
</div>
{% endif %}
......
......@@ -225,6 +225,49 @@ require([
Save, Preview, ScrollPane, Uploader) {
parser.parse();
// We don't enable the input buttons until right now otherwise fast users
// can trigger the form before the javascript is ready... not ideal
$("#update_grbevent_form input[type=submit]").attr('disabled', false);
// Update GRB form
$("#update_grbevent_form").submit(function(e) {
e.preventDefault();
// Get button and disable it to prevent multiple clicks