From 4ccf2239fb8c8aeda376f57735461174f48614f2 Mon Sep 17 00:00:00 2001 From: Jonas Heinrich Date: Thu, 15 May 2025 08:30:10 +0200 Subject: [PATCH] add support for falcon 4 --- setup.py | 2 +- src/oncall/api/v0/audit.py | 2 +- src/oncall/api/v0/bonus_events.py | 4 +- src/oncall/api/v0/event.py | 29 ++++++++--- src/oncall/api/v0/event_link.py | 22 +++++--- src/oncall/api/v0/event_override.py | 41 +++++++++++---- src/oncall/api/v0/event_swap.py | 29 +++++++---- src/oncall/api/v0/events.py | 29 ++++++++--- src/oncall/api/v0/events_link.py | 59 +++++++++++++++------- src/oncall/api/v0/ical_key_detail.py | 10 ++-- src/oncall/api/v0/ical_key_requester.py | 10 ++-- src/oncall/api/v0/ical_key_team.py | 8 +-- src/oncall/api/v0/ical_key_user.py | 16 +++--- src/oncall/api/v0/iris_settings.py | 4 +- src/oncall/api/v0/modes.py | 2 +- src/oncall/api/v0/notification_types.py | 2 +- src/oncall/api/v0/notifications.py | 2 +- src/oncall/api/v0/preview.py | 2 +- src/oncall/api/v0/public_ical.py | 2 +- src/oncall/api/v0/roles.py | 8 ++- src/oncall/api/v0/roster.py | 29 ++++++++--- src/oncall/api/v0/roster_suggest.py | 10 ++-- src/oncall/api/v0/roster_user.py | 9 ++-- src/oncall/api/v0/roster_users.py | 23 ++++++--- src/oncall/api/v0/rosters.py | 28 +++++++---- src/oncall/api/v0/schedule.py | 12 +++-- src/oncall/api/v0/schedules.py | 42 ++++++++++++---- src/oncall/api/v0/search.py | 2 +- src/oncall/api/v0/service.py | 2 +- src/oncall/api/v0/service_oncall.py | 4 +- src/oncall/api/v0/service_teams.py | 2 +- src/oncall/api/v0/services.py | 10 ++-- src/oncall/api/v0/team.py | 43 ++++++++++++---- src/oncall/api/v0/team_admins.py | 20 ++++++-- src/oncall/api/v0/team_changes.py | 2 +- src/oncall/api/v0/team_ical.py | 2 +- src/oncall/api/v0/team_iris_escalate.py | 23 ++++++--- src/oncall/api/v0/team_oncall.py | 2 +- src/oncall/api/v0/team_service.py | 2 +- src/oncall/api/v0/team_services.py | 16 ++++-- src/oncall/api/v0/team_subscriptions.py | 18 +++++-- src/oncall/api/v0/team_summary.py | 2 +- src/oncall/api/v0/team_user.py | 2 +- src/oncall/api/v0/team_users.py | 16 ++++-- src/oncall/api/v0/teams.py | 53 +++++++++++++------ src/oncall/api/v0/timezones.py | 2 +- src/oncall/api/v0/upcoming_shifts.py | 2 +- src/oncall/api/v0/user.py | 7 ++- src/oncall/api/v0/user_ical.py | 2 +- src/oncall/api/v0/user_notification.py | 12 +++-- src/oncall/api/v0/user_notifications.py | 44 ++++++++++------ src/oncall/api/v0/user_pinned_teams.py | 18 +++++-- src/oncall/api/v0/user_teams.py | 2 +- src/oncall/api/v0/users.py | 15 ++++-- src/oncall/app.py | 2 +- src/oncall/auth/__init__.py | 67 ++++++++++++++++++++----- src/oncall/auth/login.py | 18 +++++-- src/oncall/healthcheck.py | 2 +- src/oncall/scheduler/default.py | 5 +- src/oncall/ui/__init__.py | 4 +- src/oncall/utils.py | 5 +- 61 files changed, 608 insertions(+), 256 deletions(-) diff --git a/setup.py b/setup.py index 8e19a8c2..22e61db7 100644 --- a/setup.py +++ b/setup.py @@ -27,7 +27,7 @@ packages=setuptools.find_packages('src'), include_package_data=True, install_requires=[ - 'falcon==3.1.1', + 'falcon', 'falcon-cors', 'greenlet==2.0.1', 'gevent==22.10.2', diff --git a/src/oncall/api/v0/audit.py b/src/oncall/api/v0/audit.py index baa0abbf..b1cb193a 100644 --- a/src/oncall/api/v0/audit.py +++ b/src/oncall/api/v0/audit.py @@ -86,4 +86,4 @@ def on_get(req, resp): results = cursor.fetchall() cursor.close() connection.close() - resp.body = json_dumps(results) \ No newline at end of file + resp.text = json_dumps(results) \ No newline at end of file diff --git a/src/oncall/api/v0/bonus_events.py b/src/oncall/api/v0/bonus_events.py index 3997f3c1..fe2a7f0b 100644 --- a/src/oncall/api/v0/bonus_events.py +++ b/src/oncall/api/v0/bonus_events.py @@ -114,7 +114,7 @@ def on_get(self, req, resp): oncall_bonus_teams = bonus_response.json() - for event in json.loads(resp.body): + for event in json.loads(resp.text): if event['role'].lower() == 'manager': continue @@ -133,4 +133,4 @@ def on_get(self, req, resp): ldap_grouping[event['user']].append(event) resp.status = HTTP_200 - resp.body = json_dumps(ldap_grouping) + resp.text = json_dumps(ldap_grouping) diff --git a/src/oncall/api/v0/event.py b/src/oncall/api/v0/event.py index 45945c6c..0b199f35 100644 --- a/src/oncall/api/v0/event.py +++ b/src/oncall/api/v0/event.py @@ -73,7 +73,7 @@ def on_get(req, resp, event_id): connection.close() if num_found == 0: raise HTTPNotFound() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required @@ -101,12 +101,18 @@ def on_put(req, resp, event_id): 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') + raise HTTPBadRequest( + title='Invalid event update', + description='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') + raise HTTPBadRequest( + title='Invalid event update', + description='Invalid column' + ) connection = db.connect() cursor = connection.cursor(db.DictCursor) @@ -143,12 +149,17 @@ def on_put(req, resp, event_id): try: check_team_auth(event_data['team'], req) except HTTPUnauthorized: - raise HTTPBadRequest('Invalid event update', - 'Editing events in the past not allowed') + raise HTTPBadRequest( + title='Invalid event update', + description='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') + raise HTTPBadRequest( + title='Invalid event update', + description='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)) @@ -208,8 +219,10 @@ def on_delete(req, resp, event_id): ev = cursor.fetchone() check_calendar_auth(ev['team'], req) if ev['start'] < time.time() - constants.GRACE_PERIOD: - raise HTTPBadRequest('Invalid event update', - 'Deleting events in the past not allowed') + raise HTTPBadRequest( + title='Invalid event update', + description='Deleting events in the past not allowed' + ) cursor.execute('DELETE FROM `event` WHERE `id`=%s', event_id) diff --git a/src/oncall/api/v0/event_link.py b/src/oncall/api/v0/event_link.py index d8799378..313e4942 100644 --- a/src/oncall/api/v0/event_link.py +++ b/src/oncall/api/v0/event_link.py @@ -52,8 +52,10 @@ def on_delete(req, resp, link_id): event_start = min(data, key=itemgetter('start'))['start'] check_calendar_auth(ev['team'], req) if event_start < time.time() - constants.GRACE_PERIOD: - raise HTTPBadRequest('Invalid event update', - 'Deleting events in the past not allowed') + raise HTTPBadRequest( + title='Invalid event update', + description='Deleting events in the past not allowed' + ) cursor.execute('DELETE FROM `event` WHERE `link_id`=%s', link_id) context = {'team': ev['team'], 'full_name': ev['full_name'], 'role': ev['role']} @@ -90,7 +92,10 @@ def on_put(req, resp, link_id): try: update_cols = ', '.join(update_columns[col] for col in data) except KeyError: - raise HTTPBadRequest('Invalid event update', 'Invalid column') + raise HTTPBadRequest( + title='Invalid event update', + description='Invalid column' + ) connection = db.connect() cursor = connection.cursor(db.DictCursor) @@ -119,12 +124,17 @@ def on_put(req, resp, link_id): event_summary['start'] = min(event_data, key=itemgetter('start'))['start'] user = data.get('user', event_summary['user']) 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') + raise HTTPBadRequest( + title='Invalid event update', + description='Event user must be part of the team' + ) now = time.time() if event_summary['start'] < now - constants.GRACE_PERIOD: - raise HTTPBadRequest('Invalid event update', - 'Editing events in the past not allowed') + raise HTTPBadRequest( + title='Invalid event update', + description='Editing events in the past not allowed' + ) check_calendar_auth(event_summary['team'], req) update = 'UPDATE `event` SET %s WHERE link_id = %%(link_id)s' % update_cols diff --git a/src/oncall/api/v0/event_override.py b/src/oncall/api/v0/event_override.py index 2a24cf11..77b1b856 100644 --- a/src/oncall/api/v0/event_override.py +++ b/src/oncall/api/v0/event_override.py @@ -86,7 +86,9 @@ def on_post(req, resp): 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') + raise HTTPBadRequest( + title='Invalid name or list of events' + ) else: user_id = user_id['id'] team_id = events[0]['team_id'] @@ -94,19 +96,34 @@ def on_post(req, resp): 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') + raise HTTPBadRequest( + title='Invalid override request', + description='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') + raise HTTPBadRequest( + title='Invalid override request', + description='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') + raise HTTPBadRequest( + title='Invalid override request', + description='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') + raise HTTPBadRequest( + title='Invalid override request', + description='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') + raise HTTPBadRequest( + title='Invalid override request', + description='events must have the same role' + ) edit_start = [] edit_end = [] @@ -121,7 +138,10 @@ def on_post(req, resp): 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') + raise HTTPBadRequest( + title='Invalid override request', + description='events must be consecutive' + ) # Sort events into lists according to how they need to be edited if start <= e['start'] and end >= e['end']: @@ -133,7 +153,10 @@ def on_post(req, resp): elif start > e['start'] and end < e['end']: split.append(e) else: - raise HTTPBadRequest('Invalid override request', 'events must overlap with override time range') + raise HTTPBadRequest( + title='Invalid override request', + description='events must overlap with override time range' + ) # Edit events if edit_start: @@ -189,7 +212,7 @@ def on_post(req, resp): [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) + resp.text = json_dumps(ret_data) except HTTPError: raise else: diff --git a/src/oncall/api/v0/event_swap.py b/src/oncall/api/v0/event_swap.py index da7dc3ae..30998279 100644 --- a/src/oncall/api/v0/event_swap.py +++ b/src/oncall/api/v0/event_swap.py @@ -48,8 +48,10 @@ def on_post(req, resp): try: ev_0, ev_1 = data['events'] except ValueError: - raise HTTPBadRequest('Invalid event swap request', - 'Must provide 2 events') + raise HTTPBadRequest( + title='Invalid event swap request', + description='Must provide 2 events' + ) connection = db.connect() cursor = connection.cursor(db.DictCursor) @@ -58,8 +60,10 @@ def on_post(req, resp): 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')) + raise HTTPBadRequest( + title='Invalid event swap request', + description='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', @@ -77,14 +81,21 @@ def on_post(req, resp): # 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') + raise HTTPBadRequest( + title='Invalid event swap request', + description='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') + raise HTTPBadRequest( + title='Event swap not allowed', + description='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') + raise HTTPBadRequest( + title='', + description='all linked events must have the same user' + ) check_calendar_auth_by_id(events[0]['team_id'], req) diff --git a/src/oncall/api/v0/events.py b/src/oncall/api/v0/events.py index 8f925d12..692ede63 100644 --- a/src/oncall/api/v0/events.py +++ b/src/oncall/api/v0/events.py @@ -154,7 +154,9 @@ def on_get(req, resp): 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') + raise HTTPBadRequest( + title='Bad constraint param' + ) query = '''SELECT %s FROM `event` JOIN `user` ON `user`.`id` = `event`.`user_id` JOIN `team` ON `team`.`id` = `event`.`team_id` @@ -202,7 +204,7 @@ def on_get(req, resp): data = cursor.fetchall() cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required @@ -251,9 +253,15 @@ def on_post(req, resp): 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') + raise HTTPBadRequest( + title='Invalid event', + description='Creating events in the past not allowed' + ) if data['start'] >= data['end']: - raise HTTPBadRequest('Invalid event', 'Event must start before it ends') + raise HTTPBadRequest( + title='Invalid event', + description='Event must start before it ends' + ) check_calendar_auth(data['team'], req) columns = ['`start`', '`end`', '`user_id`', '`team_id`', '`role_id`'] @@ -274,7 +282,10 @@ def on_post(req, resp): 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') + raise HTTPBadRequest( + title='Invalid event', + description='User must be part of the team' + ) try: query = 'INSERT INTO `event` (%s) VALUES (%s)' % (','.join(columns), ','.join(values)) @@ -309,10 +320,14 @@ def on_post(req, resp): 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) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() resp.status = HTTP_201 - resp.body = json_dumps(event_id) + resp.text = json_dumps(event_id) diff --git a/src/oncall/api/v0/events_link.py b/src/oncall/api/v0/events_link.py index e99ae41c..9df5124c 100644 --- a/src/oncall/api/v0/events_link.py +++ b/src/oncall/api/v0/events_link.py @@ -66,16 +66,23 @@ def on_post(req, resp): """ events = load_json_body(req) if not isinstance(events, list): - raise HTTPBadRequest('Invalid argument', - 'events argument needs to be a list') + raise HTTPBadRequest( + title='Invalid argument', + description='events argument needs to be a list' + ) if not events: - raise HTTPBadRequest('Invalid argument', 'events list cannot be empty') + raise HTTPBadRequest( + title='Invalid argument', + description='events list cannot be empty' + ) now = time.time() team = events[0].get('team') if not team: - raise HTTPBadRequest('Invalid argument', - 'event missing team attribute') + raise HTTPBadRequest( + title='Invalid argument', + description='event missing team attribute' + ) check_calendar_auth(team, req) event_values = [] @@ -90,8 +97,10 @@ def on_post(req, resp): cursor.execute('SELECT `id` FROM `team` WHERE `name`=%s', team) team_id = cursor.fetchone() if not team_id: - raise HTTPBadRequest('Invalid event', - 'Invalid team name: %s' % team) + raise HTTPBadRequest( + title='Invalid event', + description='Invalid team name: %s' % team + ) values = [ '%s', @@ -105,19 +114,31 @@ def on_post(req, resp): for ev in events: if ev['start'] < now - constants.GRACE_PERIOD: - raise HTTPBadRequest('Invalid event', - 'Creating events in the past not allowed') + raise HTTPBadRequest( + title='Invalid event', + description='Creating events in the past not allowed' + ) if ev['start'] >= ev['end']: - raise HTTPBadRequest('Invalid event', - 'Event must start before it ends') + raise HTTPBadRequest( + title='Invalid event', + description='Event must start before it ends' + ) ev_team = ev.get('team') if not ev_team: - raise HTTPBadRequest('Invalid event', 'Missing team for event') + raise HTTPBadRequest( + title='Invalid event', + description='Missing team for event' + ) if team != ev_team: - raise HTTPBadRequest('Invalid event', 'Events can only be submitted to one team') + raise HTTPBadRequest( + title='Invalid event', + description='Events can only be submitted to one team' + ) if not user_in_team_by_name(cursor, ev['user'], team): - raise HTTPBadRequest('Invalid event', - 'User %s must be part of the team %s' % (ev['user'], team)) + raise HTTPBadRequest( + title='Invalid event', + description='User %s must be part of the team %s' % (ev['user'], team) + ) event_values.append((ev['start'], ev['end'], ev['user'], team_id, ev['role'], link_id, ev.get('note'))) insert_query = 'INSERT INTO `event` (%s) VALUES (%s)' % (','.join(columns), ','.join(values)) @@ -133,10 +154,14 @@ def on_post(req, resp): err_msg = 'user 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) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() resp.status = HTTP_201 - resp.body = json_dumps({'link_id': link_id, 'event_ids': ev_ids}) + resp.text = json_dumps({'link_id': link_id, 'event_ids': ev_ids}) diff --git a/src/oncall/api/v0/ical_key_detail.py b/src/oncall/api/v0/ical_key_detail.py index b11522f2..37fca8fa 100644 --- a/src/oncall/api/v0/ical_key_detail.py +++ b/src/oncall/api/v0/ical_key_detail.py @@ -13,15 +13,15 @@ def on_get(req, resp, key): challenger = req.context['user'] if not (check_ical_key_requester(key, challenger) or check_ical_key_admin(challenger)): raise HTTPForbidden( - 'Unauthorized', - 'Action not allowed: "%s" is not an admin of ical_key' % (challenger, ), + title='Unauthorized', + description='Action not allowed: "%s" is not an admin of ical_key' % (challenger, ), ) results = get_ical_key_detail(key) if not results: raise HTTPNotFound() - resp.body = json_dumps(results) + resp.text = json_dumps(results) resp.set_header('Content-Type', 'application/json') @@ -30,8 +30,8 @@ def on_delete(req, resp, key): challenger = req.context['user'] if not (check_ical_key_requester(key, challenger) or check_ical_key_admin(challenger)): raise HTTPForbidden( - 'Unauthorized', - 'Action not allowed: "%s" is not an admin of ical_key' % (challenger, ), + title='Unauthorized', + description='Action not allowed: "%s" is not an admin of ical_key' % (challenger, ), ) invalidate_ical_key(key) diff --git a/src/oncall/api/v0/ical_key_requester.py b/src/oncall/api/v0/ical_key_requester.py index 5d904f55..4cfbb8c3 100644 --- a/src/oncall/api/v0/ical_key_requester.py +++ b/src/oncall/api/v0/ical_key_requester.py @@ -16,15 +16,15 @@ def on_get(req, resp, requester): challenger = req.context['user'] if not (challenger == requester or check_ical_key_admin(challenger)): raise HTTPForbidden( - 'Unauthorized', - 'Action not allowed: "%s" is not allowed to view ical_keys of "%s"' % (challenger, requester), + title='Unauthorized', + description='Action not allowed: "%s" is not allowed to view ical_keys of "%s"' % (challenger, requester), ) results = get_ical_key_detail_by_requester(requester) if not results: raise HTTPNotFound() - resp.body = json_dumps(results) + resp.text = json_dumps(results) resp.set_header('Content-Type', 'application/json') @@ -33,8 +33,8 @@ def on_delete(req, resp, requester): challenger = req.context['user'] if not (challenger == requester or check_ical_key_admin(challenger)): raise HTTPForbidden( - 'Unauthorized', - 'Action not allowed: "%s" is not allowed to delete ical_keys of "%s"' % (challenger, requester), + title='Unauthorized', + description='Action not allowed: "%s" is not allowed to delete ical_keys of "%s"' % (challenger, requester), ) invalidate_ical_key_by_requester(requester) diff --git a/src/oncall/api/v0/ical_key_team.py b/src/oncall/api/v0/ical_key_team.py index a66d3826..11e64f83 100644 --- a/src/oncall/api/v0/ical_key_team.py +++ b/src/oncall/api/v0/ical_key_team.py @@ -34,7 +34,7 @@ def on_get(req, resp, team): if key is None: raise HTTPNotFound() - resp.body = key + resp.text = key resp.set_header('Content-Type', 'text/plain') @@ -47,15 +47,15 @@ def on_post(req, resp, team): challenger = req.context['user'] if not check_ical_team(team, challenger): raise HTTPBadRequest( - 'Invalid team name', - 'Team "%s" does not exist or is inactive' % team, + title='Invalid team name', + description='Team "%s" does not exist or is inactive' % team, ) key = generate_ical_key() update_ical_key(challenger, team, 'team', key) resp.status = HTTP_201 - resp.body = key + resp.text = key resp.set_header('Content-Type', 'text/plain') diff --git a/src/oncall/api/v0/ical_key_user.py b/src/oncall/api/v0/ical_key_user.py index c6f77ead..f378967c 100644 --- a/src/oncall/api/v0/ical_key_user.py +++ b/src/oncall/api/v0/ical_key_user.py @@ -28,15 +28,15 @@ def on_get(req, resp, user_name): challenger = req.context['user'] if challenger != user_name: raise HTTPForbidden( - 'Unauthorized', - 'Action not allowed: "%s" is not allowed to view ical_key of "%s"' % (challenger, user_name) + title='Unauthorized', + description='Action not allowed: "%s" is not allowed to view ical_key of "%s"' % (challenger, user_name) ) key = get_ical_key(challenger, user_name, 'user') if key is None: raise HTTPNotFound() - resp.body = key + resp.text = key resp.set_header('Content-Type', 'text/plain') @@ -54,15 +54,15 @@ def on_post(req, resp, user_name): challenger = req.context['user'] if challenger != user_name: raise HTTPForbidden( - 'Unauthorized', - 'Action not allowed: "%s" is not allowed to update ical_key of "%s"' % (challenger, user_name) + title='Unauthorized', + description='Action not allowed: "%s" is not allowed to update ical_key of "%s"' % (challenger, user_name) ) key = generate_ical_key() update_ical_key(challenger, user_name, 'user', key) resp.status = HTTP_201 - resp.body = key + resp.text = key resp.set_header('Content-Type', 'text/plain') @@ -78,8 +78,8 @@ def on_delete(req, resp, user_name): challenger = req.context['user'] if challenger != user_name: raise HTTPForbidden( - 'Unauthorized', - 'Action not allowed: "%s" is not allowed to delete ical_key of "%s"' % (challenger, user_name) + title='Unauthorized', + description='Action not allowed: "%s" is not allowed to delete ical_key of "%s"' % (challenger, user_name) ) delete_ical_key(challenger, user_name, 'user') diff --git a/src/oncall/api/v0/iris_settings.py b/src/oncall/api/v0/iris_settings.py index fddc53e6..5e03688f 100644 --- a/src/oncall/api/v0/iris_settings.py +++ b/src/oncall/api/v0/iris_settings.py @@ -7,6 +7,6 @@ def on_get(req, resp): if iris.settings is None: - resp.body = json_dumps({'activated': False}) + resp.text = json_dumps({'activated': False}) else: - resp.body = json_dumps(iris.settings) + resp.text = json_dumps(iris.settings) diff --git a/src/oncall/api/v0/modes.py b/src/oncall/api/v0/modes.py index f022448e..d27ce7b6 100644 --- a/src/oncall/api/v0/modes.py +++ b/src/oncall/api/v0/modes.py @@ -15,4 +15,4 @@ def on_get(req, resp): data = [row[0] for row in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) diff --git a/src/oncall/api/v0/notification_types.py b/src/oncall/api/v0/notification_types.py index 11bc38ba..7d46e51f 100644 --- a/src/oncall/api/v0/notification_types.py +++ b/src/oncall/api/v0/notification_types.py @@ -36,4 +36,4 @@ def on_get(req, resp): data = cursor.fetchall() cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) diff --git a/src/oncall/api/v0/notifications.py b/src/oncall/api/v0/notifications.py index 2ca5efa9..cff1eedc 100644 --- a/src/oncall/api/v0/notifications.py +++ b/src/oncall/api/v0/notifications.py @@ -27,4 +27,4 @@ def on_get(req, resp): data = cursor.fetchall() cursor.close() connection.close() - resp.body = json_dumps(data) \ No newline at end of file + resp.text = json_dumps(data) \ No newline at end of file diff --git a/src/oncall/api/v0/preview.py b/src/oncall/api/v0/preview.py index cbacc1a5..88fb185e 100644 --- a/src/oncall/api/v0/preview.py +++ b/src/oncall/api/v0/preview.py @@ -60,7 +60,7 @@ def on_get(req, resp, schedule_id): cursor.execute(query, (team_id, team_id, last_end)) scheduler.populate(schedule, start_time, (connection, cursor), table_name) - resp.body = scheduler.build_preview_response(cursor, start__lt, end__ge, team__eq, table_name) + resp.text = scheduler.build_preview_response(cursor, start__lt, end__ge, team__eq, table_name) cursor.execute("DROP TEMPORARY TABLE `temp_event`") cursor.close() connection.close() diff --git a/src/oncall/api/v0/public_ical.py b/src/oncall/api/v0/public_ical.py index 69150f72..eb07ad63 100644 --- a/src/oncall/api/v0/public_ical.py +++ b/src/oncall/api/v0/public_ical.py @@ -33,5 +33,5 @@ def on_get(req, resp, key): elif type == 'team': events = get_team_events(name, start, roles=roles, include_subscribed=True) - resp.body = ical.events_to_ical(events, name, contact=False) + resp.text = ical.events_to_ical(events, name, contact=False) resp.set_header('Content-Type', 'text/calendar') diff --git a/src/oncall/api/v0/roles.py b/src/oncall/api/v0/roles.py index 11dbddbc..98274cd7 100644 --- a/src/oncall/api/v0/roles.py +++ b/src/oncall/api/v0/roles.py @@ -100,7 +100,7 @@ def on_get(req, resp): data = cursor.fetchall() cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @debug_only @@ -116,7 +116,11 @@ def on_post(req, resp): err_msg = str(e.args[1]) if 'Duplicate entry' in err_msg: err_msg = 'role "%s" already existed' % new_role - raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() diff --git a/src/oncall/api/v0/roster.py b/src/oncall/api/v0/roster.py index b95dd231..87d2207e 100644 --- a/src/oncall/api/v0/roster.py +++ b/src/oncall/api/v0/roster.py @@ -87,7 +87,7 @@ def on_get(req, resp, team, roster): cursor.close() connection.close() - resp.body = json_dumps({'users': users, 'schedules': schedules}) + resp.text = json_dumps({'users': users, 'schedules': schedules}) @login_required @@ -116,7 +116,10 @@ def on_put(req, resp, team, roster): check_team_auth(team, req) if not (name or roster_order): - raise HTTPBadRequest('invalid roster update', 'missing roster name or order') + raise HTTPBadRequest( + title='invalid roster update', + description='missing roster name or order' + ) connection = db.connect() cursor = connection.cursor() @@ -130,9 +133,15 @@ def on_put(req, resp, team, roster): (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') + raise HTTPBadRequest( + title='Invalid roster order', + description='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') + raise HTTPBadRequest( + title='Invalid roster order', + description='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 @@ -144,8 +153,10 @@ def on_put(req, resp, team, roster): 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()) + raise HTTPBadRequest( + title='invalid roster name', + description='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) @@ -157,7 +168,11 @@ def on_put(req, resp, team, roster): 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) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() diff --git a/src/oncall/api/v0/roster_suggest.py b/src/oncall/api/v0/roster_suggest.py index 28c383e5..4fd4996e 100644 --- a/src/oncall/api/v0/roster_suggest.py +++ b/src/oncall/api/v0/roster_suggest.py @@ -15,13 +15,17 @@ def on_get(req, resp, team, roster, role): try: cursor.execute('SELECT id FROM role WHERE name = %s', role) if cursor.rowcount == 0: - raise HTTPBadRequest('Invalid role') + raise HTTPBadRequest( + title='Invalid role' + ) role_id = cursor.fetchone()[0] cursor.execute('''SELECT `team`.`id`, `roster`.`id` FROM `team` JOIN `roster` ON `roster`.`team_id` = `team`.`id` WHERE `roster`.`name` = %s and `team`.`name` = %s''', (roster, team)) if cursor.rowcount == 0: - raise HTTPBadRequest('Invalid roster') + raise HTTPBadRequest( + title='Invalid roster' + ) team_id, roster_id = cursor.fetchone() cursor.execute('SELECT COUNT(*) FROM roster_user WHERE roster_id = %s', roster_id) @@ -78,4 +82,4 @@ def on_get(req, resp, team, roster, role): finally: cursor.close() connection.close() - resp.body = json_dumps({'user': candidate, 'data': ret}) + resp.text = json_dumps({'user': candidate, 'data': ret}) diff --git a/src/oncall/api/v0/roster_user.py b/src/oncall/api/v0/roster_user.py index 6415667b..ffc710cb 100644 --- a/src/oncall/api/v0/roster_user.py +++ b/src/oncall/api/v0/roster_user.py @@ -75,7 +75,7 @@ def on_delete(req, resp, team, roster, user): cursor.close() connection.close() resp.status = HTTP_200 - resp.body = '[]' + resp.text = '[]' @login_required @@ -110,7 +110,10 @@ def on_put(req, resp, team, roster, user): in_rotation = data.get('in_rotation') if in_rotation is None: - raise HTTPBadRequest('incomplete data', 'missing field "in_rotation"') + raise HTTPBadRequest( + title='incomplete data', + description='missing field "in_rotation"' + ) in_rotation = int(in_rotation) connection = db.connect() cursor = connection.cursor() @@ -130,4 +133,4 @@ def on_put(req, resp, team, roster, user): cursor.close() connection.close() resp.status = HTTP_200 - resp.body = '[]' + resp.text = '[]' diff --git a/src/oncall/api/v0/roster_users.py b/src/oncall/api/v0/roster_users.py index 57958f6c..df79eff4 100644 --- a/src/oncall/api/v0/roster_users.py +++ b/src/oncall/api/v0/roster_users.py @@ -49,7 +49,7 @@ def on_get(req, resp, team, roster): data = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required @@ -103,7 +103,10 @@ def on_post(req, resp, team, roster): user_name = data.get('name') in_rotation = int(data.get('in_rotation', True)) if not user_name: - raise HTTPBadRequest('incomplete data', 'missing field "name"') + raise HTTPBadRequest( + title='incomplete data', + description='missing field "name"' + ) check_team_auth(team, req) connection = db.connect() @@ -113,7 +116,11 @@ def on_post(req, resp, team, roster): (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') + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='invalid team or user' + ) # TODO: validate roster (team_id, user_id) = results @@ -154,12 +161,14 @@ def on_post(req, resp, team, roster): 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) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='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]) + resp.text = json_dumps(get_user_data(None, {'name': user_name})[0]) diff --git a/src/oncall/api/v0/rosters.py b/src/oncall/api/v0/rosters.py index 19d7c565..f998d10d 100644 --- a/src/oncall/api/v0/rosters.py +++ b/src/oncall/api/v0/rosters.py @@ -124,16 +124,18 @@ def on_get(req, resp, team): cursor.execute('SELECT `id` FROM `team` WHERE `name`=%s', team) if cursor.rowcount != 1: - raise HTTPError('422 Unprocessable Entity', - 'IntegrityError', - 'team "%s" not found' % team) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='team "%s" not found' % team + ) team_id = cursor.fetchone()['id'] rosters = get_roster_by_team_id(cursor, team_id, req.params) cursor.close() connection.close() - resp.body = json_dumps(rosters) + resp.text = json_dumps(rosters) @login_required @@ -168,11 +170,15 @@ def on_post(req, resp, team): roster_name = data.get('name') if not roster_name: - raise HTTPBadRequest('name attribute missing from request', '') + raise HTTPBadRequest( + title='name attribute missing from request', + description='' + ) 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()) + raise HTTPBadRequest( + title='invalid roster name', + description='roster name contains invalid character "%s"' % invalid_char.group()) check_team_auth(team, req) @@ -183,9 +189,11 @@ def on_post(req, resp, team): 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)) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='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() diff --git a/src/oncall/api/v0/schedule.py b/src/oncall/api/v0/schedule.py index 2fd20780..37659ecb 100644 --- a/src/oncall/api/v0/schedule.py +++ b/src/oncall/api/v0/schedule.py @@ -76,7 +76,7 @@ def on_get(req, resp, schedule_id): } """ - resp.body = json_dumps(get_schedules({'id': schedule_id}, fields=req.get_param_as_list('fields'))[0]) + resp.text = json_dumps(get_schedules({'id': schedule_id}, fields=req.get_param_as_list('fields'))[0]) @login_required @@ -119,7 +119,10 @@ def on_put(req, resp, schedule_id): data['scheduler'] = scheduler['name'] 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') + raise HTTPBadRequest( + title='Invalid edit', + description='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) @@ -142,7 +145,10 @@ def on_put(req, resp, 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') + raise HTTPBadRequest( + title='Invalid edit', + description='schedule cannot be represented in simple mode' + ) if cols: cursor.execute(update, data) diff --git a/src/oncall/api/v0/schedules.py b/src/oncall/api/v0/schedules.py index bf8ec28a..ef74e729 100644 --- a/src/oncall/api/v0/schedules.py +++ b/src/oncall/api/v0/schedules.py @@ -90,7 +90,10 @@ def get_schedules(filter_params, dbinfo=None, fields=None): if fields is None: fields = list(columns.keys()) if any(f not in columns for f in fields): - raise HTTPBadRequest('Bad fields', 'One or more invalid fields') + raise HTTPBadRequest( + title='Bad fields', + description='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: @@ -266,7 +269,7 @@ def on_get(req, resp, team, roster): params['roster'] = roster data = get_schedules(params, fields=fields) - resp.body = json_dumps(data) + resp.text = json_dumps(data) required_params = frozenset(['events', 'role', 'advanced_mode']) @@ -376,18 +379,28 @@ def on_post(req, resp, team, 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)) + raise HTTPBadRequest( + title='invalid schedule', + description='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') + raise HTTPBadRequest( + title='invalid schedule', + description='schedule event requires both start and duration fields' + ) if sev.get('start') is None: - raise HTTPBadRequest('invalid schedule', 'schedule event start cannot be null') + raise HTTPBadRequest( + title='invalid schedule', + description='schedule event start cannot be null' + ) if sev.get('duration') is None or sev['duration'] <= 0: - raise HTTPBadRequest('invalid schedule', 'schedule event duration must be positive') + raise HTTPBadRequest( + title='invalid schedule', + description='schedule event duration must be positive' + ) if 'auto_populate_threshold' not in data: # default to autopopulate 3 weeks forward @@ -402,7 +415,10 @@ def on_post(req, resp, team, roster): if not data['advanced_mode']: if not validate_simple_schedule(schedule_events): - raise HTTPBadRequest('invalid schedule', 'invalid advanced mode setting') + raise HTTPBadRequest( + title='invalid schedule', + description='invalid advanced mode setting' + ) insert_schedule = '''INSERT INTO `schedule` (`roster_id`,`team_id`,`role_id`, `auto_populate_threshold`, `advanced_mode`, `scheduler_id`) @@ -439,7 +455,11 @@ def on_post(req, resp, team, roster): 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) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) else: connection.commit() finally: @@ -447,4 +467,4 @@ def on_post(req, resp, team, roster): connection.close() resp.status = HTTP_201 - resp.body = json_dumps({'id': schedule_id}) + resp.text = json_dumps({'id': schedule_id}) diff --git a/src/oncall/api/v0/search.py b/src/oncall/api/v0/search.py index dc154b23..e8335fe8 100644 --- a/src/oncall/api/v0/search.py +++ b/src/oncall/api/v0/search.py @@ -118,4 +118,4 @@ def on_get(req, resp): cursor.close() connection.close() - resp.body = dumps(data) + resp.text = dumps(data) diff --git a/src/oncall/api/v0/service.py b/src/oncall/api/v0/service.py index 7f7f7a4a..c39ec830 100644 --- a/src/oncall/api/v0/service.py +++ b/src/oncall/api/v0/service.py @@ -43,7 +43,7 @@ def on_get(req, resp, service): [service] = results cursor.close() connection.close() - resp.body = dumps(service) + resp.text = dumps(service) @debug_only diff --git a/src/oncall/api/v0/service_oncall.py b/src/oncall/api/v0/service_oncall.py index d8bfb5d7..634bbb86 100644 --- a/src/oncall/api/v0/service_oncall.py +++ b/src/oncall/api/v0/service_oncall.py @@ -72,7 +72,7 @@ def on_get(req, resp, service, role=None): team_ids = [row['team_id'] for row in data] team_override_numbers = {row['name']: row['override_phone_number'] for row in data} if not team_ids: - resp.body = json_dumps([]) + resp.text = json_dumps([]) cursor.close() connection.close() return @@ -103,4 +103,4 @@ def on_get(req, resp, service, role=None): cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) diff --git a/src/oncall/api/v0/service_teams.py b/src/oncall/api/v0/service_teams.py index 15e2b1d0..469bfda4 100644 --- a/src/oncall/api/v0/service_teams.py +++ b/src/oncall/api/v0/service_teams.py @@ -37,4 +37,4 @@ def on_get(req, resp, service): data = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = dumps(data) + resp.text = dumps(data) diff --git a/src/oncall/api/v0/services.py b/src/oncall/api/v0/services.py index 32d25868..0a3f6927 100755 --- a/src/oncall/api/v0/services.py +++ b/src/oncall/api/v0/services.py @@ -79,7 +79,7 @@ def on_get(req, resp): data = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @debug_only @@ -92,9 +92,11 @@ def on_post(req, resp): cursor.execute('INSERT INTO `service` (`name`) VALUES (%(name)s)', data) connection.commit() except db.IntegrityError: - raise HTTPError('422 Unprocessable Entity', - 'IntegrityError', - 'service name "%(name)s" already exists' % data) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='service name "%(name)s" already exists' % data + ) finally: cursor.close() connection.close() diff --git a/src/oncall/api/v0/team.py b/src/oncall/api/v0/team.py index bcc4b162..e433f16c 100644 --- a/src/oncall/api/v0/team.py +++ b/src/oncall/api/v0/team.py @@ -167,7 +167,7 @@ def on_get(req, resp, team): cursor.close() connection.close() - resp.body = json_dumps(team_info) + resp.text = json_dumps(team_info) @login_required @@ -205,25 +205,42 @@ def on_put(req, resp, team): 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()) + raise HTTPBadRequest( + title='invalid team name', + description='team name contains invalid character "%s"' % invalid_char.group() + ) elif data['name'] == '': - raise HTTPBadRequest('invalid team name', 'empty team name') + raise HTTPBadRequest( + title='invalid team name', + description='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) + raise HTTPBadRequest( + title='invalid iris escalation plan', + description='no iris plan named %s exists' % iris_plan + ) if 'iris_enabled' in data: - if not type(data['iris_enabled']) == bool: - raise HTTPBadRequest('invalid payload', 'iris_enabled must be boolean') + if not isinstance(data['iris_enabled'], bool): + raise HTTPBadRequest( + title='invalid payload', + description='iris_enabled must be boolean' + ) if 'api_managed_roster' in data: - if not type(data['api_managed_roster']) == bool: - raise HTTPBadRequest('invalid payload', 'api_managed_roster must be boolean') + if not isinstance(data['api_managed_roster'], bool): + raise HTTPBadRequest( + title='invalid payload', + description='api_managed_roster must be boolean' + ) if 'scheduling_timezone' in data: if data['scheduling_timezone'] not in SUPPORTED_TIMEZONES: - raise HTTPBadRequest('invalid payload', 'requested scheduling_timezone is not supported. Supported timezones: %s' % str(SUPPORTED_TIMEZONES)) + raise HTTPBadRequest( + title='invalid payload', + description='requested scheduling_timezone is not supported. Supported timezones: %s' % str(SUPPORTED_TIMEZONES) + ) 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 if d in cols) + (team,) @@ -236,7 +253,11 @@ def on_put(req, resp, team): 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) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() diff --git a/src/oncall/api/v0/team_admins.py b/src/oncall/api/v0/team_admins.py index 33c48603..697720a1 100644 --- a/src/oncall/api/v0/team_admins.py +++ b/src/oncall/api/v0/team_admins.py @@ -46,7 +46,7 @@ def on_get(req, resp, team): data = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required @@ -96,7 +96,9 @@ def on_post(req, resp, team): user_name = data.get('name') if not user_name: - raise HTTPBadRequest('name attribute missing from request') + raise HTTPBadRequest( + title='name attribute missing from request' + ) connection = db.connect() cursor = connection.cursor() @@ -106,7 +108,11 @@ def on_post(req, resp, team): (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') + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='invalid team or user' + ) (team_id, user_id) = results try: @@ -127,10 +133,14 @@ def on_post(req, resp, team): 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) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() resp.status = HTTP_201 - resp.body = json_dumps(get_user_data(None, {'name': user_name})[0]) + resp.text = json_dumps(get_user_data(None, {'name': user_name})[0]) diff --git a/src/oncall/api/v0/team_changes.py b/src/oncall/api/v0/team_changes.py index 66c187ea..41467eb3 100644 --- a/src/oncall/api/v0/team_changes.py +++ b/src/oncall/api/v0/team_changes.py @@ -15,4 +15,4 @@ def on_get(req, resp, team): data = cursor.fetchall() cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) diff --git a/src/oncall/api/v0/team_ical.py b/src/oncall/api/v0/team_ical.py index 3407263d..60b80dde 100644 --- a/src/oncall/api/v0/team_ical.py +++ b/src/oncall/api/v0/team_ical.py @@ -84,5 +84,5 @@ def on_get(req, resp, team): include_sub = True events = get_team_events(team, start, roles=roles, include_subscribed=include_sub) - resp.body = ical.events_to_ical(events, team, contact) + resp.text = ical.events_to_ical(events, team, contact) resp.set_header('Content-Type', 'text/calendar') diff --git a/src/oncall/api/v0/team_iris_escalate.py b/src/oncall/api/v0/team_iris_escalate.py index 9f3793f6..f97b48d4 100644 --- a/src/oncall/api/v0/team_iris_escalate.py +++ b/src/oncall/api/v0/team_iris_escalate.py @@ -53,20 +53,28 @@ def on_post(req, resp, 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') + raise HTTPBadRequest( + title='Iris escalation failed', + description='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') + raise HTTPBadRequest( + title='Iris escalation failed', + description='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') + raise HTTPBadRequest( + title='Iris escalation failed', + description='Escalation cannot have an empty description' + ) try: if dynamic: plan_name = plan_settings['name'] @@ -82,6 +90,9 @@ def on_post(req, resp, team): 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) + raise HTTPBadRequest( + title='Iris escalation failed', + description='Iris client error: %s' % e + ) - resp.body = str(incident_id) + resp.text = str(incident_id) diff --git a/src/oncall/api/v0/team_oncall.py b/src/oncall/api/v0/team_oncall.py index 1ac2bccd..d7f86a6c 100644 --- a/src/oncall/api/v0/team_oncall.py +++ b/src/oncall/api/v0/team_oncall.py @@ -103,4 +103,4 @@ def on_get(req, resp, team, role=None): cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) diff --git a/src/oncall/api/v0/team_service.py b/src/oncall/api/v0/team_service.py index a5e26d3f..c15f95ff 100644 --- a/src/oncall/api/v0/team_service.py +++ b/src/oncall/api/v0/team_service.py @@ -44,7 +44,7 @@ def on_get(req, resp): data = [{'team': r[0], 'service': r[1]} for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required diff --git a/src/oncall/api/v0/team_services.py b/src/oncall/api/v0/team_services.py index bae501a2..60a493f3 100644 --- a/src/oncall/api/v0/team_services.py +++ b/src/oncall/api/v0/team_services.py @@ -44,7 +44,7 @@ def on_get(req, resp, team): data = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required @@ -91,9 +91,11 @@ def on_post(req, resp, team): WHERE `service`.`name` = %s''', service) claimed_team = [r[0] for r in cursor] if claimed_team: - raise HTTPError('422 Unprocessable Entity', - 'IntegrityError', - 'service "%s" already claimed by team "%s"' % (service, claimed_team[0])) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='service "%s" already claimed by team "%s"' % (service, claimed_team[0]) + ) cursor.execute('''INSERT INTO `team_service` (`team_id`, `service_id`) VALUES ( @@ -110,7 +112,11 @@ def on_post(req, resp, team): err_msg = 'team "%s" not found' % team elif 'Duplicate entry' in err_msg: err_msg = 'service name "%s" is already associated with team %s' % (service, team) - raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() diff --git a/src/oncall/api/v0/team_subscriptions.py b/src/oncall/api/v0/team_subscriptions.py index 9dc20b97..7fc08a89 100644 --- a/src/oncall/api/v0/team_subscriptions.py +++ b/src/oncall/api/v0/team_subscriptions.py @@ -23,7 +23,7 @@ def on_get(req, resp, team): data = [row for row in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required @@ -33,9 +33,15 @@ def on_post(req, resp, team): sub_name = data.get('subscription') role_name = data.get('role') if not sub_name or not role_name: - raise HTTPBadRequest('Invalid subscription', 'Missing subscription name or role name') + raise HTTPBadRequest( + title='Invalid subscription', + description='Missing subscription name or role name' + ) if sub_name == team: - raise HTTPBadRequest('Invalid subscription', 'Subscription team must be different from subscribing team') + raise HTTPBadRequest( + title='Invalid subscription', + description='Subscription team must be different from subscribing team' + ) connection = db.connect() cursor = connection.cursor() try: @@ -56,7 +62,11 @@ def on_post(req, resp, team): err_msg = 'Subscription already exists' else: logger.exception('Unknown integrity error in team_subscriptions') - raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) else: connection.commit() finally: diff --git a/src/oncall/api/v0/team_summary.py b/src/oncall/api/v0/team_summary.py index d1cc6a14..ed83c762 100644 --- a/src/oncall/api/v0/team_summary.py +++ b/src/oncall/api/v0/team_summary.py @@ -212,4 +212,4 @@ def on_get(req, resp, team): # No current primary events exist, do nothing pass - resp.body = dumps(payload) + resp.text = dumps(payload) diff --git a/src/oncall/api/v0/team_user.py b/src/oncall/api/v0/team_user.py index 4a96745d..bbe9ab87 100644 --- a/src/oncall/api/v0/team_user.py +++ b/src/oncall/api/v0/team_user.py @@ -44,7 +44,7 @@ def on_get(req, resp): data = [{'team': r[0], 'user': r[1]} for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required diff --git a/src/oncall/api/v0/team_users.py b/src/oncall/api/v0/team_users.py index 26116b82..d6361ac5 100644 --- a/src/oncall/api/v0/team_users.py +++ b/src/oncall/api/v0/team_users.py @@ -52,7 +52,7 @@ def on_get(req, resp, team): data = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required @@ -65,7 +65,11 @@ def on_post(req, resp, team): user_name = data.get('name') if not user_name: - raise HTTPError('422 Unprocessable Entity', 'IntegrityError', 'name missing for user') + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='name missing for user' + ) connection = db.connect() cursor = connection.cursor() @@ -85,10 +89,14 @@ def on_post(req, resp, team): err_msg = 'team %s not found' % team elif 'Duplicate entry' in err_msg: err_msg = 'user name "%s" is already in team %s' % (user_name, team) - raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() resp.status = HTTP_201 - resp.body = json_dumps(get_user_data(None, {'name': user_name})[0]) + resp.text = json_dumps(get_user_data(None, {'name': user_name})[0]) diff --git a/src/oncall/api/v0/teams.py b/src/oncall/api/v0/teams.py index a5c6a390..2496b871 100755 --- a/src/oncall/api/v0/teams.py +++ b/src/oncall/api/v0/teams.py @@ -104,7 +104,7 @@ def on_get(req, resp): data = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(data) + resp.text = json_dumps(data) @login_required @@ -157,24 +157,36 @@ def on_post(req, resp): data = load_json_body(req) if not data.get('name'): - raise HTTPBadRequest('', 'name attribute missing from request') + raise HTTPBadRequest( + title='', + description='name attribute missing from request' + ) if not data.get('scheduling_timezone'): - raise HTTPBadRequest('', 'scheduling_timezone attribute missing from request') + raise HTTPBadRequest( + title='', + description='scheduling_timezone attribute missing from request' + ) team_name = unquote(data['name']).strip() 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()) + raise HTTPBadRequest( + title='invalid team name', + description='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 #') + raise HTTPBadRequest( + title='invalid slack channel', + description='slack channel name needs to start with #' + ) slack_notifications = data.get('slack_channel_notifications') if slack_notifications and slack_notifications[0] != '#': - raise HTTPBadRequest('invalid slack notifications channel', - 'slack channel notifications name needs to start with #') + raise HTTPBadRequest( + title='invalid slack notifications channel', + description='slack channel notifications name needs to start with #' + ) email = data.get('email') description = data.get('description') iris_plan = data.get('iris_plan') @@ -187,18 +199,27 @@ def on_post(req, resp): 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) + raise HTTPBadRequest( + title='invalid iris escalation plan', + description='no iris plan named %s exists' % iris_plan + ) connection = db.connect() cursor = connection.cursor() # if team creation request is coming from api use the username from the admin field in lieu of the user context var if 'user' not in req.context: if not data.get('admin'): - raise HTTPBadRequest('invalid admin', 'API requests must specify a team admin username in the admin field') + raise HTTPBadRequest( + title='invalid admin', + description='API requests must specify a team admin username in the admin field' + ) user = data.get('admin') cursor.execute('''SELECT `id` FROM `user` WHERE `name` = %s LIMIT 1''', (user, )) if cursor.rowcount == 0: - raise HTTPBadRequest('invalid admin', 'admin username %s was not found in db' % user) + raise HTTPBadRequest( + title='invalid admin', + description='admin username %s was not found in db' % user + ) req.context['user'] = user try: @@ -220,9 +241,11 @@ def on_post(req, resp): create_audit({'team_id': team_id}, team_name, TEAM_CREATED, req, cursor) connection.commit() except db.IntegrityError: - raise HTTPError('422 Unprocessable Entity', - 'IntegrityError', - 'team name "%s" already exists' % team_name) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='team name "%s" already exists' % team_name + ) finally: cursor.close() connection.close() diff --git a/src/oncall/api/v0/timezones.py b/src/oncall/api/v0/timezones.py index f4b514c1..301c569f 100644 --- a/src/oncall/api/v0/timezones.py +++ b/src/oncall/api/v0/timezones.py @@ -6,4 +6,4 @@ def on_get(req, resp): - resp.body = json_dumps(SUPPORTED_TIMEZONES) + resp.text = json_dumps(SUPPORTED_TIMEZONES) diff --git a/src/oncall/api/v0/upcoming_shifts.py b/src/oncall/api/v0/upcoming_shifts.py index 7788c871..decb0181 100644 --- a/src/oncall/api/v0/upcoming_shifts.py +++ b/src/oncall/api/v0/upcoming_shifts.py @@ -84,4 +84,4 @@ def on_get(req, resp, user_name): formatted = sorted(formatted, key=operator.itemgetter('start')) if limit is not None: formatted = formatted[:limit] - resp.body = json_dumps(formatted) \ No newline at end of file + resp.text = json_dumps(formatted) \ No newline at end of file diff --git a/src/oncall/api/v0/user.py b/src/oncall/api/v0/user.py index 21acd280..54c169f5 100644 --- a/src/oncall/api/v0/user.py +++ b/src/oncall/api/v0/user.py @@ -72,7 +72,7 @@ def on_get(req, resp, user_name): data = get_user_data(req.get_param_as_list('fields'), req.params) if not data: raise HTTPNotFound() - resp.body = json_dumps(data[0]) + resp.text = json_dumps(data[0]) @login_required @@ -165,7 +165,10 @@ def on_put(req, resp, user_name): if cursor.rowcount != 1: cursor.close() connection.close() - raise HTTPBadRequest('No User Found', 'no user exists with given name') + raise HTTPBadRequest( + title='No User Found', + description='no user exists with given name' + ) if set_contacts: contacts = [] diff --git a/src/oncall/api/v0/user_ical.py b/src/oncall/api/v0/user_ical.py index e06f7843..833a518d 100644 --- a/src/oncall/api/v0/user_ical.py +++ b/src/oncall/api/v0/user_ical.py @@ -77,5 +77,5 @@ def on_get(req, resp, user_name): excluded_teams = req.get_param_as_list('excludedTeams') events = get_user_events(user_name, start, roles=roles, excluded_teams=excluded_teams) - resp.body = ical.events_to_ical(events, user_name, contact) + resp.text = ical.events_to_ical(events, user_name, contact) resp.set_header('Content-Type', 'text/calendar') diff --git a/src/oncall/api/v0/user_notification.py b/src/oncall/api/v0/user_notification.py index 34f4273f..c74a4299 100644 --- a/src/oncall/api/v0/user_notification.py +++ b/src/oncall/api/v0/user_notification.py @@ -102,11 +102,15 @@ def on_put(req, resp, notification_id): 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') + raise HTTPBadRequest( + title='invalid setting update', + description='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') + raise HTTPBadRequest( + title='invalid setting update', + description='notification setting must define only only_if_involved' + ) if cols: cursor.execute('SELECT `user`.`name` FROM `notification_setting` ' diff --git a/src/oncall/api/v0/user_notifications.py b/src/oncall/api/v0/user_notifications.py index 44623cbe..69fe5e78 100644 --- a/src/oncall/api/v0/user_notifications.py +++ b/src/oncall/api/v0/user_notifications.py @@ -88,7 +88,7 @@ def on_get(req, resp, user_name): cursor.close() connection.close() - resp.body = json_dumps(list(data.values())) + resp.text = json_dumps(list(data.values())) @login_required @@ -162,8 +162,10 @@ def on_post(req, resp, user_name): params = set(data.keys()) missing_params = required_params - params if missing_params: - raise HTTPBadRequest('invalid notification setting', - 'missing required parameters: %s' % ', '.join(missing_params)) + raise HTTPBadRequest( + title='invalid notification setting', + description='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']) @@ -173,20 +175,28 @@ def on_post(req, resp, user_name): # 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']) + raise HTTPBadRequest( + title='invalid notification setting', + description='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) + raise HTTPBadRequest( + title='invalid notification setting', + description='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') + raise HTTPBadRequest( + title='invalid notification setting', + description='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') + raise HTTPBadRequest( + title='invalid notification setting', + description='notification setting must define only_if_involved' + ) roles = data.pop('roles') data['user'] = user_name @@ -200,7 +210,10 @@ def on_post(req, resp, user_name): cursor.execute(query, data) if cursor.rowcount != 1: - raise HTTPBadRequest('invalid request', 'unable to create notification with provided settings') + raise HTTPBadRequest( + title='invalid request', + description='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)) @@ -208,9 +221,12 @@ def on_post(req, resp, user_name): 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') + raise HTTPBadRequest( + title='invalid request', + description='unable to create notification: invalid roles' + ) connection.commit() cursor.close() connection.close() - resp.body = json_dumps({'id': setting_id}) + resp.text = json_dumps({'id': setting_id}) resp.status = HTTP_201 diff --git a/src/oncall/api/v0/user_pinned_teams.py b/src/oncall/api/v0/user_pinned_teams.py index e5ebfcb6..23ea0b54 100644 --- a/src/oncall/api/v0/user_pinned_teams.py +++ b/src/oncall/api/v0/user_pinned_teams.py @@ -39,7 +39,7 @@ def on_get(req, resp, user_name): teams = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = json_dumps(teams) + resp.text = json_dumps(teams) @login_required @@ -65,7 +65,10 @@ def on_post(req, resp, user_name): data = load_json_body(req) team = data.get('team') if team is None: - raise HTTPBadRequest('Invalid team pin', 'Missing team parameter') + raise HTTPBadRequest( + title='Invalid team pin', + description='Missing team parameter' + ) connection = db.connect() cursor = connection.cursor() try: @@ -77,7 +80,10 @@ def on_post(req, resp, user_name): except db.IntegrityError as e: # Duplicate key if e.args[0] == 1062: - raise HTTPBadRequest('Invalid team pin', 'Team already pinned for this user') + raise HTTPBadRequest( + title='Invalid team pin', + description='Team already pinned for this user' + ) # Team/user is null elif e.args[0] == 1048: err_msg = str(e.args[1]) @@ -85,7 +91,11 @@ def on_post(req, resp, user_name): err_msg = 'user "%s" not found' % user_name elif err_msg == 'Column \'team_id\' cannot be null': err_msg = 'team "%s" not found' % team - raise HTTPError('422 Unprocessable Entity', 'IntegrityError', err_msg) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description=err_msg + ) finally: cursor.close() connection.close() diff --git a/src/oncall/api/v0/user_teams.py b/src/oncall/api/v0/user_teams.py index 16a55ce4..42640458 100644 --- a/src/oncall/api/v0/user_teams.py +++ b/src/oncall/api/v0/user_teams.py @@ -42,4 +42,4 @@ def on_get(req, resp, user_name): data = [r[0] for r in cursor] cursor.close() connection.close() - resp.body = dumps(data) + resp.text = dumps(data) diff --git a/src/oncall/api/v0/users.py b/src/oncall/api/v0/users.py index d620b805..53eca89e 100644 --- a/src/oncall/api/v0/users.py +++ b/src/oncall/api/v0/users.py @@ -61,7 +61,10 @@ def get_user_data(fields, filter_params, dbinfo=None): contacts = True if any(f not in columns for f in fields): - raise HTTPBadRequest('Bad fields', 'One or more invalid fields') + raise HTTPBadRequest( + title='Bad fields', + description='One or more invalid fields' + ) fields = map(columns.__getitem__, fields) cols = ', '.join(fields) @@ -166,7 +169,7 @@ def on_get(req, resp): ] """ - resp.body = json_dumps(get_user_data(req.get_param_as_list('fields'), req.params)) + resp.text = json_dumps(get_user_data(req.get_param_as_list('fields'), req.params)) @auth.debug_only @@ -181,9 +184,11 @@ def on_post(req, resp): cursor.execute('INSERT INTO `user` (`name`) VALUES (%(name)s)', data) connection.commit() except db.IntegrityError: - raise HTTPError('422 Unprocessable Entity', - 'IntegrityError', - 'user name "%(name)s" already exists' % data) + raise HTTPError( + '422 Unprocessable Entity', + title='IntegrityError', + description='user name "%(name)s" already exists' % data + ) finally: cursor.close() connection.close() diff --git a/src/oncall/app.py b/src/oncall/app.py index 370fcf49..4177708e 100644 --- a/src/oncall/app.py +++ b/src/oncall/app.py @@ -23,7 +23,7 @@ def json_error_serializer(req, resp, exception): - resp.body = exception.to_json() + resp.text = exception.to_json() resp.content_type = 'application/json' diff --git a/src/oncall/auth/__init__.py b/src/oncall/auth/__init__.py index 19924d6c..826cf0fe 100644 --- a/src/oncall/auth/__init__.py +++ b/src/oncall/auth/__init__.py @@ -18,7 +18,10 @@ def debug_only(function): def wrapper(*args, **kwargs): - raise HTTPForbidden('', 'Admin only action') + raise HTTPForbidden( + title='', + description='Admin only action' + ) return wrapper @@ -60,7 +63,10 @@ def check_user_auth(user, req): connection.close() if user_in_query != 0 or is_god(challenger): return - raise HTTPForbidden('Unauthorized', 'Action not allowed for "%s"' % challenger) + raise HTTPForbidden( + title='Unauthorized', + description='Action not allowed for "%s"' % challenger + ) def check_team_auth(team, req): @@ -85,8 +91,9 @@ def check_team_auth(team, req): if team_in_query != 0 or is_god(challenger): return raise HTTPForbidden( - 'Unauthorized', - 'Action not allowed: "%s" is not an admin for "%s"' % (challenger, team)) + title='Unauthorized', + description='Action not allowed: "%s" is not an admin for "%s"' % (challenger, team) + ) def check_calendar_auth(team, req, user=None): @@ -105,7 +112,10 @@ def check_calendar_auth(team, req, user=None): connection.close() if user_in_team != 0 or is_god(challenger): return - raise HTTPForbidden('Unauthorized', 'Action not allowed: "%s" is not part of "%s"' % (challenger, team)) + raise HTTPForbidden( + title='Unauthorized', + description='Action not allowed: "%s" is not part of "%s"' % (challenger, team) + ) def check_calendar_auth_by_id(team_id, req): @@ -125,7 +135,10 @@ def check_calendar_auth_by_id(team_id, req): connection.close() if user_in_team != 0 or is_god(challenger): return - raise HTTPForbidden('Unauthorized', 'Action not allowed: "%s" is not a team member' % (challenger)) + raise HTTPForbidden( + title='Unauthorized', + description='Action not allowed: "%s" is not a team member' % (challenger) + ) def is_client_digest_valid(client_digest, api_key, window, method, path, body): @@ -145,7 +158,11 @@ def is_client_digest_valid(client_digest, api_key, window, method, path, body): def authenticate_application(auth_token, req): if not auth_token.startswith('hmac '): - raise HTTPUnauthorized('Authentication failure', 'Invalid digest format', '') + raise HTTPUnauthorized( + title='Authentication failure', + description='Invalid digest format', + challenges=[] + ) method = req.method path = req.env['PATH_INFO'] qs = req.env['QUERY_STRING'] @@ -176,14 +193,26 @@ def authenticate_application(auth_token, req): req.context['app'] = app_name return else: - raise HTTPUnauthorized('Authentication failure', 'Wrong digest', '') + raise HTTPUnauthorized( + title='Authentication failure', + description='Wrong digest', + challenges=[] + ) else: cursor.close() connection.close() - raise HTTPUnauthorized('Authentication failure', 'Application not found', '') + raise HTTPUnauthorized( + title='Authentication failure', + description='Application not found', + challenges=[] + ) except (ValueError, KeyError): - raise HTTPUnauthorized('Authentication failure', 'Wrong digest', '') + raise HTTPUnauthorized( + title='Authentication failure', + description='Wrong digest', + challenges=[] + ) def _authenticate_user(req): @@ -206,18 +235,30 @@ def _authenticate_user(req): if cursor.rowcount != 1: cursor.close() connection.close() - raise HTTPUnauthorized('Invalid Session', 'CSRF token missing', '') + raise HTTPUnauthorized( + title='Invalid Session', + description='CSRF token missing', + challenges=[] + ) token = cursor.fetchone()[0] if req.get_header('X-CSRF-TOKEN') != token: cursor.close() connection.close() - raise HTTPUnauthorized('Invalid Session', 'CSRF validation failed', '') + raise HTTPUnauthorized( + title='Invalid Session', + description='CSRF validation failed', + challenges=[] + ) cursor.close() connection.close() except KeyError: - raise HTTPUnauthorized('Unauthorized', 'User must be logged in', '') + raise HTTPUnauthorized( + title='Unauthorized', + description='User must be logged in', + challenges=[] + ) authenticate_user = _authenticate_user diff --git a/src/oncall/auth/login.py b/src/oncall/auth/login.py index 4708bbe2..f13da77b 100644 --- a/src/oncall/auth/login.py +++ b/src/oncall/auth/login.py @@ -18,10 +18,17 @@ def on_post(req, resp): user = login_info.get('username') password = login_info.get('password') if user is None or password is None: - raise HTTPBadRequest('Invalid login attempt', 'Missing user/password') + raise HTTPBadRequest( + title='Invalid login attempt', + description='Missing user/password' + ) if not auth_manager.authenticate(user, password): - raise HTTPUnauthorized('Authentication failure', 'bad login credentials', '') + raise HTTPUnauthorized( + title='Authentication failure', + description='bad login credentials', + challenges=[] + ) connection = db.connect() cursor = connection.cursor(db.DictCursor) @@ -39,11 +46,14 @@ def on_post(req, resp): cursor.execute('INSERT INTO `session` (`id`, `csrf_token`) VALUES (%s, %s)', (req.env['beaker.session']['_id'], csrf_token)) except db.IntegrityError: - raise HTTPBadRequest('Invalid login attempt', 'User already logged in') + raise HTTPBadRequest( + title='Invalid login attempt', + description='User already logged in' + ) connection.commit() cursor.close() connection.close() # TODO: purge out of date csrf token data[0]['csrf_token'] = csrf_token - resp.body = dumps(data[0]) + resp.text = dumps(data[0]) diff --git a/src/oncall/healthcheck.py b/src/oncall/healthcheck.py index fc4bcc6c..4d522031 100644 --- a/src/oncall/healthcheck.py +++ b/src/oncall/healthcheck.py @@ -41,7 +41,7 @@ def on_get(self, req, resp): logger.error('could not open healthcheck file') raise HTTPNotFound() resp.content_type = 'text/plain' - resp.body = status + resp.text = status def init(application, config): diff --git a/src/oncall/scheduler/default.py b/src/oncall/scheduler/default.py index 413c8abb..ebe55bb8 100644 --- a/src/oncall/scheduler/default.py +++ b/src/oncall/scheduler/default.py @@ -415,7 +415,10 @@ def populate(self, schedule, start_time, dbinfo, table_name='event'): if handoff < utc.localize(datetime.utcnow()): cursor.execute("DROP TEMPORARY TABLE IF EXISTS `temp_event`") connection.commit() - raise HTTPBadRequest('Invalid populate/preview request', 'cannot populate/preview starting in the past') + raise HTTPBadRequest( + title='Invalid populate/preview request', + description='cannot populate/preview starting in the past' + ) future_events, last_epoch = self.calculate_future_events(schedule, cursor, start_epoch) self.set_last_epoch(schedule['id'], last_epoch, cursor) diff --git a/src/oncall/ui/__init__.py b/src/oncall/ui/__init__.py index b9c60bd3..7418f436 100644 --- a/src/oncall/ui/__init__.py +++ b/src/oncall/ui/__init__.py @@ -84,10 +84,10 @@ def index(req, resp): user = req.env.get('beaker.session', {}).get('user') if user is None and LOGIN_REQUIRED: resp.content_type = 'text/html' - resp.body = jinja2_env.get_template('loginsplash.html').render() + resp.text = jinja2_env.get_template('loginsplash.html').render() else: resp.content_type = 'text/html' - resp.body = jinja2_env.get_template('index.html').render( + resp.text = jinja2_env.get_template('index.html').render( user=user, slack_instance=SLACK_INSTANCE, user_setting_note=INDEX_CONTENT_SETTING['user_setting_note'], diff --git a/src/oncall/utils.py b/src/oncall/utils.py index a0b695cf..ad252ce4 100644 --- a/src/oncall/utils.py +++ b/src/oncall/utils.py @@ -120,7 +120,10 @@ 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)) + raise HTTPBadRequest( + title='invalid JSON', + description='failed to decode json: %s' % str(e) + ) def import_custom_module(default_root, module):