diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..908de56 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log +All notable changes to this project will be documented in this file. + + +## [2.0.0] - 2023-06-06 +WARNING: this version adds a change to the MYSQL schema! Make changes to the schema before deploying new 2.0.0 version. + +### Added + - MINOR added the ability to designate teams as "api managed" which will prevent changes to team info from being done via the UI +### Changed + - MAJOR added the `api_managed_roster` column to the `team` table in the MYSQL schema. Before running 2.0.0 the MYSQL schema must be updated with the new column to avoid errors, to do so run `mysql -u root -p oncall < ./db/schema-update.v2.0.0_2023-06-06.sql` + +### Fixed diff --git a/db/schema-update.v2.0.0_2023-06-06.sql b/db/schema-update.v2.0.0_2023-06-06.sql new file mode 100644 index 0000000..93dabc2 --- /dev/null +++ b/db/schema-update.v2.0.0_2023-06-06.sql @@ -0,0 +1,6 @@ +-- ----------------------------------------------------- +-- Update to Table `team` +-- ----------------------------------------------------- + +ALTER TABLE `team` + ADD `api_managed_roster` BOOLEAN NOT NULL DEFAULT FALSE; diff --git a/db/schema.v0.sql b/db/schema.v0.sql index 34614c3..81c9b38 100644 --- a/db/schema.v0.sql +++ b/db/schema.v0.sql @@ -15,6 +15,7 @@ CREATE TABLE IF NOT EXISTS `team` ( `iris_plan` VARCHAR(255), `iris_enabled` BOOLEAN NOT NULL DEFAULT FALSE, `override_phone_number` VARCHAR(255), + `api_managed_roster` BOOLEAN NOT NULL DEFAULT FALSE, PRIMARY KEY (`id`), UNIQUE INDEX `name_unique` (`name` ASC)); diff --git a/e2e/test_teams.py b/e2e/test_teams.py index b3c2a97..1bdd0c2 100644 --- a/e2e/test_teams.py +++ b/e2e/test_teams.py @@ -70,7 +70,7 @@ def test_api_v0_get_team(team, role, roster, schedule): team = re.json() assert isinstance(team, dict) expected_set = {'users', 'admins', 'services', 'rosters', 'name', 'id', 'slack_channel', 'slack_channel_notifications', 'email', - 'scheduling_timezone', 'iris_plan', 'iris_enabled', 'override_phone_number'} + 'scheduling_timezone', 'iris_plan', 'iris_enabled', 'override_phone_number', 'api_managed_roster'} assert expected_set == set(team.keys()) # it should also support filter by fields @@ -79,7 +79,7 @@ def test_api_v0_get_team(team, role, roster, schedule): team = re.json() assert isinstance(team, dict) expected_set = {'users', 'admins', 'services', 'name', 'id', 'slack_channel', 'slack_channel_notifications', 'email', - 'scheduling_timezone', 'iris_plan', 'iris_enabled', 'override_phone_number'} + 'scheduling_timezone', 'iris_plan', 'iris_enabled', 'override_phone_number', 'api_managed_roster'} assert expected_set == set(team.keys()) @@ -113,6 +113,7 @@ def test_api_v0_update_team(team): # edit team name/email/slack re = requests.put(api_v0('teams/'+team_name), json={'name': new_team_name, 'email': email, + 'api_managed_roster': True, 'slack_channel': slack, 'slack_channel_notifications': slack_notifications, 'override_phone_number': override_num}) @@ -128,6 +129,7 @@ def test_api_v0_update_team(team): assert data['slack_channel'] == slack assert data['slack_channel_notifications'] == slack_notifications assert data['override_phone_number'] == override_num + assert data['api_managed_roster'] == 1 @prefix('test_v0_team_admin') diff --git a/src/oncall/__init__.py b/src/oncall/__init__.py index 63a5878..afced14 100644 --- a/src/oncall/__init__.py +++ b/src/oncall/__init__.py @@ -1 +1 @@ -__version__ = '1.5.5' +__version__ = '2.0.0' diff --git a/src/oncall/api/v0/team.py b/src/oncall/api/v0/team.py index fb86a6d..e006508 100644 --- a/src/oncall/api/v0/team.py +++ b/src/oncall/api/v0/team.py @@ -11,11 +11,11 @@ from .users import get_user_data from .rosters import get_roster_by_team_id from ...auth import login_required, check_team_auth from ...utils import load_json_body, invalid_char_reg, create_audit -from ...constants import TEAM_DELETED, TEAM_EDITED +from ...constants import TEAM_DELETED, TEAM_EDITED, SUPPORTED_TIMEZONES # Columns which may be modified cols = set(['name', 'slack_channel', 'slack_channel_notifications', 'email', 'scheduling_timezone', - 'iris_plan', 'iris_enabled', 'override_phone_number']) + 'iris_plan', 'iris_enabled', 'override_phone_number', 'api_managed_roster']) def populate_team_users(cursor, team_dict): @@ -148,7 +148,7 @@ def on_get(req, resp, team): connection = db.connect() cursor = connection.cursor(db.DictCursor) cursor.execute('''SELECT `id`, `name`, `email`, `slack_channel`, `slack_channel_notifications`, - `scheduling_timezone`, `iris_plan`, `iris_enabled`, `override_phone_number` + `scheduling_timezone`, `iris_plan`, `iris_enabled`, `override_phone_number`, `api_managed_roster` FROM `team` WHERE `name`=%s AND `active` = %s''', (team, active)) results = cursor.fetchall() if not results: @@ -172,7 +172,8 @@ def on_get(req, resp, team): @login_required def on_put(req, resp, team): ''' - Edit a team's information. Allows edit of: name, slack_channel, email, scheduling_timezone, iris_plan. + Edit a team's information. Allows edit of: 'name', 'slack_channel', 'slack_channel_notifications', 'email', 'scheduling_timezone', + 'iris_plan', 'iris_enabled', 'override_phone_number', 'api_managed_roster' **Example request:** @@ -213,9 +214,18 @@ def on_put(req, resp, team): 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) + if 'iris_enabled' in data: + if not type(data['iris_enabled']) == bool: + raise HTTPBadRequest('invalid payload', '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 '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)) 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,) + query_params = tuple(data[d] for d in data_cols if d in cols) + (team,) try: update_query = 'UPDATE `team` SET {0} WHERE name=%s'.format(set_clause) cursor.execute(update_query, query_params) diff --git a/src/oncall/ui/static/js/oncall.js b/src/oncall/ui/static/js/oncall.js index f026605..fa4b64f 100644 --- a/src/oncall/ui/static/js/oncall.js +++ b/src/oncall/ui/static/js/oncall.js @@ -1364,7 +1364,8 @@ var oncall = { data.isAdmin = true; } else { for (var i in data.admins) { - if (data.admins[i].name === oncall.data.user) { + // if team api managed and user is not superadmin then disable editing of team info + if (data.admins[i].name === oncall.data.user && !data.api_managed_roster) { data.isAdmin = true; } } diff --git a/src/oncall/ui/templates/index.html b/src/oncall/ui/templates/index.html index ec0bcaf..b8ccb24 100644 --- a/src/oncall/ui/templates/index.html +++ b/src/oncall/ui/templates/index.html @@ -333,6 +333,11 @@ {{slack_channel_notifications}} {{/if}} +

+ {{#if api_managed_roster}} + Managed team - this team is managed via API + {{/if}} +

{% endraw %} {% endif %} {% raw %}