From 651ca21fd29252e3586d54b2f13a9537f4b97e9c Mon Sep 17 00:00:00 2001 From: Colin Yang Date: Fri, 6 Mar 2020 13:55:45 -0800 Subject: [PATCH] CRUD operations for ical_key (#299) * CRUD operations for ical_key All HTTP endpoints around ical_key should be authenticated so that we can easily track what keys are created by this logged-in user. * add missing semicolon * store unix_timestamp as BIGINT --- db/schema.v0.sql | 13 +++++ src/oncall/api/v0/__init__.py | 4 ++ src/oncall/api/v0/ical_key.py | 64 +++++++++++++++++++++++ src/oncall/api/v0/ical_key_team.py | 69 +++++++++++++++++++++++++ src/oncall/api/v0/ical_key_user.py | 83 ++++++++++++++++++++++++++++++ 5 files changed, 233 insertions(+) create mode 100644 src/oncall/api/v0/ical_key.py create mode 100644 src/oncall/api/v0/ical_key_team.py create mode 100644 src/oncall/api/v0/ical_key_user.py diff --git a/db/schema.v0.sql b/db/schema.v0.sql index 82ee278..5a1d6aa 100644 --- a/db/schema.v0.sql +++ b/db/schema.v0.sql @@ -434,6 +434,19 @@ CREATE TABLE IF NOT EXISTS `application` ( PRIMARY KEY (`id`) ); +-- ----------------------------------------------------- +-- Table `ical_key` +-- ----------------------------------------------------- +CREATE TABLE IF NOT EXISTS `ical_key` ( + `key` CHAR(36) CHARACTER SET ascii NOT NULL, + `requester` CHAR(255) NOT NULL, + `name` CHAR(255) NOT NULL, + `type` ENUM('team', 'user') NOT NULL, + `time_created` BIGINT(20) NOT NULL, + PRIMARY KEY (`requester`, `name`, `type`), + INDEX `key_idx` (`KEY`) +); + CREATE TABLE IF NOT EXISTS `team_subscription` ( `team_id` BIGINT(20) UNSIGNED NOT NULL, `subscription_id` BIGINT(20) UNSIGNED NOT NULL, diff --git a/src/oncall/api/v0/__init__.py b/src/oncall/api/v0/__init__.py index 54b75a6..c83bbf7 100644 --- a/src/oncall/api/v0/__init__.py +++ b/src/oncall/api/v0/__init__.py @@ -102,6 +102,10 @@ def init(application, config): application.add_route('/api/v0/users/{user_name}/ical', user_ical) application.add_route('/api/v0/teams/{team}/ical', team_ical) + from . import ical_key_user, ical_key_team + application.add_route('/api/v0/ical_key/user/{user_name}', ical_key_user) + application.add_route('/api/v0/ical_key/team/{team}', ical_key_team) + # Optional Iris integration from . import iris_settings application.add_route('/api/v0/iris_settings', iris_settings) diff --git a/src/oncall/api/v0/ical_key.py b/src/oncall/api/v0/ical_key.py new file mode 100644 index 0000000..4112771 --- /dev/null +++ b/src/oncall/api/v0/ical_key.py @@ -0,0 +1,64 @@ +# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. +# See LICENSE in the project root for license information. + +from ... import db + + +def get_ical_key(requester, name, type): + connection = db.connect() + cursor = connection.cursor() + + cursor.execute( + ''' + SELECT `key` + FROM `ical_key` + WHERE + `requester` = %s AND + `name` = %s AND + `type` = %s + ''', + (requester, name, type)) + if cursor.rowcount == 0: + key = None + else: + key = cursor.fetchone()[0] + + cursor.close() + connection.close() + return key + + +def update_ical_key(requester, name, type, key): + connection = db.connect() + cursor = connection.cursor() + + cursor.execute( + ''' + INSERT INTO `ical_key` (`key`, `requester`, `name`, `type`, `time_created`) + VALUES (%s, %s, %s, %s, UNIX_TIMESTAMP()) + ON DUPLICATE KEY UPDATE `key` = %s, `time_created` = UNIX_TIMESTAMP() + ''', + (key, requester, name, type, key)) + connection.commit() + + cursor.close() + connection.close() + + +def delete_ical_key(requester, name, type): + connection = db.connect() + cursor = connection.cursor() + + cursor.execute( + ''' + DELETE FROM `ical_key` + WHERE + `requester` = %s AND + `name` = %s AND + `type` = %s + ''', + (requester, name, type)) + connection.commit() + + cursor.close() + connection.close() diff --git a/src/oncall/api/v0/ical_key_team.py b/src/oncall/api/v0/ical_key_team.py new file mode 100644 index 0000000..2299df5 --- /dev/null +++ b/src/oncall/api/v0/ical_key_team.py @@ -0,0 +1,69 @@ +# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. +# See LICENSE in the project root for license information. + +import uuid + +from falcon import HTTPNotFound, HTTP_201 + +from ...auth import login_required, check_calendar_auth +from .ical_key import get_ical_key, update_ical_key, delete_ical_key + + +@login_required +def on_get(req, resp, team): + """Get the secret key that grants public access to team's oncall + calendar for the logged-in user. + + Current policy only allows access to the team that the logged-in + user is part of. + + **Example request:** + + .. sourcecode:: http + + GET /api/v0/ical_key/team/jteam HTTP/1.1 + Content-Type: text/plain + + ef895425-5f49-11ea-8eee-10e7c6352aff + + """ + challenger = req.context['user'] + check_calendar_auth(team, req) + + key = get_ical_key(challenger, team, 'team') + if key is None: + raise HTTPNotFound() + + resp.body = key + resp.set_header('Content-Type', 'text/plain') + + +@login_required +def on_post(req, resp, team): + """Update or create the secret key that grants public access to team's + oncall calendar for the logged-in user. + + Current policy only allows access to the team that the logged-in + user is part of. + + """ + challenger = req.context['user'] + check_calendar_auth(team, req) + + update_ical_key(challenger, team, 'team', str(uuid.uuid4())) + resp.status = HTTP_201 + + +@login_required +def on_delete(req, resp, team): + """Delete the secret key that grants public access to team's oncall + calendar for the logged-in user. + + Current policy only allows access to the team that the logged-in + user is part of. + + """ + challenger = req.context['user'] + check_calendar_auth(team, req) + + delete_ical_key(challenger, team, 'team') diff --git a/src/oncall/api/v0/ical_key_user.py b/src/oncall/api/v0/ical_key_user.py new file mode 100644 index 0000000..541f703 --- /dev/null +++ b/src/oncall/api/v0/ical_key_user.py @@ -0,0 +1,83 @@ +# Copyright (c) LinkedIn Corporation. All rights reserved. Licensed under the BSD-2 Clause license. +# See LICENSE in the project root for license information. + +import uuid + +from falcon import HTTPNotFound, HTTPForbidden, HTTP_201 + +from ...auth import login_required +from .ical_key import get_ical_key, update_ical_key, delete_ical_key + + +@login_required +def on_get(req, resp, user_name): + """Get the secret key that grants public access to user_name's oncall + calendar for the logged-in user. + + Current policy only allows the logged-in user to get its own key, + so user_name parameter must be the same as the logged-in user. + + **Example request:** + + .. sourcecode:: http + + GET /api/v0/ical_key/user/jdoe HTTP/1.1 + Content-Type: text/plain + + ef895425-5f49-11ea-8eee-10e7c6352aff + + """ + 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) + ) + + key = get_ical_key(challenger, user_name, 'user') + if key is None: + raise HTTPNotFound() + + resp.body = key + resp.set_header('Content-Type', 'text/plain') + + +@login_required +def on_post(req, resp, user_name): + """Update or create the secret key that grants public access to + user_name's oncall calendar for the logged-in user. Updating the + secret key will automatically invalidate existing secret keys. A + subsequent GET will get the secret key. + + Current policy only allows the logged-in user to get its own key, + so user_name parameter must be the same as the logged-in user. + + """ + 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) + ) + + update_ical_key(challenger, user_name, 'user', str(uuid.uuid4())) + resp.status = HTTP_201 + + +@login_required +def on_delete(req, resp, user_name): + """Delete the secret key that grants public access to user_name's + oncall calendar for the logged-in user. + + Current policy only allows the logged-in user to get its own key, + so user_name parameter must be the same as the logged-in user. + + """ + 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) + ) + + delete_ical_key(challenger, user_name, 'user')