Beispiel #1
0
    def on_get(self, req, resp, source, **kwargs):
        """
        Retrieve the list of flows collected on a given SourceIP. Additionally it can be provided the
        DestinationIP and DestinationPort. This method accepts also the start_time and end_time as query parameters
        :param source: SourceIP of the flow.
        :param kwargs: It can be: only DestinationIP or DestinationIP and DestinationPort
        :return: List with flows
        """
        dimensions_query = 'SourceIP:' + source
        if kwargs:
            dimensions_query += ',' + ','.join(['{}:{}'.format(key, value) for key, value in kwargs.items()])

        req.context['query_parameters']['dimensions'] = dimensions_query

        if 'metric' not in req.context['query_parameters']:
            raise HTTPBadRequest(title='Missing Metric', description='Missing metric name to collect metrics',
                                 code='300')

        req.context['query_parameters']['name'] = req.context['query_parameters'].get('metric')
        req.context['query_parameters']['group_by'] = 'SourceIP,DestinationIP,DestinationPort,FlowID'
        req.context['query_parameters']['merge_metrics'] = 'true'

        if 'start_time' not in req.context['query_parameters']:
            d = datetime.now() - timedelta(days=1)
            req.context['query_parameters']['start_time'] = d.isoformat()

        headers = Authenticate.get_header()  # Insert the auth header
        r = request(ConfReader().get('MONASCA', 'url') + Flows.ENDPOINT, params=req.context['query_parameters'],
                    headers=headers)

        req.context['query_parameters'].pop('group_by')
        req.context['query_parameters'].pop('merge_metrics')
        req.context['query_parameters'].pop('name')

        resp.body = self.format_body(Flows.__convert_result__(r.json(), req.uri, req), from_dict=True)
        resp.status = str(r.status_code)
Beispiel #2
0
    async def on_post(self, req, res):
        try:
            result = await self.graphql_execute(req, res)
            image_info = req.context.get('image_info', None)

            if not result.errors:
                res.status = HTTP_200
                res.body = json.dumps(result.data)
            else:
                if image_info:
                    for image in image_info:
                        if os.path.isfile(image['full_path']):
                            os.remove(image['full_path'])

                raise CodeduExceptionHandler(result.errors[0].args[0])
                print(result.errors)
        except Exception as e:
            description = e.description if hasattr(
                e, 'description') else 'UNKNOWN ERROR'
            if hasattr(e, 'type'):
                raise globals()[e.type](description=description)
            else:
                print_exc()
                raise HTTPBadRequest(description=e.args)
Beispiel #3
0
def on_put(req, resp, notification_id):
    '''
    Edit user notification settings. Allows editing of the following attributes:

    - roles: list of role names
    - team: team name
    - mode: contact mode name
    - type: string defining what event to notify on. Types are detailed in notification
            POST documentation
    - time_before: in units of seconds (if reminder setting)
    - only_if_involved: boolean (if notification setting)

    **Example request**

        .. sourcecode:: http

            PUT /api/v0/events/1234 HTTP/1.1
            Content-Type: application/json

            {
                "team": "team-bar",
                "mode": "call",
                "user": "******",
                "roles": ["secondary"]
            }

    :statuscode 200: Successful edit
    :statuscode 400: Validation checks failed.
    '''
    data = load_json_body(req)
    params = data.keys()
    roles = data.pop('roles')

    cols = [columns[c] for c in data if c in columns]
    query_params = [data[c] for c in params if c in columns]
    query = 'UPDATE notification_setting SET %s WHERE id = %%s' % ', '.join(
        cols)
    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)

    try:
        notification_type = data.get('type')
        cursor.execute(
            '''SELECT `is_reminder`, `time_before`, `only_if_involved` FROM `notification_setting`
                          JOIN `notification_type` ON `notification_setting`.`type_id` = `notification_type`.`id`
                          WHERE `notification_setting`.`id` = %s''',
            notification_id)
        current_setting = cursor.fetchone()
        is_reminder = current_setting['is_reminder']
        if notification_type:
            cursor.execute(
                'SELECT is_reminder FROM notification_type WHERE name = %s',
                notification_type)
            is_reminder = cursor.fetchone()['is_reminder']
        time_before = data.get('time_before', current_setting['time_before'])
        only_if_involved = data.get('only_if_involved',
                                    current_setting['only_if_involved'])

        if is_reminder and only_if_involved is not None:
            raise HTTPBadRequest(
                'invalid setting update',
                'reminder setting must define only time_before')
        elif not is_reminder and time_before is not None:
            raise HTTPBadRequest(
                'invalid setting update',
                'notification setting must define only only_if_involved')

        if cols:
            cursor.execute(
                'SELECT `user`.`name` FROM `notification_setting` '
                'JOIN `user` ON `notification_setting`.`user_id` = `user`.`id` '
                'WHERE `notification_setting`.`id` = %s', notification_id)
            username = cursor.fetchone()['name']
            check_user_auth(username, req)
            cursor.execute(query, query_params + [notification_id])
        if roles:
            cursor.execute(
                'DELETE FROM `setting_role` WHERE `setting_id` = %s',
                notification_id)
            query_vals = ', '.join([
                '(%s, (SELECT `id` FROM `role` WHERE `name` = %%s))' %
                notification_id
            ] * len(roles))
            cursor.execute(
                'INSERT INTO `setting_role`(`setting_id`, `role_id`) VALUES ' +
                query_vals, roles)
    except:
        raise
    else:
        connection.commit()
    finally:
        cursor.close()
        connection.close()
Beispiel #4
0
def load_json_body(req):
    try:
        return json_loads(req.context['body'])
    except ValueError as e:
        raise HTTPBadRequest('invalid JSON',
                             'failed to decode json: %s' % str(e))
Beispiel #5
0
    def on_post(self, req, resp):
        form_body = uri.parse_query_string(req.context['body'])

        try:
            template_subject = form_body['templateSubject']
            template_body = form_body['templateBody']
            application = form_body['application']
        except KeyError:
            raise HTTPBadRequest('Missing keys from post body', '')

        if not application:
            resp.body = ujson.dumps({'error': 'No application found'})
            resp.status = falcon.HTTP_400
            return

        app_json = get_local(req, 'applications/%s' % application)
        sample_context_str = app_json.get('sample_context')
        if not sample_context_str:
            resp.body = ujson.dumps(
                {'error': 'Missing sample_context from application config'})
            resp.status = falcon.HTTP_400
            logger.error('Missing sample context for app %s', application)
            return

        try:
            sample_context = ujson.loads(sample_context_str)
        except Exception:
            resp.body = ujson.dumps(
                {'error': 'Invalid application sample_context'})
            resp.status = falcon.HTTP_400
            logger.exception('Bad sample context for app %s', application)
            return

        # TODO: also move iris meta var to api
        iris_sample_context = {
            "message_id": 5456900,
            "target": "user",
            "priority": "Urgent",
            "application": "Autoalerts",
            "plan": "default plan",
            "plan_id": 1843,
            "incident_id": 178293332,
            "template": "default template"
        }
        sample_context['iris'] = iris_sample_context

        environment = SandboxedEnvironment()

        try:
            subject_template = environment.from_string(template_subject)
            body_template = environment.from_string(template_body)
        except Exception as e:
            resp.body = ujson.dumps({'error': str(e), 'lineno': e.lineno})
            resp.status = falcon.HTTP_400
        else:
            resp.body = ujson.dumps({
                'template_subject':
                subject_template.render(sample_context),
                'template_body':
                body_template.render(sample_context)
            })
Beispiel #6
0
def get_schedules(filter_params, dbinfo=None, fields=None):
    """
    Helper function to get schedule data for a request.

    :param filter_params: dict mapping constraint keys with values. Valid constraints are
    defined in the global ``constraints`` dict.
    :param dbinfo: optional. If provided, defines (connection, cursor) to use in DB queries.
    Otherwise, this creates its own connection/cursor.
    :param fields: optional. If provided, defines which schedule fields to return. Valid
    fields are defined in the global ``columns`` dict. Defaults to all fields. Invalid
    fields raise a 400 Bad Request.
    :return:
    """
    events = False
    scheduler = False
    from_clause = ['`schedule`']

    if fields is None:
        fields = columns.keys()
    if any(f not in columns for f in fields):
        raise HTTPBadRequest('Bad fields', 'One or more invalid fields')
    if 'roster' in fields:
        from_clause.append('JOIN `roster` ON `roster`.`id` = `schedule`.`roster_id`')
    if 'team' in fields or 'timezone' in fields:
        from_clause.append('JOIN `team` ON `team`.`id` = `schedule`.`team_id`')
    if 'role' in fields:
        from_clause.append('JOIN `role` ON `role`.`id` = `schedule`.`role_id`')
    if 'scheduler' in fields:
        from_clause.append('JOIN `scheduler` ON `scheduler`.`id` = `schedule`.`scheduler_id`')
        scheduler = True
    if 'events' in fields:
        from_clause.append('LEFT JOIN `schedule_event` ON `schedule_event`.`schedule_id` = `schedule`.`id`')
        events = True

    if 'id' not in fields:
        fields.append('id')
    fields = map(columns.__getitem__, fields)
    cols = ', '.join(fields)
    from_clause = ' '.join(from_clause)

    connection_opened = False
    if dbinfo is None:
        connection = db.connect()
        connection_opened = True
        cursor = connection.cursor(db.DictCursor)
    else:
        connection, cursor = dbinfo

    where = ' AND '.join(constraints[key] % connection.escape(value)
                         for key, value in filter_params.iteritems()
                         if key in constraints)
    query = 'SELECT %s FROM %s' % (cols, from_clause)
    if where:
        query = '%s WHERE %s' % (query, where)

    cursor.execute(query)
    data = cursor.fetchall()
    if scheduler and data:
        schedule_ids = {d['id'] for d in data}
        cursor.execute('''SELECT `schedule_id`, `user`.`name` FROM `schedule_order`
                          JOIN `user` ON `user_id` = `user`.`id`
                          WHERE `schedule_id` IN %s
                          ORDER BY `schedule_id`,`priority`, `user_id`''',
                       schedule_ids)
        orders = {}
        # Accumulate roster orders for schedule
        for row in cursor:
            schedule_id = row['schedule_id']
            if schedule_id not in orders:
                orders[schedule_id] = []
            orders[schedule_id].append(row['name'])
    if connection_opened:
        cursor.close()
        connection.close()

    # Format schedule events
    if events:
        # end result accumulator
        ret = {}
        for row in data:
            schedule_id = row.pop('schedule_id')
            # add data row into accumulator only if not already there
            if schedule_id not in ret:
                ret[schedule_id] = row
                ret[schedule_id]['events'] = []
            start = row.pop('start')
            duration = row.pop('duration')
            ret[schedule_id]['events'].append({'start': start, 'duration': duration})
        data = ret.values()

    if scheduler:
        for schedule in data:
            scheduler_data = {'name': schedule['scheduler']}
            if schedule['id'] in orders:
                scheduler_data['data'] = orders[schedule['id']]
            schedule['scheduler'] = scheduler_data
    return data
Beispiel #7
0
def on_get(req, resp):
    """
    Search for events. Allows filtering based on a number of parameters,
    detailed below.

    **Example request**:

    .. sourcecode:: http

       GET /api/v0/events?team=foo-sre&end__gt=1487466146&role=primary HTTP/1.1
       Host: example.com

    **Example response**:

    .. sourcecode:: http

        HTTP/1.1 200 OK
        Content-Type: application/json

        [
            {
                "start": 1488441600,
                "end": 1489132800,
                "team": "foo-sre",
                "link_id": null,
                "schedule_id": null,
                "role": "primary",
                "user": "******",
                "full_name": "Foo Icecream",
                "id": 187795
            },
            {
                "start": 1488441600,
                "end": 1489132800,
                "team": "foo-sre",
                "link_id": "8a8ae77b8c52448db60c8a701e7bffc2",
                "schedule_id": 123,
                "role": "primary",
                "user": "******",
                "full_name": "Bar Apple",
                "id": 187795
            }
        ]

    :query team: team name
    :query user: user name
    :query role: role name
    :query id: id of the event
    :query start: start time (unix timestamp) of event
    :query end: end time (unix timestamp) of event
    :query start__gt: start time (unix timestamp) greater than
    :query start__ge: start time (unix timestamp) greater than or equal
    :query start__lt: start time (unix timestamp) less than
    :query start__le: start time (unix timestamp) less than or equal
    :query end__gt: end time (unix timestamp) greater than
    :query end__ge: end time (unix timestamp) greater than or equal
    :query end__lt: end time (unix timestamp) less than
    :query end__le: end time (unix timestamp) less than or equal
    :query role__eq: role name
    :query role__contains: role name contains param
    :query role__startswith: role name starts with param
    :query role__endswith: role name ends with param
    :query team__eq: team name
    :query team__contains: team name contains param
    :query team__startswith: team name starts with param
    :query team__endswith: team name ends with param
    :query team_id: team id
    :query user__eq: user name
    :query user__contains: user name contains param
    :query user__startswith: user name starts with param
    :query user__endswith: user name ends with param

    :statuscode 200: no error
    :statuscode 400: bad request
    """
    fields = req.get_param_as_list('fields')
    if fields:
        fields = [columns[f] for f in fields if f in columns]
    req.params.pop('fields', None)
    include_sub = req.get_param_as_bool('include_subscribed')
    if include_sub is None:
        include_sub = True
    req.params.pop('include_subscribed', None)
    cols = ', '.join(fields) if fields else all_columns
    if any(key not in constraints for key in req.params):
        raise HTTPBadRequest('Bad constraint param')
    query = '''SELECT %s FROM `event`
               JOIN `user` ON `user`.`id` = `event`.`user_id`
               JOIN `team` ON `team`.`id` = `event`.`team_id`
               JOIN `role` ON `role`.`id` = `event`.`role_id`''' % cols

    where_params = []
    where_vals = []
    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)

    # Build where clause. If including subscriptions, deal with team parameters later
    params = req.params.keys() - TEAM_PARAMS if include_sub else req.params
    for key in params:
        val = req.get_param(key)
        if key in constraints:
            where_params.append(constraints[key])
            where_vals.append(val)

    # Deal with team subscriptions and team parameters
    team_where = []
    subs_vals = []
    team_params = req.params.keys() & TEAM_PARAMS
    if include_sub and team_params:

        for key in team_params:
            val = req.get_param(key)
            team_where.append(constraints[key])
            subs_vals.append(val)
        subs_and = ' AND '.join(team_where)
        cursor.execute(
            '''SELECT `subscription_id`, `role_id` FROM `team_subscription`
                          JOIN `team` ON `team_id` = `team`.`id`
                          WHERE %s''' % subs_and, subs_vals)
        if cursor.rowcount != 0:
            # Build where clause based on team params and subscriptions
            subs_and = '(%s OR (%s))' % (subs_and, ' OR '.join([
                '`team`.`id` = %s AND `role`.`id` = %s' %
                (row['subscription_id'], row['role_id']) for row in cursor
            ]))
        where_params.append(subs_and)
        where_vals += subs_vals

    where_query = ' AND '.join(where_params)
    if where_query:
        query = '%s WHERE %s' % (query, where_query)
    cursor.execute(query, where_vals)
    data = cursor.fetchall()
    cursor.close()
    connection.close()
    resp.body = json_dumps(data)
Beispiel #8
0
 def validate_post(self, body):
     if not all(k in body for k in ("event_id", "details")):
         raise HTTPBadRequest('missing event_id and/or details attributes')
Beispiel #9
0
class ParseException(Exception):
    """
    An exception to raise if an error occurs while parsing an opinion page
    """


parse_error = HTTPInternalServerError({
    "title":
    "Internal server error",
    "description":
    "The opinion could not be parsed."
})

not_understood = HTTPBadRequest({
    "title":
    "Bad request",
    "description":
    "The request was not understood."
})

not_found = HTTPNotFound()

internal_server_error = HTTPInternalServerError({
    "title":
    "Internal server error",
    "description":
    "The opinion could not be retrieved."
})

missing_url_param = HTTPBadRequest({
    "title":
    "Bad request",
Beispiel #10
0
def on_post(req, resp):
    """
    Swap events. Takes an object specifying the 2 events to be swapped. Swap can
    take either single events or event sets, depending on the value of the
    "linked" attribute. If "linked" is True, the API interprets the "id"
    attribute as a link_id. Otherwise, it's assumed to be an event_id. Note
    that this allows swapping a single event with a linked event.

    **Example request**:

    .. sourcecode:: http

        POST api/v0/events/swap   HTTP/1.1
        Content-Type: application/json

        {
            "events":
            [
                {
                    "id": 1,
                    "linked": false
                },
                {
                    "id": "da515a45e2b2467bbdc9ea3bc7826d36",
                    "linked": true
                }
            ]
        }

    :statuscode 200: Successful swap
    :statuscode 400: Validation checks failed
    """
    data = load_json_body(req)

    try:
        ev_0, ev_1 = data['events']
    except ValueError:
        raise HTTPBadRequest('Invalid event swap request',
                             'Must provide 2 events')

    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)
    try:
        # Accumulate event info for each link/event id
        events = [None, None]
        for i, ev in enumerate([ev_0, ev_1]):
            if not ev.get('id'):
                raise HTTPBadRequest('Invalid event swap request',
                                     'Invalid event id: %s' % ev.get('id'))
            if ev.get('linked'):
                cursor.execute(
                    'SELECT `id`, `start`, `end`, `team_id`, `user_id`, `role_id`, '
                    '`link_id` FROM `event` WHERE `link_id` = %s', ev['id'])
            else:
                cursor.execute(
                    'SELECT `id`, `start`, `end`, `team_id`, `user_id`, `role_id`, '
                    '`link_id` FROM `event` WHERE `id` = %s', ev['id'])
            if cursor.rowcount == 0:
                raise HTTPNotFound()
            events[i] = cursor.fetchall()

        events_0, events_1 = events
        events = events_0 + events_1
        # Validation checks
        now = time.time()
        if any([ev['start'] < now - constants.GRACE_PERIOD for ev in events]):
            raise HTTPBadRequest('Invalid event swap request',
                                 'Cannot edit events in the past')
        if len(set(ev['team_id'] for ev in events)) > 1:
            raise HTTPBadRequest(
                'Event swap not allowed',
                'Swapped events must come from the same team')
        for ev_list in [events_0, events_1]:
            if len(set([ev['user_id'] for ev in ev_list])) != 1:
                raise HTTPBadRequest(
                    '', 'all linked events must have the same user')

        check_calendar_auth_by_id(events[0]['team_id'], req)

        # Swap event users
        change_queries = []
        for ev in (ev_0, ev_1):
            if not ev['linked']:
                # Break link if swapping a single event in a linked chain
                change_queries.append(
                    'UPDATE `event` SET `user_id` = %s, `link_id` = NULL WHERE `id` IN %s'
                )
            else:
                change_queries.append(
                    'UPDATE `event` SET `user_id` = %s WHERE `id` IN %s')
        user_0 = events_0[0]['user_id']
        user_1 = events_1[0]['user_id']
        first_event_0 = min(events_0, key=lambda ev: ev['start'])
        first_event_1 = min(events_1, key=lambda ev: ev['start'])
        cursor.execute(change_queries[0],
                       (user_1, [e0['id'] for e0 in events_0]))
        cursor.execute(change_queries[1],
                       (user_0, [e1['id'] for e1 in events_1]))

        cursor.execute('SELECT id, full_name FROM user WHERE id IN %s',
                       ([user_0, user_1], ))
        full_names = {row['id']: row['full_name'] for row in cursor}
        cursor.execute('SELECT name FROM team WHERE id = %s',
                       events[0]['team_id'])
        team_name = cursor.fetchone()['name']
        context = {
            'full_name_0': full_names[user_0],
            'full_name_1': full_names[user_1],
            'team': team_name
        }
        create_notification(context,
                            events[0]['team_id'],
                            {events_0[0]['role_id'], events_1[0]['role_id']},
                            EVENT_SWAPPED, [user_0, user_1],
                            cursor,
                            start_time_0=first_event_0['start'],
                            start_time_1=first_event_1['start'])
        create_audit(
            {
                'request_body': data,
                'events_swapped': (events_0, events_1)
            }, team_name, EVENT_SWAPPED, req, cursor)
        connection.commit()

    except HTTPError:
        raise
    else:
        connection.commit()
    finally:
        cursor.close()
        connection.close()
Beispiel #11
0
 def _bad_request():
     description = 'Mandatory params missing from the request. ' \
                   'Please check your request params and retry'
     logger.exception(description)
     raise HTTPBadRequest("HTTP Bad Request", description)
Beispiel #12
0
def on_post(req, resp, team):
    '''
    Escalate to a team using Iris. Configured in the 'iris_plan_integration' section of
    the configuration file. Escalation plan is specified via keyword, currently: 'urgent',
    'medium', or 'custom'. These keywords correspond to the plan specified in the
    iris_plan_integration urgent_plan key, the iris integration medium_plan key, and the team's
    iris plan defined in the DB, respectively. If no plan is specified, the team's custom plan will be
    used. If iris plan integration is not activated, this endpoint will be disabled.

    **Example request:**

    .. sourcecode:: http

        POST /v0/events   HTTP/1.1
        Content-Type: application/json

        {
            "description": "Something bad happened!",
            "plan": "urgent"
        }

    :statuscode 200: Incident created
    :statuscode 400: Escalation failed, missing description/No escalation plan specified
    for team/Iris client error.
    '''
    data = load_json_body(req)

    plan = data.get('plan')
    dynamic = False
    if plan == URGENT:
        plan_settings = iris.settings['urgent_plan']
        dynamic = True
    elif plan == MEDIUM:
        plan_settings = iris.settings['medium_plan']
        dynamic = True
    elif plan == CUSTOM or plan is None:
        # Default to team's custom plan for backwards compatibility
        connection = db.connect()
        cursor = connection.cursor()
        cursor.execute('SELECT iris_plan FROM team WHERE name = %s', team)
        if cursor.rowcount == 0:
            cursor.close()
            connection.close()
            raise HTTPBadRequest(
                'Iris escalation failed', 'No escalation plan specified '
                'and team has no custom escalation plan defined')
        plan_name = cursor.fetchone()[0]
        cursor.close()
        connection.close()
    else:
        raise HTTPBadRequest('Iris escalation failed',
                             'Invalid escalation plan')

    requester = req.context.get('user')
    if not requester:
        requester = req.context['app']
    data['requester'] = requester
    if 'description' not in data or data['description'] == '':
        raise HTTPBadRequest('Iris escalation failed',
                             'Escalation cannot have an empty description')
    try:
        if dynamic:
            plan_name = plan_settings['name']
            targets = plan_settings['dynamic_targets']
            for t in targets:
                # Set target to team name if not overridden in settings
                if 'target' not in t:
                    t['target'] = team
            re = iris.client.post(iris.client.url + 'incidents',
                                  json={
                                      'plan': plan_name,
                                      'context': data,
                                      'dynamic_targets': targets
                                  })
            re.raise_for_status()
            incident_id = re.json()
        else:
            incident_id = iris.client.incident(plan_name, context=data)
    except (ValueError, ConnectionError, HTTPError) as e:
        raise HTTPBadRequest('Iris escalation failed',
                             'Iris client error: %s' % e)

    resp.body = str(incident_id)
Beispiel #13
0
def on_post(req, resp, user_name):
    check_user_auth(user_name, req)
    data = load_json_body(req)

    params = set(data.keys())
    missing_params = required_params - params
    if missing_params:
        raise HTTPBadRequest(
            'invalid notification setting',
            'missing required parameters: %s' % ', '.join(missing_params))
    connection = db.connect()
    cursor = connection.cursor()
    cursor.execute('SELECT is_reminder FROM notification_type WHERE name = %s',
                   data['type'])

    # Validation checks: notification type must exist
    #                    only one of time_before and only_if_involved can be defined
    #                    reminder notifications must define time_before
    #                    other notifications must define only_if_involved
    if cursor.rowcount != 1:
        raise HTTPBadRequest(
            'invalid notification setting',
            'notification type %s does not exist' % data['type'])
    is_reminder = cursor.fetchone()[0]
    extra_cols = params & other_params
    if len(extra_cols) != 1:
        raise HTTPBadRequest(
            'invalid notification setting',
            'settings must define exactly one of %s' % other_params)
    extra_col = next(iter(extra_cols))
    if is_reminder and extra_col != 'time_before':
        raise HTTPBadRequest('invalid notification setting',
                             'reminder setting must define time_before')
    elif not is_reminder and extra_col != 'only_if_involved':
        raise HTTPBadRequest(
            'invalid notification setting',
            'notification setting must define only_if_involved')

    roles = data.pop('roles')
    data['user'] = user_name

    query = '''INSERT INTO `notification_setting` (`user_id`, `team_id`, `mode_id`, `type_id`, {0})
               VALUES ((SELECT `id` FROM `user` WHERE `name`= %(user)s),
                       (SELECT `id` FROM `team` WHERE `name` = %(team)s),
                       (SELECT `id` FROM `contact_mode` WHERE `name` = %(mode)s),
                       (SELECT `id` FROM `notification_type` WHERE `name` = %(type)s),
                       %({0})s)'''.format(extra_col)

    cursor.execute(query, data)
    if cursor.rowcount != 1:
        raise HTTPBadRequest(
            'invalid request',
            'unable to create notification with provided settings')
    setting_id = cursor.lastrowid

    query_vals = ', '.join(
        ['(%d, (SELECT `id` FROM `role` WHERE `name` = %%s))' % setting_id] *
        len(roles))

    try:
        cursor.execute(
            'INSERT INTO `setting_role`(`setting_id`, `role_id`) VALUES ' +
            query_vals, roles)
    except db.IntegrityError:
        raise HTTPBadRequest('invalid request',
                             'unable to create notification: invalid roles')
    connection.commit()
    cursor.close()
    connection.close()
    resp.body = json_dumps({'id': setting_id})
    resp.status = HTTP_201
Beispiel #14
0
def on_put(req, resp, event_id):
    """
    Update an event by id; anyone can update any event within the team

    **Example request:**

    .. sourcecode:: http

        PUT /api/v0/events/1234 HTTP/1.1
        Content-Type: application/json

        {
            "start": 1428336000,
            "end": 1428338000,
            "user": "******",
            "role": "secondary"
        }

    :statuscode 200: Successful update

    """
    data = load_json_body(req)

    if 'end' in data and 'start' in data and data['start'] >= data['end']:
        raise HTTPBadRequest('Invalid event update',
                             'Event must start before it ends')

    try:
        update_cols = ', '.join(update_columns[col] for col in data)
    except KeyError:
        raise HTTPBadRequest('Invalid event update', 'Invalid column')

    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)
    try:
        cursor.execute(
            '''SELECT
                `event`.`start`,
                `event`.`end`,
                `event`.`user_id`,
                `event`.`role_id`,
                `event`.`id`,
                `event`.`note`,
                `team`.`name` AS `team`,
                `role`.`name` AS `role`,
                `user`.`name` AS `user`,
                `user`.`full_name`,
                `event`.`team_id`
            FROM `event`
            JOIN `team` ON `event`.`team_id` = `team`.`id`
            JOIN `role` ON `event`.`role_id` = `role`.`id`
            JOIN `user` ON `event`.`user_id` = `user`.`id`
            WHERE `event`.`id`=%s''', event_id)
        event_data = cursor.fetchone()
        if not event_data:
            raise HTTPNotFound()
        new_event = {}
        for col in update_columns:
            new_event[col] = data.get(col, event_data[col])
        now = time.time()
        if event_data['start'] < now - constants.GRACE_PERIOD or data[
                'start'] < now - constants.GRACE_PERIOD:
            # Make an exception for editing event end times
            if not (all(event_data[key] == new_event[key]
                        for key in ('role', 'start', 'user'))
                    and data['end'] > now):
                raise HTTPBadRequest('Invalid event update',
                                     'Editing events in the past not allowed')

        check_calendar_auth(event_data['team'], req)
        if not user_in_team_by_name(cursor, new_event['user'],
                                    event_data['team']):
            raise HTTPBadRequest('Invalid event update',
                                 'Event user must be part of the team')

        update_cols += ', `link_id` = NULL'
        update = 'UPDATE `event` SET ' + update_cols + (' WHERE `id`=%d' %
                                                        int(event_id))
        cursor.execute(update, data)

        # create audit log
        new_event = ', '.join('%s: %s' % (key, data[key]) for key in data)
        create_audit({
            'old_event': event_data,
            'request_body': data
        }, event_data['team'], EVENT_EDITED, req, cursor)

        cursor.execute(
            'SELECT `user_id`, role_id FROM `event` WHERE `id` = %s',
            event_data['id'])
        new_ev_data = cursor.fetchone()
        context = {
            'full_name': event_data['full_name'],
            'role': event_data['role'],
            'team': event_data['team'],
            'new_event': new_event
        }
        create_notification(context,
                            event_data['team_id'],
                            {event_data['role_id'], new_ev_data['role_id']},
                            EVENT_EDITED,
                            {event_data['user_id'], new_ev_data['user_id']},
                            cursor,
                            start_time=event_data['start'])
    except:
        raise
    else:
        connection.commit()
    finally:
        cursor.close()
        connection.close()
Beispiel #15
0
 def validate_post(self, body):
     if not all(k in body for k in("ruleName", "state", "message")):
         raise HTTPBadRequest('missing ruleName, state and/or message attributes')
Beispiel #16
0
def on_post(req, resp, team):
    """
    Add user as a team admin. Responds with that user's info (similar to user GET).
    Subscribes this user to default notifications for the team, and adds the user
    to the team (if needed).

    **Example request**

    .. sourcecode:: http

        POST /api/v0/teams/team-foo/admins  HTTP/1.1
        Host: example.com


    **Example response**:

    .. sourcecode:: http

        HTTP/1.1 200 OK
        Content-Type: application/json

        {
            "active": 1,
            "contacts": {
                "call": "+1 111-111-1111",
                "email": "*****@*****.**",
                "im": "jdoe",
                "sms": "+1 111-111-1111"
            },
            "full_name": "John Doe",
            "id": 9535,
            "name": "jdoe",
            "photo_url": "image.example.com",
            "time_zone": "US/Pacific"
        }

    :statuscode 201: Successful admin added
    :statuscode 400: Missing name attribute in request
    :statuscode 422: Invalid team/user, or user is already a team admin
    """
    team = unquote(team)
    check_team_auth(team, req)
    data = load_json_body(req)

    user_name = data.get('name')
    if not user_name:
        raise HTTPBadRequest('name attribute missing from request')

    connection = db.connect()
    cursor = connection.cursor()

    cursor.execute('''(SELECT `id` FROM `team` WHERE `name`=%s)
                      UNION ALL
                      (SELECT `id` FROM `user` WHERE `name`=%s)''', (team, user_name))
    results = [r[0] for r in cursor]
    if len(results) < 2:
        raise HTTPError('422 Unprocessable Entity', 'IntegrityError', 'invalid team or user')
    (team_id, user_id) = results

    try:
        # also make sure user is in the team
        cursor.execute('''INSERT IGNORE INTO `team_user` (`team_id`, `user_id`) VALUES (%r, %r)''',
                       (team_id, user_id))
        cursor.execute('''INSERT INTO `team_admin` (`team_id`, `user_id`) VALUES (%r, %r)''',
                       (team_id, user_id))
        # subscribe user to team notifications
        subscribe_notifications(team, user_name, cursor)
        create_audit({'user': user_name}, team, ADMIN_CREATED, req, cursor)
        connection.commit()
    except db.IntegrityError as e:
        err_msg = str(e.args[1])
        if err_msg == "Column 'team_id' cannot be null":
            err_msg = 'team %s not found' % team
        if err_msg == "Column 'user_id' cannot be null":
            err_msg = 'user %s not found' % data['name']
        else:
            err_msg = 'user name "%s" is already an admin of team %s' % (data['name'], team)
        raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg)
    finally:
        cursor.close()
        connection.close()

    resp.status = HTTP_201
    resp.body = json_dumps(get_user_data(None, {'name': user_name})[0])
Beispiel #17
0
def on_put(req, resp, team, roster):
    """
    Change roster name. Must have team admin privileges.

        **Example request:**

    .. sourcecode:: http

        PUT /api/v0/teams/team-foo/rosters/roster-foo HTTP/1.1
        Content-Type: application/json

        {
            "name": "roster-bar",
        }

    :statuscode 400: Invalid roster name, disallowed characters
    :statuscode 422: Duplicate roster name for team
    """
    team, roster = unquote(team), unquote(roster)
    data = load_json_body(req)
    name = data.get('name')
    roster_order = data.get('roster_order')
    check_team_auth(team, req)

    if not (name or roster_order):
        raise HTTPBadRequest('invalid roster update',
                             'missing roster name or order')

    connection = db.connect()
    cursor = connection.cursor()
    try:
        if roster_order:
            cursor.execute(
                '''SELECT `user`.`name` FROM `roster_user`
                              JOIN `roster` ON `roster`.`id` = `roster_user`.`roster_id`
                              JOIN `user` ON `roster_user`.`user_id` = `user`.`id`
                              WHERE `roster_id` = (SELECT id FROM roster WHERE name = %s
                                AND team_id = (SELECT id from team WHERE name = %s))''',
                (roster, team))
            roster_users = {row[0] for row in cursor}
            if not all([x in roster_users for x in roster_order]):
                raise HTTPBadRequest(
                    'Invalid roster order',
                    'All users in provided order must be part of the roster')
            if not len(roster_order) == len(roster_users):
                raise HTTPBadRequest(
                    'Invalid roster order',
                    'Roster order must include all roster members')

            cursor.executemany(
                '''UPDATE roster_user SET roster_priority = %s
                                  WHERE roster_id = (SELECT id FROM roster WHERE name = %s
                                    AND team_id = (SELECT id FROM team WHERE name = %s))
                                  AND user_id = (SELECT id FROM user WHERE name = %s)''',
                ((idx, roster, team, user)
                 for idx, user in enumerate(roster_order)))
            connection.commit()

        if name and name != roster:
            invalid_char = invalid_char_reg.search(name)
            if invalid_char:
                raise HTTPBadRequest(
                    'invalid roster name',
                    'roster name contains invalid character "%s"' %
                    invalid_char.group())
            cursor.execute(
                '''UPDATE `roster` SET `name`=%s
                   WHERE `team_id`=(SELECT `id` FROM `team` WHERE `name`=%s)
                       AND `name`=%s''', (name, team, roster))
            create_audit({
                'old_name': roster,
                'new_name': name
            }, team, ROSTER_EDITED, req, cursor)
            connection.commit()
    except db.IntegrityError as e:
        err_msg = str(e.args[1])
        if 'Duplicate entry' in err_msg:
            err_msg = "roster '%s' already existed for team '%s'" % (name,
                                                                     team)
        raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg)
    finally:
        cursor.close()
        connection.close()
Beispiel #18
0
def on_post(req, resp):
    '''
    Endpoint for team creation. The user who creates the team is automatically added as a
    team admin. Because of this, this endpoint cannot be called using an API key, otherwise
    a team would have no admins, making many team operations impossible.

    Teams can specify a number of attributes, detailed below:

    - name: the team's name. Teams must have unique names.
    - email: email address for the team.
    - slack_channel: slack channel for the team. Must start with '#'
    - iris_plan: Iris escalation plan that incidents created from the Oncall UI will follow.

    If iris plan integration is not activated, this attribute can still be set, but its
    value is not used.

    Teams must specify ``name`` and ``scheduling_timezone``; other parameters are optional.

    **Example request:**

    .. sourcecode:: http

        POST api/v0/teams   HTTP/1.1
        Content-Type: application/json

        {
            "name": "team-foo",
            "scheduling_timezone": "US/Pacific",
            "email": "*****@*****.**",
            "slack_channel": "#team-foo",
        }

    **Example response:**

    .. sourcecode:: http

        HTTP/1.1 201 Created
        Content-Type: application/json

    :statuscode 201: Successful create
    :statuscode 400: Error in creating team. Possible errors: API key auth not allowed, invalid attributes, missing required attributes
    :statuscode 422: Duplicate team name
    '''
    if 'user' not in req.context:
        # ban API auth because we don't know who to set as team admin
        raise HTTPBadRequest('invalid login', 'API key auth is not allowed for team creation')

    data = load_json_body(req)
    if not data.get('name'):
        raise HTTPBadRequest('', 'name attribute missing from request')
    if not data.get('scheduling_timezone'):
        raise HTTPBadRequest('', 'scheduling_timezone attribute missing from request')
    team_name = unquote(data['name'])
    invalid_char = invalid_char_reg.search(team_name)
    if invalid_char:
        raise HTTPBadRequest('invalid team name',
                             'team name contains invalid character "%s"' % invalid_char.group())

    scheduling_timezone = unquote(data['scheduling_timezone'])
    slack = data.get('slack_channel')
    if slack and slack[0] != '#':
        raise HTTPBadRequest('invalid slack channel',
                             'slack channel name needs to start with #')
    email = data.get('email')
    iris_plan = data.get('iris_plan')
    iris_enabled = data.get('iris_enabled', False)
    override_number = data.get('override_phone_number')
    if not override_number:
        override_number = None

    # validate Iris plan if provided and Iris is configured
    if iris_plan is not None and iris.client is not None:
        plan_resp = iris.client.get(iris.client.url + 'plans?name=%s&active=1' % iris_plan)
        if plan_resp.status_code != 200 or plan_resp.json() == []:
            raise HTTPBadRequest('invalid iris escalation plan', 'no iris plan named %s exists' % iris_plan)

    connection = db.connect()
    cursor = connection.cursor()
    try:
        cursor.execute('''INSERT INTO `team` (`name`, `slack_channel`, `email`, `scheduling_timezone`, `iris_plan`, `iris_enabled`,
                                              `override_phone_number`)
                          VALUES (%s, %s, %s, %s, %s, %s, %s)''',
                       (team_name, slack, email, scheduling_timezone, iris_plan, iris_enabled, override_number))

        team_id = cursor.lastrowid
        query = '''
            INSERT INTO `team_user` (`team_id`, `user_id`)
            VALUES (%s, (SELECT `id` FROM `user` WHERE `name` = %s))'''
        cursor.execute(query, (team_id, req.context['user']))
        query = '''
            INSERT INTO `team_admin` (`team_id`, `user_id`)
            VALUES (%s, (SELECT `id` FROM `user` WHERE `name` = %s))'''
        cursor.execute(query, (team_id, req.context['user']))
        subscribe_notifications(team_name, req.context['user'], cursor)
        create_audit({'team_id': team_id}, data['name'], TEAM_CREATED, req, cursor)
        connection.commit()
    except db.IntegrityError:
        raise HTTPError('422 Unprocessable Entity',
                        'IntegrityError',
                        'team name "%s" already exists' % team_name)
    finally:
        cursor.close()
        connection.close()

    resp.status = HTTP_201
Beispiel #19
0
    def on_post(self, req, resp):
        '''
        This endpoint is compatible with the webhook post from Alertmanager.
        Simply configure alertmanager with a receiver pointing to iris, like
        so:

        receivers:
        - name: 'iris-team1'
          webhook_configs:
            - url: http://iris:16649/v0/webhooks/alertmanager?application=test-app&key=sdffdssdf

        Where application points to an application and key in Iris.

        For every POST from alertmanager, a new incident will be created, if the iris_plan label
        is attached to an alert.
        '''
        alert_params = ujson.loads(req.context['body'])
        self.validate_post(alert_params)

        with db.guarded_session() as session:
            plan = alert_params['groupLabels']['iris_plan']
            plan_id = session.execute(
                'SELECT `plan_id` FROM `plan_active` WHERE `name` = :plan', {
                    'plan': plan
                }).scalar()
            if not plan_id:
                raise HTTPNotFound()

            app = req.context['app']

            context_json_str = self.create_context(alert_params)

            app_template_count = session.execute(
                '''
                SELECT EXISTS (
                  SELECT 1 FROM
                  `plan_notification`
                  JOIN `template` ON `template`.`name` = `plan_notification`.`template`
                  JOIN `template_content` ON `template_content`.`template_id` = `template`.`id`
                  WHERE `plan_notification`.`plan_id` = :plan_id
                  AND `template_content`.`application_id` = :app_id
                )
            ''', {
                    'app_id': app['id'],
                    'plan_id': plan_id
                }).scalar()

            if not app_template_count:
                logger.warning('no plan template exists for this app')
                raise HTTPBadRequest(
                    'No plan template actions exist for this app')

            data = {
                'plan_id': plan_id,
                'created': datetime.datetime.utcnow(),
                'application_id': app['id'],
                'context': context_json_str,
                'current_step': 0,
                'active': True,
            }

            incident_id = session.execute(
                '''INSERT INTO `incident` (`plan_id`, `created`, `context`,
                                           `current_step`, `active`, `application_id`)
                   VALUES (:plan_id, :created, :context, 0, :active, :application_id)''',
                data).lastrowid

            session.commit()
            session.close()

        resp.status = HTTP_201
        resp.set_header('Location', '/incidents/%s' % incident_id)
        resp.body = ujson.dumps(incident_id)
Beispiel #20
0
def on_put(req, resp, team):
    '''
    Edit a team's information. Allows edit of: name, slack_channel, email, scheduling_timezone, iris_plan.

    **Example request:**

    .. sourcecode:: http

        PUT /api/v0/teams/team-foo HTTP/1.1
        Content-Type: application/json

        {
            "name": "team-bar",
            "slack_channel": "roster-bar",
            "email": 28,
            "scheduling_timezone": "US/Central"
        }

    :statuscode 200: Successful edit
    :statuscode 400: Invalid team name/iris escalation plan
    :statuscode 422: Duplicate team name
    '''
    team = unquote(team)
    check_team_auth(team, req)
    data = load_json_body(req)

    connection = db.connect()
    cursor = connection.cursor()

    data_cols = data.keys()
    if 'name' in data:
        invalid_char = invalid_char_reg.search(data['name'])
        if invalid_char:
            raise HTTPBadRequest(
                'invalid team name',
                'team name contains invalid character "%s"' %
                invalid_char.group())
        elif data['name'] == '':
            raise HTTPBadRequest('invalid team name', 'empty team name')

    if 'iris_plan' in data and data['iris_plan']:
        iris_plan = data['iris_plan']
        plan_resp = iris.client.get(iris.client.url +
                                    'plans?name=%s&active=1' % iris_plan)
        if plan_resp.status_code != 200 or plan_resp.json() == []:
            raise HTTPBadRequest('invalid iris escalation plan',
                                 'no iris plan named %s exists' % iris_plan)

    set_clause = ', '.join(
        ['`{0}`=%s'.format(d) for d in data_cols if d in cols])
    query_params = tuple(data[d] for d in data_cols) + (team, )
    try:
        update_query = 'UPDATE `team` SET {0} WHERE name=%s'.format(set_clause)
        cursor.execute(update_query, query_params)
        create_audit({'request_body': data}, team, TEAM_EDITED, req, cursor)
        connection.commit()
    except db.IntegrityError as e:
        err_msg = str(e.args[1])
        if 'Duplicate entry' in err_msg:
            err_msg = "A team named '%s' already exists" % (data['name'])
        raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg)
    finally:
        cursor.close()
        connection.close()
Beispiel #21
0
def on_post(req, resp):
    """
    Endpoint for creating event. Responds with event id for created event. Events must
    specify the following parameters:

    - start: Unix timestamp for the event start time (seconds)
    - end: Unix timestamp for the event end time (seconds)
    - user: Username for the event's user
    - team: Name for the event's team
    - role: Name for the event's role

    All of these parameters are required.

    **Example request:**

    .. sourcecode:: http

        POST api/v0/events   HTTP/1.1
        Content-Type: application/json

        {
            "start": 1493667700,
            "end": 149368700,
            "user": "******",
            "team": "team-foo",
            "role": "primary",
        }

    **Example response:**

    .. sourcecode:: http

        HTTP/1.1 201 Created
        Content-Type: application/json

        1


    :statuscode 201: Event created
    :statuscode 400: Event validation checks failed
    :statuscode 422: Event creation failed: nonexistent role/event/team
    """
    data = load_json_body(req)
    now = time.time()
    if data['start'] < now - constants.GRACE_PERIOD:
        raise HTTPBadRequest('Invalid event',
                             'Creating events in the past not allowed')
    if data['start'] >= data['end']:
        raise HTTPBadRequest('Invalid event',
                             'Event must start before it ends')
    check_calendar_auth(data['team'], req)

    columns = ['`start`', '`end`', '`user_id`', '`team_id`', '`role_id`']
    values = [
        '%(start)s', '%(end)s',
        '(SELECT `id` FROM `user` WHERE `name`=%(user)s)',
        '(SELECT `id` FROM `team` WHERE `name`=%(team)s)',
        '(SELECT `id` FROM `role` WHERE `name`=%(role)s)'
    ]

    if 'schedule_id' in data:
        columns.append('`schedule_id`')
        values.append('%(schedule_id)s')

    if 'note' in data:
        columns.append('`note`')
        values.append('%(note)s')

    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)

    if not user_in_team_by_name(cursor, data['user'], data['team']):
        raise HTTPBadRequest('Invalid event', 'User must be part of the team')

    try:
        query = 'INSERT INTO `event` (%s) VALUES (%s)' % (','.join(columns),
                                                          ','.join(values))
        cursor.execute(query, data)
        event_id = cursor.lastrowid

        cursor.execute(
            'SELECT team_id, role_id, user_id, start, full_name '
            'FROM event JOIN user ON user.`id` = user_id WHERE event.id=%s',
            event_id)
        ev_info = cursor.fetchone()
        context = {
            'team': data['team'],
            'role': data['role'],
            'full_name': ev_info['full_name']
        }
        create_notification(context,
                            ev_info['team_id'], [ev_info['role_id']],
                            EVENT_CREATED, [ev_info['user_id']],
                            cursor,
                            start_time=ev_info['start'])
        create_audit({
            'new_event_id': event_id,
            'request_body': data
        }, data['team'], EVENT_CREATED, req, cursor)
        connection.commit()
    except db.IntegrityError as e:
        err_msg = str(e.args[1])
        if err_msg == 'Column \'role_id\' cannot be null':
            err_msg = 'role "%s" not found' % data['role']
        elif err_msg == 'Column \'user_id\' cannot be null':
            err_msg = 'user "%s" not found' % data['user']
        elif err_msg == 'Column \'team_id\' cannot be null':
            err_msg = 'team "%s" not found' % data['team']
        raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg)
    finally:
        cursor.close()
        connection.close()

    resp.status = HTTP_201
    resp.body = json_dumps(event_id)
Beispiel #22
0
def on_put(req, resp, link_id):
    """
    Update an event by link_id; anyone can update any event within the team.
    Only username can be updated using this endpoint.

    **Example request:**

    .. sourcecode:: http

        PUT /api/v0/events/link/1234 HTTP/1.1
        Content-Type: application/json

        {
            "user": "******",
        }

    :statuscode 200: Successful update

    """
    data = load_json_body(req)
    user = data.get('user')
    if user is None:
        raise HTTPBadRequest('Bad request for linked event update',
                             'Missing user param')
    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)

    try:
        cursor.execute(
            '''SELECT
                        `event`.`start`,
                        `event`.`end`,
                        `event`.`user_id`,
                        `event`.`role_id`,
                        `event`.`id`,
                        `team`.`name` AS `team`,
                        `role`.`name` AS `role`,
                        `user`.`name` AS `user`,
                        `user`.`full_name`,
                        `event`.`team_id`
                    FROM `event`
                    JOIN `team` ON `event`.`team_id` = `team`.`id`
                    JOIN `role` ON `event`.`role_id` = `role`.`id`
                    JOIN `user` ON `event`.`user_id` = `user`.`id`
                    WHERE `event`.`link_id`=%s''', link_id)
        event_data = cursor.fetchall()
        if len(event_data) == 0:
            raise HTTPNotFound()
        event_summary = event_data[0].copy()
        event_summary['end'] = max(event_data, key=itemgetter('end'))['end']
        event_summary['start'] = min(event_data,
                                     key=itemgetter('start'))['start']
        if not user_in_team_by_name(cursor, user, event_summary['team']):
            raise HTTPBadRequest('Invalid event update',
                                 'Event user must be part of the team')

        now = time.time()
        if event_summary['end'] < now:
            raise HTTPBadRequest('Invalid event update',
                                 'Editing events in the past not allowed')
        check_calendar_auth(event_summary['team'], req)

        cursor.execute('SELECT `id` FROM `user` WHERE `name` = %s', user)
        if cursor.rowcount == 0:
            raise HTTPBadRequest('Invalid event update',
                                 'No user found with specified name')
        user_id = cursor.fetchone()['id']
        cursor.execute(
            '''UPDATE `event`
                          SET `user_id` = %s
                          WHERE link_id = %s''', (user_id, link_id))
        create_audit({
            'old_event': event_summary,
            'request_body': data
        }, event_summary['team'], EVENT_EDITED, req, cursor)

        context = {
            'full_name': event_summary['full_name'],
            'role': event_summary['role'],
            'team': event_summary['team'],
            'new_event': {
                'user': user
            }
        }
        create_notification(context,
                            event_summary['team_id'],
                            {event_summary['role_id']},
                            EVENT_EDITED, {event_summary['user_id'], user_id},
                            cursor,
                            start_time=event_summary['start'])
        connection.commit()
    finally:
        cursor.close()
        connection.close()
    resp.status = HTTP_204
Beispiel #23
0
def on_post(req, resp, team, roster):
    '''
    Schedule create endpoint. Schedules are templates for the auto-scheduler to follow that define
    how it should populate a certain period of time. This template is followed repeatedly to
    populate events on a team's calendar. Schedules are associated with a roster, which defines
    the pool of users that the scheduler selects from. Similarly, the schedule's role indicates
    the role that the populated events shoud have. The ``auto_populate_threshold`` parameter
    defines how far into the future the scheduler populates.

    Finally, each schedule has a list of events, each defining ``start`` and ``duration``. ``start``
    represents an offset from Sunday at 00:00 in the team's scheduling timezone, in seconds. For
    example, denote DAY and HOUR as the number of seconds in a day/hour, respectively. An
    event with ``start`` of (DAY + 9 * HOUR) starts on Monday, at 9:00 am. Duration is also given
    in seconds.

    The scheduler will start at Sunday 00:00 in the team's scheduling timezone, choose a user,
    and populate events on the calendar according to the offsets defined in the events list.
    It then repeats this process, moving to the next Sunday 00:00 after the events it has
    created.

    ``advanced_mode`` acts as a hint to the frontend on how the schedule should be displayed,
    defining whether the advanced mode toggle on the schedule edit action should be set on or off.
    Because of how the frontend displays simple schedules, a schedule can only have advanced_mode = 0
    if its events have one of 4 formats:

    1. One event that is one week long
    2. One event that is two weeks long
    3. Seven events that are 12 hours long
    4. Fourteen events that are 12 hours long

    See below for sample JSON requests.

    Assume these schedules' team defines US/Pacific as its scheduling timezone.

    Weekly 7*24 shift that starts at Monday 6PM PST:

    .. code-block:: javascript

        {
            'role': 'primary'
            'auto_populate_threshold': 21,
            'events':[
                {'start': SECONDS_IN_A_DAY + 18 * SECONDS_IN_AN_HOUR,
                 'duration': SECONDS_IN_A_WEEK}
            ],
            'advanced_mode': 0
        }

    Weekly 7*12 shift that starts at Monday 8AM PST:

    .. code-block:: javascript

        {
            'role': 'oncall',
            'events':[
                {'start': SECONDS_IN_A_DAY + 8 * SECONDS_IN_AN_HOUR,
                 'duration': 12 * SECONDS_IN_AN_HOUR},
                {'start': 2 * SECONDS_IN_A_DAY + 8 * SECONDS_IN_AN_HOUR,
                 'duration': 12 * SECONDS_IN_AN_HOUR} ... *5 more*
            ],
            'advanced_mode': 1
        }

    **Example Request**

    .. sourcecode:: http

        POST /v0/teams/team-foo/rosters/roster-foo/schedules   HTTP/1.1
        Content-Type: application/json

        {
            "advanced_mode": 0,
            "auto_populate_threshold": "21",
            "events": [
                {
                    "duration": 604800,
                    "start": 129600
                }
            ],
            "role": "primary",
        }

    **Example response**:

    .. sourcecode:: http

        HTTP/1.1 201 OK
        Content-Type: application/json

        {
            "id": 2221
        }

    :statuscode 201: Successful schedule create. Response contains created schedule's id.
    :statuscode 400: Missing required parameters
    :statuscode 422: Invalid roster specified
    '''
    data = load_json_body(req)
    data['team'] = unquote(team)
    data['roster'] = unquote(roster)
    check_team_auth(data['team'], req)
    missing_params = required_params - set(data.keys())
    if missing_params:
        raise HTTPBadRequest('invalid schedule',
                             'missing required parameters: %s' % ', '.join(missing_params))

    schedule_events = data.pop('events')
    for sev in schedule_events:
        if 'start' not in sev or 'duration' not in sev:
            raise HTTPBadRequest('invalid schedule',
                                 'schedule event requires both start and duration fields')

    if 'auto_populate_threshold' not in data:
        # default to autopopulate 3 weeks forward
        data['auto_populate_threshold'] = 21

    if 'scheduler' not in data:
        # default to "default" scheduling algorithm
        data['scheduler_name'] = 'default'
    else:
        data['scheduler_name'] = data['scheduler'].get('name', 'default')
        scheduler_data = data['scheduler'].get('data')

    if not data['advanced_mode']:
        if not validate_simple_schedule(schedule_events):
            raise HTTPBadRequest('invalid schedule', 'invalid advanced mode setting')

    insert_schedule = '''INSERT INTO `schedule` (`roster_id`,`team_id`,`role_id`,
                                                 `auto_populate_threshold`, `advanced_mode`, `scheduler_id`)
                         VALUES ((SELECT `roster`.`id` FROM `roster`
                                      JOIN `team` ON `roster`.`team_id` = `team`.`id`
                                      WHERE `roster`.`name` = %(roster)s AND `team`.`name` = %(team)s),
                                 (SELECT `id` FROM `team` WHERE `name` = %(team)s),
                                 (SELECT `id` FROM `role` WHERE `name` = %(role)s),
                                 %(auto_populate_threshold)s,
                                 %(advanced_mode)s,
                                 (SELECT `id` FROM `scheduler` WHERE `name` = %(scheduler_name)s))'''
    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)
    try:
        cursor.execute(insert_schedule, data)
        schedule_id = cursor.lastrowid
        insert_schedule_events(schedule_id, schedule_events, cursor)

        if data['scheduler_name'] == 'round-robin':
            params = [(schedule_id, name, idx) for idx, name in enumerate(scheduler_data)]
            cursor.executemany('''INSERT INTO `schedule_order` (`schedule_id`, `user_id`, `priority`)
                                  VALUES (%s, (SELECT `id` FROM `user` WHERE `name` = %s), %s)''',
                               params)
    except db.IntegrityError as e:
        err_msg = str(e.args[1])
        if err_msg == 'Column \'roster_id\' cannot be null':
            err_msg = 'roster "%s" not found' % roster
        elif err_msg == 'Column \'role_id\' cannot be null':
            err_msg = 'role not found'
        elif err_msg == 'Column \'scheduler_id\' cannot be null':
            err_msg = 'scheduler not found'
        elif err_msg == 'Column \'team_id\' cannot be null':
            err_msg = 'team "%s" not found' % team
        raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg)
    else:
        connection.commit()
    finally:
        cursor.close()
        connection.close()

    resp.status = HTTP_201
    resp.body = json_dumps({'id': schedule_id})
Beispiel #24
0
 def as_bad_request(self):
     """Translate this error to falcon's HTTP specific error exception."""
     return HTTPBadRequest(title="Representation deserialization failed",
                           description=self._get_description())
Beispiel #25
0
def on_post(req, resp, team, roster):
    """
    Add user to a roster for a team. On successful creation, returns that user's information.
    This includes id, contacts, etc, similar to the /api/v0/users GET endpoint.


    **Example request:**

    .. sourcecode:: http

        POST /v0/teams/team-foo/rosters/roster-foo/users   HTTP/1.1
        Content-Type: application/json

        {
            "name": "jdoe"
        }

    **Example response:**

    .. sourcecode:: http

        HTTP/1.1 200 OK
        Content-Type: application/json

        {
            "active": 1,
            "contacts": {
                "email": "*****@*****.**",
                "im": "jdoe",
                "sms": "+1 111-111-1111",
                "call": "+1 111-111-1111"
            },
            "full_name": "John Doe",
            "id": 1,
            "name": "jdoe",
            "photo_url": "example.image.com",
            "time_zone": "US/Pacific"
        }

    :statuscode 201: Roster user added
    :statuscode 400: Missing "name" parameter
    :statuscode 422: Invalid team/user or user is already in roster.

    """
    team, roster = unquote(team), unquote(roster)
    data = load_json_body(req)

    user_name = data.get('name')
    in_rotation = int(data.get('in_rotation', True))
    if not user_name:
        raise HTTPBadRequest('incomplete data', 'missing field "name"')
    check_team_auth(team, req)

    connection = db.connect()
    cursor = connection.cursor()
    cursor.execute('''(SELECT `id` FROM `team` WHERE `name`=%s)
                      UNION ALL
                      (SELECT `id` FROM `user` WHERE `name`=%s)''', (team, user_name))
    results = [r[0] for r in cursor]
    if len(results) < 2:
        raise HTTPError('422 Unprocessable Entity', 'IntegrityError', 'invalid team or user')

    # TODO: validate roster
    (team_id, user_id) = results
    try:
        # also make sure user is in the team
        cursor.execute('''INSERT IGNORE INTO `team_user` (`team_id`, `user_id`) VALUES (%r, %r)''',
                       (team_id, user_id))
        cursor.execute('''SELECT `roster`.`id`, COALESCE(MAX(`roster_user`.`roster_priority`), -1) + 1
                          FROM `roster`
                          LEFT JOIN `roster_user` ON `roster`.`id` = `roster_id`
                          JOIN `team` ON `team`.`id`=`roster`.`team_id`
                          WHERE `team`.`name`=%s AND `roster`.`name`=%s''',
                       (team, roster))
        if cursor.rowcount == 0:
            raise HTTPNotFound()
        roster_id, roster_priority = cursor.fetchone()
        cursor.execute('''INSERT INTO `roster_user` (`user_id`, `roster_id`, `in_rotation`, `roster_priority`)
                          VALUES (
                              %s,
                              %s,
                              %s,
                              %s
                          )''',
                       (user_id, roster_id, in_rotation, roster_priority))
        cursor.execute('''INSERT INTO `schedule_order`
                          SELECT `schedule_id`, %s, COALESCE(MAX(`schedule_order`.`priority`), -1) + 1
                          FROM `schedule_order`
                          JOIN `schedule` ON `schedule`.`id` = `schedule_order`.`schedule_id`
                          JOIN `roster` ON `roster`.`id` = `schedule`.`roster_id`
                          JOIN `team` ON `roster`.`team_id` = `team`.`id`
                          WHERE `roster`.`name` = %s AND `team`.`name` = %s
                          GROUP BY `schedule_id`''',
                       (user_id, roster, team))
        # subscribe user to notifications
        subscribe_notifications(team, user_name, cursor)

        create_audit({'roster': roster, 'user': user_name, 'request_body': data}, team,
                     ROSTER_USER_ADDED, req, cursor)
        connection.commit()
    except db.IntegrityError:
        raise HTTPError('422 Unprocessable Entity',
                        'IntegrityError',
                        'user "%(name)s" is already in the roster' % data)
    finally:
        cursor.close()
        connection.close()

    resp.status = HTTP_201
    resp.body = json_dumps(get_user_data(None, {'name': user_name})[0])
Beispiel #26
0
    def on_post(self, req, resp, plan):
        '''
        For every POST, a new incident will be created, if the plan label is
        attached to an alert. The iris application and key should be provided
        in the url params. The plan id can be taken from the post body or url
        params passed by the webhook subclass.
        '''
        alert_params = ujson.loads(req.context['body'])
        self.validate_post(alert_params)

        with db.guarded_session() as session:
            plan_id = session.execute(
                'SELECT `plan_id` FROM `plan_active` WHERE `name` = :plan', {
                    'plan': plan
                }).scalar()
            if not plan_id:
                raise HTTPNotFound()

            app = req.context['app']

            context_json_str = self.create_context(alert_params)

            app_template_count = session.execute(
                '''
                SELECT EXISTS (
                  SELECT 1 FROM
                  `plan_notification`
                  JOIN `template` ON `template`.`name` = `plan_notification`.`template`
                  JOIN `template_content` ON `template_content`.`template_id` = `template`.`id`
                  WHERE `plan_notification`.`plan_id` = :plan_id
                  AND `template_content`.`application_id` = :app_id
                )
            ''', {
                    'app_id': app['id'],
                    'plan_id': plan_id
                }).scalar()

            if not app_template_count:
                logger.warn('no plan template exists for this app')
                raise HTTPBadRequest(
                    'No plan template actions exist for this app')

            data = {
                'plan_id': plan_id,
                'created': datetime.datetime.utcnow(),
                'application_id': app['id'],
                'context': context_json_str,
                'current_step': 0,
                'active': True,
            }

            incident_id = session.execute(
                '''INSERT INTO `incident` (`plan_id`, `created`, `context`,
                                           `current_step`, `active`, `application_id`)
                   VALUES (:plan_id, :created, :context, 0, :active, :application_id)''',
                data).lastrowid

            session.commit()
            session.close()

        resp.status = HTTP_201
        resp.set_header('Location', '/incidents/%s' % incident_id)
        resp.body = ujson.dumps(incident_id)
Beispiel #27
0
def on_post(req, resp, team):
    """
    Create a roster for a team

    **Example request:**

    .. sourcecode:: http

        POST /v0/teams/team-foo/rosters  HTTP/1.1
        Content-Type: application/json

        {
            "name": "roster-foo",
        }

    **Example response:**

    .. sourcecode:: http

        HTTP/1.1 201 Created
        Content-Type: application/json


    :statuscode 201: Succesful roster creation
    :statuscode 422: Invalid character in roster name/Duplicate roster name
    """
    team = unquote(team)
    data = load_json_body(req)

    roster_name = data.get('name')
    if not roster_name:
        raise HTTPBadRequest('name attribute missing from request', '')
    invalid_char = invalid_char_reg.search(roster_name)
    if invalid_char:
        raise HTTPBadRequest(
            'invalid roster name',
            'roster name contains invalid character "%s"' %
            invalid_char.group())

    check_team_auth(team, req)

    connection = db.connect()
    cursor = connection.cursor()
    try:
        cursor.execute(
            '''INSERT INTO `roster` (`name`, `team_id`)
                          VALUES (%s, (SELECT `id` FROM `team` WHERE `name`=%s))''',
            (roster_name, team))
    except db.IntegrityError:
        raise HTTPError(
            '422 Unprocessable Entity', 'IntegrityError',
            'roster name "%s" already exists for team %s' %
            (roster_name, team))
    create_audit({
        'roster_id': cursor.lastrowid,
        'request_body': data
    }, team, ROSTER_CREATED, req, cursor)
    connection.commit()
    cursor.close()
    connection.close()

    resp.status = HTTP_201
Beispiel #28
0
def on_post(req, resp):
    """
    Override/substitute existing events. For example, if the current on-call is unexpectedly busy from 3-4, another
    user can override that event for that time period and take over the shift. Override may delete or edit
    existing events, and may create new events. The API's response contains the information for all undeleted
    events that were passed in the event_ids param, along with the events created by the override.

    Params:
        - **start**: Start time for the event substitution
        - **end**: End time for event substitution
        - **event_ids**: List of event ids to override
        - **user**: User who will be taking over

    **Example request:**

    .. sourcecode:: http

        POST api/v0/events/override   HTTP/1.1
        Content-Type: application/json

        {
            "start": 1493677400,
            "end": 1493678400,
            "event_ids": [1],
            "user": "******"
        }

    **Example response:**

    .. sourcecode:: http

        HTTP/1.1 200 OK
        Content-Type: application/json

        [
            {
                "end": 1493678400,
                "full_name": "John Doe",
                "id": 3,
                "role": "primary",
                "start": 1493677400,
                "team": "team-foo",
                "user": "******"
            }
        ]

    """
    data = load_json_body(req)
    event_ids = data['event_ids']
    start = data['start']
    end = data['end']
    user = data['user']

    get_events_query = '''SELECT `start`, `end`, `id`, `schedule_id`, `user_id`, `role_id`, `team_id`
                          FROM `event` WHERE `id` IN %s'''
    insert_event_query = 'INSERT INTO `event`(`start`, `end`, `user_id`, `team_id`, `role_id`)' \
                         'VALUES (%(start)s, %(end)s, %(user_id)s, %(team_id)s, %(role_id)s)'
    event_return_query = '''SELECT `event`.`start`, `event`.`end`, `event`.`id`, `role`.`name` AS `role`,
                                `team`.`name` AS `team`, `user`.`name` AS `user`, `user`.`full_name`
                            FROM `event` JOIN `role` ON `event`.`role_id` = `role`.`id`
                                JOIN `team` ON `event`.`team_id` = `team`.`id`
                                JOIN `user` ON `event`.`user_id` = `user`.`id`
                            WHERE `event`.`id` IN %s'''

    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)
    try:
        cursor.execute(get_events_query, (event_ids,))
        events = cursor.fetchall()
        now = time.time()

        cursor.execute('SELECT `id` FROM `user` WHERE `name` = %s', user)
        user_id = cursor.fetchone()
        if not (events and user_id):
            raise HTTPBadRequest('Invalid name or list of events')
        else:
            user_id = user_id['id']
            team_id = events[0]['team_id']

        check_calendar_auth_by_id(team_id, req)
        # Check that events are not in the past
        if start < now - constants.GRACE_PERIOD:
            raise HTTPBadRequest('Invalid override request', 'Cannot edit events in the past')
        # Check that events are from the same team
        if any([ev['team_id'] != team_id for ev in events]):
            raise HTTPBadRequest('Invalid override request', 'Events must be from the same team')
        # Check override user's membership in the team
        if not user_in_team(cursor, user_id, team_id):
            raise HTTPBadRequest('Invalid override request', 'Substituting user must be part of the team')
        # Check events have the same role
        if len(set([ev['role_id'] for ev in events])) > 1:
            raise HTTPBadRequest('Invalid override request', 'events must have the same role')
        # Check events have same user
        if len(set([ev['user_id'] for ev in events])) > 1:
            raise HTTPBadRequest('Invalid override request', 'events must have the same role')

        edit_start = []
        edit_end = []
        delete = []
        split = []
        events = sorted(events, key=lambda x: x['start'])

        # Truncate start/end if needed
        start = max(events[0]['start'], start)
        end = min(max(e['end'] for e in events), end)

        for idx, e in enumerate(events):
            # Check for consecutive events
            if idx != 0 and e['start'] != events[idx - 1]['end']:
                raise HTTPBadRequest('Invalid override request', 'events must be consecutive')

            # Sort events into lists according to how they need to be edited
            if start <= e['start'] and end >= e['end']:
                delete.append(e)
            elif start > e['start'] and start < e['end'] <= end:
                edit_end.append(e)
            elif start <= e['start'] < end and end < e['end']:
                edit_start.append(e)
            elif start > e['start'] and end < e['end']:
                split.append(e)
            else:
                raise HTTPBadRequest('Invalid override request', 'events must overlap with override time range')

        # Edit events
        if edit_start:
            ids = [e['id'] for e in edit_start]
            cursor.execute('UPDATE `event` SET `start` = %s WHERE `id` IN %s', (end, ids))
        if edit_end:
            ids = [e['id'] for e in edit_end]
            cursor.execute('UPDATE `event` SET `end` = %s WHERE `id` IN %s', (start, ids))
        if delete:
            ids = [e['id'] for e in delete]
            cursor.execute('DELETE FROM `event` WHERE `id` IN %s', (ids,))
        if split:
            create = []
            for e in split:
                left_event = e.copy()
                right_event = e.copy()
                left_event['end'] = start
                right_event['start'] = end
                create.append(left_event)
                create.append(right_event)

            ids = []
            # Create left/right events
            for e in create:
                cursor.execute(insert_event_query, e)
                ids.append(cursor.lastrowid)
                event_ids.append(cursor.lastrowid)

            # Delete the split event
            ids = [e['id'] for e in split]
            cursor.execute('DELETE FROM `event` WHERE `id` IN %s', (ids,))

        # Insert new override event
        override_event = {
            'start': start,
            'end': end,
            'role_id': events[0]['role_id'],
            'team_id': events[0]['team_id'],
            'user_id': user_id
        }
        cursor.execute('''INSERT INTO `event`(`start`, `end`, `user_id`, `team_id`, `role_id`)
                          VALUES (%(start)s, %(end)s, %(user_id)s, %(team_id)s, %(role_id)s)''',
                       override_event)
        event_ids.append(cursor.lastrowid)

        cursor.execute(event_return_query, (event_ids,))
        ret_data = cursor.fetchall()
        cursor.execute('SELECT full_name, id FROM user WHERE id IN %s', ((user_id, events[0]['user_id']),))
        full_names = {row['id']: row['full_name'] for row in cursor}
        context = {'full_name_0': full_names[user_id], 'full_name_1': full_names[events[0]['user_id']],
                   'role': ret_data[0]['role'], 'team': ret_data[0]['team']}
        create_notification(context, events[0]['team_id'], [events[0]['role_id']], EVENT_SUBSTITUTED,
                            [user_id, events[0]['user_id']], cursor, start_time=start, end_time=end)
        create_audit({'new_events': ret_data, 'request_body': data}, ret_data[0]['team'],
                     EVENT_SUBSTITUTED, req, cursor)
        resp.body = json_dumps(ret_data)
    except HTTPError:
        raise
    else:
        connection.commit()
    finally:
        cursor.close()
        connection.close()
Beispiel #29
0
def on_put(req, resp, schedule_id):
    """
    Update a schedule. Allows editing of role, team, roster, auto_populate_threshold,
    events, and advanced_mode. Only allowed for team admins. Note that simple mode
    schedules must conform to simple schedule restrictions (described in documentation
    for the /api/v0/team/{team_name}/rosters/{roster_name}/schedules GET endpoint).
    This is checked on both "events" and "advanced_mode" edits.

    **Example request:**

    .. sourcecode:: http

        PUT /api/v0/schedules/1234 HTTP/1.1
        Content-Type: application/json

        {
            "role": "primary",
            "team": "team-bar",
            "roster": "roster-bar",
            "auto_populate_threshold": 28,
            "events":
                [
                    {
                        "start": 0,
                        "duration": 100
                    }
                ]
            "advanced_mode": 1
        }
    """
    data = load_json_body(req)

    # Get rid of extraneous column data (so pymysql doesn't try to escape it)
    events = data.pop('events', None)

    data = dict((k, data[k]) for k in data if k in columns)
    if 'roster' in data and 'team' not in data:
        raise HTTPBadRequest('Invalid edit', 'team must be specified with roster')
    cols = ', '.join(columns[col] for col in data)

    update = 'UPDATE `schedule` SET ' + cols + ' WHERE `id`=%d' % int(schedule_id)
    connection = db.connect()
    cursor = connection.cursor()
    verify_auth(req, schedule_id, connection, cursor)

    # Validate simple schedule events
    if events:
        simple = validate_simple_schedule(events)
    else:
        cursor.execute('SELECT duration FROM schedule_event WHERE schedule_id = %s', schedule_id)
        existing_events = [{'duration': row[0]} for row in cursor.fetchall()]
        simple = validate_simple_schedule(existing_events)

    # Get advanced mode value (existing or new)
    advanced_mode = data.get('advanced_mode')
    if advanced_mode is None:
        cursor.execute('SELECT advanced_mode FROM schedule WHERE id = %s', schedule_id)
        advanced_mode = cursor.fetchone()[0]
    # if advanced mode is 0 and the events cannot exist as a simple schedule, raise an error
    if not advanced_mode and not simple:
        raise HTTPBadRequest('Invalid edit', 'schedule cannot be represented in simple mode')

    if cols:
        cursor.execute(update, data)
    if events:
        cursor.execute('DELETE FROM `schedule_event` WHERE `schedule_id` = %s', schedule_id)
        insert_schedule_events(schedule_id, events, cursor)
    connection.commit()
    cursor.close()
    connection.close()
Beispiel #30
0
def on_get(req, resp):
    """
    Search for events. Allows filtering based on a number of parameters, detailed below.

    **Example request**:

    .. sourcecode:: http

       GET /api/v0/events?team=foo-sre&end__gt=1487466146&role=primary HTTP/1.1
       Host: example.com

    **Example response**:

    .. sourcecode:: http

        HTTP/1.1 200 OK
        Content-Type: application/json

        [
            {
                "start": 1488441600,
                "end": 1489132800,
                "team": "foo-sre",
                "link_id": null,
                "schedule_id": null,
                "role": "primary",
                "user": "******",
                "full_name": "Foo Icecream",
                "id": 187795
            },
            {
                "start": 1488441600,
                "end": 1489132800,
                "team": "foo-sre",
                "link_id": "8a8ae77b8c52448db60c8a701e7bffc2",
                "schedule_id": 123,
                "role": "primary",
                "user": "******",
                "full_name": "Bar Apple",
                "id": 187795
            }
        ]

    :query team: team name
    :query user: user name
    :query role: role name
    :query id: id of the event
    :query start: start time (unix timestamp) of event
    :query end: end time (unix timestamp) of event
    :query start__gt: start time (unix timestamp) greater than
    :query start__ge: start time (unix timestamp) greater than or equal
    :query start__lt: start time (unix timestamp) less than
    :query start__le: start time (unix timestamp) less than or equal
    :query end__gt: end time (unix timestamp) greater than
    :query end__ge: end time (unix timestamp) greater than or equal
    :query end__lt: end time (unix timestamp) less than
    :query end__le: end time (unix timestamp) less than or equal
    :query role__eq: role name
    :query role__contains: role name contains param
    :query role__startswith: role name starts with param
    :query role__endswith: role name ends with param
    :query team__eq: team name
    :query team__contains: team name contains param
    :query team__startswith: team name starts with param
    :query team__endswith: team name ends with param
    :query team_id: team id
    :query user__eq: user name
    :query user__contains: user name contains param
    :query user__startswith: user name starts with param
    :query user__endswith: user name ends with param

    :statuscode 200: no error
    :statuscode 400: bad request
    """
    fields = req.get_param_as_list('fields', transform=columns.__getitem__)
    req.params.pop('fields', None)
    cols = ', '.join(fields) if fields else all_columns
    if any(key not in constraints for key in req.params):
        raise HTTPBadRequest('Bad constraint param')
    query = '''SELECT %s FROM `event`
               JOIN `user` ON `user`.`id` = `event`.`user_id`
               JOIN `team` ON `team`.`id` = `event`.`team_id`
               JOIN `role` ON `role`.`id` = `event`.`role_id`''' % cols

    where_params = []
    where_vals = []
    for key in req.params:
        val = req.get_param(key)
        if key in constraints:
            where_params.append(constraints[key])
            where_vals.append(val)
    where_query = ' AND '.join(where_params)
    if where_query:
        query = '%s WHERE %s' % (query, where_query)

    connection = db.connect()
    cursor = connection.cursor(db.DictCursor)
    cursor.execute(query, where_vals)
    data = cursor.fetchall()
    cursor.close()
    connection.close()
    resp.body = json_dumps(data)