diff --git a/e2e/test_audit.py b/e2e/test_audit.py index 541ebb9..873b9e8 100644 --- a/e2e/test_audit.py +++ b/e2e/test_audit.py @@ -8,16 +8,13 @@ from oncall.constants import (EVENT_CREATED, EVENT_EDITED, EVENT_SWAPPED, EVENT_ TEAM_CREATED, TEAM_EDITED, TEAM_DELETED, ROSTER_EDITED, ROSTER_USER_ADDED, ROSTER_CREATED, ROSTER_USER_EDITED, ROSTER_USER_DELETED, ROSTER_DELETED, ADMIN_DELETED, ADMIN_CREATED) -from oncall import db def get_audit_log(start, end): - connection = db.connect() - cursor = connection.cursor() - cursor.execute('SELECT action_name FROM audit WHERE timestamp BETWEEN %s AND %s', (int(start), int(end) + 1)) - ret = {row[0] for row in cursor} - cursor.close() - connection.close() - return ret + re = requests.get(api_v0('audit?start=%s&end=%s&' % (start, end))) + assert re.status_code == 200 + data = re.json() + actions = set(audit['action'] for audit in data) + return actions @prefix('test_audit') @@ -28,7 +25,7 @@ def test_audit(team, user, role, roster, event): team_name = team.create() # test team actions - start = time.time() + start = int(time.time()) team_name_2 = team.create() requests.put(api_v0('teams/'+team_name_2), json={'email': 'foo', 'slack_channel': 'bar'}) requests.delete(api_v0('teams/%s' % team_name_2)) @@ -37,7 +34,7 @@ def test_audit(team, user, role, roster, event): assert {TEAM_CREATED, TEAM_DELETED, TEAM_EDITED} <= audit # test roster actions - start = time.time() + start = int(time.time()) roster_name = roster.create(team_name) requests.put(api_v0('teams/%s/rosters/%s' % (team_name, roster_name)), json={'name': 'foo'}) requests.put(api_v0('teams/%s/rosters/foo' % team_name), json={'name': roster_name}) @@ -55,7 +52,7 @@ def test_audit(team, user, role, roster, event): ROSTER_EDITED, ROSTER_USER_EDITED} <= audit # test event actions - start = time.time() + start = int(time.time()) ev_start, ev_end = int(time.time()) + 100, int(time.time()) + 36000 user.add_to_team(user_name, team_name) user.add_to_team(user_name_2, team_name) @@ -94,7 +91,7 @@ def test_audit(team, user, role, roster, event): <= audit # add/delete admin - start = time.time() + start = int(time.time()) requests.post(api_v0('teams/%s/admins' % team_name), json={'name': user_name}) requests.delete(api_v0('teams/%s/admins/%s' % (team_name, user_name))) end = time.time() diff --git a/src/oncall/api/v0/__init__.py b/src/oncall/api/v0/__init__.py index 6e442f1..372faeb 100644 --- a/src/oncall/api/v0/__init__.py +++ b/src/oncall/api/v0/__init__.py @@ -74,6 +74,9 @@ def init(application, config): from . import search application.add_route('/api/v0/search', search) + from . import audit + application.add_route('/api/v0/audit', audit) + from . import upcoming_shifts application.add_route('/api/v0/users/{user_name}/upcoming', upcoming_shifts) diff --git a/src/oncall/api/v0/audit.py b/src/oncall/api/v0/audit.py new file mode 100644 index 0000000..fee4521 --- /dev/null +++ b/src/oncall/api/v0/audit.py @@ -0,0 +1,89 @@ +# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. +# See LICENSE in the project root for license information. + +from ujson import dumps as json_dumps +from ... import db + + +filters = {'owner': '`owner_name` = %(owner)s', + 'team': '`team_name` = %(team)s', + 'action': '`action_name` IN %(action)s', + 'start': '`timestamp` >= %(start)s', + 'end': '`timestamp` <= %(end)s'} + + +def on_get(req, resp): + ''' + Search audit log. Allows filtering based on a number of parameters, + detailed below. Returns an entry in the audit log, including the name + of the associated team, action owner, and action type, as well as a + timestamp and the action context. The context tracks different data + based on the action, which may be useful in investigating. + Audit logs are tracked for the following actions: + + * admin_created + * event_created + * event_edited + * roster_created + * roster_edited + * roster_user_added + * roster_user_deleted + * team_created + * team_edited + * event_deleted + * event_swapped + * roster_user_edited + * team_deleted + * admin_deleted + * roster_deleted + * event_substituted + + + **Example request**: + + .. sourcecode:: http + + GET /api/v0/audit?team=foo-sre&end=1487466146&action=event_created HTTP/1.1 + Host: example.com + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Content-Type: application/json + + [ + { + "context":"{"new_event_id":441072,"request_body":{"start":1518422400,"end":1518595200,"role":"primary","user":jdoe","team":"foo-sre"}}" + "timestamp": 1488441600, + "team_name": "foo-sre", + "owner_name": "jdoe" + "action_name: "event_created" + } + ] + + :query team_name: team name + :query owner_name: action owner name + :query action_name: name of action taken. If provided multiple action names, + :query id: id of the event + :query start: lower bound for audit entry's timestamp (unix timestamp) + :query end: upper bound for audit entry's timestamp (unix timestamp) + ''' + connection = db.connect() + cursor = connection.cursor(db.DictCursor) + if 'action' in req.params: + req.params['action'] = req.get_param_as_list('action') + + query = '''SELECT `owner_name` AS `owner`, `team_name` AS `team`, + `action_name` AS `action`, `timestamp`, `context` + FROM `audit`''' + where = ' AND '.join(filters[field] for field in req.params if field in filters) + if where: + query = '%s WHERE %s' % (query, where) + + cursor.execute(query, req.params) + results = cursor.fetchall() + cursor.close() + connection.close() + resp.body = json_dumps(results) \ No newline at end of file