From 9e598385f492df6d7fd336c0ef441762fc4f3a72 Mon Sep 17 00:00:00 2001 From: Joey Orlando Date: Tue, 29 Nov 2022 09:41:56 +0100 Subject: [PATCH] Add RBAC Support (#777) * Modify plugin.json to support RBAC role registration * defines 26 new custom roles in plugin.json. The main roles are: - Admin: read/write access to everything in OnCall - Reader: read access to everything in OnCall - OnCaller : read access to everything in OnCall + edit access to Alert Groups and Schedules - Editor: read/write access to everything related to - Reader: read access for - User Settings Admin: read/write access to all user's settings, not just own settings. This is in comparison to User Settings Editor which can only read/write own settings * update changelog and documentation (#686) * implement RBAC for OnCall backend This commit refactors backend authorization. It trys to use RBAC authorization if the org's grafana instance supports it, otherwise it falls back to basic role authorization. * update RBAC backend tests * add tests for RBAC changes - run backend tests as matrix where RBAC is enabled/disabled. When RBAC is enabled, the permissions granted are read from the role grants in the frontend's plugin.json file (instead of relying what we specify in RBACPermission.Permissions) - remove --reuse-db --nomigrations flags from engine/tox.ini - minor autoformatting changes to docker-compose-developer.yml * remove --ds=settings.ci-test from pytest CI command DJANGO_SETTINGS_MODULE is already specified as an env var so this is just unecessary duplication * update gitignore * update github action job name for "test" * RBAC frontend changes * refactors the use of basic roles (ex. Viewer, Editor, Admin) use RBAC permissions (when supported), or falling back to basic roles when RBAC is not supported. - updates the UserAction enum in grafana-plugin/src/state/userAction.ts. Previously this was hardcoded to a list of strings that were being returned by the OnCall API. Now the values here correspond to the permissions in plugin.json (plus a fallback role) * changes per Gabriel's comments: - get rid of group attribute in rbac roles - remove displayName role attribute - remove hidden role attribute - add back role to includes section * don't try to update user timezone if they don't have permission --- .github/workflows/ci.yml | 22 +- CHANGELOG.md | 9 +- docker-compose-developer.yml | 3 + engine/apps/alerts/tasks/notify_user.py | 10 +- engine/apps/alerts/tests/test_alert_group.py | 5 +- .../tests/test_escalation_policy_snapshot.py | 14 +- engine/apps/alerts/tests/test_notify_user.py | 26 +- engine/apps/api/permissions/__init__.py | 299 ++++- engine/apps/api/permissions/actions.py | 27 - engine/apps/api/permissions/constants.py | 14 - engine/apps/api/permissions/methods.py | 12 - engine/apps/api/permissions/owner.py | 24 - engine/apps/api/permissions/roles.py | 49 - .../apps/api/permissions/test_permissions.py | 428 +++++++ engine/apps/api/serializers/user.py | 23 +- engine/apps/api/tests/test_alert_group.py | 229 ++-- .../api/tests/test_alert_receive_channel.py | 72 +- .../test_alert_receive_channel_template.py | 30 +- engine/apps/api/tests/test_channel_filter.py | 45 +- engine/apps/api/tests/test_custom_button.py | 33 +- .../apps/api/tests/test_escalation_chain.py | 2 +- .../apps/api/tests/test_escalation_policy.py | 50 +- engine/apps/api/tests/test_gitops.py | 18 +- .../api/tests/test_integration_heartbeat.py | 83 +- engine/apps/api/tests/test_maintenance.py | 8 +- engine/apps/api/tests/test_oncall_shift.py | 88 +- engine/apps/api/tests/test_organization.py | 56 +- .../api/tests/test_postmortem_messages.py | 42 +- .../apps/api/tests/test_public_api_tokens.py | 115 ++ engine/apps/api/tests/test_schedule_export.py | 38 +- engine/apps/api/tests/test_schedules.py | 84 +- .../api/tests/test_set_general_log_channel.py | 13 +- engine/apps/api/tests/test_slack_channels.py | 25 +- .../api/tests/test_slack_team_settings.py | 34 +- engine/apps/api/tests/test_subscription.py | 8 +- engine/apps/api/tests/test_team.py | 18 +- .../apps/api/tests/test_telegram_channel.py | 39 +- .../apps/api/tests/test_terraform_renderer.py | 2 +- engine/apps/api/tests/test_user.py | 1032 +++++++---------- engine/apps/api/tests/test_user_groups.py | 13 +- .../tests/test_user_notification_policy.py | 8 +- .../api/tests/test_user_schedule_export.py | 69 +- engine/apps/api/views/alert_group.py | 46 +- .../apps/api/views/alert_receive_channel.py | 32 +- .../views/alert_receive_channel_template.py | 13 +- engine/apps/api/views/channel_filter.py | 18 +- engine/apps/api/views/custom_button.py | 16 +- engine/apps/api/views/escalation_chain.py | 16 +- engine/apps/api/views/escalation_policy.py | 24 +- .../apps/api/views/integration_heartbeat.py | 17 +- engine/apps/api/views/live_setting.py | 11 +- engine/apps/api/views/maintenance.py | 18 +- engine/apps/api/views/on_call_shifts.py | 19 +- engine/apps/api/views/organization.py | 27 +- engine/apps/api/views/public_api_tokens.py | 13 +- engine/apps/api/views/resolution_note.py | 15 +- engine/apps/api/views/schedule.py | 38 +- engine/apps/api/views/slack_team_settings.py | 10 +- engine/apps/api/views/telegram_channels.py | 13 +- engine/apps/api/views/user.py | 85 +- .../api/views/user_notification_policy.py | 43 +- engine/apps/auth_token/auth.py | 4 +- engine/apps/base/constants.py | 23 - .../user_notification_policy_log_record.py | 8 +- engine/apps/grafana_plugin/helpers/client.py | 106 +- engine/apps/grafana_plugin/helpers/gcom.py | 7 +- engine/apps/grafana_plugin/tasks/sync.py | 4 +- .../tests/test_grafana_api_client.py | 60 + .../tests/test_self_hosted_install.py | 8 + engine/apps/grafana_plugin/tests/test_sync.py | 4 +- .../views/self_hosted_install.py | 6 +- .../models/cloud_connector.py | 3 +- .../views/cloud_connection.py | 8 +- .../oss_installation/views/cloud_heartbeat.py | 7 +- .../oss_installation/views/cloud_users.py | 38 +- engine/apps/public_api/serializers/users.py | 16 +- engine/apps/public_api/tests/test_users.py | 15 +- engine/apps/public_api/views/users.py | 6 +- engine/apps/schedules/ical_utils.py | 11 +- .../apps/schedules/tests/test_ical_utils.py | 16 +- .../schedules/tests/test_on_call_schedule.py | 6 +- .../apps/slack/models/slack_team_identity.py | 9 +- engine/apps/slack/models/slack_usergroup.py | 6 +- .../slack/scenarios/alertgroup_appearance.py | 4 +- .../apps/slack/scenarios/distribute_alerts.py | 27 +- engine/apps/slack/scenarios/scenario_step.py | 10 - engine/apps/slack/scenarios/step_mixins.py | 10 +- engine/apps/slack/tests/test_reset_slack.py | 12 +- engine/apps/slack/views.py | 8 +- .../updates/update_handlers/button_press.py | 5 +- .../migrations/0005_rbac_permissions.py | 23 + .../user_management/models/organization.py | 1 + engine/apps/user_management/models/user.py | 59 +- engine/apps/user_management/sync.py | 34 +- ...t_free_public_beta_subcription_strategy.py | 14 +- .../apps/user_management/tests/test_sync.py | 24 +- .../apps/user_management/tests/test_user.py | 16 +- engine/common/constants/role.py | 11 - engine/conftest.py | 121 +- engine/tox.ini | 2 +- grafana-plugin/package.json | 2 +- .../src/__mocks__/grafana/app/core/core.ts | 3 - .../AlertTemplates/AlertTemplatesForm.tsx | 6 +- .../NewScheduleSelector.tsx | 4 +- .../components/Policy/EscalationPolicy.tsx | 28 +- .../components/Policy/NotificationPolicy.tsx | 2 +- .../Unauthorized/Unauthorized.module.css | 9 + .../Unauthorized/Unauthorized.test.tsx | 48 + .../__snapshots__/Unauthorized.test.tsx.snap | 241 ++++ .../src/components/Unauthorized/index.tsx | 40 + .../src/components/UserGroups/UserGroups.tsx | 2 +- .../components/UsersFilters/UsersFilters.tsx | 42 +- .../src/containers/AlertRules/AlertRules.tsx | 32 +- .../parts/connectors/SlackConnector.tsx | 15 +- .../parts/connectors/TelegramConnector.tsx | 6 +- .../ApiTokenSettings/ApiTokenSettings.tsx | 8 +- .../AttachIncidentForm/AttachIncidentForm.tsx | 4 +- .../DefaultPageLayout/DefaultPageLayout.tsx | 4 +- .../EscalationChainSteps.tsx | 4 +- .../GrafanaTeamSelect/GrafanaTeamSelect.tsx | 4 +- .../HeartbeatModal/HeartbeatForm.tsx | 6 +- .../IntegrationSettings/parts/Autoresolve.tsx | 4 +- .../MaintenanceForm/MaintenanceForm.tsx | 4 +- .../OutgoingWebhookForm.tsx | 4 +- .../PersonalNotificationSettings.tsx | 4 +- .../src/containers/Rotations/Rotations.tsx | 4 +- .../Rotations/ScheduleOverrides.tsx | 4 +- .../containers/ScheduleForm/ScheduleForm.tsx | 4 +- .../SlackIntegrationButton.tsx | 6 +- .../TelegramIntegrationButton.tsx | 4 +- .../UserSettings/UserSettings.types.ts | 3 +- .../parts/connectors/ICalConnector.tsx | 6 +- .../CloudPhoneSettings/CloudPhoneSettings.tsx | 4 +- .../PhoneVerification/PhoneVerification.tsx | 6 +- .../parts/tabs/UserInfoTab/UserInfoTab.tsx | 7 +- .../WithPermissionControl.tsx | 2 +- .../WithPermissionControl.tsx | 2 +- grafana-plugin/src/index.d.ts | 8 +- grafana-plugin/src/models/team/team.types.ts | 2 - grafana-plugin/src/models/user.ts | 6 - grafana-plugin/src/models/user/user.config.ts | 10 - .../src/models/user/user.helpers.tsx | 28 +- grafana-plugin/src/models/user/user.ts | 9 +- grafana-plugin/src/models/user/user.types.ts | 9 - .../escalation-chains/EscalationChains.tsx | 10 +- .../src/pages/incident/Incident.helpers.tsx | 12 +- .../src/pages/incident/Incident.tsx | 6 +- .../src/pages/incidents/Incidents.tsx | 10 +- .../pages/incidents/parts/SilenceDropdown.tsx | 4 +- grafana-plugin/src/pages/index.tsx | 16 +- .../src/pages/integrations/Integrations.tsx | 6 +- .../src/pages/maintenance/Maintenance.tsx | 6 +- .../outgoing_webhooks/OutgoingWebhooks.tsx | 10 +- .../src/pages/schedule/Schedule.tsx | 4 +- .../src/pages/schedules/Schedules.tsx | 8 +- .../src/pages/settings/SettingsPage.tsx | 9 +- .../tabs/SlackSettings/SlackSettings.tsx | 14 +- .../pages/settings/tabs/Cloud/CloudPage.tsx | 1 + .../tabs/LiveSettings/LiveSettingsPage.tsx | 6 +- .../tabs/MainSettings/MainSettings.tsx | 4 +- grafana-plugin/src/pages/test/Test.tsx | 4 +- .../src/pages/users/Users.helpers.ts | 14 +- grafana-plugin/src/pages/users/Users.tsx | 38 +- grafana-plugin/src/plugin.json | 420 +++++++ .../src/plugin/GrafanaPluginRootPage.tsx | 10 +- .../src/state/rootBaseStore/index.ts | 14 +- .../state/rootBaseStore/rootBaseStore.test.ts | 27 +- grafana-plugin/src/state/userAction.ts | 23 - grafana-plugin/src/types.ts | 7 +- .../utils/authorization/authorization.test.ts | 76 ++ .../src/utils/authorization/index.ts | 156 +++ grafana-plugin/yarn.lock | 608 +++++++--- 172 files changed, 4424 insertions(+), 2194 deletions(-) delete mode 100644 engine/apps/api/permissions/actions.py delete mode 100644 engine/apps/api/permissions/constants.py delete mode 100644 engine/apps/api/permissions/methods.py delete mode 100644 engine/apps/api/permissions/owner.py delete mode 100644 engine/apps/api/permissions/roles.py create mode 100644 engine/apps/api/permissions/test_permissions.py create mode 100644 engine/apps/api/tests/test_public_api_tokens.py delete mode 100644 engine/apps/base/constants.py create mode 100644 engine/apps/grafana_plugin/tests/test_grafana_api_client.py create mode 100644 engine/apps/user_management/migrations/0005_rbac_permissions.py delete mode 100644 engine/common/constants/role.py delete mode 100644 grafana-plugin/src/__mocks__/grafana/app/core/core.ts create mode 100644 grafana-plugin/src/components/Unauthorized/Unauthorized.module.css create mode 100644 grafana-plugin/src/components/Unauthorized/Unauthorized.test.tsx create mode 100644 grafana-plugin/src/components/Unauthorized/__snapshots__/Unauthorized.test.tsx.snap create mode 100644 grafana-plugin/src/components/Unauthorized/index.tsx delete mode 100644 grafana-plugin/src/models/user/user.config.ts delete mode 100644 grafana-plugin/src/state/userAction.ts create mode 100644 grafana-plugin/src/utils/authorization/authorization.test.ts create mode 100644 grafana-plugin/src/utils/authorization/index.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a8eda99f..908f4d48 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,7 +27,7 @@ jobs: run: | pre-commit run --all-files - test: + test-frontend: runs-on: ubuntu-latest container: python:3.9 steps: @@ -56,8 +56,12 @@ jobs: docker run -v ${PWD}/docs/sources:/hugo/content/docs/oncall/latest -e HUGO_REFLINKSERRORLEVEL=ERROR --rm grafana/docs-base:latest /bin/bash -c 'make hugo' unit-test-backend-mysql-rabbitmq: + name: "Backend Tests: MySQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})" runs-on: ubuntu-latest container: python:3.9 + strategy: + matrix: + rbac_enabled: ["True", "False"] env: DJANGO_SETTINGS_MODULE: settings.ci-test SLACK_CLIENT_OAUTH_ID: 1 @@ -72,7 +76,6 @@ jobs: env: MYSQL_DATABASE: oncall_local_dev MYSQL_ROOT_PASSWORD: local_dev_pwd - steps: - uses: actions/checkout@v2 - name: Unit Test Backend @@ -80,11 +83,15 @@ jobs: apt-get update && apt-get install -y netcat cd engine/ pip install -r requirements.txt - ./wait_for_test_mysql_start.sh && pytest --ds=settings.ci-test -x + ./wait_for_test_mysql_start.sh && ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x unit-test-backend-postgresql-rabbitmq: + name: "Backend Tests: PostgreSQL + RabbitMQ (RBAC enabled: ${{ matrix.rbac_enabled }})" runs-on: ubuntu-latest container: python:3.9 + strategy: + matrix: + rbac_enabled: ["True", "False"] env: DATABASE_TYPE: postgresql DJANGO_SETTINGS_MODULE: settings.ci-test @@ -112,11 +119,15 @@ jobs: run: | cd engine/ pip install -r requirements.txt - pytest --ds=settings.ci-test -x + ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x unit-test-backend-sqlite-redis: + name: "Backend Tests: SQLite + Redis (RBAC enabled: ${{ matrix.rbac_enabled }})" runs-on: ubuntu-latest container: python:3.9 + strategy: + matrix: + rbac_enabled: ["True", "False"] env: DATABASE_TYPE: sqlite3 BROKER_TYPE: redis @@ -131,7 +142,6 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 - steps: - uses: actions/checkout@v2 - name: Unit Test Backend @@ -139,7 +149,7 @@ jobs: apt-get update && apt-get install -y netcat cd engine/ pip install -r requirements.txt - pytest --ds=settings.ci-test -x + ONCALL_TESTING_RBAC_ENABLED=${{ matrix.rbac_enabled }} pytest -x docker-build: runs-on: ubuntu-latest diff --git a/CHANGELOG.md b/CHANGELOG.md index e635768e..9ca8d4ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,21 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## v1.1.6 (TBD) +## v1.2.0 (TBD) + +### Added + +- RBAC permission support ### Fixed + - Got 500 error when saving Outgoing Webhook ([#890](https://github.com/grafana/oncall/issues/890)) ### Changed + - When editing templates for alert group presentation or outgoing webhooks, errors and warnings are now displayed in the UI as notification popups or displayed in the preview. - Errors and warnings that occur when rendering templates during notification or webhooks will now render and display the error/warning as the result. + ## v1.1.5 (2022-11-24) ### Added diff --git a/docker-compose-developer.yml b/docker-compose-developer.yml index e8208bbb..df98806c 100644 --- a/docker-compose-developer.yml +++ b/docker-compose-developer.yml @@ -13,6 +13,9 @@ x-oncall-volumes: &oncall-volumes # https://stackoverflow.com/a/60456034 - ${ENTERPRISE_ENGINE:-/dev/null}:/etc/app/extensions/engine_enterprise - ${SQLITE_DB_FILE:-/dev/null}:/var/lib/oncall/oncall.db + # this is mounted for testing purposes. Some of the authorization tests + # reference this file + - ./grafana-plugin/src/plugin.json:/etc/grafana-plugin/src/plugin.json x-env-files: &oncall-env-files - ./dev/.env.dev diff --git a/engine/apps/alerts/tasks/notify_user.py b/engine/apps/alerts/tasks/notify_user.py index b841aa3f..3fbca1af 100644 --- a/engine/apps/alerts/tasks/notify_user.py +++ b/engine/apps/alerts/tasks/notify_user.py @@ -53,13 +53,13 @@ def notify_user_task( organization = alert_group.channel.organization if not user.is_notification_allowed: - task_logger.info(f"notify_user_task: user {user.pk} notification is not allowed for role {user.role}") + task_logger.info(f"notify_user_task: user {user.pk} notification is not allowed") UserNotificationPolicyLogRecord( author=user, type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, - reason=f"notification is not allowed for user with role {user.role}", + reason=f"notification is not allowed for user", alert_group=alert_group, - notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE, + notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN, ).save() return @@ -252,9 +252,9 @@ def perform_notification(log_record_pk): UserNotificationPolicyLogRecord( author=user, type=UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED, - reason=f"notification is not allowed for user with role {user.role}", + reason=f"notification is not allowed for user", alert_group=alert_group, - notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE, + notification_error_code=UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN, ).save() return diff --git a/engine/apps/alerts/tests/test_alert_group.py b/engine/apps/alerts/tests/test_alert_group.py index 16b3daf3..272ef0b1 100644 --- a/engine/apps/alerts/tests/test_alert_group.py +++ b/engine/apps/alerts/tests/test_alert_group.py @@ -4,7 +4,6 @@ from apps.alerts.incident_appearance.renderers.phone_call_renderer import AlertG from apps.alerts.models import AlertGroup from apps.alerts.tasks.delete_alert_group import delete_alert_group from apps.slack.models import SlackMessage -from common.constants.role import Role @pytest.mark.django_db @@ -14,7 +13,7 @@ def test_render_for_phone_call( make_alert_group, make_alert, ): - organization, slack_team_identity = make_organization_with_slack_team_identity() + organization, _ = make_organization_with_slack_team_identity() alert_receive_channel = make_alert_receive_channel(organization, integration_slack_channel_id="CWER1ASD") alert_group = make_alert_group(alert_receive_channel) @@ -59,7 +58,7 @@ def test_delete( organization, slack_team_identity = make_organization_with_slack_team_identity() slack_channel = make_slack_channel(slack_team_identity, name="general", slack_id="CWER1ASD") - user = make_user(organization=organization, role=Role.ADMIN) + user = make_user(organization=organization) alert_receive_channel = make_alert_receive_channel(organization, integration_slack_channel_id="CWER1ASD") diff --git a/engine/apps/alerts/tests/test_escalation_policy_snapshot.py b/engine/apps/alerts/tests/test_escalation_policy_snapshot.py index 6a4e1630..e84050b2 100644 --- a/engine/apps/alerts/tests/test_escalation_policy_snapshot.py +++ b/engine/apps/alerts/tests/test_escalation_policy_snapshot.py @@ -8,9 +8,9 @@ from apps.alerts.escalation_snapshot.serializers.escalation_policy_snapshot impo from apps.alerts.escalation_snapshot.snapshot_classes import EscalationPolicySnapshot from apps.alerts.escalation_snapshot.utils import eta_for_escalation_step_notify_if_time from apps.alerts.models import AlertGroupLogRecord, EscalationPolicy +from apps.api.permissions import LegacyAccessControlRole from apps.schedules.ical_utils import list_users_to_notify_from_ical from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar -from common.constants.role import Role def get_escalation_policy_snapshot_from_model(escalation_policy): @@ -213,8 +213,8 @@ def test_escalation_step_notify_on_call_schedule_viewer_user( make_schedule, make_on_call_shift, ): - organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup - viewer = make_user_for_organization(organization=organization, role=Role.VIEWER) + organization, _, _, channel_filter, alert_group, reason = escalation_step_test_setup + viewer = make_user_for_organization(organization=organization, role=LegacyAccessControlRole.VIEWER) schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) # create on_call_shift with user to notify @@ -263,7 +263,7 @@ def test_escalation_step_notify_user_group( make_slack_user_group, make_escalation_policy, ): - organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup + organization, _, _, channel_filter, alert_group, reason = escalation_step_test_setup slack_team_identity = make_slack_team_identity() organization.slack_team_identity = slack_team_identity organization.save() @@ -295,7 +295,7 @@ def test_escalation_step_notify_if_time( escalation_step_test_setup, make_escalation_policy, ): - organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup + _, _, _, channel_filter, alert_group, reason = escalation_step_test_setup # current time is not between from_time and to_time, step returns eta now = timezone.now() @@ -358,7 +358,7 @@ def test_escalation_step_notify_if_time( def test_escalation_step_notify_if_num_alerts_in_window( mocked_execute_tasks, escalation_step_test_setup, make_escalation_policy, make_alert ): - organization, user, _, channel_filter, alert_group, reason = escalation_step_test_setup + _, _, _, channel_filter, alert_group, reason = escalation_step_test_setup make_alert(alert_group=alert_group, raw_request_data={}) make_alert(alert_group=alert_group, raw_request_data={}) @@ -419,7 +419,7 @@ def test_escalation_step_trigger_custom_button( make_custom_action, make_escalation_policy, ): - organization, _, alert_receive_channel, channel_filter, alert_group, reason = escalation_step_test_setup + organization, _, _, channel_filter, alert_group, reason = escalation_step_test_setup custom_button = make_custom_action(organization=organization) diff --git a/engine/apps/alerts/tests/test_notify_user.py b/engine/apps/alerts/tests/test_notify_user.py index 0f43305b..e6cffe1c 100644 --- a/engine/apps/alerts/tests/test_notify_user.py +++ b/engine/apps/alerts/tests/test_notify_user.py @@ -3,9 +3,11 @@ from unittest.mock import patch import pytest from apps.alerts.tasks.notify_user import notify_user_task, perform_notification +from apps.api.permissions import LegacyAccessControlRole from apps.base.models.user_notification_policy import UserNotificationPolicy from apps.base.models.user_notification_policy_log_record import UserNotificationPolicyLogRecord -from common.constants.role import Role + +NOTIFICATION_UNAUTHORIZED_MSG = "notification is not allowed for user" @pytest.mark.django_db @@ -131,7 +133,9 @@ def test_notify_user_perform_notification_error_if_viewer( make_user_notification_policy_log_record, ): organization = make_organization() - user_1 = make_user(organization=organization, role=Role.VIEWER, _verified_phone_number="1234567890") + user_1 = make_user( + organization=organization, role=LegacyAccessControlRole.VIEWER, _verified_phone_number="1234567890" + ) user_notification_policy = make_user_notification_policy( user=user_1, step=UserNotificationPolicy.Step.NOTIFY, @@ -150,11 +154,8 @@ def test_notify_user_perform_notification_error_if_viewer( error_log_record = UserNotificationPolicyLogRecord.objects.last() assert error_log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED - assert error_log_record.reason == f"notification is not allowed for user with role {user_1.role}" - assert ( - error_log_record.notification_error_code - == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE - ) + assert error_log_record.reason == NOTIFICATION_UNAUTHORIZED_MSG + assert error_log_record.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN @pytest.mark.django_db @@ -165,7 +166,9 @@ def test_notify_user_error_if_viewer( make_alert_group, ): organization = make_organization() - user_1 = make_user(organization=organization, role=Role.VIEWER, _verified_phone_number="1234567890") + user_1 = make_user( + organization=organization, role=LegacyAccessControlRole.VIEWER, _verified_phone_number="1234567890" + ) alert_receive_channel = make_alert_receive_channel(organization=organization) alert_group = make_alert_group(alert_receive_channel=alert_receive_channel) @@ -173,8 +176,5 @@ def test_notify_user_error_if_viewer( error_log_record = UserNotificationPolicyLogRecord.objects.last() assert error_log_record.type == UserNotificationPolicyLogRecord.TYPE_PERSONAL_NOTIFICATION_FAILED - assert error_log_record.reason == f"notification is not allowed for user with role {user_1.role}" - assert ( - error_log_record.notification_error_code - == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE - ) + assert error_log_record.reason == NOTIFICATION_UNAUTHORIZED_MSG + assert error_log_record.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN diff --git a/engine/apps/api/permissions/__init__.py b/engine/apps/api/permissions/__init__.py index d3f1d47e..2287db47 100644 --- a/engine/apps/api/permissions/__init__.py +++ b/engine/apps/api/permissions/__init__.py @@ -1,5 +1,294 @@ -from .actions import ActionPermission # noqa: F401 -from .constants import ALL_BASE_ACTIONS, MODIFY_ACTIONS, READ_ACTIONS # noqa: F401 -from .methods import MethodPermission # noqa: F401 -from .owner import IsOwner, IsOwnerOrAdmin, IsOwnerOrAdminOrEditor # noqa: F401 -from .roles import AnyRole, IsAdmin, IsAdminOrEditor, IsEditor, IsStaff, IsViewer # noqa: F401 +import enum +import typing + +from rest_framework import permissions +from rest_framework.authentication import BasicAuthentication, SessionAuthentication +from rest_framework.request import Request +from rest_framework.views import APIView +from rest_framework.viewsets import ViewSet, ViewSetMixin + +from common.utils import getattrd + +ACTION_PREFIX = "grafana-oncall-app" +RBAC_PERMISSIONS_ATTR = "rbac_permissions" +RBAC_OBJECT_PERMISSIONS_ATTR = "rbac_object_permissions" + +ViewSetOrAPIView = typing.Union[ViewSet, APIView] + + +class GrafanaAPIPermission(typing.TypedDict): + action: str + + +class Resources(enum.Enum): + ALERT_GROUPS = "alert-groups" + INTEGRATIONS = "integrations" + ESCALATION_CHAINS = "escalation-chains" + SCHEDULES = "schedules" + CHATOPS = "chatops" + OUTGOING_WEBHOOKS = "outgoing-webhooks" + MAINTENANCE = "maintenance" + API_KEYS = "api-keys" + NOTIFICATIONS = "notifications" + + NOTIFICATION_SETTINGS = "notification-settings" + USER_SETTINGS = "user-settings" + OTHER_SETTINGS = "other-settings" + + +class Actions(enum.Enum): + READ = "read" + WRITE = "write" + ADMIN = "admin" + TEST = "test" + EXPORT = "export" + UPDATE_SETTINGS = "update-settings" + + +class LegacyAccessControlRole(enum.IntEnum): + ADMIN = 0 + EDITOR = 1 + VIEWER = 2 + + @classmethod + def choices(cls): + return tuple((option.value, option.name) for option in cls) + + +class LegacyAccessControlCompatiblePermission: + def __init__(self, resource: Resources, action: Actions, fallback_role: LegacyAccessControlRole) -> None: + self.value = f"{ACTION_PREFIX}.{resource.value}:{action.value}" + self.fallback_role = fallback_role + + +def get_most_authorized_role( + permissions: typing.List[LegacyAccessControlCompatiblePermission], +) -> LegacyAccessControlRole: + if not permissions: + return LegacyAccessControlRole.VIEWER + + # ex. Admin is 0, Viewer is 2, thereby min makes sense here + return min({p.fallback_role for p in permissions}, key=lambda r: r.value) + + +def user_is_authorized(user, required_permissions: typing.List[LegacyAccessControlCompatiblePermission]) -> bool: + if user.organization.is_rbac_permissions_enabled: + user_permissions = [u["action"] for u in user.permissions] + required_permissions = [p.value for p in required_permissions] + return all(permission in user_permissions for permission in required_permissions) + return user.role <= get_most_authorized_role(required_permissions).value + + +class RBACPermission(permissions.BasePermission): + class Permissions: + ALERT_GROUPS_READ = LegacyAccessControlCompatiblePermission( + Resources.ALERT_GROUPS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + ALERT_GROUPS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.ALERT_GROUPS, Actions.WRITE, LegacyAccessControlRole.EDITOR + ) + + INTEGRATIONS_READ = LegacyAccessControlCompatiblePermission( + Resources.INTEGRATIONS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + INTEGRATIONS_TEST = LegacyAccessControlCompatiblePermission( + Resources.INTEGRATIONS, Actions.TEST, LegacyAccessControlRole.EDITOR + ) + INTEGRATIONS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.INTEGRATIONS, Actions.WRITE, LegacyAccessControlRole.ADMIN + ) + + ESCALATION_CHAINS_READ = LegacyAccessControlCompatiblePermission( + Resources.ESCALATION_CHAINS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + ESCALATION_CHAINS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.ESCALATION_CHAINS, Actions.WRITE, LegacyAccessControlRole.ADMIN + ) + + SCHEDULES_READ = LegacyAccessControlCompatiblePermission( + Resources.SCHEDULES, Actions.READ, LegacyAccessControlRole.VIEWER + ) + SCHEDULES_WRITE = LegacyAccessControlCompatiblePermission( + Resources.SCHEDULES, Actions.WRITE, LegacyAccessControlRole.ADMIN + ) + SCHEDULES_EXPORT = LegacyAccessControlCompatiblePermission( + Resources.SCHEDULES, Actions.EXPORT, LegacyAccessControlRole.EDITOR + ) + + CHATOPS_READ = LegacyAccessControlCompatiblePermission( + Resources.CHATOPS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + CHATOPS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.CHATOPS, Actions.WRITE, LegacyAccessControlRole.EDITOR + ) + CHATOPS_UPDATE_SETTINGS = LegacyAccessControlCompatiblePermission( + Resources.CHATOPS, Actions.UPDATE_SETTINGS, LegacyAccessControlRole.ADMIN + ) + + OUTGOING_WEBHOOKS_READ = LegacyAccessControlCompatiblePermission( + Resources.OUTGOING_WEBHOOKS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + OUTGOING_WEBHOOKS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.OUTGOING_WEBHOOKS, Actions.WRITE, LegacyAccessControlRole.ADMIN + ) + + MAINTENANCE_READ = LegacyAccessControlCompatiblePermission( + Resources.MAINTENANCE, Actions.READ, LegacyAccessControlRole.VIEWER + ) + MAINTENANCE_WRITE = LegacyAccessControlCompatiblePermission( + Resources.MAINTENANCE, Actions.WRITE, LegacyAccessControlRole.EDITOR + ) + + API_KEYS_READ = LegacyAccessControlCompatiblePermission( + Resources.API_KEYS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + API_KEYS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.API_KEYS, Actions.WRITE, LegacyAccessControlRole.EDITOR + ) + + NOTIFICATIONS_READ = LegacyAccessControlCompatiblePermission( + Resources.NOTIFICATIONS, Actions.READ, LegacyAccessControlRole.EDITOR + ) + + NOTIFICATION_SETTINGS_READ = LegacyAccessControlCompatiblePermission( + Resources.NOTIFICATION_SETTINGS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + NOTIFICATION_SETTINGS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.NOTIFICATION_SETTINGS, Actions.WRITE, LegacyAccessControlRole.EDITOR + ) + + USER_SETTINGS_READ = LegacyAccessControlCompatiblePermission( + Resources.USER_SETTINGS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + USER_SETTINGS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.USER_SETTINGS, Actions.WRITE, LegacyAccessControlRole.EDITOR + ) + USER_SETTINGS_ADMIN = LegacyAccessControlCompatiblePermission( + Resources.USER_SETTINGS, Actions.ADMIN, LegacyAccessControlRole.ADMIN + ) + + OTHER_SETTINGS_READ = LegacyAccessControlCompatiblePermission( + Resources.OTHER_SETTINGS, Actions.READ, LegacyAccessControlRole.VIEWER + ) + OTHER_SETTINGS_WRITE = LegacyAccessControlCompatiblePermission( + Resources.OTHER_SETTINGS, Actions.WRITE, LegacyAccessControlRole.ADMIN + ) + + @staticmethod + def _get_view_action(request: Request, view: ViewSetOrAPIView) -> str: + """ + For right now this needs to support being used in both a ViewSet as well as APIView, we use both interchangably + + Note: `request.method` is returned uppercase + """ + return view.action if isinstance(view, ViewSetMixin) else request.method.lower() + + def has_permission(self, request: Request, view: ViewSetOrAPIView) -> bool: + action = self._get_view_action(request, view) + + rbac_permissions: RBACPermissionsAttribute = getattr(view, RBAC_PERMISSIONS_ATTR, None) + + # first check that the rbac_permissions dict attribute is defined + assert ( + rbac_permissions is not None + ), f"Must define a {RBAC_PERMISSIONS_ATTR} dict on the ViewSet that is consuming the RBACPermission class" + + action_required_permissions: typing.Union[None, typing.List] = rbac_permissions.get(action, None) + + # next check that the action in question is defined within the rbac_permissions dict attribute + assert ( + action_required_permissions is not None + ), f"""Each action must be defined within the {RBAC_PERMISSIONS_ATTR} dict on the ViewSet. +\nIf an action requires no permissions, its value should explicitly be set to an empty list""" + + return user_is_authorized(request.user, action_required_permissions) + + def has_object_permission(self, request: Request, view: ViewSetOrAPIView, obj: typing.Any) -> bool: + rbac_object_permissions: RBACObjectPermissionsAttribute = getattr(view, RBAC_OBJECT_PERMISSIONS_ATTR, None) + + if rbac_object_permissions: + action = self._get_view_action(request, view) + + for permission_class, actions in rbac_object_permissions.items(): + if action in actions: + return permission_class.has_object_permission(request, view, obj) + return False + + # has_object_permission is called after has_permission, so return True if in view there is not + # RBAC_OBJECT_PERMISSIONS_ATTR attr which mean no additional check involving object required + return True + + +class IsOwner(permissions.BasePermission): + def __init__(self, ownership_field: typing.Optional[str] = None) -> None: + self.ownership_field = ownership_field + + def has_object_permission(self, request: Request, _view: ViewSet, obj: typing.Any) -> bool: + owner = obj if self.ownership_field is None else getattrd(obj, self.ownership_field) + return owner == request.user + + +class HasRBACPermissions(permissions.BasePermission): + def __init__(self, required_permissions: typing.List[LegacyAccessControlCompatiblePermission]) -> None: + self.required_permissions = required_permissions + + def has_object_permission(self, request: Request, _view: ViewSetOrAPIView, _obj: typing.Any) -> bool: + return user_is_authorized(request.user, self.required_permissions) + + +class IsOwnerOrHasRBACPermissions(permissions.BasePermission): + def __init__( + self, + required_permissions: typing.List[LegacyAccessControlCompatiblePermission], + ownership_field: typing.Optional[str] = None, + ) -> None: + self.IsOwner = IsOwner(ownership_field) + self.HasRBACPermissions = HasRBACPermissions(required_permissions) + + def has_object_permission(self, request: Request, view: ViewSetOrAPIView, obj: typing.Any) -> bool: + return self.IsOwner.has_object_permission(request, view, obj) or self.HasRBACPermissions.has_object_permission( + request, view, obj + ) + + +class IsStaff(permissions.BasePermission): + STAFF_AUTH_CLASSES = [BasicAuthentication, SessionAuthentication] + + def has_permission(self, request: Request, _view: ViewSet) -> bool: + user = request.user + if not any(isinstance(request._authenticator, x) for x in self.STAFF_AUTH_CLASSES): + return False + if user and user.is_authenticated: + return user.is_staff + return False + + +RBACPermissionsAttribute = typing.Dict[str, typing.List[LegacyAccessControlCompatiblePermission]] +RBACObjectPermissionsAttribute = typing.Dict[permissions.BasePermission, typing.List[str]] + + +# The below is legacy, it is only needed currently for backward compatibility w/ users running +# older "pinned" version of Grafana in Grafana Cloud +_DONT_USE_LEGACY_VIEWER_PERMISSIONS = [] +_DONT_USE_LEGACY_EDITOR_PERMISSIONS = ["update_incidents", "update_own_settings", "view_other_users"] +_DONT_USE_LEGACY_ADMIN_PERMISSIONS = _DONT_USE_LEGACY_EDITOR_PERMISSIONS + [ + "update_alert_receive_channels", + "update_escalation_policies", + "update_notification_policies", + "update_general_log_channel_id", + "update_other_users_settings", + "update_integrations", + "update_schedules", + "update_custom_actions", + "update_api_tokens", + "update_teams", + "update_maintenances", + "update_global_settings", + "send_demo_alert", +] + +DONT_USE_LEGACY_PERMISSION_MAPPING: typing.Dict[LegacyAccessControlRole, typing.List[str]] = { + LegacyAccessControlRole.VIEWER: _DONT_USE_LEGACY_VIEWER_PERMISSIONS, + LegacyAccessControlRole.EDITOR: _DONT_USE_LEGACY_EDITOR_PERMISSIONS, + LegacyAccessControlRole.ADMIN: _DONT_USE_LEGACY_ADMIN_PERMISSIONS, +} diff --git a/engine/apps/api/permissions/actions.py b/engine/apps/api/permissions/actions.py deleted file mode 100644 index 74136e12..00000000 --- a/engine/apps/api/permissions/actions.py +++ /dev/null @@ -1,27 +0,0 @@ -from typing import Any - -from rest_framework import permissions -from rest_framework.request import Request -from rest_framework.viewsets import ViewSet - - -class ActionPermission(permissions.BasePermission): - def has_permission(self, request: Request, view: ViewSet) -> bool: - for permission, actions in getattr(view, "action_permissions", {}).items(): - if view.action in actions: - return permission().has_permission(request, view) - - return False - - def has_object_permission(self, request: Request, view: ViewSet, obj: Any) -> bool: - # action_object_permissions attr should be used in case permission check require lookup - # for some object's properties e.g. team. - if getattr(view, "action_object_permissions", None): - for permission, actions in getattr(view, "action_object_permissions", {}).items(): - if view.action in actions: - return permission().has_object_permission(request, view, obj) - return False - else: - # has_object_permission is called after has_permission, so return True if in view there is not - # action_object_permission attr which mean no additional check involving object required - return True diff --git a/engine/apps/api/permissions/constants.py b/engine/apps/api/permissions/constants.py deleted file mode 100644 index 29e828ce..00000000 --- a/engine/apps/api/permissions/constants.py +++ /dev/null @@ -1,14 +0,0 @@ -READ_ACTIONS = ( - "list", - "retrieve", - "metadata", -) - -MODIFY_ACTIONS = ( - "create", - "update", - "partial_update", - "destroy", -) - -ALL_BASE_ACTIONS = READ_ACTIONS + MODIFY_ACTIONS diff --git a/engine/apps/api/permissions/methods.py b/engine/apps/api/permissions/methods.py deleted file mode 100644 index 6ff1b110..00000000 --- a/engine/apps/api/permissions/methods.py +++ /dev/null @@ -1,12 +0,0 @@ -from rest_framework import permissions -from rest_framework.request import Request -from rest_framework.viewsets import ViewSet - - -class MethodPermission(permissions.BasePermission): - def has_permission(self, request: Request, view: ViewSet) -> bool: - for permission, methods in getattr(view, "method_permissions", {}).items(): - if request.method in methods: - return permission().has_permission(request, view) - - return False diff --git a/engine/apps/api/permissions/owner.py b/engine/apps/api/permissions/owner.py deleted file mode 100644 index 4a4fc69e..00000000 --- a/engine/apps/api/permissions/owner.py +++ /dev/null @@ -1,24 +0,0 @@ -from typing import Any - -from rest_framework import permissions -from rest_framework.request import Request -from rest_framework.viewsets import ViewSet - -from apps.api.permissions.roles import IsAdmin, IsEditor -from common.utils import getattrd - - -class IsOwner(permissions.BasePermission): - def has_object_permission(self, request: Request, view: ViewSet, obj: Any) -> bool: - ownership_field = getattr(view, "ownership_field", None) - if ownership_field is None: - owner = obj - else: - owner = getattrd(obj, ownership_field) - - return owner == request.user - - -IsOwnerOrAdmin = IsOwner | IsAdmin - -IsOwnerOrAdminOrEditor = IsOwner | IsAdmin | IsEditor diff --git a/engine/apps/api/permissions/roles.py b/engine/apps/api/permissions/roles.py deleted file mode 100644 index 3ae9d548..00000000 --- a/engine/apps/api/permissions/roles.py +++ /dev/null @@ -1,49 +0,0 @@ -from typing import Any - -from rest_framework import permissions -from rest_framework.authentication import BasicAuthentication, SessionAuthentication -from rest_framework.request import Request -from rest_framework.viewsets import ViewSet - -from common.constants.role import Role - - -class RolePermission(permissions.BasePermission): - ROLE = None - - def has_permission(self, request: Request, view: ViewSet) -> bool: - return request.user.role == type(self).ROLE - - def has_object_permission(self, request: Request, view: ViewSet, obj: Any) -> bool: - return self.has_permission(request, view) - - -class IsAdmin(RolePermission): - ROLE = Role.ADMIN - - -class IsEditor(RolePermission): - ROLE = Role.EDITOR - - -class IsViewer(RolePermission): - ROLE = Role.VIEWER - - -IsAdminOrEditor = IsAdmin | IsEditor -AnyRole = IsAdmin | IsEditor | IsViewer - - -class IsStaff(permissions.BasePermission): - STAFF_AUTH_CLASSES = [BasicAuthentication, SessionAuthentication] - - def has_permission(self, request: Request, view: ViewSet) -> bool: - user = request.user - if not any(isinstance(request._authenticator, x) for x in self.STAFF_AUTH_CLASSES): - return False - if user and user.is_authenticated: - return user.is_staff - return False - - def has_object_permission(self, request: Request, view: ViewSet, obj: Any) -> bool: - return self.has_permission(request, view) diff --git a/engine/apps/api/permissions/test_permissions.py b/engine/apps/api/permissions/test_permissions.py new file mode 100644 index 00000000..e0d39b10 --- /dev/null +++ b/engine/apps/api/permissions/test_permissions.py @@ -0,0 +1,428 @@ +import typing + +import pytest +from rest_framework.views import APIView +from rest_framework.viewsets import ViewSetMixin + +from . import ( + RBAC_PERMISSIONS_ATTR, + GrafanaAPIPermission, + HasRBACPermissions, + IsOwner, + IsOwnerOrHasRBACPermissions, + LegacyAccessControlCompatiblePermission, + RBACObjectPermissionsAttribute, + RBACPermission, + RBACPermissionsAttribute, + get_most_authorized_role, + user_is_authorized, +) + + +class MockedOrg: + def __init__(self, org_has_rbac_enabled: bool) -> None: + self.is_rbac_permissions_enabled = org_has_rbac_enabled + + +class MockedUser: + def __init__( + self, permissions: typing.List[LegacyAccessControlCompatiblePermission], org_has_rbac_enabled=True + ) -> None: + self.permissions = [GrafanaAPIPermission(action=perm.value) for perm in permissions] + self.role = get_most_authorized_role(permissions) + self.organization = MockedOrg(org_has_rbac_enabled) + + +class MockedSchedule: + def __init__(self, user: MockedUser) -> None: + self.user = user + + +class MockedRequest: + def __init__(self, user: typing.Optional[MockedUser] = None, method: typing.Optional[str] = None) -> None: + if user: + self.user = user + if method: + self.method = method + + +class MockedViewSet(ViewSetMixin): + def __init__( + self, + action: str, + rbac_permissions: typing.Optional[RBACPermissionsAttribute] = None, + rbac_object_permissions: typing.Optional[RBACObjectPermissionsAttribute] = None, + ) -> None: + super().__init__() + self.action = action + + if rbac_permissions: + self.rbac_permissions = rbac_permissions + if rbac_object_permissions: + self.rbac_object_permissions = rbac_object_permissions + + +class MockedAPIView(APIView): + def __init__( + self, + rbac_permissions: typing.Optional[RBACPermissionsAttribute] = None, + rbac_object_permissions: typing.Optional[RBACObjectPermissionsAttribute] = None, + ) -> None: + super().__init__() + + if rbac_permissions: + self.rbac_permissions = rbac_permissions + if rbac_object_permissions: + self.rbac_object_permissions = rbac_object_permissions + + +@pytest.mark.parametrize( + "user_permissions,required_permissions,org_has_rbac_enabled,expected_result", + [ + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ], + [RBACPermission.Permissions.ALERT_GROUPS_READ], + True, + True, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ], + [RBACPermission.Permissions.ALERT_GROUPS_READ], + False, + True, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + True, + True, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + False, + True, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + [RBACPermission.Permissions.ALERT_GROUPS_READ], + True, + False, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + [RBACPermission.Permissions.ALERT_GROUPS_READ], + False, + True, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ], + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + False, + False, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ], + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + True, + False, + ), + ], +) +def test_user_is_authorized(user_permissions, required_permissions, org_has_rbac_enabled, expected_result) -> None: + user = MockedUser(user_permissions, org_has_rbac_enabled=org_has_rbac_enabled) + assert user_is_authorized(user, required_permissions) == expected_result + + +@pytest.mark.parametrize( + "permissions,expected_role", + [ + ([RBACPermission.Permissions.ALERT_GROUPS_READ], RBACPermission.Permissions.ALERT_GROUPS_READ.fallback_role), + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + RBACPermission.Permissions.ALERT_GROUPS_WRITE.fallback_role, + ), + ( + [ + RBACPermission.Permissions.USER_SETTINGS_READ, + RBACPermission.Permissions.USER_SETTINGS_WRITE, + RBACPermission.Permissions.USER_SETTINGS_ADMIN, + ], + RBACPermission.Permissions.USER_SETTINGS_ADMIN.fallback_role, + ), + ], +) +def test_get_most_authorized_role(permissions, expected_role) -> None: + assert get_most_authorized_role(permissions) == expected_role + + +class TestRBACPermission: + def test_get_view_action(self) -> None: + viewset_action = "viewset_action" + viewset = MockedViewSet(viewset_action) + + apiview = MockedAPIView() + + method = "APIVIEW_ACTION" + request = MockedRequest(method=method) + + assert RBACPermission._get_view_action(request, viewset) == viewset_action, "it works with a ViewSet" + assert RBACPermission._get_view_action(request, apiview) == method.lower(), "it works with an APIView" + + def test_has_permission_works_on_a_viewset_view(self) -> None: + required_permission = RBACPermission.Permissions.ALERT_GROUPS_READ + + action = "hello" + viewset = MockedViewSet( + action=action, + rbac_permissions={ + action: [required_permission], + }, + ) + + viewset_with_no_required_permissions = MockedViewSet( + action=action, + rbac_permissions={ + action: [], + }, + ) + + user_with_permission = MockedUser([required_permission]) + user_without_permission = MockedUser([RBACPermission.Permissions.ALERT_GROUPS_WRITE]) + + assert ( + RBACPermission().has_permission(MockedRequest(user_with_permission), viewset) is True + ), "it works on a viewset when the user does have permission" + + assert ( + RBACPermission().has_permission(MockedRequest(user_without_permission), viewset) is False + ), "it works on a viewset when the user does have permission" + + assert ( + RBACPermission().has_permission( + MockedRequest(user_without_permission), viewset_with_no_required_permissions + ) + is True + ), "it works on a viewset when the viewset action does not require permissions" + + def test_has_permission_works_on_an_apiview_view(self) -> None: + required_permission = RBACPermission.Permissions.ALERT_GROUPS_READ + + method = "hello" + apiview = MockedAPIView( + rbac_permissions={ + method: [required_permission], + } + ) + apiview_with_no_permissions = MockedAPIView( + rbac_permissions={ + method: [], + } + ) + + user1 = MockedUser([required_permission]) + user2 = MockedUser([RBACPermission.Permissions.ALERT_GROUPS_WRITE]) + + class Request(MockedRequest): + def __init__(self, user: typing.Optional[MockedUser] = None) -> None: + super().__init__(user, method) + + assert ( + RBACPermission().has_permission(Request(user1), apiview) is True + ), "it works on an APIView when the user has permission" + + assert ( + RBACPermission().has_permission(Request(user2), apiview) is False + ), "it works on an APIView when the user does not have permission" + + assert ( + RBACPermission().has_permission(Request(user2), apiview_with_no_permissions) is True + ), "it works on a viewset when the viewset action does not require permissions" + + def test_has_permission_throws_assertion_error_if_developer_forgets_to_specify_rbac_permissions(self) -> None: + action_slash_method = "hello" + error_msg = ( + f"Must define a {RBAC_PERMISSIONS_ATTR} dict on the ViewSet that is consuming the RBACPermission class" + ) + + viewset = MockedViewSet(action_slash_method) + apiview = MockedAPIView() + + with pytest.raises(AssertionError, match=error_msg): + RBACPermission().has_permission(MockedRequest(), viewset) + + with pytest.raises(AssertionError, match=error_msg): + RBACPermission().has_permission(MockedRequest(method=action_slash_method), apiview) + + def test_has_permission_throws_assertion_error_if_developer_forgets_to_specify_an_action_in_rbac_permissions( + self, + ) -> None: + action_slash_method = "hello" + other_action_rbac_permissions = {"bonjour": []} + error_msg = f"""Each action must be defined within the {RBAC_PERMISSIONS_ATTR} dict on the ViewSet. +\nIf an action requires no permissions, its value should explicitly be set to an empty list""" + + viewset = MockedViewSet(action_slash_method, other_action_rbac_permissions) + apiview = MockedAPIView(rbac_permissions=other_action_rbac_permissions) + + with pytest.raises(AssertionError, match=error_msg): + RBACPermission().has_permission(MockedRequest(), viewset) + + with pytest.raises(AssertionError, match=error_msg): + RBACPermission().has_permission(MockedRequest(method=action_slash_method), apiview) + + def test_has_object_permission_returns_true_if_rbac_object_permissions_not_specified(self) -> None: + request = MockedRequest() + assert RBACPermission().has_object_permission(request, MockedAPIView(), None) is True + assert RBACPermission().has_object_permission(request, MockedViewSet("potato"), None) is True + + def test_has_object_permission_works_if_no_permission_class_specified_for_action(self) -> None: + action = "hello" + + request = MockedRequest(None, action) + apiview = MockedAPIView(rbac_object_permissions={}) + viewset = MockedViewSet(action, rbac_object_permissions={}) + + assert RBACPermission().has_object_permission(request, apiview, None) is True + assert RBACPermission().has_object_permission(request, viewset, None) is True + + def test_has_object_permission_works_when_permission_class_specified_for_action(self) -> None: + action = "hello" + mocked_permission_class_response = "asdfasdfasdf" + + class MockedPermissionClass: + def has_object_permission(self, _req, _view, _obj) -> None: + return mocked_permission_class_response + + rbac_object_permissions = {MockedPermissionClass(): (action,)} + request = MockedRequest(None, action) + apiview = MockedAPIView(rbac_object_permissions=rbac_object_permissions) + viewset = MockedViewSet(action, rbac_object_permissions=rbac_object_permissions) + + assert RBACPermission().has_object_permission(request, apiview, None) == mocked_permission_class_response + assert RBACPermission().has_object_permission(request, viewset, None) == mocked_permission_class_response + + +class TestIsOwner: + def test_it_works_when_comparing_user_to_object(self) -> None: + user1 = MockedUser([]) + user2 = MockedUser([]) + request = MockedRequest(user1) + IsUser = IsOwner() + + assert IsUser.has_object_permission(request, None, user1) is True + assert IsUser.has_object_permission(request, None, user2) is False + + def test_it_works_when_comparing_user_to_ownership_field_object(self) -> None: + user1 = MockedUser([]) + user2 = MockedUser([]) + schedule = MockedSchedule(user1) + IsScheduleOwner = IsOwner("user") + + assert IsScheduleOwner.has_object_permission(MockedRequest(user1), None, schedule) is True + assert IsScheduleOwner.has_object_permission(MockedRequest(user2), None, schedule) is False + + def test_it_works_when_comparing_user_to_nested_ownership_field_object(self) -> None: + class Thingy: + def __init__(self, schedule: MockedSchedule) -> None: + self.schedule = schedule + + user1 = MockedUser([]) + user2 = MockedUser([]) + schedule = MockedSchedule(user1) + thingy = Thingy(schedule) + IsScheduleOwner = IsOwner("schedule.user") + + assert IsScheduleOwner.has_object_permission(MockedRequest(user1), None, thingy) is True + assert IsScheduleOwner.has_object_permission(MockedRequest(user2), None, thingy) is False + + +@pytest.mark.parametrize( + "user_permissions,required_permissions,expected_result", + [ + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ], + [RBACPermission.Permissions.ALERT_GROUPS_READ], + True, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + True, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + [RBACPermission.Permissions.ALERT_GROUPS_READ], + False, + ), + ( + [RBACPermission.Permissions.ALERT_GROUPS_READ], + [RBACPermission.Permissions.ALERT_GROUPS_READ, RBACPermission.Permissions.ALERT_GROUPS_WRITE], + False, + ), + ], +) +def test_HasRBACPermission(user_permissions, required_permissions, expected_result) -> None: + request = MockedRequest(MockedUser(user_permissions)) + assert HasRBACPermissions(required_permissions).has_object_permission(request, None, None) == expected_result + + +class TestIsOwnerOrHasRBACPermissions: + required_permission = RBACPermission.Permissions.SCHEDULES_READ + required_permissions = [required_permission] + + def test_it_works_when_user_is_owner_and_does_not_have_permissions(self) -> None: + user1 = MockedUser([]) + schedule = MockedSchedule(user1) + request = MockedRequest(user1) + + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions) + assert PermClass.has_object_permission(request, None, user1) is True + + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "user") + assert PermClass.has_object_permission(request, None, schedule) is True + + def test_it_works_when_user_is_owner_and_has_permissions(self) -> None: + user1 = MockedUser(self.required_permissions) + schedule = MockedSchedule(user1) + request = MockedRequest(user1) + + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions) + assert PermClass.has_object_permission(request, None, user1) is True + + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "user") + assert PermClass.has_object_permission(request, None, schedule) is True + + def test_it_works_when_user_is_not_owner_and_does_not_have_permissions(self) -> None: + user1 = MockedUser([]) + user2 = MockedUser([]) + schedule = MockedSchedule(user1) + request = MockedRequest(user2) + + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions) + assert PermClass.has_object_permission(request, None, user1) is False + + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "user") + assert PermClass.has_object_permission(request, None, schedule) is False + + def test_it_works_when_user_is_not_owner_and_has_permissions(self) -> None: + user1 = MockedUser([]) + user2 = MockedUser(self.required_permissions) + schedule = MockedSchedule(user1) + request = MockedRequest(user2) + + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions) + assert PermClass.has_object_permission(request, None, user1) is True + + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "user") + assert PermClass.has_object_permission(request, None, schedule) is True + + class Thingy: + def __init__(self, schedule: MockedSchedule) -> None: + self.schedule = schedule + + thingy = Thingy(schedule) + PermClass = IsOwnerOrHasRBACPermissions(self.required_permissions, "schedule.user") + + assert PermClass.has_object_permission(request, None, thingy) is True + assert PermClass.has_object_permission(MockedRequest(MockedUser([])), None, thingy) is False diff --git a/engine/apps/api/serializers/user.py b/engine/apps/api/serializers/user.py index e3a0d784..3ed2ecda 100644 --- a/engine/apps/api/serializers/user.py +++ b/engine/apps/api/serializers/user.py @@ -1,12 +1,13 @@ import math import time +import typing import pytz from django.conf import settings from rest_framework import serializers +from apps.api.permissions import DONT_USE_LEGACY_PERMISSION_MAPPING from apps.api.serializers.telegram import TelegramToUserConnectorSerializer -from apps.base.constants import ADMIN_PERMISSIONS, ALL_ROLES_PERMISSIONS, EDITOR_PERMISSIONS from apps.base.messaging import get_messaging_backends from apps.base.models import UserNotificationPolicy from apps.base.utils import live_settings @@ -16,7 +17,6 @@ from apps.user_management.models import User from apps.user_management.models.user import default_working_hours from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField from common.api_helpers.mixins import EagerLoadingMixin -from common.constants.role import Role from .custom_serializers import DynamicFieldsModelSerializer from .organization import FastOrganizationSerializer @@ -52,7 +52,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin): "email", "username", "name", - "role", + "role", # LEGACY.. this should get removed eventually "avatar", "avatar_full", "timezone", @@ -62,7 +62,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin): "slack_user_identity", "telegram_configuration", "messaging_backends", - "permissions", + "permissions", # LEGACY.. this should get removed eventually "notification_chain_verbal", "cloud_connection_status", "hide_phone_number", @@ -71,7 +71,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin): "email", "username", "name", - "role", + "role", # LEGACY.. this should get removed eventually "verified_phone_number", ] @@ -139,13 +139,8 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin): serialized_data[backend_id] = backend.serialize_user(obj) return serialized_data - def get_permissions(self, obj): - if obj.role == Role.ADMIN: - return ADMIN_PERMISSIONS - elif obj.role == Role.EDITOR: - return EDITOR_PERMISSIONS - else: - return ALL_ROLES_PERMISSIONS + def get_permissions(self, obj) -> typing.List[str]: + return DONT_USE_LEGACY_PERMISSION_MAPPING[obj.role] def get_notification_chain_verbal(self, obj): default, important = UserNotificationPolicy.get_short_verbals_for_user(user=obj) @@ -180,7 +175,7 @@ class UserSerializer(DynamicFieldsModelSerializer, EagerLoadingMixin): class UserHiddenFieldsSerializer(UserSerializer): - available_for_all_roles_fields = [ + fields_available_for_all_users = [ "pk", "organization", "current_team", @@ -196,7 +191,7 @@ class UserHiddenFieldsSerializer(UserSerializer): ret = super(UserSerializer, self).to_representation(instance) if instance.id != self.context["request"].user.id: for field in ret: - if field not in self.available_for_all_roles_fields: + if field not in self.fields_available_for_all_users: ret[field] = "******" ret["hidden_fields"] = True return ret diff --git a/engine/apps/api/tests/test_alert_group.py b/engine/apps/api/tests/test_alert_group.py index 0b236b7d..ebc2f41c 100644 --- a/engine/apps/api/tests/test_alert_group.py +++ b/engine/apps/api/tests/test_alert_group.py @@ -9,7 +9,7 @@ from rest_framework.response import Response from rest_framework.test import APIClient from apps.alerts.models import AlertGroup, AlertGroupLogRecord -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole alert_raw_request_data = { "evalMatches": [ @@ -25,15 +25,6 @@ alert_raw_request_data = { } -# # This function is for creating token and do not to change fixture alert_group_internal_api_setup return values. -# # To create token amixr team is needed but in most tests using fixture alert_group_internal_api_setup team is redundant -# # So it just extract amixr team form alert_groups. -# def create_token_from_initial_test_data(make_func, alert_groups, role): -# organization = alert_groups[0].channel.organization -# _, token_user_role = make_func(organization, role) -# return token_user_role - - @pytest.fixture() def alert_group_internal_api_setup( make_organization_and_user_with_plugin_token, @@ -52,7 +43,7 @@ def alert_group_internal_api_setup( @pytest.mark.django_db def test_get_filter_started_at(alert_group_internal_api_setup, make_user_auth_headers): - user, token, alert_groups = alert_group_internal_api_setup + user, token, _ = alert_group_internal_api_setup client = APIClient() url = reverse("api-internal:alertgroup-list") @@ -69,7 +60,7 @@ def test_get_filter_started_at(alert_group_internal_api_setup, make_user_auth_he @pytest.mark.django_db def test_get_filter_resolved_at_alertgroup_empty_result(alert_group_internal_api_setup, make_user_auth_headers): client = APIClient() - user, token, alert_groups = alert_group_internal_api_setup + user, token, _ = alert_group_internal_api_setup url = reverse("api-internal:alertgroup-list") response = client.get( @@ -84,7 +75,7 @@ def test_get_filter_resolved_at_alertgroup_empty_result(alert_group_internal_api @pytest.mark.django_db def test_get_filter_resolved_at_alertgroup_invalid_format(alert_group_internal_api_setup, make_user_auth_headers): client = APIClient() - user, token, alert_groups = alert_group_internal_api_setup + user, token, _ = alert_group_internal_api_setup url = reverse("api-internal:alertgroup-list") response = client.get( @@ -660,19 +651,25 @@ def test_get_filter_with_resolution_note_after_delete_resolution_note( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_acknowledge_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() + url = reverse("api-internal:alertgroup-acknowledge", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -689,19 +686,24 @@ def test_alert_group_acknowledge_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_unacknowledge_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-unacknowledge", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -718,19 +720,24 @@ def test_alert_group_unacknowledge_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_resolve_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-resolve", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -747,19 +754,24 @@ def test_alert_group_resolve_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_unresolve_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-unresolve", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -776,19 +788,24 @@ def test_alert_group_unresolve_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_silence_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-silence", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -805,19 +822,24 @@ def test_alert_group_silence_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_unsilence_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-unsilence", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -834,19 +856,24 @@ def test_alert_group_unsilence_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_attach_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-attach", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -863,19 +890,24 @@ def test_alert_group_attach_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_unattach_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-unattach", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -892,19 +924,24 @@ def test_alert_group_unattach_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_alert_group_list_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-list") with patch( @@ -921,19 +958,24 @@ def test_alert_group_list_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_alert_group_stats_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-stats") with patch( @@ -950,19 +992,24 @@ def test_alert_group_stats_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_bulk_action_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-bulk-action") with patch( @@ -977,19 +1024,24 @@ def test_alert_group_bulk_action_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_alert_group_filters_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-filters") with patch( @@ -1006,19 +1058,24 @@ def test_alert_group_filters_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_alert_group_detail_permissions( - make_user_for_organization, alert_group_internal_api_setup, make_user_auth_headers, role, expected_status + alert_group_internal_api_setup, + make_user_for_organization, + make_user_auth_headers, + role, + expected_status, ): - client = APIClient() _, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups organization = new_alert_group.channel.organization user = make_user_for_organization(organization, role) + + client = APIClient() url = reverse("api-internal:alertgroup-detail", kwargs={"pk": new_alert_group.public_primary_key}) with patch( @@ -1032,10 +1089,7 @@ def test_alert_group_detail_permissions( @pytest.mark.django_db -def test_silence( - alert_group_internal_api_setup, - make_user_auth_headers, -): +def test_silence(alert_group_internal_api_setup, make_user_auth_headers): client = APIClient() user, token, alert_groups = alert_group_internal_api_setup _, _, new_alert_group, _ = alert_groups @@ -1396,9 +1450,9 @@ def test_alert_group_status_field( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_group_preview_template_permissions( @@ -1414,6 +1468,7 @@ def test_alert_group_preview_template_permissions( alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) + client = APIClient() url = reverse("api-internal:alertgroup-preview-template", kwargs={"pk": alert_group.public_primary_key}) @@ -1436,7 +1491,7 @@ def test_alert_group_preview_body_non_existent_template_var( make_alert_group, make_alert, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) @@ -1459,7 +1514,7 @@ def test_alert_group_preview_body_invalid_template_syntax( make_alert_group, make_alert, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) make_alert(alert_group=alert_group, raw_request_data=alert_receive_channel.config.example_payload) diff --git a/engine/apps/api/tests/test_alert_receive_channel.py b/engine/apps/api/tests/test_alert_receive_channel.py index f849cc5f..2ce07341 100644 --- a/engine/apps/api/tests/test_alert_receive_channel.py +++ b/engine/apps/api/tests/test_alert_receive_channel.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from rest_framework.test import APIClient from apps.alerts.models import AlertReceiveChannel, EscalationPolicy -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.fixture() @@ -205,9 +205,9 @@ def test_integration_search( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_receive_channel_create_permissions( @@ -235,9 +235,9 @@ def test_alert_receive_channel_create_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_receive_channel_update_permissions( @@ -272,9 +272,9 @@ def test_alert_receive_channel_update_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_receive_channel_delete_permissions( @@ -303,7 +303,11 @@ def test_alert_receive_channel_delete_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_alert_receive_channel_list_permissions( make_organization_and_user_with_plugin_token, @@ -311,7 +315,7 @@ def test_alert_receive_channel_list_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:alert_receive_channel-list") @@ -330,7 +334,11 @@ def test_alert_receive_channel_list_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_alert_receive_channel_detail_permissions( make_organization_and_user_with_plugin_token, @@ -360,9 +368,9 @@ def test_alert_receive_channel_detail_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_receive_channel_send_demo_alert_permissions( @@ -395,9 +403,9 @@ def test_alert_receive_channel_send_demo_alert_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_alert_receive_channel_integration_options_permissions( @@ -426,9 +434,9 @@ def test_alert_receive_channel_integration_options_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_receive_channel_preview_template_permissions( @@ -501,9 +509,9 @@ def test_alert_receive_channel_preview_template_require_notification_channel( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_receive_channel_change_team_permissions( @@ -597,9 +605,9 @@ def test_alert_receive_channel_change_team( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_alert_receive_channel_counters_permissions( @@ -608,7 +616,7 @@ def test_alert_receive_channel_counters_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse( @@ -630,9 +638,9 @@ def test_alert_receive_channel_counters_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_alert_receive_channel_counters_per_integration_permissions( diff --git a/engine/apps/api/tests/test_alert_receive_channel_template.py b/engine/apps/api/tests/test_alert_receive_channel_template.py index 16330810..0c2d658d 100644 --- a/engine/apps/api/tests/test_alert_receive_channel_template.py +++ b/engine/apps/api/tests/test_alert_receive_channel_template.py @@ -6,17 +6,17 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient +from apps.api.permissions import LegacyAccessControlRole from apps.base.messaging import BaseMessagingBackend -from common.constants.role import Role @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_receive_channel_template_update_permissions( @@ -48,9 +48,9 @@ def test_alert_receive_channel_template_update_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_alert_receive_channel_template_detail_permissions( @@ -83,7 +83,7 @@ def test_alert_receive_channel_template_include_additional_backend_templates( make_user_auth_headers, make_alert_receive_channel, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel( organization, messaging_backends_templates={"TESTONLY": {"title": "the-title", "message": "the-message", "image_url": "url"}}, @@ -109,7 +109,7 @@ def test_alert_receive_channel_template_include_additional_backend_templates_usi make_user_auth_headers, make_alert_receive_channel, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel(organization, messaging_backends_templates=None) client = APIClient() @@ -138,7 +138,7 @@ def test_update_alert_receive_channel_backend_template_invalid_template( make_user_auth_headers, make_alert_receive_channel, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel(organization, messaging_backends_templates=None) client = APIClient() @@ -160,7 +160,7 @@ def test_update_alert_receive_channel_backend_template_invalid_url( make_user_auth_headers, make_alert_receive_channel, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel(organization, messaging_backends_templates=None) client = APIClient() @@ -182,7 +182,7 @@ def test_update_alert_receive_channel_backend_template_empty_values_allowed( make_user_auth_headers, make_alert_receive_channel, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel(organization, messaging_backends_templates=None) client = APIClient() @@ -208,7 +208,7 @@ def test_update_alert_receive_channel_backend_template_update_values( make_user_auth_headers, make_alert_receive_channel, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel( organization, messaging_backends_templates={ @@ -249,7 +249,7 @@ def test_preview_alert_receive_channel_backend_templater( make_alert_group, make_alert, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel(organization) default_channel_filter = make_channel_filter(alert_receive_channel, is_default=True) alert_group = make_alert_group(alert_receive_channel, channel_filter=default_channel_filter) @@ -280,7 +280,7 @@ def test_update_alert_receive_channel_templates( # set url here to pass *_url templates validation return "https://grafana.com" - organization, user, token = make_organization_and_user_with_plugin_token(role=Role.ADMIN) + organization, user, token = make_organization_and_user_with_plugin_token() alert_receive_channel = make_alert_receive_channel( organization, messaging_backends_templates={"TESTONLY": {"title": "the-title", "message": "the-message", "image_url": "url"}}, diff --git a/engine/apps/api/tests/test_channel_filter.py b/engine/apps/api/tests/test_channel_filter.py index f70c8956..fe02e97b 100644 --- a/engine/apps/api/tests/test_channel_filter.py +++ b/engine/apps/api/tests/test_channel_filter.py @@ -6,21 +6,20 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_channel_filter_create_permissions( make_organization_and_user_with_plugin_token, - make_alert_receive_channel, make_user_auth_headers, role, expected_status, @@ -45,9 +44,9 @@ def test_channel_filter_create_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_channel_filter_update_permissions( @@ -83,7 +82,11 @@ def test_channel_filter_update_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_channel_filter_list_permissions( make_organization_and_user_with_plugin_token, @@ -114,7 +117,11 @@ def test_channel_filter_list_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_channel_filter_retrieve_permissions( make_organization_and_user_with_plugin_token, @@ -146,9 +153,9 @@ def test_channel_filter_retrieve_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_channel_filter_delete_permissions( @@ -181,9 +188,9 @@ def test_channel_filter_delete_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_channel_filter_move_to_position_permissions( @@ -216,9 +223,9 @@ def test_channel_filter_move_to_position_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_alert_receive_channel_send_demo_alert_permissions( diff --git a/engine/apps/api/tests/test_custom_button.py b/engine/apps/api/tests/test_custom_button.py index f45acb77..3ed57a4f 100644 --- a/engine/apps/api/tests/test_custom_button.py +++ b/engine/apps/api/tests/test_custom_button.py @@ -8,7 +8,7 @@ from rest_framework.response import Response from rest_framework.test import APIClient from apps.alerts.models import CustomButton -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole TEST_URL = "https://amixr.io" @@ -275,14 +275,13 @@ def test_delete_custom_button(custom_button_internal_api_setup, make_user_auth_h @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_custom_button_create_permissions( make_organization_and_user_with_plugin_token, - make_custom_action, make_user_auth_headers, role, expected_status, @@ -307,9 +306,9 @@ def test_custom_button_create_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_custom_button_update_permissions( @@ -343,7 +342,11 @@ def test_custom_button_update_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_custom_button_list_permissions( make_organization_and_user_with_plugin_token, @@ -372,7 +375,11 @@ def test_custom_button_list_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_custom_button_retrieve_permissions( make_organization_and_user_with_plugin_token, @@ -402,9 +409,9 @@ def test_custom_button_retrieve_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_custom_button_delete_permissions( diff --git a/engine/apps/api/tests/test_escalation_chain.py b/engine/apps/api/tests/test_escalation_chain.py index ee88fa18..abe925f8 100644 --- a/engine/apps/api/tests/test_escalation_chain.py +++ b/engine/apps/api/tests/test_escalation_chain.py @@ -24,7 +24,7 @@ def test_delete_escalation_chain(escalation_chain_internal_api_setup, make_user_ @pytest.mark.django_db -def test_update_escalation_chain(escalation_chain_internal_api_setup, make_user_auth_headers, make_organization): +def test_update_escalation_chain(escalation_chain_internal_api_setup, make_user_auth_headers): user, token, escalation_chain = escalation_chain_internal_api_setup client = APIClient() url = reverse("api-internal:escalation_chain-detail", kwargs={"pk": escalation_chain.public_primary_key}) diff --git a/engine/apps/api/tests/test_escalation_policy.py b/engine/apps/api/tests/test_escalation_policy.py index f1e4c804..54fc2301 100644 --- a/engine/apps/api/tests/test_escalation_policy.py +++ b/engine/apps/api/tests/test_escalation_policy.py @@ -9,7 +9,7 @@ from rest_framework.response import Response from rest_framework.test import APIClient from apps.alerts.models import EscalationPolicy -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.fixture() @@ -93,9 +93,9 @@ def test_move_to_position(escalation_policy_internal_api_setup, make_user_auth_h @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_escalation_policy_create_permissions( @@ -130,9 +130,9 @@ def test_escalation_policy_create_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_escalation_policy_update_permissions( @@ -171,9 +171,9 @@ def test_escalation_policy_update_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_escalation_policy_list_permissions( @@ -208,9 +208,9 @@ def test_escalation_policy_list_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_escalation_policy_retrieve_permissions( @@ -245,9 +245,9 @@ def test_escalation_policy_retrieve_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_escalation_policy_delete_permissions( @@ -282,9 +282,9 @@ def test_escalation_policy_delete_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_escalation_policy_escalation_options_permissions( @@ -319,9 +319,9 @@ def test_escalation_policy_escalation_options_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_escalation_policy_delay_options_permissions( @@ -357,9 +357,9 @@ def test_escalation_policy_delay_options_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_escalation_policy_move_to_position_permissions( diff --git a/engine/apps/api/tests/test_gitops.py b/engine/apps/api/tests/test_gitops.py index ca196433..0152f5e3 100644 --- a/engine/apps/api/tests/test_gitops.py +++ b/engine/apps/api/tests/test_gitops.py @@ -3,16 +3,16 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_terraform_gitops_permissions( @@ -22,7 +22,7 @@ def test_terraform_gitops_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role) + organization, user, token = make_organization_and_user_with_plugin_token(role=role) make_escalation_chain(organization) client = APIClient() @@ -38,15 +38,15 @@ def test_terraform_gitops_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_terraform_state_permissions( make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status ): - _, user, token = make_organization_and_user_with_plugin_token(role) + _, user, token = make_organization_and_user_with_plugin_token(role=role) client = APIClient() url = reverse("api-internal:terraform_imports") diff --git a/engine/apps/api/tests/test_integration_heartbeat.py b/engine/apps/api/tests/test_integration_heartbeat.py index 048b5121..8954b74a 100644 --- a/engine/apps/api/tests/test_integration_heartbeat.py +++ b/engine/apps/api/tests/test_integration_heartbeat.py @@ -8,8 +8,8 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient +from apps.api.permissions import LegacyAccessControlRole from apps.heartbeat.models import IntegrationHeartBeat -from common.constants.role import Role MOCK_LAST_HEARTBEAT_TIME_VERBAL = "a moment" @@ -151,7 +151,7 @@ def test_create_empty_alert_receive_channel_integration_heartbeat( integration_heartbeat_internal_api_setup, make_user_auth_headers, ): - user, token, alert_receive_channel, integration_heartbeat = integration_heartbeat_internal_api_setup + user, token, _, _ = integration_heartbeat_internal_api_setup client = APIClient() url = reverse("api-internal:integration_heartbeat-list") @@ -185,9 +185,39 @@ def test_update_integration_heartbeat( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), + ], +) +def test_integration_heartbeat_create_permissions( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + role, + expected_status, +): + _, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + + url = reverse("api-internal:integration_heartbeat-list") + + with patch( + "apps.api.views.integration_heartbeat.IntegrationHeartBeatView.create", + return_value=Response( + status=status.HTTP_200_OK, + ), + ): + response = client.post(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_integration_heartbeat_update_permissions( @@ -223,7 +253,11 @@ def test_integration_heartbeat_update_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_integration_heartbeat_list_permissions( make_organization_and_user_with_plugin_token, @@ -255,9 +289,40 @@ def test_integration_heartbeat_list_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], +) +def test_integration_heartbeat_timeout_options_permissions( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + role, + expected_status, +): + _, user, token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + + url = reverse("api-internal:integration_heartbeat-timeout-options") + + with patch( + "apps.api.views.integration_heartbeat.IntegrationHeartBeatView.timeout_options", + return_value=Response( + status=status.HTTP_200_OK, + ), + ): + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_integration_heartbeat_retrieve_permissions( diff --git a/engine/apps/api/tests/test_maintenance.py b/engine/apps/api/tests/test_maintenance.py index dc140d67..6441d063 100644 --- a/engine/apps/api/tests/test_maintenance.py +++ b/engine/apps/api/tests/test_maintenance.py @@ -6,6 +6,8 @@ from rest_framework.test import APIClient from apps.alerts.models import AlertReceiveChannel from apps.user_management.models import Organization +# TODO: should probably modify these tests to take into account new rbac permissions + @pytest.fixture() def maintenance_internal_api_setup( @@ -23,7 +25,7 @@ def maintenance_internal_api_setup( def test_start_maintenance_integration( maintenance_internal_api_setup, mock_start_disable_maintenance_task, make_user_auth_headers ): - token, organization, user, alert_receive_channel = maintenance_internal_api_setup + token, _, user, alert_receive_channel = maintenance_internal_api_setup client = APIClient() url = reverse("api-internal:start_maintenance") @@ -50,7 +52,7 @@ def test_stop_maintenance_integration( mock_start_disable_maintenance_task, make_user_auth_headers, ): - token, organization, user, alert_receive_channel = maintenance_internal_api_setup + token, _, user, alert_receive_channel = maintenance_internal_api_setup client = APIClient() mode = AlertReceiveChannel.MAINTENANCE duration = AlertReceiveChannel.DURATION_ONE_HOUR.seconds @@ -161,7 +163,7 @@ def test_maintenances_list( def test_empty_maintenances_list( maintenance_internal_api_setup, mock_start_disable_maintenance_task, make_user_auth_headers ): - token, organization, user, alert_receive_channel = maintenance_internal_api_setup + token, _, user, alert_receive_channel = maintenance_internal_api_setup client = APIClient() url = reverse("api-internal:maintenance") response = client.get(url, format="json", **make_user_auth_headers(user, token)) diff --git a/engine/apps/api/tests/test_oncall_shift.py b/engine/apps/api/tests/test_oncall_shift.py index 8d5db17f..0235775d 100644 --- a/engine/apps/api/tests/test_oncall_shift.py +++ b/engine/apps/api/tests/test_oncall_shift.py @@ -7,8 +7,8 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient +from apps.api.permissions import LegacyAccessControlRole from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleWeb -from common.constants.role import Role @pytest.fixture() @@ -26,7 +26,7 @@ def on_call_shift_internal_api_setup( @pytest.mark.django_db def test_create_on_call_shift_rotation(on_call_shift_internal_api_setup, make_user_auth_headers): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -58,7 +58,7 @@ def test_create_on_call_shift_rotation(on_call_shift_internal_api_setup, make_us @pytest.mark.django_db def test_create_on_call_shift_override(on_call_shift_internal_api_setup, make_user_auth_headers): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -98,7 +98,7 @@ def test_get_on_call_shift( make_on_call_shift, make_user_auth_headers, ): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = timezone.now().replace(microsecond=0) @@ -144,7 +144,7 @@ def test_list_on_call_shift( make_on_call_shift, make_user_auth_headers, ): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = timezone.now().replace(microsecond=0) @@ -270,7 +270,7 @@ def test_update_future_on_call_shift( make_user_auth_headers, ): """Test updating the shift that has not started (rotation_start > now)""" - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() + timezone.timedelta(days=1)).replace(microsecond=0) @@ -337,7 +337,7 @@ def test_update_started_on_call_shift( ): """Test updating the shift that has started (rotation_start < now)""" - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) @@ -409,7 +409,7 @@ def test_update_old_on_call_shift_with_future_version( make_user_auth_headers, ): """Test updating the shift that has the newer version (updated_shift is not None)""" - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() now = timezone.now().replace(microsecond=0) @@ -498,7 +498,7 @@ def test_update_started_on_call_shift_title( ): """Test updating the title for the shift that has started (rotation_start < now)""" - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) @@ -560,7 +560,7 @@ def test_delete_started_on_call_shift( ): """Test deleting the shift that has started (rotation_start < now)""" - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() - timezone.timedelta(hours=1)).replace(microsecond=0) @@ -598,7 +598,7 @@ def test_delete_future_on_call_shift( ): """Test deleting the shift that has not started (rotation_start > now)""" - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() start_date = (timezone.now() + timezone.timedelta(days=1)).replace(microsecond=0) @@ -631,7 +631,7 @@ def test_create_on_call_shift_invalid_data_rotation_start( on_call_shift_internal_api_setup, make_user_auth_headers, ): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -660,7 +660,7 @@ def test_create_on_call_shift_invalid_data_rotation_start( @pytest.mark.django_db def test_create_on_call_shift_invalid_data_until(on_call_shift_internal_api_setup, make_user_auth_headers): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -713,7 +713,7 @@ def test_create_on_call_shift_invalid_data_until(on_call_shift_internal_api_setu @pytest.mark.django_db def test_create_on_call_shift_invalid_data_by_day(on_call_shift_internal_api_setup, make_user_auth_headers): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -763,7 +763,7 @@ def test_create_on_call_shift_invalid_data_by_day(on_call_shift_internal_api_set @pytest.mark.django_db def test_create_on_call_shift_invalid_data_interval(on_call_shift_internal_api_setup, make_user_auth_headers): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -813,7 +813,7 @@ def test_create_on_call_shift_invalid_data_interval(on_call_shift_internal_api_s @pytest.mark.django_db def test_create_on_call_shift_invalid_data_shift_end(on_call_shift_internal_api_setup, make_user_auth_headers): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -866,7 +866,7 @@ def test_create_on_call_shift_invalid_data_rolling_users( on_call_shift_internal_api_setup, make_user_auth_headers, ): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, user2, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -894,7 +894,7 @@ def test_create_on_call_shift_invalid_data_rolling_users( @pytest.mark.django_db def test_create_on_call_shift_override_invalid_data(on_call_shift_internal_api_setup, make_user_auth_headers): - token, user1, user2, organization, schedule = on_call_shift_internal_api_setup + token, user1, _, _, schedule = on_call_shift_internal_api_setup client = APIClient() url = reverse("api-internal:oncall_shifts-list") start_date = timezone.now().replace(microsecond=0, tzinfo=None) @@ -925,9 +925,9 @@ def test_create_on_call_shift_override_invalid_data(on_call_shift_internal_api_s @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_201_CREATED), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_create_permissions( @@ -936,7 +936,7 @@ def test_on_call_shift_create_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() @@ -957,9 +957,9 @@ def test_on_call_shift_create_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_update_permissions( @@ -1005,9 +1005,9 @@ def test_on_call_shift_update_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_on_call_shift_list_permissions( @@ -1016,7 +1016,7 @@ def test_on_call_shift_list_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:oncall_shifts-list") @@ -1036,9 +1036,9 @@ def test_on_call_shift_list_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_on_call_shift_retrieve_permissions( @@ -1079,9 +1079,9 @@ def test_on_call_shift_retrieve_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_delete_permissions( @@ -1122,9 +1122,9 @@ def test_on_call_shift_delete_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_on_call_shift_frequency_options_permissions( @@ -1153,9 +1153,9 @@ def test_on_call_shift_frequency_options_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_on_call_shift_days_options_permissions( @@ -1184,9 +1184,9 @@ def test_on_call_shift_days_options_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_on_call_shift_preview_permissions( diff --git a/engine/apps/api/tests/test_organization.py b/engine/apps/api/tests/test_organization.py index ed13fb2c..518c9f5a 100644 --- a/engine/apps/api/tests/test_organization.py +++ b/engine/apps/api/tests/test_organization.py @@ -6,30 +6,25 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_current_team_retrieve_permissions( - make_organization, - make_user_for_organization, - make_token_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - org = make_organization() - tester = make_user_for_organization(org, role=role) - _, token = make_token_for_organization(org) - + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:api-current-team") @@ -48,23 +43,18 @@ def test_current_team_retrieve_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_current_team_update_permissions( - make_organization, - make_user_for_organization, - make_token_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - org = make_organization() - tester = make_user_for_organization(org, role=role) - _, token = make_token_for_organization(org) - + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:api-current-team") @@ -84,9 +74,9 @@ def test_current_team_update_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_current_team_get_telegram_verification_code_permissions( @@ -95,8 +85,7 @@ def test_current_team_get_telegram_verification_code_permissions( role, expected_status, ): - organization, tester, token = make_organization_and_user_with_plugin_token(role) - + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:api-get-telegram-verification-code") @@ -109,9 +98,9 @@ def test_current_team_get_telegram_verification_code_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_current_team_get_channel_verification_code_permissions( @@ -120,8 +109,7 @@ def test_current_team_get_channel_verification_code_permissions( role, expected_status, ): - organization, tester, token = make_organization_and_user_with_plugin_token(role) - + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:api-get-channel-verification-code") + "?backend=TESTONLY" @@ -135,8 +123,7 @@ def test_current_team_get_channel_verification_code_ok( make_organization_and_user_with_plugin_token, make_user_auth_headers, ): - organization, tester, token = make_organization_and_user_with_plugin_token(Role.ADMIN) - + organization, tester, token = make_organization_and_user_with_plugin_token() client = APIClient() url = reverse("api-internal:api-get-channel-verification-code") + "?backend=TESTONLY" @@ -156,8 +143,7 @@ def test_current_team_get_channel_verification_code_invalid( make_organization_and_user_with_plugin_token, make_user_auth_headers, ): - organization, tester, token = make_organization_and_user_with_plugin_token(Role.ADMIN) - + _, tester, token = make_organization_and_user_with_plugin_token() client = APIClient() url = reverse("api-internal:api-get-channel-verification-code") + "?backend=INVALID" diff --git a/engine/apps/api/tests/test_postmortem_messages.py b/engine/apps/api/tests/test_postmortem_messages.py index fe45ded0..e2877ce0 100644 --- a/engine/apps/api/tests/test_postmortem_messages.py +++ b/engine/apps/api/tests/test_postmortem_messages.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from rest_framework.test import APIClient from apps.alerts.models import ResolutionNote -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @@ -212,9 +212,9 @@ def test_delete_resolution_note( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_resolution_note_create_permissions( @@ -224,7 +224,7 @@ def test_resolution_note_create_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:resolution_note-list") @@ -245,9 +245,9 @@ def test_resolution_note_create_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_resolution_note_update_permissions( @@ -260,7 +260,7 @@ def test_resolution_note_update_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) resolution_note = make_resolution_note( @@ -289,9 +289,9 @@ def test_resolution_note_update_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_204_NO_CONTENT), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_resolution_note_delete_permissions( @@ -304,7 +304,7 @@ def test_resolution_note_delete_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) resolution_note = make_resolution_note( @@ -331,9 +331,9 @@ def test_resolution_note_delete_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_resolution_note_list_permissions( @@ -343,7 +343,7 @@ def test_resolution_note_list_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:resolution_note-list") @@ -363,9 +363,9 @@ def test_resolution_note_list_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_resolution_note_detail_permissions( @@ -378,7 +378,7 @@ def test_resolution_note_detail_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) resolution_note = make_resolution_note( diff --git a/engine/apps/api/tests/test_public_api_tokens.py b/engine/apps/api/tests/test_public_api_tokens.py new file mode 100644 index 00000000..54987e98 --- /dev/null +++ b/engine/apps/api/tests/test_public_api_tokens.py @@ -0,0 +1,115 @@ +import pytest +from django.urls import reverse +from rest_framework import status +from rest_framework.test import APIClient + +from apps.api.permissions import LegacyAccessControlRole + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], +) +def test_public_api_tokens_retrieve_permissions( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + make_public_api_token, + role, + expected_status, +): + organization, user, plugin_token = make_organization_and_user_with_plugin_token(role) + api_token, _ = make_public_api_token(user, organization) + client = APIClient() + + url = reverse("api-internal:api_token-detail", kwargs={"pk": api_token.id}) + response = client.get(url, format="json", **make_user_auth_headers(user, plugin_token)) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], +) +def test_public_api_tokens_list_permissions( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + make_public_api_token, + role, + expected_status, +): + organization, user, plugin_token = make_organization_and_user_with_plugin_token(role) + make_public_api_token(user, organization) + client = APIClient() + + url = reverse("api-internal:api_token-list") + response = client.get(url, format="json", **make_user_auth_headers(user, plugin_token)) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED), + (LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), + ], +) +def test_public_api_tokens_create_permissions( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + role, + expected_status, +): + _, user, plugin_token = make_organization_and_user_with_plugin_token(role) + client = APIClient() + + url = reverse("api-internal:api_token-list") + response = client.post( + url, + data={ + "name": "helloooo", + }, + format="json", + **make_user_auth_headers(user, plugin_token), + ) + + assert response.status_code == expected_status + + +@pytest.mark.django_db +@pytest.mark.parametrize( + "role,expected_status", + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), + ], +) +def test_public_api_tokens_delete_permissions( + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + make_public_api_token, + role, + expected_status, +): + organization, user, plugin_token = make_organization_and_user_with_plugin_token(role) + api_token, _ = make_public_api_token(user, organization) + client = APIClient() + + url = reverse("api-internal:api_token-detail", kwargs={"pk": api_token.id}) + response = client.delete(url, format="json", **make_user_auth_headers(user, plugin_token)) + + assert response.status_code == expected_status diff --git a/engine/apps/api/tests/test_schedule_export.py b/engine/apps/api/tests/test_schedule_export.py index ecbdec7e..02dc6a5b 100644 --- a/engine/apps/api/tests/test_schedule_export.py +++ b/engine/apps/api/tests/test_schedule_export.py @@ -3,9 +3,9 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient +from apps.api.permissions import LegacyAccessControlRole from apps.auth_token.models import ScheduleExportAuthToken from apps.schedules.models import OnCallScheduleICal -from common.constants.role import Role ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano72p69rt78%40group.calendar.google.com/private-1d00a680ba5be7426c3eb3ef1616e26d/basic.ics" # noqa @@ -14,9 +14,9 @@ ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_get_schedule_export_token( @@ -26,8 +26,7 @@ def test_get_schedule_export_token( role, expected_status, ): - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -50,9 +49,9 @@ def test_get_schedule_export_token( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_404_NOT_FOUND), - (Role.EDITOR, status.HTTP_404_NOT_FOUND), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND), + (LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_export_token_not_found( @@ -62,8 +61,7 @@ def test_schedule_export_token_not_found( role, expected_status, ): - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -84,9 +82,9 @@ def test_schedule_export_token_not_found( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_201_CREATED), - (Role.EDITOR, status.HTTP_201_CREATED), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED), + (LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_create_export_token( @@ -96,8 +94,7 @@ def test_schedule_create_export_token( role, expected_status, ): - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -118,9 +115,9 @@ def test_schedule_create_export_token( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_204_NO_CONTENT), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_delete_export_token( @@ -130,8 +127,7 @@ def test_schedule_delete_export_token( role, expected_status, ): - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, diff --git a/engine/apps/api/tests/test_schedules.py b/engine/apps/api/tests/test_schedules.py index 9c73027d..61e241fb 100644 --- a/engine/apps/api/tests/test_schedules.py +++ b/engine/apps/api/tests/test_schedules.py @@ -10,6 +10,7 @@ from rest_framework.serializers import ValidationError from rest_framework.test import APIClient from apps.alerts.models import EscalationPolicy +from apps.api.permissions import LegacyAccessControlRole from apps.schedules.ical_utils import memoized_users_in_ical from apps.schedules.models import ( CustomOnCallShift, @@ -18,7 +19,6 @@ from apps.schedules.models import ( OnCallScheduleICal, OnCallScheduleWeb, ) -from common.constants.role import Role ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano72p69rt78%40group.calendar.google.com/private-1d00a680ba5be7426c3eb3ef1616e26d/basic.ics" @@ -1062,7 +1062,7 @@ def test_merging_same_shift_events( user_a = make_user_for_organization(organization) user_b = make_user_for_organization(organization) - user_c = make_user_for_organization(organization, role=Role.VIEWER) + user_c = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) # clear users pks <-> organization cache (persisting between tests) memoized_users_in_ical.cache_clear() @@ -1158,9 +1158,9 @@ def test_filter_events_invalid_type( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_create_permissions( @@ -1170,7 +1170,7 @@ def test_schedule_create_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -1196,9 +1196,9 @@ def test_schedule_create_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_update_permissions( @@ -1208,7 +1208,7 @@ def test_schedule_update_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -1237,7 +1237,11 @@ def test_schedule_update_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_schedule_list_permissions( make_organization_and_user_with_plugin_token, @@ -1246,7 +1250,7 @@ def test_schedule_list_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -1271,7 +1275,11 @@ def test_schedule_list_permissions( @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", - [(Role.ADMIN, status.HTTP_200_OK), (Role.EDITOR, status.HTTP_200_OK), (Role.VIEWER, status.HTTP_200_OK)], + [ + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), + ], ) def test_schedule_retrieve_permissions( make_organization_and_user_with_plugin_token, @@ -1280,7 +1288,7 @@ def test_schedule_retrieve_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -1306,9 +1314,9 @@ def test_schedule_retrieve_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_schedule_delete_permissions( @@ -1318,7 +1326,7 @@ def test_schedule_delete_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -1344,9 +1352,9 @@ def test_schedule_delete_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_events_permissions( @@ -1356,7 +1364,7 @@ def test_events_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -1382,9 +1390,9 @@ def test_events_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_reload_ical_permissions( @@ -1394,7 +1402,7 @@ def test_reload_ical_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) schedule = make_schedule( organization, schedule_class=OnCallScheduleICal, @@ -1420,9 +1428,9 @@ def test_reload_ical_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_schedule_notify_oncall_shift_freq_options_permissions( @@ -1432,7 +1440,7 @@ def test_schedule_notify_oncall_shift_freq_options_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:schedule-notify-oncall-shift-freq-options") client = APIClient() response = client.get(url, format="json", **make_user_auth_headers(user, token)) @@ -1444,9 +1452,9 @@ def test_schedule_notify_oncall_shift_freq_options_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_schedule_notify_empty_oncall_options_permissions( @@ -1456,7 +1464,7 @@ def test_schedule_notify_empty_oncall_options_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:schedule-notify-empty-oncall-options") client = APIClient() response = client.get(url, format="json", **make_user_auth_headers(user, token)) @@ -1468,9 +1476,9 @@ def test_schedule_notify_empty_oncall_options_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_schedule_mention_options_permissions( @@ -1480,7 +1488,7 @@ def test_schedule_mention_options_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:schedule-mention-options") client = APIClient() response = client.get(url, format="json", **make_user_auth_headers(user, token)) diff --git a/engine/apps/api/tests/test_set_general_log_channel.py b/engine/apps/api/tests/test_set_general_log_channel.py index 703dd324..cdcce180 100644 --- a/engine/apps/api/tests/test_set_general_log_channel.py +++ b/engine/apps/api/tests/test_set_general_log_channel.py @@ -6,7 +6,7 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole # Testing permissions, not view itself. So mock is ok here @@ -14,13 +14,16 @@ from common.constants.role import Role @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_set_general_log_channel_permissions( - make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + role, + expected_status, ): _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() diff --git a/engine/apps/api/tests/test_slack_channels.py b/engine/apps/api/tests/test_slack_channels.py index 37d2c05c..70a083b1 100644 --- a/engine/apps/api/tests/test_slack_channels.py +++ b/engine/apps/api/tests/test_slack_channels.py @@ -6,20 +6,23 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_slack_channels_list_permissions( - make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + role, + expected_status, ): _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() @@ -40,13 +43,17 @@ def test_slack_channels_list_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_slack_channels_detail_permissions( - make_organization_and_user_with_plugin_token, make_user_auth_headers, role, make_slack_channel, expected_status + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + make_slack_channel, + role, + expected_status, ): organization, user, token = make_organization_and_user_with_plugin_token(role) slack_channel = make_slack_channel(organization.slack_team_identity) diff --git a/engine/apps/api/tests/test_slack_team_settings.py b/engine/apps/api/tests/test_slack_team_settings.py index 31df5d83..9de0e2ba 100644 --- a/engine/apps/api/tests/test_slack_team_settings.py +++ b/engine/apps/api/tests/test_slack_team_settings.py @@ -6,16 +6,16 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_get_slack_settings_permissions( @@ -24,7 +24,7 @@ def test_get_slack_settings_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:slack-settings") @@ -43,9 +43,9 @@ def test_get_slack_settings_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_update_slack_settings_permissions( @@ -54,7 +54,7 @@ def test_update_slack_settings_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:slack-settings") @@ -73,9 +73,9 @@ def test_update_slack_settings_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_get_acknowledge_remind_options_permissions( @@ -84,7 +84,7 @@ def test_get_acknowledge_remind_options_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:acknowledge-reminder-options") @@ -103,9 +103,9 @@ def test_get_acknowledge_remind_options_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_get_unacknowledge_timeout_options_permissions( @@ -114,7 +114,7 @@ def test_get_unacknowledge_timeout_options_permissions( role, expected_status, ): - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:unacknowledge-timeout-options") diff --git a/engine/apps/api/tests/test_subscription.py b/engine/apps/api/tests/test_subscription.py index ef61c949..2753784b 100644 --- a/engine/apps/api/tests/test_subscription.py +++ b/engine/apps/api/tests/test_subscription.py @@ -6,16 +6,16 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_subscription_retrieve_permissions( diff --git a/engine/apps/api/tests/test_team.py b/engine/apps/api/tests/test_team.py index 40df30d8..acdfaddb 100644 --- a/engine/apps/api/tests/test_team.py +++ b/engine/apps/api/tests/test_team.py @@ -3,9 +3,9 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient +from apps.api.permissions import LegacyAccessControlRole from apps.schedules.models import OnCallScheduleCalendar from apps.user_management.models import Team -from common.constants.role import Role GENERAL_TEAM = Team(public_primary_key=None, name="General", email=None, avatar_url=None) @@ -64,28 +64,24 @@ def test_list_teams_for_non_member( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_list_teams_permissions( - make_organization, - make_token_for_organization, - make_user_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - _, token = make_token_for_organization(organization) - user = make_user_for_organization(organization, role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:team-list") response = client.get(url, format="json", **make_user_auth_headers(user, token)) - assert response.status_code == status.HTTP_200_OK + assert response.status_code == expected_status @pytest.mark.django_db diff --git a/engine/apps/api/tests/test_telegram_channel.py b/engine/apps/api/tests/test_telegram_channel.py index 6bf26b9c..fa340ec4 100644 --- a/engine/apps/api/tests/test_telegram_channel.py +++ b/engine/apps/api/tests/test_telegram_channel.py @@ -3,14 +3,14 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db def test_not_authorized(make_organization_and_user_with_plugin_token, make_telegram_channel): client = APIClient() - organization, user, _ = make_organization_and_user_with_plugin_token() + organization, _, _ = make_organization_and_user_with_plugin_token() telegram_channel = make_telegram_channel(organization=organization) url = reverse("api-internal:telegram_channel-list") @@ -34,9 +34,9 @@ def test_not_authorized(make_organization_and_user_with_plugin_token, make_teleg @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_list_telegram_channels_permissions( @@ -46,8 +46,7 @@ def test_list_telegram_channels_permissions( expected_status, ): client = APIClient() - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:telegram_channel-list") response = client.get(url, **make_user_auth_headers(user, token)) @@ -59,9 +58,9 @@ def test_list_telegram_channels_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_get_telegram_channels_permissions( @@ -72,8 +71,7 @@ def test_get_telegram_channels_permissions( expected_status, ): client = APIClient() - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) telegram_channel = make_telegram_channel(organization=organization) url = reverse("api-internal:telegram_channel-detail", kwargs={"pk": telegram_channel.public_primary_key}) @@ -86,9 +84,9 @@ def test_get_telegram_channels_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_delete_telegram_channels_permissions( @@ -100,7 +98,7 @@ def test_delete_telegram_channels_permissions( ): client = APIClient() - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) telegram_channel = make_telegram_channel(organization=organization) url = reverse("api-internal:telegram_channel-detail", kwargs={"pk": telegram_channel.public_primary_key}) @@ -113,9 +111,9 @@ def test_delete_telegram_channels_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_set_default_telegram_channels_permissions( @@ -127,8 +125,7 @@ def test_set_default_telegram_channels_permissions( ): client = APIClient() - organization, user, token = make_organization_and_user_with_plugin_token(role=role) - + organization, user, token = make_organization_and_user_with_plugin_token(role) telegram_channel = make_telegram_channel(organization=organization) url = reverse("api-internal:telegram_channel-set-default", kwargs={"pk": telegram_channel.public_primary_key}) diff --git a/engine/apps/api/tests/test_terraform_renderer.py b/engine/apps/api/tests/test_terraform_renderer.py index 16e5f654..ffa33155 100644 --- a/engine/apps/api/tests/test_terraform_renderer.py +++ b/engine/apps/api/tests/test_terraform_renderer.py @@ -18,7 +18,7 @@ def test_get_terraform_file( @pytest.mark.django_db def test_get_terraform_imports(make_organization_and_user_with_plugin_token, make_user_auth_headers): - organization, user, token = make_organization_and_user_with_plugin_token() + _, user, token = make_organization_and_user_with_plugin_token() client = APIClient() url = reverse("api-internal:terraform_imports") response = client.get(url, format="text/plain", **make_user_auth_headers(user, token)) diff --git a/engine/apps/api/tests/test_user.py b/engine/apps/api/tests/test_user.py index ee7f9809..ee8c74cc 100644 --- a/engine/apps/api/tests/test_user.py +++ b/engine/apps/api/tests/test_user.py @@ -8,10 +8,9 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient -from apps.base.constants import ADMIN_PERMISSIONS, EDITOR_PERMISSIONS +from apps.api.permissions import DONT_USE_LEGACY_PERMISSION_MAPPING, LegacyAccessControlRole from apps.base.models import UserNotificationPolicy from apps.user_management.models.user import default_working_hours -from common.constants.role import Role @pytest.mark.django_db @@ -81,7 +80,7 @@ def test_update_user_cant_change_email_and_username( } }, "cloud_connection_status": 0, - "permissions": ADMIN_PERMISSIONS, + "permissions": DONT_USE_LEGACY_PERMISSION_MAPPING[admin.role], "notification_chain_verbal": {"default": "", "important": ""}, "slack_user_identity": None, "avatar": admin.avatar_url, @@ -101,7 +100,7 @@ def test_list_users( ): organization = make_organization() admin = make_user_for_organization(organization) - editor = make_user_for_organization(organization, role=Role.EDITOR) + editor = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) _, token = make_token_for_organization(organization) client = APIClient() @@ -131,7 +130,7 @@ def test_list_users( "user": admin.username, } }, - "permissions": ADMIN_PERMISSIONS, + "permissions": DONT_USE_LEGACY_PERMISSION_MAPPING[admin.role], "notification_chain_verbal": {"default": "", "important": ""}, "slack_user_identity": None, "avatar": admin.avatar_url, @@ -157,7 +156,7 @@ def test_list_users( "user": editor.username, } }, - "permissions": EDITOR_PERMISSIONS, + "permissions": DONT_USE_LEGACY_PERMISSION_MAPPING[editor.role], "notification_chain_verbal": {"default": "", "important": ""}, "slack_user_identity": None, "avatar": editor.avatar_url, @@ -235,22 +234,18 @@ def test_notification_chain_verbal( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_update_self_permissions( - make_organization, - make_user_for_organization, - make_token_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": tester.public_primary_key}) with patch( @@ -268,23 +263,20 @@ def test_user_update_self_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_update_other_permissions( - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, role, expected_status, ): - organization = make_organization() + organization, tester, token = make_organization_and_user_with_plugin_token(role) admin = make_user_for_organization(organization) - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": admin.public_primary_key}) @@ -299,22 +291,18 @@ def test_user_update_other_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_list_permissions( - make_organization, - make_user_for_organization, - make_token_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:user-list") @@ -333,22 +321,18 @@ def test_user_list_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_user_detail_self_permissions( - make_organization, - make_user_for_organization, - make_token_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": tester.public_primary_key}) @@ -367,23 +351,20 @@ def test_user_detail_self_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_detail_other_permissions( - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, role, expected_status, ): - organization = make_organization() + organization, tester, token = make_organization_and_user_with_plugin_token(role) admin = make_user_for_organization(organization) - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": admin.public_primary_key}) @@ -396,22 +377,18 @@ def test_user_detail_other_permissions( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_get_own_verification_code( - make_organization, - make_user_for_organization, - make_token_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:user-get-verification-code", kwargs={"pk": tester.public_primary_key}) @@ -430,23 +407,20 @@ def test_user_get_own_verification_code( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_get_other_verification_code( - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, role, expected_status, ): - organization = make_organization() + organization, tester, token = make_organization_and_user_with_plugin_token(role) admin = make_user_for_organization(organization) - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) client = APIClient() url = reverse("api-internal:user-get-verification-code", kwargs={"pk": admin.public_primary_key}) @@ -460,22 +434,18 @@ def test_user_get_other_verification_code( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_verify_own_phone( - make_organization, - make_user_for_organization, - make_token_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:user-verify-number", kwargs={"pk": tester.public_primary_key}) @@ -499,23 +469,20 @@ Tests below are outdated @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_verify_another_phone( - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=role) - other_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, tester, token = make_organization_and_user_with_plugin_token(role) + other_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-verify-number", kwargs={"pk": other_user.public_primary_key}) @@ -530,22 +497,18 @@ def test_user_verify_another_phone( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_get_own_telegram_verification_code( - make_organization, - make_user_for_organization, - make_token_for_organization, + make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=role) - _, token = make_token_for_organization(organization) + _, tester, token = make_organization_and_user_with_plugin_token(role) client = APIClient() url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": tester.public_primary_key}) @@ -558,23 +521,20 @@ def test_user_get_own_telegram_verification_code( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_get_another_telegram_verification_code( - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, role, expected_status, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=role) - other_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, tester, token = make_organization_and_user_with_plugin_token(role) + other_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": other_user.public_primary_key}) @@ -585,270 +545,16 @@ def test_user_get_another_telegram_verification_code( @pytest.mark.django_db def test_admin_can_update_user( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, ): - organization = make_organization() - tester = make_user_for_organization(organization, role=Role.ADMIN) - other_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token() + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.ADMIN) client = APIClient() data = { "email": "test@amixr.io", - "role": Role.ADMIN, - "username": "updated_test_username", - "unverified_phone_number": "+1234567890", - "slack_login": "", - } - url = reverse("api-internal:user-detail", kwargs={"pk": other_user.public_primary_key}) - response = client.put(url, format="json", data=data, **make_user_auth_headers(tester, token)) - - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_admin_can_update_himself( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - _, token = make_token_for_organization(organization) - - client = APIClient() - data = { - "email": "test@amixr.io", - "role": Role.ADMIN, - "username": "updated_test_username", - "unverified_phone_number": "+1234567890", - "slack_login": "", - } - - url = reverse("api-internal:user-detail", kwargs={"pk": admin.public_primary_key}) - response = client.put(url, format="json", data=data, **make_user_auth_headers(admin, token)) - - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_admin_can_list_users( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - _, token = make_token_for_organization(organization) - - client = APIClient() - - url = reverse("api-internal:user-list") - response = client.get(url, format="json", **make_user_auth_headers(admin, token)) - - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_admin_can_detail_users( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - editor = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) - - client = APIClient() - - url = reverse("api-internal:user-detail", kwargs={"pk": editor.public_primary_key}) - response = client.get(url, format="json", **make_user_auth_headers(admin, token)) - - assert response.status_code == status.HTTP_200_OK - - -@patch("apps.twilioapp.phone_manager.PhoneManager.send_verification_code", return_value=Mock()) -@pytest.mark.django_db -def test_admin_can_get_own_verification_code( - mock_verification_start, - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = reverse("api-internal:user-get-verification-code", kwargs={"pk": admin.public_primary_key}) - - response = client.get(url, format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - - -@patch("apps.twilioapp.phone_manager.PhoneManager.send_verification_code", return_value=Mock()) -@pytest.mark.django_db -def test_admin_can_get_another_user_verification_code( - mock_verification_start, - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - editor = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = reverse("api-internal:user-get-verification-code", kwargs={"pk": editor.public_primary_key}) - response = client.get(url, format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - - -@patch("apps.twilioapp.phone_manager.PhoneManager.verify_phone_number", return_value=(True, None)) -@pytest.mark.django_db -def test_admin_can_verify_own_phone( - mocked_verification_check, - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = reverse("api-internal:user-verify-number", kwargs={"pk": admin.public_primary_key}) - - response = client.put(f"{url}?token=12345", format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - - -@patch("apps.twilioapp.phone_manager.PhoneManager.verify_phone_number", return_value=(True, None)) -@pytest.mark.django_db -def test_admin_can_verify_another_user_phone( - mocked_verification_check, - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - editor = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = reverse("api-internal:user-verify-number", kwargs={"pk": editor.public_primary_key}) - - response = client.put(f"{url}?token=12345", format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_admin_can_get_own_telegram_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": admin.public_primary_key}) - - response = client.get(url, format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_admin_can_get_another_user_telegram_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - editor = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": editor.public_primary_key}) - - response = client.get(url, format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_admin_can_get_another_user_backend_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - editor = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = ( - reverse("api-internal:user-get-backend-verification-code", kwargs={"pk": editor.public_primary_key}) - + "?backend=TESTONLY" - ) - - response = client.get(url, format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_admin_can_unlink_another_user_backend_account( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - editor = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = reverse("api-internal:user-unlink-backend", kwargs={"pk": editor.public_primary_key}) + "?backend=TESTONLY" - - response = client.post(url, format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - - -@pytest.mark.django_db -def test_admin_can_unlink_another_user_slack_account( - make_organization_with_slack_team_identity, - make_user_for_organization, - make_user_with_slack_user_identity, - make_token_for_organization, - make_user_auth_headers, -): - organization, slack_team_identity = make_organization_with_slack_team_identity() - admin = make_user_for_organization(organization, role=Role.ADMIN) - editor, slack_user_identity_1 = make_user_with_slack_user_identity( - slack_team_identity, organization, slack_id="user_1", role=Role.EDITOR - ) - - _, token = make_token_for_organization(organization) - client = APIClient() - url = reverse("api-internal:user-unlink-slack", kwargs={"pk": editor.public_primary_key}) - - response = client.post(url, format="json", **make_user_auth_headers(admin, token)) - assert response.status_code == status.HTTP_200_OK - editor.refresh_from_db() - assert editor.slack_user_identity is None - - -"""Test user permissions""" - - -@pytest.mark.django_db -def test_user_cant_update_user( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) - - client = APIClient() - data = { - "email": "test@amixr.io", - "role": Role.ADMIN, "username": "updated_test_username", "unverified_phone_number": "+1234567890", "slack_login": "", @@ -856,21 +562,16 @@ def test_user_cant_update_user( url = reverse("api-internal:user-detail", kwargs={"pk": first_user.public_primary_key}) response = client.put(url, format="json", data=data, **make_user_auth_headers(second_user, token)) - assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.status_code == status.HTTP_200_OK @pytest.mark.django_db -def test_user_can_update_themself( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) +def test_admin_can_update_himself(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.ADMIN) client = APIClient() data = { "email": "test@amixr.io", - "role": Role.EDITOR, "username": "updated_test_username", "unverified_phone_number": "+1234567890", "slack_login": "", @@ -883,49 +584,267 @@ def test_user_can_update_themself( @pytest.mark.django_db -def test_user_can_list_users( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - editor = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) +def test_admin_can_list_users(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.ADMIN) client = APIClient() url = reverse("api-internal:user-list") - response = client.get(url, format="json", **make_user_auth_headers(editor, token)) + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_admin_can_detail_users( + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, +): + organization, first_user, token = make_organization_and_user_with_plugin_token() + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + + url = reverse("api-internal:user-detail", kwargs={"pk": first_user.public_primary_key}) + response = client.get(url, format="json", **make_user_auth_headers(second_user, token)) + + assert response.status_code == status.HTTP_200_OK + + +@patch("apps.twilioapp.phone_manager.PhoneManager.send_verification_code", return_value=Mock()) +@pytest.mark.django_db +def test_admin_can_get_own_verification_code( + mock_verification_start, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + url = reverse("api-internal:user-get-verification-code", kwargs={"pk": user.public_primary_key}) + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + + +@patch("apps.twilioapp.phone_manager.PhoneManager.send_verification_code", return_value=Mock()) +@pytest.mark.django_db +def test_admin_can_get_another_user_verification_code( + mock_verification_start, + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, +): + organization, first_user, token = make_organization_and_user_with_plugin_token() + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + url = reverse("api-internal:user-get-verification-code", kwargs={"pk": first_user.public_primary_key}) + response = client.get(url, format="json", **make_user_auth_headers(second_user, token)) + assert response.status_code == status.HTTP_200_OK + + +@patch("apps.twilioapp.phone_manager.PhoneManager.verify_phone_number", return_value=(True, None)) +@pytest.mark.django_db +def test_admin_can_verify_own_phone( + mocked_verification_check, + make_organization_and_user_with_plugin_token, + make_user_auth_headers, +): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + url = reverse("api-internal:user-verify-number", kwargs={"pk": user.public_primary_key}) + + response = client.put(f"{url}?token=12345", format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + + +@patch("apps.twilioapp.phone_manager.PhoneManager.verify_phone_number", return_value=(True, None)) +@pytest.mark.django_db +def test_admin_can_verify_another_user_phone( + mocked_verification_check, + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, +): + organization, first_user, token = make_organization_and_user_with_plugin_token() + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + url = reverse("api-internal:user-verify-number", kwargs={"pk": first_user.public_primary_key}) + + response = client.put(f"{url}?token=12345", format="json", **make_user_auth_headers(second_user, token)) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_admin_can_get_own_telegram_verification_code( + make_organization_and_user_with_plugin_token, make_user_auth_headers +): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": user.public_primary_key}) + + response = client.get(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_admin_can_get_another_user_telegram_verification_code( + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, +): + organization, first_user, token = make_organization_and_user_with_plugin_token() + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": first_user.public_primary_key}) + + response = client.get(url, format="json", **make_user_auth_headers(second_user, token)) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_admin_can_get_another_user_backend_verification_code( + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, +): + organization, first_user, token = make_organization_and_user_with_plugin_token() + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + url = ( + reverse("api-internal:user-get-backend-verification-code", kwargs={"pk": first_user.public_primary_key}) + + "?backend=TESTONLY" + ) + + response = client.get(url, format="json", **make_user_auth_headers(second_user, token)) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_admin_can_unlink_another_user_backend_account( + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, +): + organization, first_user, token = make_organization_and_user_with_plugin_token() + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.ADMIN) + + client = APIClient() + url = ( + reverse("api-internal:user-unlink-backend", kwargs={"pk": first_user.public_primary_key}) + "?backend=TESTONLY" + ) + + response = client.post(url, format="json", **make_user_auth_headers(second_user, token)) + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_admin_can_unlink_another_user_slack_account( + make_organization_with_slack_team_identity, + make_user_for_organization, + make_user_with_slack_user_identity, + make_token_for_organization, + make_user_auth_headers, +): + organization, slack_team_identity = make_organization_with_slack_team_identity() + _, token = make_token_for_organization(organization) + + user, _ = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="user_2", role=LegacyAccessControlRole.ADMIN + ) + other_user = make_user_for_organization(organization) + + client = APIClient() + url = reverse("api-internal:user-unlink-slack", kwargs={"pk": other_user.public_primary_key}) + + response = client.post(url, format="json", **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + other_user.refresh_from_db() + assert other_user.slack_user_identity is None + + +"""Test user permissions""" + + +@pytest.mark.django_db +def test_user_cant_update_user( + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, +): + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) + + client = APIClient() + data = { + "email": "test@amixr.io", + "username": "updated_test_username", + "unverified_phone_number": "+1234567890", + "slack_login": "", + } + url = reverse("api-internal:user-detail", kwargs={"pk": first_user.public_primary_key}) + response = client.put(url, format="json", data=data, **make_user_auth_headers(second_user, token)) + + assert response.status_code == status.HTTP_403_FORBIDDEN + + +@pytest.mark.django_db +def test_user_can_update_themself(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + + client = APIClient() + data = { + "email": "test@amixr.io", + "username": "updated_test_username", + "unverified_phone_number": "+1234567890", + "slack_login": "", + } + + url = reverse("api-internal:user-detail", kwargs={"pk": user.public_primary_key}) + response = client.put(url, format="json", data=data, **make_user_auth_headers(user, token)) + + assert response.status_code == status.HTTP_200_OK + + +@pytest.mark.django_db +def test_user_can_list_users(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + + client = APIClient() + + url = reverse("api-internal:user-list") + response = client.get(url, format="json", **make_user_auth_headers(user, token)) assert response.status_code == status.HTTP_200_OK @pytest.mark.django_db def test_user_can_detail_users( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers ): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - editor = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) client = APIClient() - url = reverse("api-internal:user-detail", kwargs={"pk": admin.public_primary_key}) + url = reverse("api-internal:user-detail", kwargs={"pk": first_user.public_primary_key}) - response = client.get(url, format="json", **make_user_auth_headers(editor, token)) + response = client.get(url, format="json", **make_user_auth_headers(second_user, token)) assert response.status_code == status.HTTP_403_FORBIDDEN @patch("apps.twilioapp.phone_manager.PhoneManager.send_verification_code", return_value=Mock()) @pytest.mark.django_db def test_user_can_get_own_verification_code( - mock_verification_start, - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, + mock_verification_start, make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-get-verification-code", kwargs={"pk": user.public_primary_key}) @@ -938,15 +857,12 @@ def test_user_can_get_own_verification_code( @pytest.mark.django_db def test_user_cant_get_another_user_verification_code( mock_verification_start, - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-get-verification-code", kwargs={"pk": first_user.public_primary_key}) @@ -958,15 +874,9 @@ def test_user_cant_get_another_user_verification_code( @patch("apps.twilioapp.phone_manager.PhoneManager.verify_phone_number", return_value=(True, None)) @pytest.mark.django_db def test_user_can_verify_own_phone( - mocked_verification_check, - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, + mocked_verification_check, make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-verify-number", kwargs={"pk": user.public_primary_key}) @@ -979,15 +889,12 @@ def test_user_can_verify_own_phone( @pytest.mark.django_db def test_user_cant_verify_another_user_phone( mocked_verification_check, - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-verify-number", kwargs={"pk": first_user.public_primary_key}) @@ -998,11 +905,9 @@ def test_user_cant_verify_another_user_phone( @pytest.mark.django_db def test_user_can_get_own_telegram_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": user.public_primary_key}) @@ -1013,12 +918,12 @@ def test_user_can_get_own_telegram_verification_code( @pytest.mark.django_db def test_user_cant_get_another_user_telegram_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": first_user.public_primary_key}) @@ -1029,11 +934,9 @@ def test_user_cant_get_another_user_telegram_verification_code( @pytest.mark.django_db def test_user_can_get_own_backend_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = ( @@ -1054,12 +957,12 @@ def test_user_can_get_own_backend_verification_code( @pytest.mark.django_db def test_user_cant_get_another_user_backend_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, + make_user_for_organization, + make_user_auth_headers, ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) client = APIClient() url = ( @@ -1079,8 +982,8 @@ def test_user_can_unlink_own_slack_account( make_user_auth_headers, ): organization, slack_team_identity = make_organization_with_slack_team_identity() - user, slack_user_identity_1 = make_user_with_slack_user_identity( - slack_team_identity, organization, slack_id="user_1", role=Role.EDITOR + user, _ = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="user_2", role=LegacyAccessControlRole.EDITOR ) _, token = make_token_for_organization(organization) @@ -1094,12 +997,8 @@ def test_user_can_unlink_own_slack_account( @pytest.mark.django_db -def test_user_can_unlink_backend_own_account( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) +def test_user_can_unlink_backend_own_account(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-unlink-backend", kwargs={"pk": user.public_primary_key}) + "?backend=TESTONLY" @@ -1110,12 +1009,8 @@ def test_user_can_unlink_backend_own_account( @pytest.mark.django_db -def test_user_unlink_backend_invalid_backend_id( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) +def test_user_unlink_backend_invalid_backend_id(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-unlink-backend", kwargs={"pk": user.public_primary_key}) + "?backend=INVALID" @@ -1127,11 +1022,9 @@ def test_user_unlink_backend_invalid_backend_id( @pytest.mark.django_db def test_user_unlink_backend_backend_account_not_found( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-unlink-backend", kwargs={"pk": user.public_primary_key}) + "?backend=TESTONLY" @@ -1149,11 +1042,12 @@ def test_user_cant_unlink_slack_another_user( make_user_auth_headers, ): organization, slack_team_identity = make_organization_with_slack_team_identity() - first_user, slack_user_identity_1 = make_user_with_slack_user_identity( - slack_team_identity, organization, slack_id="user_1", role=Role.EDITOR + + first_user, _ = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="user_1", role=LegacyAccessControlRole.EDITOR ) - second_user, slack_user_identity_2 = make_user_with_slack_user_identity( - slack_team_identity, organization, slack_id="user_2", role=Role.EDITOR + second_user, _ = make_user_with_slack_user_identity( + slack_team_identity, organization, slack_id="user_2", role=LegacyAccessControlRole.EDITOR ) _, token = make_token_for_organization(organization) @@ -1168,12 +1062,10 @@ def test_user_cant_unlink_slack_another_user( @pytest.mark.django_db def test_user_cant_unlink_backend__another_user( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) client = APIClient() url = ( @@ -1187,40 +1079,16 @@ def test_user_cant_unlink_backend__another_user( """Test stakeholder permissions""" -@pytest.mark.django_db -def test_viewer_cant_create_user( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) - - client = APIClient() - url = reverse("api-internal:user-list") - data = { - "email": "test@amixr.io", - "role": Role.ADMIN, - "username": "test_username", - "unverified_phone_number": None, - "slack_login": "", - } - response = client.post(url, format="json", data=data, **make_user_auth_headers(user, token)) - - assert response.status_code == status.HTTP_403_FORBIDDEN - - @pytest.mark.django_db def test_viewer_cant_update_user( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.VIEWER) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) data = { "email": "test@amixr.io", - "role": Role.EDITOR, + "role": LegacyAccessControlRole.EDITOR, "username": "updated_test_username", "unverified_phone_number": "+1234567890", "slack_login": "", @@ -1234,16 +1102,12 @@ def test_viewer_cant_update_user( @pytest.mark.django_db -def test_viewer_cant_update_himself( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) +def test_viewer_cant_update_himself(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.VIEWER) data = { "email": "test@amixr.io", - "role": Role.VIEWER, + "role": LegacyAccessControlRole.VIEWER, "username": "updated_test_username", "unverified_phone_number": "+1234567890", "slack_login": "", @@ -1257,12 +1121,8 @@ def test_viewer_cant_update_himself( @pytest.mark.django_db -def test_viewer_cant_list_users( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) +def test_viewer_cant_list_users(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-list") @@ -1273,12 +1133,10 @@ def test_viewer_cant_list_users( @pytest.mark.django_db def test_viewer_cant_detail_users( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": first_user.public_primary_key}) @@ -1290,15 +1148,9 @@ def test_viewer_cant_detail_users( @patch("apps.twilioapp.phone_manager.PhoneManager.send_verification_code", return_value=Mock()) @pytest.mark.django_db def test_viewer_cant_get_own_verification_code( - mock_verification_start, - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, + mock_verification_start, make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-get-verification-code", kwargs={"pk": user.public_primary_key}) @@ -1311,15 +1163,12 @@ def test_viewer_cant_get_own_verification_code( @pytest.mark.django_db def test_viewer_cant_get_another_user_verification_code( mock_verification_start, - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-get-verification-code", kwargs={"pk": first_user.public_primary_key}) @@ -1331,15 +1180,9 @@ def test_viewer_cant_get_another_user_verification_code( @patch("apps.twilioapp.phone_manager.PhoneManager.verify_phone_number", return_value=(True, None)) @pytest.mark.django_db def test_viewer_cant_verify_own_phone( - mocked_verification_check, - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, + mocked_verification_check, make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-verify-number", kwargs={"pk": user.public_primary_key}) @@ -1352,15 +1195,12 @@ def test_viewer_cant_verify_own_phone( @pytest.mark.django_db def test_viewer_cant_verify_another_user_phone( mocked_verification_check, - make_organization, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-verify-number", kwargs={"pk": first_user.public_primary_key}) @@ -1371,11 +1211,9 @@ def test_viewer_cant_verify_another_user_phone( @pytest.mark.django_db def test_viewer_cant_get_own_telegram_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": user.public_primary_key}) @@ -1386,12 +1224,10 @@ def test_viewer_cant_get_own_telegram_verification_code( @pytest.mark.django_db def test_viewer_cant_get_another_user_telegram_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-get-telegram-verification-code", kwargs={"pk": first_user.public_primary_key}) @@ -1404,34 +1240,30 @@ def test_viewer_cant_get_another_user_telegram_verification_code( @pytest.mark.parametrize( "role,expected_status,initial_unverified_number,initial_verified_number", [ - (Role.ADMIN, status.HTTP_200_OK, "+1234567890", None), - (Role.EDITOR, status.HTTP_200_OK, "+1234567890", None), - (Role.VIEWER, status.HTTP_403_FORBIDDEN, "+1234567890", None), - (Role.ADMIN, status.HTTP_200_OK, None, "+1234567890"), - (Role.EDITOR, status.HTTP_200_OK, None, "+1234567890"), - (Role.VIEWER, status.HTTP_403_FORBIDDEN, None, "+1234567890"), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK, "+1234567890", None), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK, "+1234567890", None), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN, "+1234567890", None), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK, None, "+1234567890"), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK, None, "+1234567890"), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN, None, "+1234567890"), ], ) def test_forget_own_number( - make_organization, - make_team, + make_organization_and_user_with_plugin_token, make_user_for_organization, - make_token_for_organization, make_user_auth_headers, role, expected_status, initial_unverified_number, initial_verified_number, ): - organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) + organization, admin, token = make_organization_and_user_with_plugin_token() user = make_user_for_organization( organization, role=role, unverified_phone_number=initial_unverified_number, _verified_phone_number=initial_verified_number, ) - _, token = make_token_for_organization(organization) client = APIClient() url = reverse("api-internal:user-forget-number", kwargs={"pk": user.public_primary_key}) @@ -1456,17 +1288,16 @@ def test_forget_own_number( @pytest.mark.parametrize( "role,expected_status,initial_unverified_number,initial_verified_number", [ - (Role.ADMIN, status.HTTP_200_OK, "+1234567890", None), - (Role.EDITOR, status.HTTP_403_FORBIDDEN, "+1234567890", None), - (Role.VIEWER, status.HTTP_403_FORBIDDEN, "+1234567890", None), - (Role.ADMIN, status.HTTP_200_OK, None, "+1234567890"), - (Role.EDITOR, status.HTTP_403_FORBIDDEN, None, "+1234567890"), - (Role.VIEWER, status.HTTP_403_FORBIDDEN, None, "+1234567890"), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK, "+1234567890", None), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN, "+1234567890", None), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN, "+1234567890", None), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK, None, "+1234567890"), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN, None, "+1234567890"), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN, None, "+1234567890"), ], ) def test_forget_other_number( make_organization, - make_team, make_user_for_organization, make_token_for_organization, make_user_auth_headers, @@ -1476,26 +1307,26 @@ def test_forget_other_number( initial_verified_number, ): organization = make_organization() - user = make_user_for_organization( - organization, - role=Role.ADMIN, - unverified_phone_number=initial_unverified_number, - _verified_phone_number=initial_verified_number, - ) - other_user = make_user_for_organization(organization, role=role) _, token = make_token_for_organization(organization) + admin = make_user_for_organization( + organization, unverified_phone_number=initial_unverified_number, _verified_phone_number=initial_verified_number + ) + other_user = make_user_for_organization(organization, role=role) + admin_primary_key = admin.public_primary_key + client = APIClient() - url = reverse("api-internal:user-forget-number", kwargs={"pk": user.public_primary_key}) + url = reverse("api-internal:user-forget-number", kwargs={"pk": admin_primary_key}) with patch( "apps.twilioapp.phone_manager.PhoneManager.notify_about_changed_verified_phone_number", return_value=None ): response = client.put(url, None, format="json", **make_user_auth_headers(other_user, token)) assert response.status_code == expected_status - user_detail_url = reverse("api-internal:user-detail", kwargs={"pk": user.public_primary_key}) - response = client.get(user_detail_url, None, format="json", **make_user_auth_headers(user, token)) + user_detail_url = reverse("api-internal:user-detail", kwargs={"pk": admin_primary_key}) + response = client.get(user_detail_url, None, format="json", **make_user_auth_headers(admin, token)) assert response.status_code == status.HTTP_200_OK + if expected_status == status.HTTP_200_OK: assert not response.json()["unverified_phone_number"] assert not response.json()["verified_phone_number"] @@ -1506,11 +1337,9 @@ def test_forget_other_number( @pytest.mark.django_db def test_viewer_cant_get_own_backend_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_auth_headers ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.VIEWER) client = APIClient() url = ( @@ -1524,12 +1353,10 @@ def test_viewer_cant_get_own_backend_verification_code( @pytest.mark.django_db def test_viewer_cant_get_another_user_backend_verification_code( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) client = APIClient() url = ( @@ -1542,13 +1369,8 @@ def test_viewer_cant_get_another_user_backend_verification_code( @pytest.mark.django_db -def test_viewer_cant_unlink_backend_own_user( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) - +def test_viewer_cant_unlink_backend_own_user(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.VIEWER) client = APIClient() url = reverse("api-internal:user-unlink-backend", kwargs={"pk": user.public_primary_key}) + "?backend=TESTONLY" @@ -1558,12 +1380,10 @@ def test_viewer_cant_unlink_backend_own_user( @pytest.mark.django_db def test_viewer_cant_unlink_backend_another_user( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers + make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_auth_headers ): - organization = make_organization() - first_user = make_user_for_organization(organization, role=Role.EDITOR) - second_user = make_user_for_organization(organization, role=Role.VIEWER) - _, token = make_token_for_organization(organization) + organization, first_user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) + second_user = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) client = APIClient() url = ( @@ -1575,12 +1395,8 @@ def test_viewer_cant_unlink_backend_another_user( @pytest.mark.django_db -def test_change_timezone( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) +def test_change_timezone(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": user.public_primary_key}) @@ -1595,12 +1411,8 @@ def test_change_timezone( @pytest.mark.django_db @pytest.mark.parametrize("timezone", ["", 1, "NotATimezone"]) -def test_invalid_timezone( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers, timezone -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) +def test_invalid_timezone(make_organization_and_user_with_plugin_token, make_user_auth_headers, timezone): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": user.public_primary_key}) @@ -1612,12 +1424,8 @@ def test_invalid_timezone( @pytest.mark.django_db -def test_change_working_hours( - make_organization, make_user_for_organization, make_token_for_organization, make_user_auth_headers -): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) +def test_change_working_hours(make_organization_and_user_with_plugin_token, make_user_auth_headers): + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": user.public_primary_key}) @@ -1651,15 +1459,9 @@ def test_change_working_hours( ], ) def test_invalid_working_hours( - make_organization, - make_user_for_organization, - make_token_for_organization, - make_user_auth_headers, - working_hours_extra, + make_organization_and_user_with_plugin_token, make_user_auth_headers, working_hours_extra ): - organization = make_organization() - user = make_user_for_organization(organization, role=Role.EDITOR) - _, token = make_token_for_organization(organization) + _, user, token = make_organization_and_user_with_plugin_token(role=LegacyAccessControlRole.EDITOR) client = APIClient() url = reverse("api-internal:user-detail", kwargs={"pk": user.public_primary_key}) diff --git a/engine/apps/api/tests/test_user_groups.py b/engine/apps/api/tests/test_user_groups.py index ce7494a1..2e45e727 100644 --- a/engine/apps/api/tests/test_user_groups.py +++ b/engine/apps/api/tests/test_user_groups.py @@ -3,7 +3,7 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @@ -52,13 +52,16 @@ def test_usergroup_list_without_slack_installed( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_200_OK), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_200_OK), ], ) def test_usergroup_permissions( - make_organization_and_user_with_plugin_token, make_user_auth_headers, role, expected_status + make_organization_and_user_with_plugin_token, + make_user_auth_headers, + role, + expected_status, ): _, user, token = make_organization_and_user_with_plugin_token(role) client = APIClient() diff --git a/engine/apps/api/tests/test_user_notification_policy.py b/engine/apps/api/tests/test_user_notification_policy.py index 1eb39e61..4bc9c306 100644 --- a/engine/apps/api/tests/test_user_notification_policy.py +++ b/engine/apps/api/tests/test_user_notification_policy.py @@ -6,8 +6,8 @@ from django.utils import timezone from rest_framework import status from rest_framework.test import APIClient +from apps.api.permissions import LegacyAccessControlRole from apps.base.models import UserNotificationPolicy -from common.constants.role import Role DEFAULT_NOTIFICATION_CHANNEL = UserNotificationPolicy.NotificationChannel.SLACK @@ -17,7 +17,7 @@ def user_notification_policy_internal_api_setup( make_organization_and_user_with_plugin_token, make_user_for_organization, make_user_notification_policy ): organization, admin, token = make_organization_and_user_with_plugin_token() - user = make_user_for_organization(organization, Role.EDITOR) + user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) wait_notification_step = make_user_notification_policy( admin, UserNotificationPolicy.Step.WAIT, wait_delay=timezone.timedelta(minutes=15), important=False @@ -49,7 +49,7 @@ def user_notification_policy_internal_api_setup( @pytest.mark.django_db def test_create_notification_policy(user_notification_policy_internal_api_setup, make_user_auth_headers): - token, steps, users = user_notification_policy_internal_api_setup + token, _, users = user_notification_policy_internal_api_setup admin, _ = users client = APIClient() url = reverse("api-internal:notification_policy-list") @@ -69,7 +69,7 @@ def test_create_notification_policy(user_notification_policy_internal_api_setup, def test_admin_can_create_notification_policy_for_user( user_notification_policy_internal_api_setup, make_user_auth_headers ): - token, steps, users = user_notification_policy_internal_api_setup + token, _, users = user_notification_policy_internal_api_setup admin, user = users client = APIClient() url = reverse("api-internal:notification_policy-list") diff --git a/engine/apps/api/tests/test_user_schedule_export.py b/engine/apps/api/tests/test_user_schedule_export.py index a465a934..fd467477 100644 --- a/engine/apps/api/tests/test_user_schedule_export.py +++ b/engine/apps/api/tests/test_user_schedule_export.py @@ -3,8 +3,8 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient +from apps.api.permissions import LegacyAccessControlRole from apps.auth_token.models import UserScheduleExportAuthToken -from common.constants.role import Role ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano72p69rt78%40group.calendar.google.com/private-1d00a680ba5be7426c3eb3ef1616e26d/basic.ics" # noqa @@ -13,9 +13,9 @@ ICAL_URL = "https://calendar.google.com/calendar/ical/amixr.io_37gttuakhrtr75ano @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_200_OK), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_200_OK), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_get_user_schedule_export_token( @@ -24,8 +24,7 @@ def test_get_user_schedule_export_token( role, expected_status, ): - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) UserScheduleExportAuthToken.create_auth_token( user=user, @@ -45,9 +44,9 @@ def test_get_user_schedule_export_token( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_404_NOT_FOUND), - (Role.EDITOR, status.HTTP_404_NOT_FOUND), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND), + (LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_schedule_export_token_not_found( @@ -56,8 +55,7 @@ def test_user_schedule_export_token_not_found( role, expected_status, ): - - _, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:user-export-token", kwargs={"pk": user.public_primary_key}) @@ -72,9 +70,9 @@ def test_user_schedule_export_token_not_found( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_201_CREATED), - (Role.EDITOR, status.HTTP_201_CREATED), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_201_CREATED), + (LegacyAccessControlRole.EDITOR, status.HTTP_201_CREATED), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_schedule_create_export_token( @@ -83,8 +81,7 @@ def test_user_schedule_create_export_token( role, expected_status, ): - - _, user, token = make_organization_and_user_with_plugin_token(role=role) + _, user, token = make_organization_and_user_with_plugin_token(role) url = reverse("api-internal:user-export-token", kwargs={"pk": user.public_primary_key}) @@ -99,9 +96,9 @@ def test_user_schedule_create_export_token( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_409_CONFLICT), - (Role.EDITOR, status.HTTP_409_CONFLICT), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_409_CONFLICT), + (LegacyAccessControlRole.EDITOR, status.HTTP_409_CONFLICT), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_schedule_create_multiple_export_tokens_fails( @@ -110,8 +107,7 @@ def test_user_schedule_create_multiple_export_tokens_fails( role, expected_status, ): - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) UserScheduleExportAuthToken.create_auth_token( user=user, @@ -131,9 +127,9 @@ def test_user_schedule_create_multiple_export_tokens_fails( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_204_NO_CONTENT), - (Role.EDITOR, status.HTTP_204_NO_CONTENT), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.EDITOR, status.HTTP_204_NO_CONTENT), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_schedule_delete_export_token( @@ -142,8 +138,7 @@ def test_user_schedule_delete_export_token( role, expected_status, ): - - organization, user, token = make_organization_and_user_with_plugin_token(role=role) + organization, user, token = make_organization_and_user_with_plugin_token(role) instance, _ = UserScheduleExportAuthToken.create_auth_token( user=user, @@ -168,9 +163,9 @@ def test_user_schedule_delete_export_token( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_404_NOT_FOUND), - (Role.EDITOR, status.HTTP_404_NOT_FOUND), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND), + (LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_cannot_get_another_users_schedule_token( @@ -179,9 +174,8 @@ def test_user_cannot_get_another_users_schedule_token( role, expected_status, ): - - organization1, user1, _ = make_organization_and_user_with_plugin_token(role=role) - _, user2, token2 = make_organization_and_user_with_plugin_token(role=role) + organization1, user1, _ = make_organization_and_user_with_plugin_token(role) + _, user2, token2 = make_organization_and_user_with_plugin_token(role) UserScheduleExportAuthToken.create_auth_token( user=user1, @@ -201,9 +195,9 @@ def test_user_cannot_get_another_users_schedule_token( @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_404_NOT_FOUND), - (Role.EDITOR, status.HTTP_404_NOT_FOUND), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_404_NOT_FOUND), + (LegacyAccessControlRole.EDITOR, status.HTTP_404_NOT_FOUND), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_user_cannot_delete_another_users_schedule_token( @@ -212,9 +206,8 @@ def test_user_cannot_delete_another_users_schedule_token( role, expected_status, ): - - organization1, user1, _ = make_organization_and_user_with_plugin_token(role=role) - _, user2, token2 = make_organization_and_user_with_plugin_token(role=role) + organization1, user1, _ = make_organization_and_user_with_plugin_token(role) + _, user2, token2 = make_organization_and_user_with_plugin_token(role) UserScheduleExportAuthToken.create_auth_token( user=user1, diff --git a/engine/apps/api/views/alert_group.py b/engine/apps/api/views/alert_group.py index 0af8fb9b..91a49579 100644 --- a/engine/apps/api/views/alert_group.py +++ b/engine/apps/api/views/alert_group.py @@ -12,7 +12,7 @@ from rest_framework.response import Response from apps.alerts.constants import ActionSource from apps.alerts.models import Alert, AlertGroup, AlertReceiveChannel -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdminOrEditor +from apps.api.permissions import RBACPermission from apps.api.serializers.alert_group import AlertGroupListSerializer, AlertGroupSerializer from apps.auth_token.auth import PluginAuthentication from apps.mobile_app.auth import MobileAppAuthTokenAuthentication @@ -160,29 +160,29 @@ class AlertGroupView( MobileAppAuthTokenAuthentication, PluginAuthentication, ) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - action_permissions = { - IsAdminOrEditor: ( - *MODIFY_ACTIONS, - "acknowledge", - "unacknowledge", - "resolve", - "unresolve", - "attach", - "unattach", - "silence", - "unsilence", - "bulk_action", - "preview_template", - ), - AnyRole: ( - *READ_ACTIONS, - "stats", - "filters", - "silence_options", - "bulk_action_options", - ), + rbac_permissions = { + "metadata": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "list": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "retrieve": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "stats": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "filters": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "silence_options": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "bulk_action_options": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "create": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "update": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "destroy": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "acknowledge": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "unacknowledge": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "resolve": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "unresolve": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "attach": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "unattach": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "silence": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "unsilence": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "bulk_action": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "preview_template": [RBACPermission.Permissions.INTEGRATIONS_TEST], } http_method_names = ["get", "post"] diff --git a/engine/apps/api/views/alert_receive_channel.py b/engine/apps/api/views/alert_receive_channel.py index 866620a8..e62599fc 100644 --- a/engine/apps/api/views/alert_receive_channel.py +++ b/engine/apps/api/views/alert_receive_channel.py @@ -9,7 +9,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from apps.alerts.models import AlertReceiveChannel -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin, IsAdminOrEditor +from apps.api.permissions import RBACPermission from apps.api.serializers.alert_receive_channel import ( AlertReceiveChannelSerializer, AlertReceiveChannelUpdateSerializer, @@ -66,19 +66,7 @@ class AlertReceiveChannelView( ModelViewSet, ): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) - action_permissions = { - IsAdmin: (*MODIFY_ACTIONS, "stop_maintenance", "start_maintenance", "change_team"), - IsAdminOrEditor: ("send_demo_alert", "preview_template"), - AnyRole: ( - *READ_ACTIONS, - "integration_options", - "maintenance_duration_options", - "maintenance_mode_options", - "counters", - "counters_per_integration", - ), - } + permission_classes = (IsAuthenticated, RBACPermission) model = AlertReceiveChannel serializer_class = AlertReceiveChannelSerializer @@ -90,6 +78,22 @@ class AlertReceiveChannelView( filterset_class = AlertReceiveChannelFilter + rbac_permissions = { + "metadata": [RBACPermission.Permissions.INTEGRATIONS_READ], + "list": [RBACPermission.Permissions.INTEGRATIONS_READ], + "retrieve": [RBACPermission.Permissions.INTEGRATIONS_READ], + "integration_options": [RBACPermission.Permissions.INTEGRATIONS_READ], + "counters": [RBACPermission.Permissions.INTEGRATIONS_READ], + "counters_per_integration": [RBACPermission.Permissions.INTEGRATIONS_READ], + "send_demo_alert": [RBACPermission.Permissions.INTEGRATIONS_TEST], + "preview_template": [RBACPermission.Permissions.INTEGRATIONS_TEST], + "create": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "change_team": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + } + def create(self, request, *args, **kwargs): if request.data["integration"] is not None and ( request.data["integration"] in AlertReceiveChannel.WEB_INTEGRATION_CHOICES diff --git a/engine/apps/api/views/alert_receive_channel_template.py b/engine/apps/api/views/alert_receive_channel_template.py index c6800ee5..d7963a4c 100644 --- a/engine/apps/api/views/alert_receive_channel_template.py +++ b/engine/apps/api/views/alert_receive_channel_template.py @@ -3,7 +3,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from apps.alerts.models import AlertReceiveChannel -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.alert_receive_channel import AlertReceiveChannelTemplatesSerializer from apps.auth_token.auth import PluginAuthentication from common.api_helpers.mixins import PublicPrimaryKeyMixin @@ -18,11 +18,14 @@ class AlertReceiveChannelTemplateView( viewsets.GenericViewSet, ): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - action_permissions = { - IsAdmin: MODIFY_ACTIONS, - AnyRole: READ_ACTIONS, + rbac_permissions = { + "metadata": [RBACPermission.Permissions.INTEGRATIONS_READ], + "list": [RBACPermission.Permissions.INTEGRATIONS_READ], + "retrieve": [RBACPermission.Permissions.INTEGRATIONS_READ], + "update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], } model = AlertReceiveChannel diff --git a/engine/apps/api/views/channel_filter.py b/engine/apps/api/views/channel_filter.py index efe397d1..b1664951 100644 --- a/engine/apps/api/views/channel_filter.py +++ b/engine/apps/api/views/channel_filter.py @@ -6,7 +6,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from apps.alerts.models import ChannelFilter -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin, IsAdminOrEditor +from apps.api.permissions import RBACPermission from apps.api.serializers.channel_filter import ( ChannelFilterCreateSerializer, ChannelFilterSerializer, @@ -23,11 +23,17 @@ from common.insight_log import EntityEvent, write_resource_insight_log class ChannelFilterView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) - action_permissions = { - IsAdmin: (*MODIFY_ACTIONS, "move_to_position"), - IsAdminOrEditor: ("send_demo_alert",), - AnyRole: READ_ACTIONS, + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "metadata": [RBACPermission.Permissions.INTEGRATIONS_READ], + "list": [RBACPermission.Permissions.INTEGRATIONS_READ], + "retrieve": [RBACPermission.Permissions.INTEGRATIONS_READ], + "create": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "destroy": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "move_to_position": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "send_demo_alert": [RBACPermission.Permissions.INTEGRATIONS_TEST], } model = ChannelFilter diff --git a/engine/apps/api/views/custom_button.py b/engine/apps/api/views/custom_button.py index 09228d27..99037a85 100644 --- a/engine/apps/api/views/custom_button.py +++ b/engine/apps/api/views/custom_button.py @@ -4,7 +4,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.viewsets import ModelViewSet from apps.alerts.models import CustomButton -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.custom_button import CustomButtonSerializer from apps.auth_token.auth import PluginAuthentication from common.api_helpers.mixins import PublicPrimaryKeyMixin, TeamFilteringMixin @@ -13,10 +13,16 @@ from common.insight_log import EntityEvent, write_resource_insight_log class CustomButtonView(TeamFilteringMixin, PublicPrimaryKeyMixin, ModelViewSet): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) - action_permissions = { - IsAdmin: MODIFY_ACTIONS, - AnyRole: READ_ACTIONS, + permission_classes = (IsAuthenticated, RBACPermission) + + rbac_permissions = { + "metadata": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ], + "list": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ], + "retrieve": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ], + "create": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE], + "update": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE], + "partial_update": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE], + "destroy": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE], } model = CustomButton diff --git a/engine/apps/api/views/escalation_chain.py b/engine/apps/api/views/escalation_chain.py index 72c73d3a..05cdc216 100644 --- a/engine/apps/api/views/escalation_chain.py +++ b/engine/apps/api/views/escalation_chain.py @@ -7,7 +7,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from apps.alerts.models import EscalationChain -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.escalation_chain import EscalationChainListSerializer, EscalationChainSerializer from apps.auth_token.auth import PluginAuthentication from common.api_helpers.exceptions import BadRequest @@ -17,11 +17,17 @@ from common.insight_log import EntityEvent, write_resource_insight_log class EscalationChainViewSet(TeamFilteringMixin, PublicPrimaryKeyMixin, ListSerializerMixin, viewsets.ModelViewSet): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - action_permissions = { - IsAdmin: (*MODIFY_ACTIONS, "copy"), - AnyRole: (*READ_ACTIONS, "details"), + rbac_permissions = { + "metadata": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "list": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "retrieve": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "details": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "create": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], + "update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], + "destroy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], + "copy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], } filter_backends = [SearchFilter] diff --git a/engine/apps/api/views/escalation_policy.py b/engine/apps/api/views/escalation_policy.py index c05a2f0e..a8090648 100644 --- a/engine/apps/api/views/escalation_policy.py +++ b/engine/apps/api/views/escalation_policy.py @@ -7,7 +7,7 @@ from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet from apps.alerts.models import EscalationPolicy -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.escalation_policy import ( EscalationPolicyCreateSerializer, EscalationPolicySerializer, @@ -21,15 +21,19 @@ from common.insight_log import EntityEvent, write_resource_insight_log class EscalationPolicyView(PublicPrimaryKeyMixin, CreateSerializerMixin, UpdateSerializerMixin, ModelViewSet): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) - action_permissions = { - IsAdmin: (*MODIFY_ACTIONS, "move_to_position"), - AnyRole: ( - *READ_ACTIONS, - "escalation_options", - "delay_options", - "num_minutes_in_window_options", - ), + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "metadata": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "list": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "retrieve": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "escalation_options": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "delay_options": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "num_minutes_in_window_options": [RBACPermission.Permissions.ESCALATION_CHAINS_READ], + "create": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], + "update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], + "partial_update": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], + "destroy": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], + "move_to_position": [RBACPermission.Permissions.ESCALATION_CHAINS_WRITE], } model = EscalationPolicy diff --git a/engine/apps/api/views/integration_heartbeat.py b/engine/apps/api/views/integration_heartbeat.py index fa50e29b..0e1fa96d 100644 --- a/engine/apps/api/views/integration_heartbeat.py +++ b/engine/apps/api/views/integration_heartbeat.py @@ -3,7 +3,7 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.integration_heartbeat import IntegrationHeartBeatSerializer from apps.auth_token.auth import PluginAuthentication from apps.heartbeat.models import IntegrationHeartBeat @@ -20,10 +20,17 @@ class IntegrationHeartBeatView( viewsets.GenericViewSet, ): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) - action_permissions = { - IsAdmin: (*MODIFY_ACTIONS, "activate", "deactivate"), - AnyRole: (*READ_ACTIONS, "timeout_options"), + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "metadata": [RBACPermission.Permissions.INTEGRATIONS_READ], + "list": [RBACPermission.Permissions.INTEGRATIONS_READ], + "retrieve": [RBACPermission.Permissions.INTEGRATIONS_READ], + "timeout_options": [RBACPermission.Permissions.INTEGRATIONS_READ], + "create": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "partial_update": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "activate": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + "deactivate": [RBACPermission.Permissions.INTEGRATIONS_WRITE], } model = IntegrationHeartBeat diff --git a/engine/apps/api/views/live_setting.py b/engine/apps/api/views/live_setting.py index 57566f52..02ca047c 100644 --- a/engine/apps/api/views/live_setting.py +++ b/engine/apps/api/views/live_setting.py @@ -6,7 +6,7 @@ from rest_framework import status, viewsets from rest_framework.permissions import IsAuthenticated from telegram import error -from apps.api.permissions import IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.live_setting import LiveSettingSerializer from apps.auth_token.auth import PluginAuthentication from apps.base.models import LiveSetting @@ -21,7 +21,14 @@ from common.api_helpers.mixins import PublicPrimaryKeyMixin class LiveSettingViewSet(PublicPrimaryKeyMixin, viewsets.ModelViewSet): serializer_class = LiveSettingSerializer authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "list": [RBACPermission.Permissions.OTHER_SETTINGS_READ], + "retrieve": [RBACPermission.Permissions.OTHER_SETTINGS_READ], + "create": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + "update": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + "destroy": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + } def dispatch(self, request, *args, **kwargs): if not settings.FEATURE_LIVE_SETTINGS_ENABLED: diff --git a/engine/apps/api/views/maintenance.py b/engine/apps/api/views/maintenance.py index aa69dd9b..31fd8cd4 100644 --- a/engine/apps/api/views/maintenance.py +++ b/engine/apps/api/views/maintenance.py @@ -5,7 +5,7 @@ from rest_framework.views import APIView from apps.alerts.models import AlertReceiveChannel from apps.alerts.models.maintainable_object import MaintainableObject -from apps.api.permissions import IsAdmin +from apps.api.permissions import RBACPermission from apps.auth_token.auth import PluginAuthentication from common.api_helpers.exceptions import BadRequest from common.exceptions import MaintenanceCouldNotBeStartedError @@ -39,7 +39,11 @@ class GetObjectMixin: class MaintenanceAPIView(APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated,) + permission_classes = (IsAuthenticated, RBACPermission) + + rbac_permissions = { + "get": [RBACPermission.Permissions.MAINTENANCE_READ], + } def get(self, request): organization = self.request.auth.organization @@ -77,7 +81,10 @@ class MaintenanceAPIView(APIView): class MaintenanceStartAPIView(GetObjectMixin, APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "post": [RBACPermission.Permissions.MAINTENANCE_WRITE], + } def post(self, request): mode = request.data.get("mode", None) @@ -110,7 +117,10 @@ class MaintenanceStartAPIView(GetObjectMixin, APIView): class MaintenanceStopAPIView(GetObjectMixin, APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "post": [RBACPermission.Permissions.MAINTENANCE_WRITE], + } def post(self, request): instance = self.get_object(request) diff --git a/engine/apps/api/views/on_call_shifts.py b/engine/apps/api/views/on_call_shifts.py index 548a9df6..b7b6df75 100644 --- a/engine/apps/api/views/on_call_shifts.py +++ b/engine/apps/api/views/on_call_shifts.py @@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.on_call_shifts import OnCallShiftSerializer, OnCallShiftUpdateSerializer from apps.auth_token.auth import PluginAuthentication from apps.schedules.models import CustomOnCallShift @@ -18,11 +18,20 @@ from common.insight_log import EntityEvent, write_resource_insight_log class OnCallShiftView(PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - action_permissions = { - IsAdmin: (*MODIFY_ACTIONS, "preview"), - AnyRole: (*READ_ACTIONS, "details", "frequency_options", "days_options"), + rbac_permissions = { + "metadata": [RBACPermission.Permissions.SCHEDULES_READ], + "list": [RBACPermission.Permissions.SCHEDULES_READ], + "retrieve": [RBACPermission.Permissions.SCHEDULES_READ], + "details": [RBACPermission.Permissions.SCHEDULES_READ], + "frequency_options": [RBACPermission.Permissions.SCHEDULES_READ], + "days_options": [RBACPermission.Permissions.SCHEDULES_READ], + "create": [RBACPermission.Permissions.SCHEDULES_WRITE], + "update": [RBACPermission.Permissions.SCHEDULES_WRITE], + "partial_update": [RBACPermission.Permissions.SCHEDULES_WRITE], + "destroy": [RBACPermission.Permissions.SCHEDULES_WRITE], + "preview": [RBACPermission.Permissions.SCHEDULES_WRITE], } model = CustomOnCallShift diff --git a/engine/apps/api/views/organization.py b/engine/apps/api/views/organization.py index 9545ffb3..86bae0bc 100644 --- a/engine/apps/api/views/organization.py +++ b/engine/apps/api/views/organization.py @@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from apps.api.permissions import AnyRole, IsAdmin, MethodPermission +from apps.api.permissions import RBACPermission from apps.api.serializers.organization import CurrentOrganizationSerializer from apps.auth_token.auth import PluginAuthentication from apps.base.messaging import get_messaging_backend_from_id @@ -16,9 +16,12 @@ from common.insight_log import EntityEvent, write_resource_insight_log class CurrentOrganizationView(APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, MethodPermission) + permission_classes = (IsAuthenticated, RBACPermission) - method_permissions = {IsAdmin: ("PUT",), AnyRole: ("GET",)} + rbac_permissions = { + "get": [], + "put": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + } def get(self, request): organization = request.auth.organization @@ -46,7 +49,11 @@ class CurrentOrganizationView(APIView): class GetTelegramVerificationCode(APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + + rbac_permissions = { + "get": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + } def get(self, request): organization = request.auth.organization @@ -66,7 +73,11 @@ class GetTelegramVerificationCode(APIView): class GetChannelVerificationCode(APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + + rbac_permissions = { + "get": [RBACPermission.Permissions.INTEGRATIONS_WRITE], + } def get(self, request): organization = request.auth.organization @@ -81,7 +92,11 @@ class GetChannelVerificationCode(APIView): class SetGeneralChannel(APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + + rbac_permissions = { + "post": [RBACPermission.Permissions.CHATOPS_UPDATE_SETTINGS], + } def post(self, request): SlackChannel = apps.get_model("slack", "SlackChannel") diff --git a/engine/apps/api/views/public_api_tokens.py b/engine/apps/api/views/public_api_tokens.py index 55833ce7..2bded740 100644 --- a/engine/apps/api/views/public_api_tokens.py +++ b/engine/apps/api/views/public_api_tokens.py @@ -2,7 +2,7 @@ from rest_framework import mixins, status, viewsets from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.public_api_token import PublicApiTokenSerializer from apps.auth_token.auth import PluginAuthentication from apps.auth_token.constants import MAX_PUBLIC_API_TOKENS_PER_USER @@ -19,9 +19,14 @@ class PublicApiTokenView( viewsets.GenericViewSet, ): authentication_classes = [PluginAuthentication] - permission_classes = [IsAuthenticated] - - action_permissions = {IsAdmin: (*MODIFY_ACTIONS, *READ_ACTIONS)} + permission_classes = [IsAuthenticated, RBACPermission] + rbac_permissions = { + "metadata": [RBACPermission.Permissions.API_KEYS_READ], + "list": [RBACPermission.Permissions.API_KEYS_READ], + "retrieve": [RBACPermission.Permissions.API_KEYS_READ], + "create": [RBACPermission.Permissions.API_KEYS_WRITE], + "destroy": [RBACPermission.Permissions.API_KEYS_WRITE], + } model = ApiAuthToken serializer_class = PublicApiTokenSerializer diff --git a/engine/apps/api/views/resolution_note.py b/engine/apps/api/views/resolution_note.py index 8400addd..02a77771 100644 --- a/engine/apps/api/views/resolution_note.py +++ b/engine/apps/api/views/resolution_note.py @@ -3,7 +3,7 @@ from rest_framework.viewsets import ModelViewSet from apps.alerts.models import ResolutionNote from apps.alerts.tasks import send_update_resolution_note_signal -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdminOrEditor +from apps.api.permissions import RBACPermission from apps.api.serializers.resolution_note import ResolutionNoteSerializer, ResolutionNoteUpdateSerializer from apps.auth_token.auth import PluginAuthentication from common.api_helpers.mixins import PublicPrimaryKeyMixin, UpdateSerializerMixin @@ -11,11 +11,16 @@ from common.api_helpers.mixins import PublicPrimaryKeyMixin, UpdateSerializerMix class ResolutionNoteView(PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - action_permissions = { - IsAdminOrEditor: MODIFY_ACTIONS, - AnyRole: READ_ACTIONS, + rbac_permissions = { + "metadata": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "list": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "retrieve": [RBACPermission.Permissions.ALERT_GROUPS_READ], + "create": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "update": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "partial_update": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], + "destroy": [RBACPermission.Permissions.ALERT_GROUPS_WRITE], } model = ResolutionNote diff --git a/engine/apps/api/views/schedule.py b/engine/apps/api/views/schedule.py index 7ecebaf4..8dcf3632 100644 --- a/engine/apps/api/views/schedule.py +++ b/engine/apps/api/views/schedule.py @@ -14,7 +14,7 @@ from rest_framework.views import Response from rest_framework.viewsets import ModelViewSet from apps.alerts.models import EscalationChain, EscalationPolicy -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin, IsAdminOrEditor +from apps.api.permissions import RBACPermission from apps.api.serializers.schedule_base import ScheduleFastSerializer from apps.api.serializers.schedule_polymorphic import ( PolymorphicScheduleCreateSerializer, @@ -56,24 +56,26 @@ class ScheduleView( ModelViewSet, ): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) - action_permissions = { - IsAdmin: ( - *MODIFY_ACTIONS, - "reload_ical", - ), - IsAdminOrEditor: ("export_token",), - AnyRole: ( - *READ_ACTIONS, - "events", - "filter_events", - "next_shifts_per_user", - "notify_empty_oncall_options", - "notify_oncall_shift_freq_options", - "mention_options", - "related_escalation_chains", - ), + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "metadata": [RBACPermission.Permissions.SCHEDULES_READ], + "list": [RBACPermission.Permissions.SCHEDULES_READ], + "retrieve": [RBACPermission.Permissions.SCHEDULES_READ], + "events": [RBACPermission.Permissions.SCHEDULES_READ], + "filter_events": [RBACPermission.Permissions.SCHEDULES_READ], + "next_shifts_per_user": [RBACPermission.Permissions.SCHEDULES_READ], + "notify_empty_oncall_options": [RBACPermission.Permissions.SCHEDULES_READ], + "notify_oncall_shift_freq_options": [RBACPermission.Permissions.SCHEDULES_READ], + "mention_options": [RBACPermission.Permissions.SCHEDULES_READ], + "related_escalation_chains": [RBACPermission.Permissions.SCHEDULES_READ], + "create": [RBACPermission.Permissions.SCHEDULES_WRITE], + "update": [RBACPermission.Permissions.SCHEDULES_WRITE], + "partial_update": [RBACPermission.Permissions.SCHEDULES_WRITE], + "destroy": [RBACPermission.Permissions.SCHEDULES_WRITE], + "reload_ical": [RBACPermission.Permissions.SCHEDULES_WRITE], + "export_token": [RBACPermission.Permissions.SCHEDULES_EXPORT], } + filter_backends = [SearchFilter] search_fields = ("name",) diff --git a/engine/apps/api/views/slack_team_settings.py b/engine/apps/api/views/slack_team_settings.py index 0da7525b..e52f250b 100644 --- a/engine/apps/api/views/slack_team_settings.py +++ b/engine/apps/api/views/slack_team_settings.py @@ -2,7 +2,7 @@ from rest_framework import views from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from apps.api.permissions import AnyRole, IsAdmin, MethodPermission +from apps.api.permissions import RBACPermission from apps.api.serializers.organization_slack_settings import OrganizationSlackSettingsSerializer from apps.auth_token.auth import PluginAuthentication from apps.user_management.models import Organization @@ -11,11 +11,11 @@ from common.insight_log import EntityEvent, write_resource_insight_log class SlackTeamSettingsAPIView(views.APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, MethodPermission) + permission_classes = (IsAuthenticated, RBACPermission) - method_permissions = { - IsAdmin: ("PUT",), - AnyRole: ("GET",), + rbac_permissions = { + "get": [RBACPermission.Permissions.CHATOPS_READ], + "put": [RBACPermission.Permissions.CHATOPS_UPDATE_SETTINGS], } serializer_class = OrganizationSlackSettingsSerializer diff --git a/engine/apps/api/views/telegram_channels.py b/engine/apps/api/views/telegram_channels.py index a8d5cdbb..7fd9975d 100644 --- a/engine/apps/api/views/telegram_channels.py +++ b/engine/apps/api/views/telegram_channels.py @@ -4,7 +4,7 @@ from rest_framework.decorators import action from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response -from apps.api.permissions import MODIFY_ACTIONS, READ_ACTIONS, ActionPermission, AnyRole, IsAdmin +from apps.api.permissions import RBACPermission from apps.api.serializers.telegram import TelegramToOrganizationConnectorSerializer from apps.auth_token.auth import PluginAuthentication from common.api_helpers.mixins import PublicPrimaryKeyMixin @@ -19,11 +19,14 @@ class TelegramChannelViewSet( viewsets.GenericViewSet, ): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - action_permissions = { - IsAdmin: (*MODIFY_ACTIONS, "set_default"), - AnyRole: READ_ACTIONS, + rbac_permissions = { + "metadata": [RBACPermission.Permissions.CHATOPS_READ], + "list": [RBACPermission.Permissions.CHATOPS_READ], + "retrieve": [RBACPermission.Permissions.CHATOPS_READ], + "destroy": [RBACPermission.Permissions.CHATOPS_UPDATE_SETTINGS], + "set_default": [RBACPermission.Permissions.CHATOPS_UPDATE_SETTINGS], } serializer_class = TelegramToOrganizationConnectorSerializer diff --git a/engine/apps/api/views/user.py b/engine/apps/api/views/user.py index 0126abaf..be7ff475 100644 --- a/engine/apps/api/views/user.py +++ b/engine/apps/api/views/user.py @@ -16,12 +16,10 @@ from rest_framework.response import Response from rest_framework.views import APIView from apps.api.permissions import ( - MODIFY_ACTIONS, - READ_ACTIONS, - ActionPermission, - AnyRole, - IsAdminOrEditor, - IsOwnerOrAdmin, + IsOwnerOrHasRBACPermissions, + LegacyAccessControlRole, + RBACPermission, + user_is_authorized, ) from apps.api.serializers.team import TeamSerializer from apps.api.serializers.user import FilterUserSerializer, UserHiddenFieldsSerializer, UserSerializer @@ -41,7 +39,6 @@ from common.api_helpers.exceptions import Conflict from common.api_helpers.mixins import FilterSerializerMixin, PublicPrimaryKeyMixin from common.api_helpers.paginators import HundredPageSizePaginator from common.api_helpers.utils import create_engine_url -from common.constants.role import Role from common.insight_log import ( ChatOpsEvent, ChatOpsType, @@ -51,6 +48,7 @@ from common.insight_log import ( ) logger = logging.getLogger(__name__) +IsOwnerOrHasUserSettingsAdminPermission = IsOwnerOrHasRBACPermissions([RBACPermission.Permissions.USER_SETTINGS_ADMIN]) class CurrentUserView(APIView): @@ -89,7 +87,9 @@ class UserFilter(filters.FilterSet): """ email = filters.CharFilter(field_name="email", lookup_expr="icontains") - roles = filters.MultipleChoiceFilter(field_name="role", choices=Role.choices()) + roles = filters.MultipleChoiceFilter( + field_name="role", choices=LegacyAccessControlRole.choices() + ) # LEGACY.. this should get removed eventually class Meta: model = User @@ -109,35 +109,36 @@ class UserView( PluginAuthentication, ) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - # Non-admin users are allowed to list and retrieve users - # The overridden get_serializer_class will return - # another Serializer for non-admin users with sensitive information hidden - action_permissions = { - IsAdminOrEditor: ( - *MODIFY_ACTIONS, - "list", - "metadata", - "verify_number", - "forget_number", - "get_verification_code", - "get_backend_verification_code", - "get_telegram_verification_code", - "unlink_slack", - "unlink_telegram", - "unlink_backend", - "make_test_call", - "export_token", - "mobile_app_auth_token", - ), - AnyRole: ("retrieve", "timezone_options"), + rbac_permissions = { + "retrieve": [RBACPermission.Permissions.USER_SETTINGS_READ], + "timezone_options": [RBACPermission.Permissions.USER_SETTINGS_READ], + "metadata": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "list": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "update": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "partial_update": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "verify_number": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "forget_number": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "get_verification_code": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "get_backend_verification_code": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "get_telegram_verification_code": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "unlink_slack": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "unlink_telegram": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "unlink_backend": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "make_test_call": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "export_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "mobile_app_auth_token": [RBACPermission.Permissions.USER_SETTINGS_WRITE], } - action_object_permissions = { - IsOwnerOrAdmin: ( - *MODIFY_ACTIONS, - *READ_ACTIONS, + rbac_object_permissions = { + IsOwnerOrHasUserSettingsAdminPermission: [ + "metadata", + "list", + "retrieve", + "update", + "partial_update", + "destroy", "verify_number", "forget_number", "get_verification_code", @@ -149,7 +150,7 @@ class UserView( "make_test_call", "export_token", "mobile_app_auth_token", - ), + ], } filter_serializer_class = FilterUserSerializer @@ -172,14 +173,18 @@ class UserView( filterset_class = UserFilter def get_serializer_class(self): - is_filters_request = self.request.query_params.get("filters", "false") == "true" + request = self.request + user = request.user + kwargs = self.kwargs + + is_filters_request = request.query_params.get("filters", "false") == "true" if self.action in ["list"] and is_filters_request: return self.get_filter_serializer_class() - is_users_own_data = ( - self.kwargs.get("pk") is not None and self.kwargs.get("pk") == self.request.user.public_primary_key - ) - if is_users_own_data or self.request.user.role == Role.ADMIN: + is_users_own_data = kwargs.get("pk") is not None and kwargs.get("pk") == user.public_primary_key + has_admin_permission = user_is_authorized(user, [RBACPermission.Permissions.USER_SETTINGS_ADMIN]) + + if is_users_own_data or has_admin_permission: return UserSerializer return UserHiddenFieldsSerializer diff --git a/engine/apps/api/views/user_notification_policy.py b/engine/apps/api/views/user_notification_policy.py index c1d7e553..e43622a0 100644 --- a/engine/apps/api/views/user_notification_policy.py +++ b/engine/apps/api/views/user_notification_policy.py @@ -6,14 +6,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.viewsets import ModelViewSet -from apps.api.permissions import ( - MODIFY_ACTIONS, - READ_ACTIONS, - ActionPermission, - AnyRole, - IsAdminOrEditor, - IsOwnerOrAdmin, -) +from apps.api.permissions import IsOwnerOrHasRBACPermissions, RBACPermission from apps.api.serializers.user_notification_policy import ( UserNotificationPolicySerializer, UserNotificationPolicyUpdateSerializer, @@ -31,18 +24,34 @@ from common.insight_log import EntityEvent, write_resource_insight_log class UserNotificationPolicyView(UpdateSerializerMixin, ModelViewSet): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - action_permissions = { - IsAdminOrEditor: (*MODIFY_ACTIONS, "move_to_position"), - AnyRole: (*READ_ACTIONS, "delay_options", "notify_by_options"), - } - action_object_permissions = { - IsOwnerOrAdmin: (*MODIFY_ACTIONS, "move_to_position"), - AnyRole: READ_ACTIONS, + rbac_permissions = { + "metadata": [RBACPermission.Permissions.USER_SETTINGS_READ], + "list": [RBACPermission.Permissions.USER_SETTINGS_READ], + "retrieve": [RBACPermission.Permissions.USER_SETTINGS_READ], + "delay_options": [RBACPermission.Permissions.USER_SETTINGS_READ], + "notify_by_options": [RBACPermission.Permissions.USER_SETTINGS_READ], + "create": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "update": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "partial_update": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "destroy": [RBACPermission.Permissions.USER_SETTINGS_WRITE], + "move_to_position": [RBACPermission.Permissions.USER_SETTINGS_WRITE], } - ownership_field = "user" + IsOwnerOrHasUserSettingsAdminPermission = IsOwnerOrHasRBACPermissions( + required_permissions=[RBACPermission.Permissions.USER_SETTINGS_ADMIN], ownership_field="user" + ) + + rbac_object_permissions = { + IsOwnerOrHasUserSettingsAdminPermission: [ + "create", + "update", + "partial_update", + "destroy", + "move_to_position", + ], + } model = UserNotificationPolicy serializer_class = UserNotificationPolicySerializer diff --git a/engine/apps/auth_token/auth.py b/engine/apps/auth_token/auth.py index 47d8ece9..8b0da92a 100644 --- a/engine/apps/auth_token/auth.py +++ b/engine/apps/auth_token/auth.py @@ -8,11 +8,11 @@ from rest_framework import exceptions from rest_framework.authentication import BaseAuthentication, get_authorization_header from rest_framework.request import Request +from apps.api.permissions import RBACPermission, user_is_authorized from apps.grafana_plugin.helpers.gcom import check_token from apps.user_management.models import User from apps.user_management.models.organization import Organization from apps.user_management.models.region import OrganizationMovedException -from common.constants.role import Role from .constants import SCHEDULE_EXPORT_TOKEN_NAME, SLACK_AUTH_TOKEN_NAME from .exceptions import InvalidToken @@ -29,7 +29,7 @@ class ApiTokenAuthentication(BaseAuthentication): auth = get_authorization_header(request).decode("utf-8") user, auth_token = self.authenticate_credentials(auth) - if user.role != Role.ADMIN: + if not user_is_authorized(user, [RBACPermission.Permissions.API_KEYS_WRITE]): raise exceptions.AuthenticationFailed( "Only users with Admin permissions are allowed to perform this action." ) diff --git a/engine/apps/base/constants.py b/engine/apps/base/constants.py deleted file mode 100644 index 3e719f8c..00000000 --- a/engine/apps/base/constants.py +++ /dev/null @@ -1,23 +0,0 @@ -# This is temporary solution to not to hardcode permissions on frontend -# Is should be removed with one which will collect permission from action_permission views' attribute -ALL_PERMISSIONS = [ - "update_incidents", - "update_alert_receive_channels", - "update_escalation_policies", - "update_notification_policies", - "update_general_log_channel_id", - "update_own_settings", - "update_other_users_settings", - "update_integrations", - "update_schedules", - "update_custom_actions", - "update_api_tokens", - "update_teams", - "update_maintenances", - "update_global_settings", - "send_demo_alert", - "view_other_users", -] -ADMIN_PERMISSIONS = ALL_PERMISSIONS -EDITOR_PERMISSIONS = ["update_incidents", "update_own_settings", "view_other_users"] -ALL_ROLES_PERMISSIONS = [] diff --git a/engine/apps/base/models/user_notification_policy_log_record.py b/engine/apps/base/models/user_notification_policy_log_record.py index 3d9e366b..efd8d6e1 100644 --- a/engine/apps/base/models/user_notification_policy_log_record.py +++ b/engine/apps/base/models/user_notification_policy_log_record.py @@ -68,7 +68,7 @@ class UserNotificationPolicyLogRecord(models.Model): ERROR_NOTIFICATION_IN_SLACK_CHANNEL_IS_ARCHIVED, ERROR_NOTIFICATION_IN_SLACK_RATELIMIT, ERROR_NOTIFICATION_MESSAGING_BACKEND_ERROR, - ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE, + ERROR_NOTIFICATION_FORBIDDEN, ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED, ) = range(27) @@ -258,10 +258,8 @@ class UserNotificationPolicyLogRecord(models.Model): result += f"failed to notify {user_verbal} in Slack, because channel is archived" elif self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_IN_SLACK_RATELIMIT: result += f"failed to notify {user_verbal} in Slack due to Slack rate limit" - elif ( - self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_NOT_ALLOWED_USER_ROLE - ): - result += f"failed to notify {user_verbal}, not allowed role" + elif self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_FORBIDDEN: + result += f"failed to notify {user_verbal}, not allowed" elif ( self.notification_error_code == UserNotificationPolicyLogRecord.ERROR_NOTIFICATION_TELEGRAM_USER_IS_DEACTIVATED diff --git a/engine/apps/grafana_plugin/helpers/client.py b/engine/apps/grafana_plugin/helpers/client.py index c23c5f90..3aeaaa15 100644 --- a/engine/apps/grafana_plugin/helpers/client.py +++ b/engine/apps/grafana_plugin/helpers/client.py @@ -1,7 +1,7 @@ import json import logging import time -from typing import Optional, Tuple +from typing import Dict, List, Optional, Tuple, TypedDict from urllib.parse import urljoin import requests @@ -9,23 +9,51 @@ from django.conf import settings from rest_framework import status from rest_framework.response import Response +from apps.api.permissions import ACTION_PREFIX, GrafanaAPIPermission + logger = logging.getLogger(__name__) +class GrafanaUser(TypedDict): + orgId: int + userId: int + email: str + name: str + avatarUrl: str + login: str + role: str + lastSeenAt: str + lastSeenAtAge: str + + +class GrafanaUserWithPermissions(GrafanaUser): + permissions: List[GrafanaAPIPermission] + + +class GCOMInstanceInfo(TypedDict): + id: int + orgId: int + slug: str + orgSlug: str + orgName: str + url: str + status: str + + class APIClient: def __init__(self, api_url: str, api_token: str): self.api_url = api_url self.api_token = api_token + def api_head(self, endpoint: str, body: dict = None) -> Tuple[Optional[Response], dict]: + return self.call_api(endpoint, requests.head, body) + def api_get(self, endpoint: str) -> Tuple[Optional[Response], dict]: return self.call_api(endpoint, requests.get) def api_post(self, endpoint: str, body: dict = None) -> Tuple[Optional[Response], dict]: return self.call_api(endpoint, requests.post, body) - def api_head(self, endpoint: str, body: dict = None) -> Tuple[Optional[Response], dict]: - return self.call_api(endpoint, requests.head, body) - def call_api(self, endpoint: str, http_method, body: dict = None) -> Tuple[Optional[Response], dict]: request_start = time.perf_counter() call_status = { @@ -72,30 +100,60 @@ class APIClient: class GrafanaAPIClient(APIClient): + USER_PERMISSION_ENDPOINT = f"api/access-control/users/permissions?actionPrefix={ACTION_PREFIX}" + def __init__(self, api_url: str, api_token: str): super().__init__(api_url, api_token) def check_token(self) -> Tuple[Optional[Response], dict]: return self.api_head("api/org") - def get_users(self) -> Tuple[Optional[Response], dict]: + def get_users_permissions(self, rbac_is_enabled_for_org: bool) -> Dict[str, List[GrafanaAPIPermission]]: """ - Response example: - [ - { - 'orgId': 1, - 'userId': 1, - 'email': 'user@example.com', - 'name': 'User User', - 'avatarUrl': '/avatar/79163f696e9e08958c0d3f73c160e2cc', - 'login': 'user', - 'role': 'Admin', - 'lastSeenAt': '2021-06-21T07:01:45Z', - 'lastSeenAtAge': '9m' - }, - ] + It is possible that this endpoint may not be available for certain Grafana orgs. + Ex: for Grafana Cloud orgs whom have pinned their Grafana version to an earlier version + where this endpoint is not available + + The response from the Grafana endpoint will look something like this: + { + "1": { + "grafana-oncall-app.alert-groups:read": [ + "" + ], + "grafana-oncall-app.alert-groups:write": [ + "" + ] + } + } """ - return self.api_get("api/org/users") + if not rbac_is_enabled_for_org: + return {} + data, _ = self.api_get(self.USER_PERMISSION_ENDPOINT) + if data is None: + return {} + + all_users_permissions = {} + for user_id, user_permissions in data.items(): + all_users_permissions[user_id] = [GrafanaAPIPermission(action=key) for key, _ in user_permissions.items()] + + return all_users_permissions + + def is_rbac_enabled_for_organization(self) -> bool: + _, resp_status = self.api_head(self.USER_PERMISSION_ENDPOINT) + return resp_status["status_code"] == status.HTTP_200_OK + + def get_users(self, rbac_is_enabled_for_org: bool) -> List[GrafanaUserWithPermissions]: + users, _ = self.api_get("api/org/users") + + if not users: + return [] + + user_permissions = self.get_users_permissions(rbac_is_enabled_for_org) + + # merge the users permissions response into the org users response + for user in users: + user["permissions"] = user_permissions.get(str(user["userId"]), []) + return users def get_teams(self): return self.api_get("api/teams/search?perpage=1000000") @@ -127,6 +185,7 @@ class GcomAPIClient(APIClient): ACTIVE_INSTANCE_QUERY = "instances?status=active" DELETED_INSTANCE_QUERY = "instances?status=deleted&includeDeleted=true" STACK_STATUS_DELETED = "deleted" + STACK_STATUS_ACTIVE = "active" def __init__(self, api_token: str): super().__init__(settings.GRAFANA_COM_API_URL, api_token) @@ -134,14 +193,15 @@ class GcomAPIClient(APIClient): def check_token(self): return self.api_post("api-keys/check", {"token": self.api_token}) - def get_instance_info(self, stack_id: str): - return self.api_get(f"instances/{stack_id}") + def get_instance_info(self, stack_id: str) -> Optional[GCOMInstanceInfo]: + data, _ = self.api_get(f"instances/{stack_id}?config=true") + return data def get_instances(self, query: str): return self.api_get(query) def is_stack_deleted(self, stack_id: str) -> bool: - instance_info, call_status = self.get_instance_info(stack_id) + instance_info = self.get_instance_info(stack_id) return instance_info and instance_info.get("status") == self.STACK_STATUS_DELETED def post_active_users(self, body): diff --git a/engine/apps/grafana_plugin/helpers/gcom.py b/engine/apps/grafana_plugin/helpers/gcom.py index 407f70e5..f4b5b47b 100644 --- a/engine/apps/grafana_plugin/helpers/gcom.py +++ b/engine/apps/grafana_plugin/helpers/gcom.py @@ -40,10 +40,12 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]: logger.debug(f"Start authenticate by making request to gcom api for org={org_id}, stack_id={stack_id}") client = GcomAPIClient(token_string) - instance_info, status = client.get_instance_info(stack_id) + instance_info = client.get_instance_info(stack_id) if not instance_info or str(instance_info["orgId"]) != org_id: raise InvalidToken + rbac_is_enabled = client.is_rbac_enabled_for_organization() + if not organization: DynamicSetting = apps.get_model("base", "DynamicSetting") allow_signup = DynamicSetting.objects.get_or_create( @@ -60,6 +62,7 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]: region_slug=instance_info["regionSlug"], gcom_token=token_string, gcom_token_org_last_time_synced=timezone.now(), + is_rbac_permissions_enabled=rbac_is_enabled, ) else: organization.stack_slug = instance_info["slug"] @@ -69,6 +72,7 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]: organization.grafana_url = instance_info["url"] organization.gcom_token = token_string organization.gcom_token_org_last_time_synced = timezone.now() + organization.is_rbac_permissions_enabled = rbac_is_enabled organization.save( update_fields=[ "stack_slug", @@ -78,6 +82,7 @@ def check_gcom_permission(token_string: str, context) -> Optional["GcomToken"]: "grafana_url", "gcom_token", "gcom_token_org_last_time_synced", + "is_rbac_permissions_enabled", ] ) logger.debug(f"Finish authenticate by making request to gcom api for org={org_id}, stack_id={stack_id}") diff --git a/engine/apps/grafana_plugin/tasks/sync.py b/engine/apps/grafana_plugin/tasks/sync.py index ed58968d..204797d9 100644 --- a/engine/apps/grafana_plugin/tasks/sync.py +++ b/engine/apps/grafana_plugin/tasks/sync.py @@ -68,8 +68,8 @@ def run_organization_sync(organization_pk, force_sync): return if settings.GRAFANA_COM_API_TOKEN and settings.LICENSE == settings.CLOUD_LICENSE_NAME: client = GcomAPIClient(settings.GRAFANA_COM_API_TOKEN) - instance_info, status = client.get_instance_info(organization.stack_id) - if not instance_info or instance_info["status"] != "active": + instance_info = client.get_instance_info(organization.stack_id) + if not instance_info or instance_info["status"] != client.STACK_STATUS_ACTIVE: logger.debug(f"Canceling sync for Organization {organization_pk}, as it is no longer active.") return diff --git a/engine/apps/grafana_plugin/tests/test_grafana_api_client.py b/engine/apps/grafana_plugin/tests/test_grafana_api_client.py new file mode 100644 index 00000000..ef4af42e --- /dev/null +++ b/engine/apps/grafana_plugin/tests/test_grafana_api_client.py @@ -0,0 +1,60 @@ +from unittest.mock import patch + +import pytest +from rest_framework import status + +from apps.grafana_plugin.helpers.client import GrafanaAPIClient + +API_URL = "/foo/bar" +API_TOKEN = "dfjkfdjkfd" + + +class TestGetUsersPermissions: + def test_rbac_is_not_enabled_for_org(self): + api_client = GrafanaAPIClient(API_URL, API_TOKEN) + permissions = api_client.get_users_permissions(False) + assert len(permissions.keys()) == 0 + + @patch("apps.grafana_plugin.views.self_hosted_install.GrafanaAPIClient.api_get") + def test_api_call_returns_none(self, mocked_grafana_api_client_api_get): + mocked_grafana_api_client_api_get.return_value = (None, "dfkjfdkj") + + api_client = GrafanaAPIClient(API_URL, API_TOKEN) + + permissions = api_client.get_users_permissions(True) + assert len(permissions.keys()) == 0 + + @patch("apps.grafana_plugin.views.self_hosted_install.GrafanaAPIClient.api_get") + def test_it_properly_transforms_the_data(self, mocked_grafana_api_client_api_get): + mocked_grafana_api_client_api_get.return_value = ( + {"1": {"grafana-oncall-app.alert-groups:read": [""], "grafana-oncall-app.alert-groups:write": [""]}}, + "asdfasdf", + ) + + api_client = GrafanaAPIClient(API_URL, API_TOKEN) + + permissions = api_client.get_users_permissions(True) + assert permissions == { + "1": [ + {"action": "grafana-oncall-app.alert-groups:read"}, + {"action": "grafana-oncall-app.alert-groups:write"}, + ] + } + + +class TestIsRbacEnabledForOrganization: + @pytest.mark.parametrize( + "grafana_api_status_code,expected", + [ + (status.HTTP_200_OK, True), + (status.HTTP_404_NOT_FOUND, False), + ], + ) + @patch("apps.grafana_plugin.views.self_hosted_install.GrafanaAPIClient.api_head") + def test_it_returns_based_on_status_code_of_head_call( + self, mocked_grafana_api_client_api_head, grafana_api_status_code, expected + ): + mocked_grafana_api_client_api_head.return_value = (None, {"status_code": grafana_api_status_code}) + + api_client = GrafanaAPIClient(API_URL, API_TOKEN) + assert api_client.is_rbac_enabled_for_organization() == expected diff --git a/engine/apps/grafana_plugin/tests/test_self_hosted_install.py b/engine/apps/grafana_plugin/tests/test_self_hosted_install.py index 62d835d5..244d8d4d 100644 --- a/engine/apps/grafana_plugin/tests/test_self_hosted_install.py +++ b/engine/apps/grafana_plugin/tests/test_self_hosted_install.py @@ -99,6 +99,7 @@ def test_if_organization_exists_it_is_updated( mocked_provision_plugin.return_value = provision_plugin_response mocked_grafana_api_client.return_value.check_token.return_value = (None, {"status_code": status.HTTP_200_OK}) + mocked_grafana_api_client.return_value.is_rbac_enabled_for_organization.return_value = True client = APIClient() url = reverse("grafana-plugin:self-hosted-install") @@ -106,6 +107,8 @@ def test_if_organization_exists_it_is_updated( assert mocked_grafana_api_client.called_once_with(api_url=GRAFANA_API_URL, api_token=GRAFANA_TOKEN) assert mocked_grafana_api_client.return_value.check_token.called_once_with() + assert mocked_grafana_api_client.return_value.is_rbac_enabled_for_organization.called_once_with() + assert mocked_sync_organization.called_once_with(organization) assert mocked_provision_plugin.called_once_with() assert mocked_revoke_plugin.called_once_with() @@ -117,6 +120,7 @@ def test_if_organization_exists_it_is_updated( assert organization.grafana_url == GRAFANA_API_URL assert organization.api_token == GRAFANA_TOKEN + assert organization.is_rbac_permissions_enabled is True @override_settings(SELF_HOSTED_SETTINGS=SELF_HOSTED_SETTINGS) @@ -136,6 +140,7 @@ def test_if_organization_does_not_exist_it_is_created( mocked_provision_plugin.return_value = provision_plugin_response mocked_grafana_api_client.return_value.check_token.return_value = (None, {"status_code": status.HTTP_200_OK}) + mocked_grafana_api_client.return_value.is_rbac_enabled_for_organization.return_value = True client = APIClient() url = reverse("grafana-plugin:self-hosted-install") @@ -146,6 +151,8 @@ def test_if_organization_does_not_exist_it_is_created( assert mocked_grafana_api_client.called_once_with(api_url=GRAFANA_API_URL, api_token=GRAFANA_TOKEN) assert mocked_grafana_api_client.return_value.check_token.called_once_with() + assert mocked_grafana_api_client.return_value.is_rbac_enabled_for_organization.called_once_with() + assert mocked_sync_organization.called_once_with(organization) assert mocked_provision_plugin.called_once_with() assert not mocked_revoke_plugin.called @@ -160,3 +167,4 @@ def test_if_organization_does_not_exist_it_is_created( assert organization.region_slug == REGION_SLUG assert organization.grafana_url == GRAFANA_API_URL assert organization.api_token == GRAFANA_TOKEN + assert organization.is_rbac_permissions_enabled is True diff --git a/engine/apps/grafana_plugin/tests/test_sync.py b/engine/apps/grafana_plugin/tests/test_sync.py index f37b2e41..7cf8f11f 100644 --- a/engine/apps/grafana_plugin/tests/test_sync.py +++ b/engine/apps/grafana_plugin/tests/test_sync.py @@ -26,6 +26,8 @@ class TestGcomAPIClient: info = None status = None + STACK_STATUS_ACTIVE = "active" + def reset(self): self.called = False self.info = None @@ -39,7 +41,7 @@ class TestGcomAPIClient: def get_instance_info(self, stack_id: str): self.called = True - return self.info, self.status + return self.info @pytest.mark.django_db diff --git a/engine/apps/grafana_plugin/views/self_hosted_install.py b/engine/apps/grafana_plugin/views/self_hosted_install.py index 7117cf63..8a40fed0 100644 --- a/engine/apps/grafana_plugin/views/self_hosted_install.py +++ b/engine/apps/grafana_plugin/views/self_hosted_install.py @@ -43,11 +43,14 @@ class SelfHostedInstallView(GrafanaHeadersMixin, APIView): return Response(data=provisioning_info, status=status.HTTP_400_BAD_REQUEST) organization = Organization.objects.filter(stack_id=stack_id, org_id=org_id).first() + rbac_is_enabled = grafana_api_client.is_rbac_enabled_for_organization() + if organization: organization.revoke_plugin() organization.grafana_url = grafana_url organization.api_token = grafana_api_token - organization.save(update_fields=["grafana_url", "api_token"]) + organization.is_rbac_permissions_enabled = rbac_is_enabled + organization.save(update_fields=["grafana_url", "api_token", "is_rbac_permissions_enabled"]) else: organization = Organization.objects.create( stack_id=stack_id, @@ -58,6 +61,7 @@ class SelfHostedInstallView(GrafanaHeadersMixin, APIView): region_slug=settings.SELF_HOSTED_SETTINGS["REGION_SLUG"], grafana_url=grafana_url, api_token=grafana_api_token, + is_rbac_permissions_enabled=rbac_is_enabled, ) sync_organization(organization) diff --git a/engine/apps/oss_installation/models/cloud_connector.py b/engine/apps/oss_installation/models/cloud_connector.py index 07eb6724..3a828a51 100644 --- a/engine/apps/oss_installation/models/cloud_connector.py +++ b/engine/apps/oss_installation/models/cloud_connector.py @@ -8,7 +8,6 @@ from apps.base.utils import live_settings from apps.oss_installation.models.cloud_user_identity import CloudUserIdentity from apps.user_management.models import User from common.api_helpers.utils import create_engine_url -from common.constants.role import Role from settings.base import GRAFANA_CLOUD_ONCALL_API_URL logger = logging.getLogger(__name__) @@ -61,7 +60,7 @@ class CloudConnector(models.Model): logger.warning("Unable to sync with cloud. GRAFANA_CLOUD_ONCALL_TOKEN is not set") error_msg = "GRAFANA_CLOUD_ONCALL_TOKEN is not set" - existing_emails = list(User.objects.filter(role__in=(Role.ADMIN, Role.EDITOR)).values_list("email", flat=True)) + existing_emails = [user.email for user in User.objects.all() if user.is_notification_allowed] matching_users = [] users_url = create_engine_url("api/v1/users", override_base=GRAFANA_CLOUD_ONCALL_API_URL) diff --git a/engine/apps/oss_installation/views/cloud_connection.py b/engine/apps/oss_installation/views/cloud_connection.py index 21b6624c..de73343c 100644 --- a/engine/apps/oss_installation/views/cloud_connection.py +++ b/engine/apps/oss_installation/views/cloud_connection.py @@ -3,7 +3,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from apps.api.permissions import IsAdmin +from apps.api.permissions import RBACPermission from apps.auth_token.auth import PluginAuthentication from apps.base.models import LiveSetting from apps.base.utils import live_settings @@ -13,7 +13,11 @@ from apps.oss_installation.models import CloudConnector, CloudHeartbeat class CloudConnectionView(APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "get": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + "delete": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + } def get(self, request): connector = CloudConnector.objects.first() diff --git a/engine/apps/oss_installation/views/cloud_heartbeat.py b/engine/apps/oss_installation/views/cloud_heartbeat.py index 932087c3..a3a2973e 100644 --- a/engine/apps/oss_installation/views/cloud_heartbeat.py +++ b/engine/apps/oss_installation/views/cloud_heartbeat.py @@ -3,7 +3,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from apps.api.permissions import IsAdmin +from apps.api.permissions import RBACPermission from apps.auth_token.auth import PluginAuthentication from apps.oss_installation.cloud_heartbeat import get_heartbeat_link, setup_heartbeat_integration from apps.oss_installation.models import CloudConnector, CloudHeartbeat @@ -11,7 +11,10 @@ from apps.oss_installation.models import CloudConnector, CloudHeartbeat class CloudHeartbeatView(APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + rbac_permissions = { + "post": [RBACPermission.Permissions.OTHER_SETTINGS_WRITE], + } def post(self, request): connector = CloudConnector.objects.first() diff --git a/engine/apps/oss_installation/views/cloud_users.py b/engine/apps/oss_installation/views/cloud_users.py index 3eb7685b..75d3886a 100644 --- a/engine/apps/oss_installation/views/cloud_users.py +++ b/engine/apps/oss_installation/views/cloud_users.py @@ -6,7 +6,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from apps.api.permissions import ActionPermission, AnyRole, IsAdmin, IsOwnerOrAdmin +from apps.api.permissions import IsOwnerOrHasRBACPermissions, RBACPermission from apps.auth_token.auth import PluginAuthentication from apps.oss_installation.models import CloudConnector, CloudUserIdentity from apps.oss_installation.serializers import CloudUserSerializer @@ -14,17 +14,26 @@ from apps.oss_installation.utils import cloud_user_identity_status from apps.user_management.models import User from common.api_helpers.mixins import PublicPrimaryKeyMixin from common.api_helpers.paginators import HundredPageSizePaginator -from common.constants.role import Role + +PERMISSIONS = [RBACPermission.Permissions.OTHER_SETTINGS_WRITE] class CloudUsersView(HundredPageSizePaginator, APIView): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, IsAdmin) + permission_classes = (IsAuthenticated, RBACPermission) + + rbac_permissions = { + "get": PERMISSIONS, + "post": PERMISSIONS, + } def get(self, request): organization = request.user.organization - queryset = User.objects.filter(organization=organization, role__in=[Role.ADMIN, Role.EDITOR]) + queryset = User.objects.filter( + organization=organization, + **User.build_permissions_query(RBACPermission.Permissions.NOTIFICATIONS_READ, organization), + ) if request.user.current_team is not None: queryset = queryset.filter(teams=request.user.current_team).distinct() @@ -81,15 +90,24 @@ class CloudUserView( viewsets.GenericViewSet, ): authentication_classes = (PluginAuthentication,) - permission_classes = (IsAuthenticated, ActionPermission) + permission_classes = (IsAuthenticated, RBACPermission) - action_permissions = { - AnyRole: ("retrieve",), - IsAdmin: ("sync",), + rbac_permissions = { + "retrieve": PERMISSIONS, + "sync": PERMISSIONS, } - action_object_permissions = { - IsOwnerOrAdmin: ("retrieve", "sync"), + + IsOwnerOrHasUserSettingsAdminPermission = IsOwnerOrHasRBACPermissions( + [RBACPermission.Permissions.USER_SETTINGS_ADMIN] + ) + + rbac_object_permissions = { + IsOwnerOrHasUserSettingsAdminPermission: [ + "retrieve", + "sync", + ], } + serializer_class = CloudUserSerializer def get_queryset(self): diff --git a/engine/apps/public_api/serializers/users.py b/engine/apps/public_api/serializers/users.py index 8afdedaf..6b0f8f26 100644 --- a/engine/apps/public_api/serializers/users.py +++ b/engine/apps/public_api/serializers/users.py @@ -1,9 +1,9 @@ from rest_framework import serializers +from apps.api.permissions import LegacyAccessControlRole from apps.slack.models import SlackUserIdentity from apps.user_management.models import User from common.api_helpers.mixins import EagerLoadingMixin -from common.constants.role import Role class SlackUserIdentitySerializer(serializers.ModelSerializer): @@ -21,7 +21,7 @@ class SlackUserIdentitySerializer(serializers.ModelSerializer): class FastUserSerializer(serializers.ModelSerializer): id = serializers.ReadOnlyField(read_only=True, source="public_primary_key") email = serializers.EmailField(read_only=True) - role = serializers.SerializerMethodField() + role = serializers.SerializerMethodField() # LEGACY, should be removed eventually is_phone_number_verified = serializers.SerializerMethodField() class Meta: @@ -30,7 +30,10 @@ class FastUserSerializer(serializers.ModelSerializer): @staticmethod def get_role(obj): - return Role(obj.role).name.lower() + """ + LEGACY, should be removed eventually + """ + return LegacyAccessControlRole(obj.role).name.lower() def get_is_phone_number_verified(self, obj): return obj.verified_phone_number is not None @@ -39,8 +42,8 @@ class FastUserSerializer(serializers.ModelSerializer): class UserSerializer(serializers.ModelSerializer, EagerLoadingMixin): id = serializers.ReadOnlyField(read_only=True, source="public_primary_key") email = serializers.EmailField(read_only=True) - role = serializers.SerializerMethodField() slack = SlackUserIdentitySerializer(read_only=True, source="slack_user_identity") + role = serializers.SerializerMethodField() # LEGACY, should be removed eventually is_phone_number_verified = serializers.SerializerMethodField() SELECT_RELATED = [ @@ -54,7 +57,10 @@ class UserSerializer(serializers.ModelSerializer, EagerLoadingMixin): @staticmethod def get_role(obj): - return Role(obj.role).name.lower() + """ + LEGACY, should be removed eventually + """ + return LegacyAccessControlRole(obj.role).name.lower() def get_is_phone_number_verified(self, obj): return obj.verified_phone_number is not None diff --git a/engine/apps/public_api/tests/test_users.py b/engine/apps/public_api/tests/test_users.py index 892fbc38..0a0cf612 100644 --- a/engine/apps/public_api/tests/test_users.py +++ b/engine/apps/public_api/tests/test_users.py @@ -3,7 +3,7 @@ from django.urls import reverse from rest_framework import status from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.fixture() @@ -20,7 +20,7 @@ def user_public_api_setup( def test_get_user( user_public_api_setup, ): - organization, user, token, slack_team_identity, slack_user_identity = user_public_api_setup + _, user, token, slack_team_identity, slack_user_identity = user_public_api_setup client = APIClient() @@ -93,7 +93,7 @@ def test_get_users_list_short( user_public_api_setup, make_user_for_organization, ): - organization, user_1, token, slack_team_identity, slack_user_identity = user_public_api_setup + organization, user_1, token, _, _ = user_public_api_setup user_2 = make_user_for_organization(organization) client = APIClient() @@ -145,13 +145,10 @@ def test_forbidden_access( @pytest.mark.django_db -def test_get_users_list_all_role_users( - user_public_api_setup, - make_user_for_organization, -): +def test_get_users_list_all_role_users(user_public_api_setup, make_user_for_organization): organization, admin, token, _, _ = user_public_api_setup - editor = make_user_for_organization(organization, role=Role.EDITOR) - viewer = make_user_for_organization(organization, role=Role.VIEWER) + editor = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) + viewer = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) client = APIClient() diff --git a/engine/apps/public_api/views/users.py b/engine/apps/public_api/views/users.py index 84851042..1eed6e1d 100644 --- a/engine/apps/public_api/views/users.py +++ b/engine/apps/public_api/views/users.py @@ -5,6 +5,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.views import Response from rest_framework.viewsets import ReadOnlyModelViewSet +from apps.api.permissions import LegacyAccessControlRole from apps.auth_token.auth import ApiTokenAuthentication, UserScheduleExportAuthentication from apps.public_api.custom_renderers import CalendarRenderer from apps.public_api.serializers import FastUserSerializer, UserSerializer @@ -14,7 +15,6 @@ from apps.schedules.models import OnCallSchedule from apps.user_management.models import User from common.api_helpers.mixins import RateLimitHeadersMixin, ShortSerializerMixin from common.api_helpers.paginators import HundredPageSizePaginator -from common.constants.role import Role class UserFilter(filters.FilterSet): @@ -23,7 +23,9 @@ class UserFilter(filters.FilterSet): """ email = filters.CharFilter(field_name="email", lookup_expr="iexact") - roles = filters.MultipleChoiceFilter(field_name="role", choices=Role.choices()) + roles = filters.MultipleChoiceFilter( + field_name="role", choices=LegacyAccessControlRole.choices() + ) # LEGACY, should be removed eventually username = filters.CharFilter(field_name="username", lookup_expr="iexact") class Meta: diff --git a/engine/apps/schedules/ical_utils.py b/engine/apps/schedules/ical_utils.py index 3e6756cd..77863e2b 100644 --- a/engine/apps/schedules/ical_utils.py +++ b/engine/apps/schedules/ical_utils.py @@ -13,6 +13,7 @@ from django.db.models import Q from django.utils import timezone from icalendar import Calendar +from apps.api.permissions import RBACPermission from apps.schedules.constants import ( ICAL_ATTENDEE, ICAL_DATETIME_END, @@ -25,7 +26,6 @@ from apps.schedules.constants import ( RE_PRIORITY, ) from apps.schedules.ical_events import ical_events -from common.constants.role import Role from common.utils import timed_lru_cache """ @@ -40,11 +40,16 @@ if TYPE_CHECKING: def users_in_ical(usernames_from_ical, organization, include_viewers=False): """ Parse ical file and return list of users found + NOTE: only grafana username will be used, consider adding grafana email and id """ - # Only grafana username will be used, consider adding grafana email and id + from apps.user_management.models import User + users_found_in_ical = organization.users if not include_viewers: - users_found_in_ical = users_found_in_ical.filter(role__in=(Role.ADMIN, Role.EDITOR)) + # TODO: this is a breaking change.... + users_found_in_ical = users_found_in_ical.filter( + **User.build_permissions_query(RBACPermission.Permissions.SCHEDULES_WRITE, organization) + ) user_emails = [v.lower() for v in usernames_from_ical] users_found_in_ical = users_found_in_ical.filter( diff --git a/engine/apps/schedules/tests/test_ical_utils.py b/engine/apps/schedules/tests/test_ical_utils.py index bb13cf5b..08d4ef44 100644 --- a/engine/apps/schedules/tests/test_ical_utils.py +++ b/engine/apps/schedules/tests/test_ical_utils.py @@ -5,6 +5,7 @@ import pytest import pytz from django.utils import timezone +from apps.api.permissions import LegacyAccessControlRole from apps.schedules.ical_utils import ( list_of_oncall_shifts_from_ical, list_users_to_notify_from_ical, @@ -12,7 +13,6 @@ from apps.schedules.ical_utils import ( users_in_ical, ) from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar -from common.constants.role import Role @pytest.mark.django_db @@ -26,13 +26,10 @@ def test_users_in_ical_email_case_insensitive(make_organization_and_user, make_u @pytest.mark.django_db -@pytest.mark.parametrize( - "include_viewers", - [True, False], -) +@pytest.mark.parametrize("include_viewers", [True, False]) def test_users_in_ical_viewers_inclusion(make_organization_and_user, make_user_for_organization, include_viewers): organization, user = make_organization_and_user() - viewer = make_user_for_organization(organization, Role.VIEWER) + viewer = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) usernames = [user.username, viewer.username] result = users_in_ical(usernames, organization, include_viewers=include_viewers) @@ -43,15 +40,12 @@ def test_users_in_ical_viewers_inclusion(make_organization_and_user, make_user_f @pytest.mark.django_db -@pytest.mark.parametrize( - "include_viewers", - [True, False], -) +@pytest.mark.parametrize("include_viewers", [True, False]) def test_list_users_to_notify_from_ical_viewers_inclusion( make_organization_and_user, make_user_for_organization, make_schedule, make_on_call_shift, include_viewers ): organization, user = make_organization_and_user() - viewer = make_user_for_organization(organization, Role.VIEWER) + viewer = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar) date = timezone.now().replace(tzinfo=None, microsecond=0) diff --git a/engine/apps/schedules/tests/test_on_call_schedule.py b/engine/apps/schedules/tests/test_on_call_schedule.py index 32a1d826..a7d0ceb5 100644 --- a/engine/apps/schedules/tests/test_on_call_schedule.py +++ b/engine/apps/schedules/tests/test_on_call_schedule.py @@ -4,9 +4,9 @@ import pytest import pytz from django.utils import timezone +from apps.api.permissions import LegacyAccessControlRole from apps.schedules.ical_utils import memoized_users_in_ical from apps.schedules.models import CustomOnCallShift, OnCallSchedule, OnCallScheduleCalendar, OnCallScheduleWeb -from common.constants.role import Role @pytest.mark.django_db @@ -18,7 +18,7 @@ def test_filter_events(make_organization, make_user_for_organization, make_sched name="test_web_schedule", ) user = make_user_for_organization(organization) - viewer = make_user_for_organization(organization, role=Role.VIEWER) + viewer = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) @@ -190,7 +190,7 @@ def test_filter_events_include_empty(make_organization, make_user_for_organizati schedule_class=OnCallScheduleWeb, name="test_web_schedule", ) - user = make_user_for_organization(organization, role=Role.VIEWER) + user = make_user_for_organization(organization, role=LegacyAccessControlRole.VIEWER) now = timezone.now().replace(hour=0, minute=0, second=0, microsecond=0) start_date = now - timezone.timedelta(days=7) diff --git a/engine/apps/slack/models/slack_team_identity.py b/engine/apps/slack/models/slack_team_identity.py index 3abefc6e..2c8bc4a8 100644 --- a/engine/apps/slack/models/slack_team_identity.py +++ b/engine/apps/slack/models/slack_team_identity.py @@ -4,10 +4,11 @@ from django.apps import apps from django.db import models from django.db.models import JSONField +from apps.api.permissions import RBACPermission from apps.slack.constants import SLACK_INVALID_AUTH_RESPONSE, SLACK_WRONG_TEAM_NAMES from apps.slack.slack_client import SlackClientWithErrorHandling from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException -from common.constants.role import Role +from apps.user_management.models.user import User from common.insight_log.chatops_insight_logs import ChatOpsEvent, ChatOpsType, write_chatops_insight_log logger = logging.getLogger(__name__) @@ -127,8 +128,10 @@ class SlackTeamIdentity(models.Model): sc = SlackClientWithErrorHandling(self.bot_access_token) members = self.get_conversation_members(sc, channel_id) - users = organization.users.filter(slack_user_identity__slack_id__in=members, role__in=[Role.ADMIN, Role.EDITOR]) - return users + return organization.users.filter( + slack_user_identity__slack_id__in=members, + **User.build_permissions_query(RBACPermission.Permissions.CHATOPS_WRITE, organization), + ) def get_conversation_members(self, slack_client, channel_id): try: diff --git a/engine/apps/slack/models/slack_usergroup.py b/engine/apps/slack/models/slack_usergroup.py index 2b5f8fb6..a319f133 100644 --- a/engine/apps/slack/models/slack_usergroup.py +++ b/engine/apps/slack/models/slack_usergroup.py @@ -7,9 +7,10 @@ from django.db import models from django.db.models import JSONField from django.utils import timezone +from apps.api.permissions import RBACPermission from apps.slack.slack_client import SlackClientWithErrorHandling from apps.slack.slack_client.exceptions import SlackAPIException -from common.constants.role import Role +from apps.user_management.models.user import User from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length logger = logging.getLogger(__name__) @@ -105,7 +106,8 @@ class SlackUserGroup(models.Model): def get_users_from_members_for_organization(self, organization): return organization.users.filter( - slack_user_identity__slack_id__in=self.members, role__in=[Role.ADMIN, Role.EDITOR] + slack_user_identity__slack_id__in=self.members, + **User.build_permissions_query(RBACPermission.Permissions.CHATOPS_WRITE, organization), ) @classmethod diff --git a/engine/apps/slack/scenarios/alertgroup_appearance.py b/engine/apps/slack/scenarios/alertgroup_appearance.py index 7526843f..8208919d 100644 --- a/engine/apps/slack/scenarios/alertgroup_appearance.py +++ b/engine/apps/slack/scenarios/alertgroup_appearance.py @@ -5,8 +5,8 @@ from django.db import transaction from jinja2 import TemplateSyntaxError from rest_framework.response import Response +from apps.api.permissions import RBACPermission from apps.slack.scenarios import scenario_step -from common.constants.role import Role from common.insight_log import EntityEvent, write_resource_insight_log from common.jinja_templater import jinja_template_env @@ -21,7 +21,7 @@ class OpenAlertAppearanceDialogStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "open Alert Appearance" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): diff --git a/engine/apps/slack/scenarios/distribute_alerts.py b/engine/apps/slack/scenarios/distribute_alerts.py index 62b0be00..ea0bcb6b 100644 --- a/engine/apps/slack/scenarios/distribute_alerts.py +++ b/engine/apps/slack/scenarios/distribute_alerts.py @@ -14,6 +14,7 @@ from apps.alerts.incident_appearance.renderers.slack_renderer import AlertSlackR from apps.alerts.models import AlertGroup, AlertGroupLogRecord, AlertReceiveChannel, Invitation from apps.alerts.tasks import custom_button_result from apps.alerts.utils import render_curl_command +from apps.api.permissions import RBACPermission from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME, SLACK_RATE_LIMIT_DELAY from apps.slack.scenarios import scenario_step from apps.slack.scenarios.slack_renderer import AlertGroupLogSlackRenderer @@ -31,7 +32,6 @@ from apps.slack.tasks import ( update_incident_slack_message, ) from apps.slack.utils import get_cache_key_update_incident_slack_message -from common.constants.role import Role from common.utils import clean_markup, is_string_with_visible_characters from .step_mixins import CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin @@ -222,7 +222,7 @@ class InviteOtherPersonToIncident( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "invite to incident" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -263,7 +263,7 @@ class SilenceGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "silence incident" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -293,7 +293,7 @@ class UnSilenceGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "unsilence incident" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -317,7 +317,7 @@ class SelectAttachGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "Select Incident for Attaching to" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -473,7 +473,7 @@ class AttachGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "Attach incident" def process_signal(self, log_record): @@ -536,7 +536,7 @@ class UnAttachGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "Unattach incident" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -555,7 +555,7 @@ class StopInvitationProcess(CheckAlertIsUnarchivedMixin, IncidentActionsAccessCo scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "stop invitation" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -580,7 +580,8 @@ class CustomButtonProcessStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + # TODO: + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "click custom button" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -642,7 +643,7 @@ class ResolveGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "resolve incident" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -688,7 +689,7 @@ class UnResolveGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "unresolve incident" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -711,7 +712,7 @@ class AcknowledgeGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "acknowledge incident" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -737,7 +738,7 @@ class UnAcknowledgeGroupStep( scenario_step.ScenarioStep.TAG_INCIDENT_ROUTINE, ] - ALLOWED_ROLES = [Role.ADMIN, Role.EDITOR] + REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE] ACTION_VERBOSE = "unacknowledge incident" def process_scenario(self, slack_user_identity, slack_team_identity, payload, action=None): diff --git a/engine/apps/slack/scenarios/scenario_step.py b/engine/apps/slack/scenarios/scenario_step.py index 007e9535..16be0f21 100644 --- a/engine/apps/slack/scenarios/scenario_step.py +++ b/engine/apps/slack/scenarios/scenario_step.py @@ -13,7 +13,6 @@ from apps.slack.slack_client.exceptions import ( SlackAPIRateLimitException, SlackAPITokenException, ) -from common.constants.role import Role logger = logging.getLogger(__name__) @@ -162,15 +161,6 @@ class ScenarioStep(object): step = step_class(slack_team_identity) step.process_scenario(slack_user_identity, slack_team_identity, payload, action=action, **kwargs) - def get_permission_denied_prompt(self): - current_role = self.user.get_role_display() - admins_queryset = self.organization.users.filter(role=Role.ADMIN).select_related("slack_user_identity") - admins_verbal = "No admins" - if admins_queryset.count() > 0: - admins_verbal = ", ".join(["<@{}>".format(admin.slack_user_identity.slack_id) for admin in admins_queryset]) - - return current_role, admins_verbal - def open_warning_window(self, payload, warning_text, title=None): if title is None: title = ":warning: Warning" diff --git a/engine/apps/slack/scenarios/step_mixins.py b/engine/apps/slack/scenarios/step_mixins.py index 1c7fdf0b..03ebdd91 100644 --- a/engine/apps/slack/scenarios/step_mixins.py +++ b/engine/apps/slack/scenarios/step_mixins.py @@ -1,11 +1,13 @@ import logging from abc import ABC, abstractmethod +from apps.api.permissions import user_is_authorized + logger = logging.getLogger(__name__) class AccessControl(ABC): - ALLOWED_ROLES = [] + REQUIRED_PERMISSIONS = [] ACTION_VERBOSE = "" def dispatch(self, slack_user_identity, slack_team_identity, payload, action=None): @@ -15,7 +17,7 @@ class AccessControl(ABC): self.send_denied_message(payload) def check_membership(self): - return self.user.role in self.ALLOWED_ROLES + return user_is_authorized(self.user, self.REQUIRED_PERMISSIONS) @abstractmethod def send_denied_message(self, payload): @@ -62,9 +64,7 @@ class IncidentActionsAccessControlMixin(AccessControl): class CheckAlertIsUnarchivedMixin(object): - - ALLOWED_ROLES = [] - + REQUIRED_PERMISSIONS = [] ACTION_VERBOSE = "" def check_alert_is_unarchived(self, slack_team_identity, payload, alert_group, warning=True): diff --git a/engine/apps/slack/tests/test_reset_slack.py b/engine/apps/slack/tests/test_reset_slack.py index 229f1534..b64e7b3b 100644 --- a/engine/apps/slack/tests/test_reset_slack.py +++ b/engine/apps/slack/tests/test_reset_slack.py @@ -7,24 +7,24 @@ from rest_framework import status from rest_framework.response import Response from rest_framework.test import APIClient -from common.constants.role import Role +from apps.api.permissions import LegacyAccessControlRole @pytest.mark.django_db @pytest.mark.parametrize( "role,expected_status", [ - (Role.ADMIN, status.HTTP_200_OK), - (Role.EDITOR, status.HTTP_403_FORBIDDEN), - (Role.VIEWER, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.ADMIN, status.HTTP_200_OK), + (LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN), + (LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN), ], ) def test_reset_slack_integration_permissions( - make_organization_and_user_with_plugin_token, role, expected_status, load_slack_urls, make_user_auth_headers + make_organization_and_user_with_plugin_token, load_slack_urls, make_user_auth_headers, role, expected_status ): settings.FEATURE_SLACK_INTEGRATION_ENABLED = True - _, user, token = make_organization_and_user_with_plugin_token(role) + _, user, token = make_organization_and_user_with_plugin_token(role=role) client = APIClient() url = reverse("reset-slack") diff --git a/engine/apps/slack/views.py b/engine/apps/slack/views.py index b22e0ba2..439a8f64 100644 --- a/engine/apps/slack/views.py +++ b/engine/apps/slack/views.py @@ -11,7 +11,7 @@ from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView -from apps.api.permissions import IsAdmin, MethodPermission +from apps.api.permissions import RBACPermission from apps.auth_token.auth import PluginAuthentication from apps.base.utils import live_settings from apps.slack.scenarios.alertgroup_appearance import STEPS_ROUTING as ALERTGROUP_APPEARANCE_ROUTING @@ -533,10 +533,12 @@ class SlackEventApiEndpointView(APIView): class ResetSlackView(APIView): - permission_classes = (IsAuthenticated, MethodPermission) + permission_classes = (IsAuthenticated, RBACPermission) authentication_classes = [PluginAuthentication] - method_permissions = {IsAdmin: {"POST"}} + rbac_permissions = { + "post": [RBACPermission.Permissions.CHATOPS_UPDATE_SETTINGS], + } def post(self, request): organization = request.auth.organization diff --git a/engine/apps/telegram/updates/update_handlers/button_press.py b/engine/apps/telegram/updates/update_handlers/button_press.py index 6afa11a5..005460fe 100644 --- a/engine/apps/telegram/updates/update_handlers/button_press.py +++ b/engine/apps/telegram/updates/update_handlers/button_press.py @@ -4,12 +4,12 @@ from typing import Callable, Optional, Tuple from apps.alerts.constants import ActionSource from apps.alerts.models import AlertGroup +from apps.api.permissions import RBACPermission, user_is_authorized from apps.telegram.models import TelegramToUserConnector from apps.telegram.renderers.keyboard import Action from apps.telegram.updates.update_handlers import UpdateHandler from apps.telegram.utils import CallbackQueryFactory from apps.user_management.models import User -from common.constants.role import Role logger = logging.getLogger(__name__) @@ -58,7 +58,8 @@ class ButtonPressHandler(UpdateHandler): if not user: return False - return user.organization == alert_group.channel.organization and user.role in [Role.ADMIN, Role.EDITOR] + has_permission = user_is_authorized(user, [RBACPermission.Permissions.CHATOPS_WRITE]) + return user.organization == alert_group.channel.organization and has_permission @staticmethod def _get_action_context(data: str) -> ActionContext: diff --git a/engine/apps/user_management/migrations/0005_rbac_permissions.py b/engine/apps/user_management/migrations/0005_rbac_permissions.py new file mode 100644 index 00000000..560aa144 --- /dev/null +++ b/engine/apps/user_management/migrations/0005_rbac_permissions.py @@ -0,0 +1,23 @@ +# Generated by Django 3.2.15 on 2022-10-25 11:18 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user_management', '0004_auto_20221025_0316'), + ] + + operations = [ + migrations.AddField( + model_name='organization', + name='is_rbac_permissions_enabled', + field=models.BooleanField(default=False), + ), + migrations.AddField( + model_name='user', + name='permissions', + field=models.JSONField(default=list), + ), + ] diff --git a/engine/apps/user_management/models/organization.py b/engine/apps/user_management/models/organization.py index 0be08493..f428b32d 100644 --- a/engine/apps/user_management/models/organization.py +++ b/engine/apps/user_management/models/organization.py @@ -192,6 +192,7 @@ class Organization(MaintainableObject): pricing_version = models.PositiveIntegerField(choices=PRICING_CHOICES, default=FREE_PUBLIC_BETA_PRICING) is_amixr_migration_started = models.BooleanField(default=False) + is_rbac_permissions_enabled = models.BooleanField(default=False) class Meta: unique_together = ("stack_id", "org_id") diff --git a/engine/apps/user_management/models/user.py b/engine/apps/user_management/models/user.py index 81af4e70..2bfb7998 100644 --- a/engine/apps/user_management/models/user.py +++ b/engine/apps/user_management/models/user.py @@ -1,4 +1,6 @@ +import json import logging +import typing from urllib.parse import urljoin from django.apps import apps @@ -9,13 +11,26 @@ from django.db.models.signals import post_save from django.dispatch import receiver from emoji import demojize +from apps.api.permissions import ( + LegacyAccessControlCompatiblePermission, + LegacyAccessControlRole, + RBACPermission, + user_is_authorized, +) from apps.schedules.tasks import drop_cached_ical_for_custom_events_for_organization -from common.constants.role import Role from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length logger = logging.getLogger(__name__) +class PermissionsRegexQuery(typing.TypedDict): + permissions__regex: str + + +class RoleInQuery(typing.TypedDict): + role__in: typing.List[int] + + def generate_public_primary_key_for_user(): prefix = "U" new_public_primary_key = generate_public_primary_key(prefix) @@ -60,8 +75,9 @@ class UserManager(models.Manager): email=user["email"], name=user["name"], username=user["login"], - role=Role[user["role"].upper()], + role=LegacyAccessControlRole[user["role"].upper()], avatar_url=user["avatarUrl"], + permissions=user["permissions"], ) for user in grafana_users.values() if user["userId"] not in existing_user_ids @@ -76,23 +92,31 @@ class UserManager(models.Manager): users_to_update = [] for user in organization.users.filter(user_id__in=existing_user_ids): grafana_user = grafana_users[user.user_id] - g_user_role = Role[grafana_user["role"].upper()] + g_user_role = LegacyAccessControlRole[grafana_user["role"].upper()] + if ( user.email != grafana_user["email"] or user.name != grafana_user["name"] or user.username != grafana_user["login"] or user.role != g_user_role or user.avatar_url != grafana_user["avatarUrl"] + # instead of looping through the array of permission objects, simply take the hash + # of the string representation of the data structures and compare. + # Need to first convert the lists of objects to strings because lists/dicts are not hashable + # (because lists and dicts are not hashable.. as they are mutable) + # https://stackoverflow.com/a/22003440 + or hash(json.dumps(user.permissions)) != hash(json.dumps(grafana_user["permissions"])) ): user.email = grafana_user["email"] user.name = grafana_user["name"] user.username = grafana_user["login"] user.role = g_user_role user.avatar_url = grafana_user["avatarUrl"] + user.permissions = grafana_user["permissions"] users_to_update.append(user) organization.users.bulk_update( - users_to_update, ["email", "name", "username", "role", "avatar_url"], batch_size=5000 + users_to_update, ["email", "name", "username", "role", "avatar_url", "permissions"], batch_size=5000 ) @@ -135,7 +159,7 @@ class User(models.Model): email = models.EmailField() name = models.CharField(max_length=300) username = models.CharField(max_length=300) - role = models.PositiveSmallIntegerField(choices=Role.choices()) + role = models.PositiveSmallIntegerField(choices=LegacyAccessControlRole.choices()) avatar_url = models.URLField() # don't use "_timezone" directly, use the "timezone" property since it can be populated via slack user identity @@ -154,6 +178,7 @@ class User(models.Model): # is_active = None is used to be able to have multiple deleted users with the same user_id is_active = models.BooleanField(null=True, default=True) + permissions = models.JSONField(null=False, default=list) def __str__(self): return f"{self.pk}: {self.username}" @@ -187,13 +212,14 @@ class User(models.Model): return hasattr(self, "telegram_connection") def self_or_admin(self, user_to_check, organization) -> bool: + has_admin_permission = user_is_authorized(user_to_check, [RBACPermission.Permissions.USER_SETTINGS_ADMIN]) return user_to_check.pk == self.pk or ( - user_to_check.role == Role.ADMIN and organization.pk == user_to_check.organization_id + has_admin_permission and organization.pk == user_to_check.organization_id ) @property def is_notification_allowed(self): - return self.role in (Role.ADMIN, Role.EDITOR) + return user_is_authorized(self, [RBACPermission.Permissions.NOTIFICATIONS_READ]) # using in-memory cache instead of redis to avoid pickling python objects # @timed_lru_cache(timeout=100) @@ -249,6 +275,7 @@ class User(models.Model): result = { "username": self.username, + # LEGACY.. role should get removed eventually.. it's probably safe to remove it now? "role": self.get_role_display(), "notification_policies": notification_policies_verbal, } @@ -262,6 +289,24 @@ class User(models.Model): def insight_logs_metadata(self): return {} + @staticmethod + def build_permissions_query( + permission: LegacyAccessControlCompatiblePermission, organization + ) -> typing.Union[PermissionsRegexQuery, RoleInQuery]: + """ + This method returns a django query filter that is compatible with RBAC + as well as legacy "basic" role based authorization. If a permission is provided we simply do + a regex search where the permission column contains the permission value (need to use regex because + the JSON contains method is not supported by sqlite) + + If RBAC is not supported for the org, we make the assumption that we are looking for any users with AT LEAST + the fallback role. Ex: if the fallback role were editor than we would get editors and admins. + """ + if organization.is_rbac_permissions_enabled: + # https://stackoverflow.com/a/50251879 + return PermissionsRegexQuery(permissions__regex=r".*{0}.*".format(permission.value)) + return RoleInQuery(role__lte=permission.fallback_role.value) + # TODO: check whether this signal can be moved to save method of the model @receiver(post_save, sender=User) diff --git a/engine/apps/user_management/sync.py b/engine/apps/user_management/sync.py index 80826c5a..740b3332 100644 --- a/engine/apps/user_management/sync.py +++ b/engine/apps/user_management/sync.py @@ -14,9 +14,23 @@ logger.setLevel(logging.DEBUG) def sync_organization(organization): client = GrafanaAPIClient(api_url=organization.grafana_url, api_token=organization.api_token) - api_users, call_status = client.get_users() + rbac_is_enabled = client.is_rbac_enabled_for_organization() + organization.is_rbac_permissions_enabled = rbac_is_enabled - sync_instance_info(organization) + if organization.gcom_token: + gcom_client = GcomAPIClient(organization.gcom_token) + instance_info = gcom_client.get_instance_info(organization.stack_id) + if not instance_info or str(instance_info["orgId"]) != organization.org_id: + return + + organization.stack_slug = instance_info["slug"] + organization.org_slug = instance_info["orgSlug"] + organization.org_title = instance_info["orgName"] + organization.region_slug = instance_info["regionSlug"] + organization.grafana_url = instance_info["url"] + organization.gcom_token_org_last_time_synced = timezone.now() + + api_users = client.get_users(rbac_is_enabled) if api_users: organization.api_token_status = Organization.API_TOKEN_STATUS_OK @@ -34,25 +48,11 @@ def sync_organization(organization): "last_time_synced", "api_token_status", "gcom_token_org_last_time_synced", + "is_rbac_permissions_enabled", ] ) -def sync_instance_info(organization): - if organization.gcom_token: - gcom_client = GcomAPIClient(organization.gcom_token) - instance_info, _ = gcom_client.get_instance_info(organization.stack_id) - if not instance_info or str(instance_info["orgId"]) != organization.org_id: - return - - organization.stack_slug = instance_info["slug"] - organization.org_slug = instance_info["orgSlug"] - organization.org_title = instance_info["orgName"] - organization.region_slug = instance_info["regionSlug"] - organization.grafana_url = instance_info["url"] - organization.gcom_token_org_last_time_synced = timezone.now() - - def sync_users_and_teams(client, api_users, organization): # check if api_users are shaped correctly. e.g. for paused instance, the response is not a list. if not api_users or not isinstance(api_users, (tuple, list)): diff --git a/engine/apps/user_management/tests/test_free_public_beta_subcription_strategy.py b/engine/apps/user_management/tests/test_free_public_beta_subcription_strategy.py index c26b4216..b3b26e4f 100644 --- a/engine/apps/user_management/tests/test_free_public_beta_subcription_strategy.py +++ b/engine/apps/user_management/tests/test_free_public_beta_subcription_strategy.py @@ -1,7 +1,7 @@ import pytest +from apps.api.permissions import LegacyAccessControlRole from apps.twilioapp.constants import TwilioCallStatuses, TwilioMessageStatuses -from common.constants.role import Role @pytest.mark.django_db @@ -13,8 +13,8 @@ def test_phone_calls_left( make_alert_group, ): organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - user = make_user_for_organization(organization, role=Role.EDITOR) + admin = make_user_for_organization(organization) + user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) make_phone_call(receiver=admin, status=TwilioCallStatuses.COMPLETED, represents_alert_group=alert_group) @@ -28,8 +28,8 @@ def test_sms_left( make_organization, make_user_for_organization, make_sms, make_alert_receive_channel, make_alert_group ): organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - user = make_user_for_organization(organization, role=Role.EDITOR) + admin = make_user_for_organization(organization) + user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) make_sms(receiver=admin, status=TwilioMessageStatuses.SENT, represents_alert_group=alert_group) @@ -48,8 +48,8 @@ def test_phone_calls_and_sms_counts_together( make_alert_group, ): organization = make_organization() - admin = make_user_for_organization(organization, role=Role.ADMIN) - user = make_user_for_organization(organization, role=Role.EDITOR) + admin = make_user_for_organization(organization) + user = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) alert_receive_channel = make_alert_receive_channel(organization) alert_group = make_alert_group(alert_receive_channel) make_phone_call(receiver=admin, status=TwilioCallStatuses.COMPLETED, represents_alert_group=alert_group) diff --git a/engine/apps/user_management/tests/test_sync.py b/engine/apps/user_management/tests/test_sync.py index c7e03bb9..c896e725 100644 --- a/engine/apps/user_management/tests/test_sync.py +++ b/engine/apps/user_management/tests/test_sync.py @@ -6,6 +6,7 @@ from django.core.exceptions import ObjectDoesNotExist from apps.grafana_plugin.helpers.client import GcomAPIClient, GrafanaAPIClient from apps.user_management.models import Team, User from apps.user_management.sync import cleanup_organization, sync_organization +from conftest import IS_RBAC_ENABLED @pytest.mark.django_db @@ -21,6 +22,7 @@ def test_sync_users_for_organization(make_organization, make_user_for_organizati "login": "test", "role": "admin", "avatarUrl": "/test/1234", + "permissions": [], } for user_id in (2, 3) ) @@ -97,11 +99,7 @@ def test_sync_users_for_team(make_organization, make_user_for_organization, make @pytest.mark.django_db -def test_sync_organization( - make_organization, - make_team, - make_user_for_organization, -): +def test_sync_organization(make_organization, make_team, make_user_for_organization): organization = make_organization() api_users_response = ( @@ -112,6 +110,7 @@ def test_sync_organization( "login": "test", "role": "admin", "avatarUrl": "test.test/test", + "permissions": [], }, ) @@ -135,10 +134,11 @@ def test_sync_organization( }, ) - with patch.object(GrafanaAPIClient, "get_users", return_value=(api_users_response, {"status_code": 200})): - with patch.object(GrafanaAPIClient, "get_teams", return_value=(api_teams_response, None)): - with patch.object(GrafanaAPIClient, "get_team_members", return_value=(api_members_response, None)): - sync_organization(organization) + with patch.object(GrafanaAPIClient, "is_rbac_enabled_for_organization", return_value=IS_RBAC_ENABLED): + with patch.object(GrafanaAPIClient, "get_users", return_value=api_users_response): + with patch.object(GrafanaAPIClient, "get_teams", return_value=(api_teams_response, None)): + with patch.object(GrafanaAPIClient, "get_team_members", return_value=(api_members_response, None)): + sync_organization(organization) # check that users are populated assert organization.users.count() == 1 @@ -154,6 +154,9 @@ def test_sync_organization( assert team.users.count() == 1 assert team.users.get() == user + # check that the rbac flag is properly set on the org + assert organization.is_rbac_permissions_enabled == IS_RBAC_ENABLED + @pytest.mark.django_db def test_duplicate_user_ids(make_organization, make_user_for_organization): @@ -178,6 +181,7 @@ def test_duplicate_user_ids(make_organization, make_user_for_organization): "login": "test", "role": "admin", "avatarUrl": "test.test/test", + "permissions": [], } ] @@ -192,7 +196,7 @@ def test_duplicate_user_ids(make_organization, make_user_for_organization): def test_cleanup_organization_deleted(make_organization): organization = make_organization(gcom_token="TEST_GCOM_TOKEN") - with patch.object(GcomAPIClient, "get_instance_info", return_value=({"status": "deleted"}, None)): + with patch.object(GcomAPIClient, "get_instance_info", return_value={"status": "deleted"}): cleanup_organization(organization.id) with pytest.raises(ObjectDoesNotExist): diff --git a/engine/apps/user_management/tests/test_user.py b/engine/apps/user_management/tests/test_user.py index 74440cd1..6928d489 100644 --- a/engine/apps/user_management/tests/test_user.py +++ b/engine/apps/user_management/tests/test_user.py @@ -1,20 +1,15 @@ -# from unittest.mock import Mock, patch - import pytest +from apps.api.permissions import LegacyAccessControlRole from apps.user_management.models import User -from common.constants.role import Role @pytest.mark.django_db -def test_self_or_admin( - make_organization, - make_user_for_organization, -): +def test_self_or_admin(make_organization, make_user_for_organization): organization = make_organization() admin = make_user_for_organization(organization) second_admin = make_user_for_organization(organization) - editor = make_user_for_organization(organization, role=Role.EDITOR) + editor = make_user_for_organization(organization, role=LegacyAccessControlRole.EDITOR) another_organization = make_organization() admin_from_another_organization = make_user_for_organization(another_organization) @@ -26,10 +21,7 @@ def test_self_or_admin( @pytest.mark.django_db -def test_lower_email_filter( - make_organization, - make_user_for_organization, -): +def test_lower_email_filter(make_organization, make_user_for_organization): organization = make_organization() user = make_user_for_organization(organization, email="TestingUser@test.com") make_user_for_organization(organization, email="testing_user@test.com") diff --git a/engine/common/constants/role.py b/engine/common/constants/role.py deleted file mode 100644 index 69a05d04..00000000 --- a/engine/common/constants/role.py +++ /dev/null @@ -1,11 +0,0 @@ -from enum import IntEnum - - -class Role(IntEnum): - ADMIN = 0 - EDITOR = 1 - VIEWER = 2 - - @classmethod - def choices(cls): - return tuple((option.value, option.name) for option in cls) diff --git a/engine/conftest.py b/engine/conftest.py index 7ac895b7..11db43ec 100644 --- a/engine/conftest.py +++ b/engine/conftest.py @@ -1,4 +1,5 @@ import json +import os import sys import typing import uuid @@ -35,6 +36,13 @@ from apps.alerts.tests.factories import ( ResolutionNoteFactory, ResolutionNoteSlackMessageFactory, ) +from apps.api.permissions import ( + ACTION_PREFIX, + GrafanaAPIPermission, + LegacyAccessControlCompatiblePermission, + LegacyAccessControlRole, + RBACPermission, +) from apps.auth_token.models import ApiAuthToken, PluginAuthToken from apps.base.models.user_notification_policy_log_record import ( UserNotificationPolicyLogRecord, @@ -72,7 +80,6 @@ from apps.telegram.tests.factories import ( from apps.twilioapp.tests.factories import PhoneCallFactory, SMSFactory from apps.user_management.models.user import User, listen_for_user_model_save from apps.user_management.tests.factories import OrganizationFactory, RegionFactory, TeamFactory, UserFactory -from common.constants.role import Role register(OrganizationFactory) register(UserFactory) @@ -112,6 +119,8 @@ register(EmailMessageFactory) register(IntegrationHeartBeatFactory) register(LiveSettingFactory) +IS_RBAC_ENABLED = os.getenv("ONCALL_TESTING_RBAC_ENABLED", "True") == "True" + @pytest.fixture(autouse=True) def mock_slack_api_call(monkeypatch): @@ -142,18 +151,16 @@ def mock_telegram_bot_username(monkeypatch): @pytest.fixture def make_organization(): def _make_organization(**kwargs): - organization = OrganizationFactory(**kwargs) - - return organization + return OrganizationFactory(**kwargs, is_rbac_permissions_enabled=IS_RBAC_ENABLED) return _make_organization @pytest.fixture -def make_user_for_organization(): - def _make_user_for_organization(organization, role=Role.ADMIN, **kwargs): +def make_user_for_organization(make_user): + def _make_user_for_organization(organization, role: typing.Optional[LegacyAccessControlRole] = None, **kwargs): post_save.disconnect(listen_for_user_model_save, sender=User) - user = UserFactory(organization=organization, role=role, **kwargs) + user = make_user(organization=organization, role=role, **kwargs) post_save.disconnect(listen_for_user_model_save, sender=User) return user @@ -200,19 +207,84 @@ def make_user_auth_headers(): return _make_user_auth_headers +RoleMapping = typing.Dict[LegacyAccessControlRole, typing.List[LegacyAccessControlCompatiblePermission]] + + +def get_user_permission_role_mapping_from_frontend_plugin_json() -> RoleMapping: + """ + This is used to take the RBAC permission -> basic role grants on the frontend + and test that the RBAC grants work the same way against the backend in terms of authorization + """ + + class PluginJSONRoleDefinition(typing.TypedDict): + permissions: typing.List[GrafanaAPIPermission] + + class PluginJSONRole(typing.TypedDict): + role: PluginJSONRoleDefinition + grants: typing.List[str] + + class PluginJSON(typing.TypedDict): + roles: typing.List[PluginJSONRole] + + with open("../grafana-plugin/src/plugin.json") as fp: + plugin_json: PluginJSON = json.load(fp) + + role_mapping: RoleMapping = { + LegacyAccessControlRole.VIEWER: [], + LegacyAccessControlRole.EDITOR: [], + LegacyAccessControlRole.ADMIN: [], + } + + all_permission_classes: typing.Dict[str, LegacyAccessControlCompatiblePermission] = { + getattr(RBACPermission.Permissions, attr).value: getattr(RBACPermission.Permissions, attr) + for attr in dir(RBACPermission.Permissions) + if not attr.startswith("_") + } + + # we just care about getting the basic role grants, everything else can be ignored + for role in plugin_json["roles"]: + if grants := role["grants"]: + for permission in role["role"]["permissions"]: + # only concerned with grafana-oncall-app specific grants + # ignore things like plugins.app:access actions + action = permission["action"] + permission_class = None + + if action.startswith(ACTION_PREFIX): + permission_class = all_permission_classes[action] + + if permission_class: + for grant in grants: + try: + role = LegacyAccessControlRole[grant.upper()] + if role not in role_mapping[role]: + role_mapping[role].append(permission_class) + except KeyError: + # may come across grants like "Grafana Admin" + # which we can ignore + continue + + return role_mapping + + +ROLE_PERMISSION_MAPPING = get_user_permission_role_mapping_from_frontend_plugin_json() + + @pytest.fixture def make_user(): - def _make_user(role=Role.ADMIN, **kwargs): - user = UserFactory(role=role, **kwargs) - - return user + def _make_user(role: typing.Optional[LegacyAccessControlRole] = None, **kwargs): + role = LegacyAccessControlRole.ADMIN if role is None else role + permissions = ROLE_PERMISSION_MAPPING[role] if IS_RBAC_ENABLED else [] + return UserFactory( + role=role, permissions=[GrafanaAPIPermission(action=perm.value) for perm in permissions], **kwargs + ) return _make_user @pytest.fixture def make_organization_and_user(make_organization, make_user_for_organization): - def _make_organization_and_user(role=Role.ADMIN): + def _make_organization_and_user(role: typing.Optional[LegacyAccessControlRole] = None): organization = make_organization() user = make_user_for_organization(organization=organization, role=role) return organization, user @@ -224,33 +296,31 @@ def make_organization_and_user(make_organization, make_user_for_organization): def make_organization_and_user_with_slack_identities( make_organization_with_slack_team_identity, make_user_with_slack_user_identity ): - def _make_organization_and_user_with_slack_identities(role=Role.ADMIN): + def _make_organization_and_user_with_slack_identities(role: typing.Optional[LegacyAccessControlRole] = None): organization, slack_team_identity = make_organization_with_slack_team_identity() user, slack_user_identity = make_user_with_slack_user_identity(slack_team_identity, organization, role=role) - return organization, user, slack_team_identity, slack_user_identity return _make_organization_and_user_with_slack_identities @pytest.fixture -def make_user_with_slack_user_identity(): - def _make_slack_user_identity_with_user(slack_team_identity, organization, role=Role.ADMIN, **kwargs): - slack_user_identity = SlackUserIdentityFactory( - slack_team_identity=slack_team_identity, - **kwargs, - ) - user = UserFactory(slack_user_identity=slack_user_identity, organization=organization, role=role) +def make_user_with_slack_user_identity(make_user): + def _make_slack_user_identity_with_user( + slack_team_identity, organization, role: typing.Optional[LegacyAccessControlRole] = None, **kwargs + ): + slack_user_identity = SlackUserIdentityFactory(slack_team_identity=slack_team_identity, **kwargs) + user = make_user(slack_user_identity=slack_user_identity, organization=organization, role=role) return user, slack_user_identity return _make_slack_user_identity_with_user @pytest.fixture -def make_organization_with_slack_team_identity(make_slack_team_identity): +def make_organization_with_slack_team_identity(make_slack_team_identity, make_organization): def _make_slack_team_identity_with_organization(**kwargs): slack_team_identity = make_slack_team_identity(**kwargs) - organization = OrganizationFactory(slack_team_identity=slack_team_identity) + organization = make_organization(slack_team_identity=slack_team_identity) return organization, slack_team_identity return _make_slack_team_identity_with_organization @@ -565,10 +635,9 @@ def mock_start_disable_maintenance_task(monkeypatch): @pytest.fixture() def make_organization_and_user_with_plugin_token(make_organization_and_user, make_token_for_organization): - def _make_organization_and_user_with_plugin_token(role=Role.ADMIN): - organization, user = make_organization_and_user(role=role) + def _make_organization_and_user_with_plugin_token(role: typing.Optional[LegacyAccessControlRole] = None): + organization, user = make_organization_and_user(role) _, token = make_token_for_organization(organization) - return organization, user, token return _make_organization_and_user_with_plugin_token diff --git a/engine/tox.ini b/engine/tox.ini index 0a721f1e..7cabc843 100644 --- a/engine/tox.ini +++ b/engine/tox.ini @@ -9,6 +9,6 @@ banned-modules = [pytest] # https://pytest-django.readthedocs.io/en/latest/configuring_django.html#order-of-choosing-settings # https://pytest-django.readthedocs.io/en/latest/database.html -addopts = --reuse-db --nomigrations --color=yes --showlocals +addopts = --color=yes --showlocals # https://pytest-django.readthedocs.io/en/latest/faq.html#my-tests-are-not-being-found-why python_files = tests.py test_*.py *_tests.py diff --git a/grafana-plugin/package.json b/grafana-plugin/package.json index 78b76592..0c74a2a9 100644 --- a/grafana-plugin/package.json +++ b/grafana-plugin/package.json @@ -56,7 +56,7 @@ "@babel/preset-typescript": "^7.18.6", "@grafana/data": "^9.2.4", "@grafana/eslint-config": "^5.0.0", - "@grafana/runtime": "^9.2.4", + "@grafana/runtime": "9.3.0-beta1", "@grafana/toolkit": "^9.2.4", "@grafana/ui": "^9.2.4", "@jest/globals": "^27.5.1", diff --git a/grafana-plugin/src/__mocks__/grafana/app/core/core.ts b/grafana-plugin/src/__mocks__/grafana/app/core/core.ts deleted file mode 100644 index abe4971e..00000000 --- a/grafana-plugin/src/__mocks__/grafana/app/core/core.ts +++ /dev/null @@ -1,3 +0,0 @@ -export const contextSrv = { - hasRole: jest.fn(), -}; diff --git a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx index 4166833d..873bdd57 100644 --- a/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx +++ b/grafana-plugin/src/components/AlertTemplates/AlertTemplatesForm.tsx @@ -17,8 +17,8 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { Alert } from 'models/alertgroup/alertgroup.types'; import { makeRequest } from 'network'; -import { UserAction } from 'state/userAction'; import LocationHelper from 'utils/LocationHelper'; +import { UserActions } from 'utils/authorization'; import styles from './AlertTemplatesForm.module.css'; @@ -153,7 +153,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { There are no alerts from this monitoring yet. {demoAlertEnabled ? ( - + @@ -240,7 +240,7 @@ const AlertTemplatesForm = (props: AlertTemplatesFormProps) => { ))} - + diff --git a/grafana-plugin/src/components/NewScheduleSelector/NewScheduleSelector.tsx b/grafana-plugin/src/components/NewScheduleSelector/NewScheduleSelector.tsx index 782eb0d3..b9beecb3 100644 --- a/grafana-plugin/src/components/NewScheduleSelector/NewScheduleSelector.tsx +++ b/grafana-plugin/src/components/NewScheduleSelector/NewScheduleSelector.tsx @@ -8,7 +8,7 @@ import Text from 'components/Text/Text'; import ScheduleForm from 'containers/ScheduleForm/ScheduleForm'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { Schedule, ScheduleType } from 'models/schedule/schedule.types'; -import { UserAction } from 'state/userAction'; +import { UserActions } from 'utils/authorization'; import styles from './NewScheduleSelector.module.css'; @@ -49,7 +49,7 @@ const NewScheduleSelector: FC = (props) => { Configure rotations and shifts directly in Grafana On-Call - + diff --git a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx index 52c47cd1..05eca27f 100644 --- a/grafana-plugin/src/components/Policy/EscalationPolicy.tsx +++ b/grafana-plugin/src/components/Policy/EscalationPolicy.tsx @@ -20,7 +20,7 @@ import { } from 'models/escalation_policy/escalation_policy.types'; import { WaitDelay } from 'models/wait_delay'; import { SelectOption } from 'state/types'; -import { UserAction } from 'state/userAction'; +import { UserActions } from 'utils/authorization'; import DragHandle from './DragHandle'; import PolicyNote from './PolicyNote'; @@ -53,14 +53,14 @@ export class EscalationPolicy extends React.Component - + {escalationOption && reactStringReplace(escalationOption.display_name, /\{\{([^}]+)\}\}/g, this.replacePlaceholder)} {this._renderNote()} {is_final ? null : ( - + + + + { - + Edit teams diff --git a/grafana-plugin/src/containers/HeartbeatModal/HeartbeatForm.tsx b/grafana-plugin/src/containers/HeartbeatModal/HeartbeatForm.tsx index 8695d4e6..41047296 100644 --- a/grafana-plugin/src/containers/HeartbeatModal/HeartbeatForm.tsx +++ b/grafana-plugin/src/containers/HeartbeatModal/HeartbeatForm.tsx @@ -12,8 +12,8 @@ import { HeartGreenIcon, HeartRedIcon } from 'icons'; import { AlertReceiveChannel } from 'models/alert_receive_channel/alert_receive_channel.types'; import { SelectOption } from 'state/types'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; +import { UserActions } from 'utils/authorization'; import styles from './HeartbeatForm.module.css'; @@ -90,7 +90,7 @@ const HeartbeatForm = observer(({ alertReceveChannelId, onUpdate }: HeartBeatMod

OnCall will issue an incident if no alert is received every - + {

- + diff --git a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx index 47317d42..ff96a67f 100644 --- a/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx +++ b/grafana-plugin/src/containers/OutgoingWebhookForm/OutgoingWebhookForm.tsx @@ -9,7 +9,7 @@ import Text from 'components/Text/Text'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { OutgoingWebhook } from 'models/outgoing_webhook/outgoing_webhook.types'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; +import { UserActions } from 'utils/authorization'; import { form } from './OutgoingWebhookForm.config'; @@ -56,7 +56,7 @@ const OutgoingWebhookForm = observer((props: OutgoingWebhookFormProps) => { >
- + diff --git a/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx b/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx index 16c5ad07..41aa5b16 100644 --- a/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx +++ b/grafana-plugin/src/containers/PersonalNotificationSettings/PersonalNotificationSettings.tsx @@ -14,7 +14,7 @@ import { NotificationPolicyType } from 'models/notification_policy'; import { User as UserType } from 'models/user/user.types'; import { AppFeature } from 'state/features'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; +import { UserActions } from 'utils/authorization'; import { getColor } from './PersonalNotificationSettings.helpers'; import img from './img/default-step.png'; @@ -105,7 +105,7 @@ const PersonalNotificationSettings = observer((props: PersonalNotificationSettin const user = userStore.items[userPk]; - const userAction = isCurrent ? UserAction.UpdateOwnSettings : UserAction.UpdateNotificationPolicies; + const userAction = isCurrent ? UserActions.UserSettingsWrite : UserActions.NotificationSettingsWrite; const getPhoneStatus = () => { if (store.hasFeature(AppFeature.CloudNotifications)) { return user.cloud_connection_status; diff --git a/grafana-plugin/src/containers/Rotations/Rotations.tsx b/grafana-plugin/src/containers/Rotations/Rotations.tsx index fe8eb348..ec1a65a3 100644 --- a/grafana-plugin/src/containers/Rotations/Rotations.tsx +++ b/grafana-plugin/src/containers/Rotations/Rotations.tsx @@ -16,8 +16,8 @@ import { getColor, getFromString } from 'models/schedule/schedule.helpers'; import { Layer, Schedule, ScheduleType, Shift } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { WithStoreProps } from 'state/types'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; +import { UserActions } from 'utils/authorization'; import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config'; import { findColor } from './Rotations.helpers'; @@ -112,7 +112,7 @@ class Rotations extends Component {
) : ( - + diff --git a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx index 811797dc..ff3bc9e4 100644 --- a/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx +++ b/grafana-plugin/src/containers/Rotations/ScheduleOverrides.tsx @@ -15,8 +15,8 @@ import { getOverrideColor, getOverridesFromStore } from 'models/schedule/schedul import { Schedule, ScheduleType, Shift, ShiftEvents } from 'models/schedule/schedule.types'; import { Timezone } from 'models/timezone/timezone.types'; import { WithStoreProps } from 'state/types'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; +import { UserActions } from 'utils/authorization'; import { DEFAULT_TRANSITION_TIMEOUT } from './Rotations.config'; import { findColor } from './Rotations.helpers'; @@ -94,7 +94,7 @@ class ScheduleOverrides extends Component ) : ( - + diff --git a/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx b/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx index 2fb0f480..a926fb4c 100644 --- a/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx +++ b/grafana-plugin/src/containers/ScheduleForm/ScheduleForm.tsx @@ -9,7 +9,7 @@ import Text from 'components/Text/Text'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { Schedule, ScheduleType } from 'models/schedule/schedule.types'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; +import { UserActions } from 'utils/authorization'; import { apiForm, calendarForm, iCalForm } from './ScheduleForm.config'; import { prepareForEdit } from './ScheduleForm.helpers'; @@ -77,7 +77,7 @@ const ScheduleForm = observer((props: ScheduleFormProps) => {
- + diff --git a/grafana-plugin/src/containers/SlackIntegrationButton/SlackIntegrationButton.tsx b/grafana-plugin/src/containers/SlackIntegrationButton/SlackIntegrationButton.tsx index 2b1fdbc5..2eab10f6 100644 --- a/grafana-plugin/src/containers/SlackIntegrationButton/SlackIntegrationButton.tsx +++ b/grafana-plugin/src/containers/SlackIntegrationButton/SlackIntegrationButton.tsx @@ -6,7 +6,7 @@ import { observer } from 'mobx-react'; import WithConfirm from 'components/WithConfirm/WithConfirm'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; +import { UserActions } from 'utils/authorization'; const SlackIntegrationButton = observer((props: { className: string; disabled?: boolean }) => { const { className, disabled } = props; @@ -35,7 +35,7 @@ const SlackIntegrationButton = observer((props: { className: string; disabled?: if (store.teamStore.currentTeam?.slack_team_identity) { return ( - + diff --git a/grafana-plugin/src/containers/UserSettings/UserSettings.types.ts b/grafana-plugin/src/containers/UserSettings/UserSettings.types.ts index 78d2d144..028672af 100644 --- a/grafana-plugin/src/containers/UserSettings/UserSettings.types.ts +++ b/grafana-plugin/src/containers/UserSettings/UserSettings.types.ts @@ -1,4 +1,4 @@ -import { User, UserRole } from 'models/user/user.types'; +import { User } from 'models/user/user.types'; export enum UserSettingsTab { UserInfo, @@ -12,5 +12,4 @@ export enum UserSettingsTab { export interface UserFormData extends Partial { slack_user_identity_name?: string; telegram_configuration_telegram_nick_name?: string; - role?: UserRole; } diff --git a/grafana-plugin/src/containers/UserSettings/parts/connectors/ICalConnector.tsx b/grafana-plugin/src/containers/UserSettings/parts/connectors/ICalConnector.tsx index d5637b0e..1b58108d 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/connectors/ICalConnector.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/connectors/ICalConnector.tsx @@ -8,8 +8,8 @@ import Text from 'components/Text/Text'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; import { User } from 'models/user/user.types'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; import { openNotification } from 'utils'; +import { UserActions } from 'utils/authorization'; import styles from './index.module.css'; @@ -88,7 +88,7 @@ const ICalConnector = (props: ICalConnectorProps) => { In case you lost your iCal link you can revoke it and generate a new one. - + diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx index 7758b450..9eecb73e 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/CloudPhoneSettings/CloudPhoneSettings.tsx @@ -9,8 +9,8 @@ import { User } from 'models/user/user.types'; import { AppFeature } from 'state/features'; import { WithStoreProps } from 'state/types'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; +import { UserActions } from 'utils/authorization'; interface CloudPhoneSettingsProps extends WithStoreProps { userPk?: User['pk']; @@ -119,7 +119,7 @@ const CloudPhoneSettings = observer((props: CloudPhoneSettingsProps) => { return ( <> - {store.isUserActionAllowed(UserAction.UpdateOtherUsersSettings) ? ( + {store.isUserActionAllowed(UserActions.OtherSettingsWrite) ? ( OnCall use Grafana Cloud for SMS and phone call notifications diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/PhoneVerification/PhoneVerification.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/PhoneVerification/PhoneVerification.tsx index a06208dc..b8936524 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/PhoneVerification/PhoneVerification.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/PhoneVerification/PhoneVerification.tsx @@ -10,8 +10,8 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm import { User } from 'models/user/user.types'; import { AppFeature } from 'state/features'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; import { openErrorNotification } from 'utils'; +import { UserAction, UserActions } from 'utils/authorization'; import styles from './PhoneVerification.module.css'; @@ -137,7 +137,7 @@ const PhoneVerification = observer((props: PhoneVerificationProps) => { const isPhoneValid = phoneHasMinimumLength && PHONE_REGEX.test(phone); const showPhoneInputError = phoneHasMinimumLength && !isPhoneValid && !isPhoneNumberHidden && !isLoading; - const action = isCurrentUser ? UserAction.UpdateOwnSettings : UserAction.UpdateOtherUsersSettings; + const action = isCurrentUser ? UserActions.UserSettingsWrite : UserActions.UserSettingsAdmin; const isButtonDisabled = phone === user.verified_phone_number || (!isCodeSent && !isPhoneValid) || !isTwilioConfigured; @@ -264,7 +264,7 @@ function ForgetPhoneScreen({ phone, onCancel, onForget }: ForgetPhoneScreenProps } interface PhoneVerificationButtonsGroupProps { - action: UserAction.UpdateOwnSettings | UserAction.UpdateOtherUsersSettings; + action: UserAction; isCodeSent: boolean; isButtonDisabled: boolean; diff --git a/grafana-plugin/src/containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab.tsx b/grafana-plugin/src/containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab.tsx index d5d81090..a506af70 100644 --- a/grafana-plugin/src/containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab.tsx +++ b/grafana-plugin/src/containers/UserSettings/parts/tabs/UserInfoTab/UserInfoTab.tsx @@ -6,7 +6,6 @@ import cn from 'classnames/bind'; import Text from 'components/Text/Text'; import { UserSettingsTab } from 'containers/UserSettings/UserSettings.types'; import { Connectors } from 'containers/UserSettings/parts/connectors'; -import { getRole } from 'models/user/user.helpers'; import { User } from 'models/user/user.types'; import { useStore } from 'state/useStore'; @@ -31,7 +30,7 @@ export const UserInfoTab = (props: UserInfoTabProps) => { <>
- To edit user details such as Username, email, and role, please visit{' '} + To edit user details such as Username, email, and roles, please visit{' '} Grafana User settings.
@@ -43,10 +42,6 @@ export const UserInfoTab = (props: UserInfoTabProps) => { {storeUser.email || '—'}
-
- - {getRole(storeUser.role)} -
); diff --git a/grafana-plugin/src/containers/WithPermissionControl/WithPermissionControl.tsx b/grafana-plugin/src/containers/WithPermissionControl/WithPermissionControl.tsx index 2215b7a8..2f28b504 100644 --- a/grafana-plugin/src/containers/WithPermissionControl/WithPermissionControl.tsx +++ b/grafana-plugin/src/containers/WithPermissionControl/WithPermissionControl.tsx @@ -5,7 +5,7 @@ import cn from 'classnames/bind'; import { observer } from 'mobx-react'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; +import { UserAction } from 'utils/authorization'; import styles from './WithPermissionControl.module.css'; diff --git a/grafana-plugin/src/containers/WithPermissionControl2/WithPermissionControl.tsx b/grafana-plugin/src/containers/WithPermissionControl2/WithPermissionControl.tsx index b5074956..9ea83545 100644 --- a/grafana-plugin/src/containers/WithPermissionControl2/WithPermissionControl.tsx +++ b/grafana-plugin/src/containers/WithPermissionControl2/WithPermissionControl.tsx @@ -4,7 +4,7 @@ import { Tooltip } from '@grafana/ui'; import { observer } from 'mobx-react'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; +import { UserAction } from 'utils/authorization'; interface WithPermissionControlProps { userAction: UserAction; diff --git a/grafana-plugin/src/index.d.ts b/grafana-plugin/src/index.d.ts index 98c6c4b4..05637e54 100644 --- a/grafana-plugin/src/index.d.ts +++ b/grafana-plugin/src/index.d.ts @@ -22,6 +22,12 @@ declare module 'grafana/app/core/core' { // https://github.com/grafana/grafana/blob/main/public/app/core/services/context_srv.ts#L59 export const contextSrv: { - hasRole(role: OrgRole): boolean; + user: { + orgRole: OrgRole | ''; + permissions?: Record; + }; + + hasAccess(action: string, fallBack: boolean): boolean; + accessControlEnabled(): boolean; }; } diff --git a/grafana-plugin/src/models/team/team.types.ts b/grafana-plugin/src/models/team/team.types.ts index 505052db..cf511c96 100644 --- a/grafana-plugin/src/models/team/team.types.ts +++ b/grafana-plugin/src/models/team/team.types.ts @@ -1,5 +1,4 @@ import { SlackChannel } from 'models/slack_channel/slack_channel.types'; -import { UserRole } from 'models/user/user.types'; export enum SubscriptionStatus { OK, @@ -63,7 +62,6 @@ export interface Team { // ex team settings archive_alerts_from: string; is_resolution_note_required: boolean; - user_role_by_default: UserRole; env_status: { twilio_configured: boolean; diff --git a/grafana-plugin/src/models/user.ts b/grafana-plugin/src/models/user.ts index 2c96b801..3b267954 100644 --- a/grafana-plugin/src/models/user.ts +++ b/grafana-plugin/src/models/user.ts @@ -1,7 +1,3 @@ -import { UserAction } from 'state/userAction'; - -import { UserRole } from './user/user.types'; - export interface UserDTO { pk: number; slack_login: string; @@ -16,7 +12,6 @@ export interface UserDTO { verified_phone_number?: string; unverified_phone_number?: string; phone_verified: boolean; - role: UserRole; telegram_configuration: { telegram_nick_name: string; telegram_chat_id: number; @@ -29,6 +24,5 @@ export interface UserDTO { inviter_name: string | null; video_conference_link: string | null; }; - permissions: UserAction[]; trigger_video_call?: boolean; } diff --git a/grafana-plugin/src/models/user/user.config.ts b/grafana-plugin/src/models/user/user.config.ts deleted file mode 100644 index 6c2dcdf3..00000000 --- a/grafana-plugin/src/models/user/user.config.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { UserRole } from 'models/user/user.types'; - -export const DEFAULT_USER_ROLES = [ - { display_name: 'Admin', value: UserRole.ADMIN }, - { display_name: 'Editor', value: UserRole.EDITOR }, - { - display_name: 'Viewer', - value: UserRole.VIEWER, - }, -]; diff --git a/grafana-plugin/src/models/user/user.helpers.tsx b/grafana-plugin/src/models/user/user.helpers.tsx index 7d21b87d..0911b4e9 100644 --- a/grafana-plugin/src/models/user/user.helpers.tsx +++ b/grafana-plugin/src/models/user/user.helpers.tsx @@ -2,33 +2,7 @@ import React from 'react'; import { pick } from 'lodash-es'; -import { User, UserRole } from './user.types'; - -export const getIconType = (role: UserRole) => { - switch (role) { - case UserRole.ADMIN: - return 'crown'; - case UserRole.EDITOR: - return 'user'; - case UserRole.VIEWER: - return 'eye'; - default: - return 'user'; - } -}; - -export const getRole = (role: UserRole) => { - switch (role) { - case UserRole.ADMIN: - return 'Admin'; - case UserRole.EDITOR: - return 'Editor'; - case UserRole.VIEWER: - return 'Viewer'; - default: - return ''; - } -}; +import { User } from './user.types'; export const getTimezone = (user: User) => { return user.timezone || 'UTC'; diff --git a/grafana-plugin/src/models/user/user.ts b/grafana-plugin/src/models/user/user.ts index edcea94d..0c5a822e 100644 --- a/grafana-plugin/src/models/user/user.ts +++ b/grafana-plugin/src/models/user/user.ts @@ -8,6 +8,7 @@ import { makeRequest } from 'network'; import { Mixpanel } from 'services/mixpanel'; import { RootStore } from 'state'; import { move } from 'state/helpers'; +import { UserActions } from 'utils/authorization'; import { getTimezone, prepareForUpdate } from './user.helpers'; import { User } from './user.types'; @@ -55,7 +56,7 @@ export class UserStore extends BaseStore { const response = await makeRequest('/user/', {}); let timezone; - if (!response.timezone) { + if (!response.timezone && this.rootStore.isUserActionAllowed(UserActions.UserSettingsWrite)) { timezone = dayjs.tz.guess(); this.update(response.pk, { timezone }); } @@ -101,11 +102,11 @@ export class UserStore extends BaseStore { } @action - async updateItems(f: any = { searchTerm: '', roles: undefined }, page = 1) { + async updateItems(f: any = { searchTerm: '' }, page = 1) { const filters = typeof f === 'string' ? { searchTerm: f } : f; // for GSelect compatibility - const { searchTerm: search, roles } = filters; + const { searchTerm: search } = filters; const { count, results } = await makeRequest(this.path, { - params: { search, roles, page }, + params: { search, page }, }); this.items = { diff --git a/grafana-plugin/src/models/user/user.types.ts b/grafana-plugin/src/models/user/user.types.ts index 4c7b6fb8..58de40b2 100644 --- a/grafana-plugin/src/models/user/user.types.ts +++ b/grafana-plugin/src/models/user/user.types.ts @@ -1,12 +1,5 @@ import { Team } from 'models/team/team.types'; import { Timezone } from 'models/timezone/timezone.types'; -import { UserAction } from 'state/userAction'; - -export enum UserRole { - ADMIN, - EDITOR, - VIEWER, -} export interface MessagingBackends { [key: string]: any; @@ -25,7 +18,6 @@ export interface User { username: string; slack_id: string; phone_verified: boolean; - role: UserRole; telegram_configuration: { telegram_nick_name: string; telegram_chat_id: number; // TODO check if string @@ -51,7 +43,6 @@ export interface User { inviter_name: string | null; video_conference_link: string | null; }; - permissions: UserAction[]; trigger_video_call?: boolean; export_url?: string; status?: number; diff --git a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx index 16a65c55..d01bd347 100644 --- a/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx +++ b/grafana-plugin/src/pages/escalation-chains/EscalationChains.tsx @@ -27,9 +27,9 @@ import { WithPermissionControl } from 'containers/WithPermissionControl/WithPerm import { EscalationChain } from 'models/escalation_chain/escalation_chain.types'; import { pages } from 'pages'; import { PageProps, WithStoreProps } from 'state/types'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import LocationHelper from 'utils/LocationHelper'; +import { UserActions } from 'utils/authorization'; import styles from './EscalationChains.module.css'; @@ -151,7 +151,7 @@ class EscalationChainsPage extends React.Component
- + @@ -149,7 +149,7 @@ export function getActionButtons(incident: AlertType, cx: any, callbacks: { [key ); const unacknowledgeButton = ( - + @@ -157,7 +157,7 @@ export function getActionButtons(incident: AlertType, cx: any, callbacks: { [key ); const unresolveButton = ( - + @@ -165,7 +165,7 @@ export function getActionButtons(incident: AlertType, cx: any, callbacks: { [key ); const acknowledgeButton = ( - + @@ -189,7 +189,7 @@ export function getActionButtons(incident: AlertType, cx: any, callbacks: { [key if (incident.status === IncidentStatus.Silenced) { buttons.push( - + diff --git a/grafana-plugin/src/pages/incident/Incident.tsx b/grafana-plugin/src/pages/incident/Incident.tsx index c4f48065..4be78be1 100644 --- a/grafana-plugin/src/pages/incident/Incident.tsx +++ b/grafana-plugin/src/pages/incident/Incident.tsx @@ -49,10 +49,10 @@ import { ResolutionNoteSourceTypesToDisplayName } from 'models/resolution_note/r import { pages } from 'pages'; import { PageProps, WithStoreProps } from 'state/types'; import { useStore } from 'state/useStore'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import { openNotification } from 'utils'; import LocationHelper from 'utils/LocationHelper'; +import { UserActions } from 'utils/authorization'; import sanitize from 'utils/sanitize'; import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from './Incident.helpers'; @@ -229,7 +229,7 @@ class IncidentPage extends React.Component #{incident.root_alert_group.inside_organization_number}{' '} {incident.root_alert_group.render_for_web.title} {' '} - + @@ -646,7 +646,7 @@ function AttachedIncidentsList({ #{incident.inside_organization_number} {incident.render_for_web.title} - + diff --git a/grafana-plugin/src/pages/incidents/Incidents.tsx b/grafana-plugin/src/pages/incidents/Incidents.tsx index 198c2da4..10503e7b 100644 --- a/grafana-plugin/src/pages/incidents/Incidents.tsx +++ b/grafana-plugin/src/pages/incidents/Incidents.tsx @@ -24,9 +24,9 @@ import { pages } from 'pages'; import { getActionButtons, getIncidentStatusTag, renderRelatedUsers } from 'pages/incident/Incident.helpers'; import { move } from 'state/helpers'; import { PageProps, WithStoreProps } from 'state/types'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import LocationHelper from 'utils/LocationHelper'; +import { UserActions } from 'utils/authorization'; import SilenceDropdown from './parts/SilenceDropdown'; @@ -197,7 +197,7 @@ class Incidents extends React.Component
{'resolve' in store.alertGroupStore.bulkActions && ( - +
- + @@ -165,12 +165,12 @@ class OutgoingWebhooks extends React.Component { return ( - + - + @@ -355,10 +355,10 @@ class SchedulesPage extends React.Component { return ( - + - + diff --git a/grafana-plugin/src/pages/settings/SettingsPage.tsx b/grafana-plugin/src/pages/settings/SettingsPage.tsx index be6af07b..c56bb504 100644 --- a/grafana-plugin/src/pages/settings/SettingsPage.tsx +++ b/grafana-plugin/src/pages/settings/SettingsPage.tsx @@ -5,13 +5,13 @@ import { PluginPage } from 'PluginPage'; import cn from 'classnames/bind'; import { observer } from 'mobx-react'; -import { pages } from 'pages'; import ChatOpsPage from 'pages/settings/tabs/ChatOps/ChatOps'; import MainSettings from 'pages/settings/tabs/MainSettings/MainSettings'; import { isTopNavbar } from 'plugin/GrafanaPluginRootPage.helpers'; import { AppFeature } from 'state/features'; import { RootBaseStore } from 'state/rootBaseStore'; import { withMobXProviderContext } from 'state/withStore'; +import { UserActions } from 'utils/authorization'; import { SettingsPageTab } from './SettingsPage.types'; import CloudPage from './tabs/Cloud/CloudPage'; @@ -50,13 +50,10 @@ class SettingsPage extends React.Component this.setState({ activeTab: tab }); }; - const grafanaUser = window.grafanaBootData.user; const hasLiveSettings = store.hasFeature(AppFeature.LiveSettings); const hasCloudPage = store.hasFeature(AppFeature.CloudConnection); - const showCloudPage = - hasCloudPage && (pages['cloud'].role === 'Admin' ? pages['cloud'].role === grafanaUser.orgRole : true); - const showLiveSettings = - hasLiveSettings && (pages['cloud'].role === 'Admin' ? pages['cloud'].role === grafanaUser.orgRole : true); + const showCloudPage = hasCloudPage && store.isUserActionAllowed(UserActions.OtherSettingsWrite); + const showLiveSettings = hasLiveSettings && store.isUserActionAllowed(UserActions.OtherSettingsRead); if (isTopNavbar()) { return ( diff --git a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx index 3bd3e13e..392acfb3 100644 --- a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx +++ b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx @@ -16,8 +16,8 @@ import { PRIVATE_CHANNEL_NAME } from 'models/slack_channel/slack_channel.config' import { SlackChannel } from 'models/slack_channel/slack_channel.types'; import { AppFeature } from 'state/features'; import { WithStoreProps } from 'state/types'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; +import { UserActions } from 'utils/authorization'; import styles from './SlackSettings.module.css'; @@ -108,7 +108,7 @@ class SlackSettings extends Component {
- + { - + }
diff --git a/grafana-plugin/src/pages/users/Users.helpers.ts b/grafana-plugin/src/pages/users/Users.helpers.ts index bc4fd318..991434fd 100644 --- a/grafana-plugin/src/pages/users/Users.helpers.ts +++ b/grafana-plugin/src/pages/users/Users.helpers.ts @@ -1,4 +1,4 @@ -import { User as UserType, UserRole } from 'models/user/user.types'; +import { User as UserType } from 'models/user/user.types'; export const getUserRowClassNameFn = (userPkToEdit?: UserType['pk'], currentUserPk?: UserType['pk']) => { return (user: UserType) => { @@ -13,15 +13,3 @@ export const getUserRowClassNameFn = (userPkToEdit?: UserType['pk'], currentUser return ''; }; }; - -export const getRealFilters = (filters: any) => { - let realFilters = { ...filters }; - if (!realFilters.roles || !realFilters.roles.length) { - realFilters = { - ...filters, - roles: [UserRole.ADMIN, UserRole.EDITOR, UserRole.VIEWER], - }; - } - - return realFilters; -}; diff --git a/grafana-plugin/src/pages/users/Users.tsx b/grafana-plugin/src/pages/users/Users.tsx index 6faa3b55..6f191fc5 100644 --- a/grafana-plugin/src/pages/users/Users.tsx +++ b/grafana-plugin/src/pages/users/Users.tsx @@ -19,15 +19,14 @@ import Text from 'components/Text/Text'; import UsersFilters from 'components/UsersFilters/UsersFilters'; import UserSettings from 'containers/UserSettings/UserSettings'; import { WithPermissionControl } from 'containers/WithPermissionControl/WithPermissionControl'; -import { getRole } from 'models/user/user.helpers'; -import { User as UserType, UserRole } from 'models/user/user.types'; +import { User as UserType } from 'models/user/user.types'; import { pages } from 'pages'; import { PageProps, WithStoreProps } from 'state/types'; -import { UserAction } from 'state/userAction'; import { withMobXProviderContext } from 'state/withStore'; import LocationHelper from 'utils/LocationHelper'; +import { UserActions } from 'utils/authorization'; -import { getRealFilters, getUserRowClassNameFn } from './Users.helpers'; +import { getUserRowClassNameFn } from './Users.helpers'; import styles from './Users.module.css'; @@ -43,7 +42,6 @@ interface UsersState extends PageBaseState { userPkToEdit?: UserType['pk'] | 'new'; usersFilters?: { searchTerm: string; - roles?: UserRole[]; }; } @@ -55,7 +53,6 @@ class Users extends React.Component { userPkToEdit: undefined, usersFilters: { searchTerm: '', - roles: [UserRole.ADMIN, UserRole.EDITOR, UserRole.VIEWER], }, errorData: initErrorDataState(), @@ -77,18 +74,18 @@ class Users extends React.Component { const { usersFilters, page } = this.state; const { userStore } = store; - if (!store.isUserActionAllowed(UserAction.ViewOtherUsers)) { + if (!store.isUserActionAllowed(UserActions.UserSettingsWrite)) { return; } LocationHelper.update({ p: page }, 'partial'); - return await userStore.updateItems(getRealFilters(usersFilters), page); + return await userStore.updateItems(usersFilters, page); }; componentDidUpdate(prevProps: UsersProps) { const { store } = this.props; - if (!this.initialUsersLoaded && store.isUserActionAllowed(UserAction.ViewOtherUsers)) { + if (!this.initialUsersLoaded && store.isUserActionAllowed(UserActions.UserSettingsWrite)) { this.updateUsers(); this.initialUsersLoaded = true; } @@ -131,12 +128,6 @@ class Users extends React.Component { title: 'User', render: this.renderTitle, }, - { - width: '5%', - title: 'Role', - key: 'role', - render: this.renderRole, - }, { width: '20%', title: 'Status', @@ -163,12 +154,9 @@ class Users extends React.Component { ]; const handleClear = () => - this.setState( - { usersFilters: { searchTerm: '', roles: [UserRole.ADMIN, UserRole.EDITOR, UserRole.VIEWER] } }, - () => { - this.debouncedUpdateUsers(); - } - ); + this.setState({ usersFilters: { searchTerm: '' } }, () => { + this.debouncedUpdateUsers(); + }); const { count, results } = userStore.getSearchResult(); @@ -202,7 +190,7 @@ class Users extends React.Component { - {store.isUserActionAllowed(UserAction.ViewOtherUsers) ? ( + {store.isUserActionAllowed(UserActions.UserSettingsRead) ? ( <>
{ ); }; - renderRole = (user: UserType) => { - return getRole(user.role); - }; - renderNotificationsChain = (user: UserType) => { return user.notification_chain_verbal.default; }; @@ -299,7 +283,7 @@ class Users extends React.Component { const { userStore } = store; const isCurrent = userStore.currentUserPk === user.pk; - const action = isCurrent ? UserAction.UpdateOwnSettings : UserAction.UpdateOtherUsersSettings; + const action = isCurrent ? UserActions.UserSettingsWrite : UserActions.UserSettingsAdmin; return ( diff --git a/grafana-plugin/src/plugin.json b/grafana-plugin/src/plugin.json index 93e971a1..9e26e3a5 100644 --- a/grafana-plugin/src/plugin.json +++ b/grafana-plugin/src/plugin.json @@ -34,6 +34,7 @@ "name": "Alert Groups", "path": "/a/grafana-oncall-app/?page=incidents", "role": "Viewer", + "action": "grafana-oncall-app.alert-groups:read", "defaultNav": true, "addToNav": true }, @@ -42,6 +43,7 @@ "name": "Users", "path": "/a/grafana-oncall-app/?page=users", "role": "Viewer", + "action": "grafana-oncall-app.user-settings:read", "addToNav": true }, { @@ -49,6 +51,7 @@ "name": "Integrations", "path": "/a/grafana-oncall-app/?page=integrations", "role": "Viewer", + "action": "grafana-oncall-app.integrations:read", "addToNav": true }, { @@ -56,6 +59,7 @@ "name": "Escalation Chains", "path": "/a/grafana-oncall-app/?page=escalations", "role": "Viewer", + "action": "grafana-oncall-app.escalation-chains:read", "addToNav": true }, { @@ -63,6 +67,7 @@ "name": "Schedules", "path": "/a/grafana-oncall-app/?page=schedules", "role": "Viewer", + "action": "grafana-oncall-app.schedules:read", "addToNav": true }, { @@ -70,6 +75,7 @@ "name": "Outgoing Webhooks", "path": "/a/grafana-oncall-app/?page=outgoing_webhooks", "role": "Viewer", + "action": "grafana-oncall-app.outgoing-webhooks:read", "addToNav": true }, { @@ -77,6 +83,7 @@ "name": "Maintenance", "path": "/a/grafana-oncall-app/?page=maintenance", "role": "Viewer", + "action": "grafana-oncall-app.maintenance:read", "addToNav": true }, { @@ -84,6 +91,7 @@ "name": "Settings", "path": "/a/grafana-oncall-app/?page=settings", "role": "Viewer", + "action": "grafana-oncall-app.other-settings:read", "addToNav": true } ], @@ -175,6 +183,418 @@ ] } ], + "roles": [ + { + "role": { + "name": "Admin", + "description": "Read/write access to everything in OnCall", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + + { "action": "grafana-oncall-app.alert-groups:read" }, + { "action": "grafana-oncall-app.alert-groups:write" }, + + { "action": "grafana-oncall-app.integrations:read" }, + { "action": "grafana-oncall-app.integrations:write" }, + { "action": "grafana-oncall-app.integrations:test" }, + + { "action": "grafana-oncall-app.escalation-chains:read" }, + { "action": "grafana-oncall-app.escalation-chains:write" }, + + { "action": "grafana-oncall-app.schedules:read" }, + { "action": "grafana-oncall-app.schedules:write" }, + { "action": "grafana-oncall-app.schedules:export" }, + + { "action": "grafana-oncall-app.chatops:read" }, + { "action": "grafana-oncall-app.chatops:write" }, + { "action": "grafana-oncall-app.chatops:update-settings" }, + + { "action": "grafana-oncall-app.outgoing-webhooks:read" }, + { "action": "grafana-oncall-app.outgoing-webhooks:write" }, + + { "action": "grafana-oncall-app.maintenance:read" }, + { "action": "grafana-oncall-app.maintenance:write" }, + + { "action": "grafana-oncall-app.api-keys:read" }, + { "action": "grafana-oncall-app.api-keys:write" }, + + { "action": "grafana-oncall-app.notifications:read" }, + + { "action": "grafana-oncall-app.notification-settings:read" }, + { "action": "grafana-oncall-app.notification-settings:write" }, + + { "action": "grafana-oncall-app.user-settings:read" }, + { "action": "grafana-oncall-app.user-settings:write" }, + { "action": "grafana-oncall-app.user-settings:admin" }, + + { "action": "grafana-oncall-app.other-settings:read" }, + { "action": "grafana-oncall-app.other-settings:write" } + ], + "hidden": true + }, + "grants": ["Grafana Admin", "Admin"] + }, + { + "role": { + "name": "Editor", + "description": "Similar to the Admin role, minus the abilities to: create Integrations, create Escalation Chains, create Schedules, create Outgoing Webhooks, update ChatOps settings, update other user's settings, and update general OnCall setings.", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + + { "action": "grafana-oncall-app.alert-groups:read" }, + { "action": "grafana-oncall-app.alert-groups:write" }, + + { "action": "grafana-oncall-app.integrations:read" }, + { "action": "grafana-oncall-app.integrations:test" }, + + { "action": "grafana-oncall-app.escalation-chains:read" }, + + { "action": "grafana-oncall-app.schedules:read" }, + { "action": "grafana-oncall-app.schedules:export" }, + + { "action": "grafana-oncall-app.chatops:read" }, + { "action": "grafana-oncall-app.chatops:write" }, + + { "action": "grafana-oncall-app.outgoing-webhooks:read" }, + + { "action": "grafana-oncall-app.maintenance:read" }, + { "action": "grafana-oncall-app.maintenance:write" }, + + { "action": "grafana-oncall-app.api-keys:read" }, + { "action": "grafana-oncall-app.api-keys:write" }, + + { "action": "grafana-oncall-app.notifications:read" }, + + { "action": "grafana-oncall-app.notification-settings:read" }, + { "action": "grafana-oncall-app.notification-settings:write" }, + + { "action": "grafana-oncall-app.user-settings:read" }, + { "action": "grafana-oncall-app.user-settings:write" }, + + { "action": "grafana-oncall-app.other-settings:read" } + ] + }, + "grants": ["Editor"] + }, + { + "role": { + "name": "Reader", + "description": "Read-only access to everything in OnCall", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + + { "action": "grafana-oncall-app.alert-groups:read" }, + { "action": "grafana-oncall-app.integrations:read" }, + { "action": "grafana-oncall-app.escalation-chains:read" }, + { "action": "grafana-oncall-app.schedules:read" }, + { "action": "grafana-oncall-app.chatops:read" }, + { "action": "grafana-oncall-app.outgoing-webhooks:read" }, + { "action": "grafana-oncall-app.maintenance:read" }, + { "action": "grafana-oncall-app.api-keys:read" }, + { "action": "grafana-oncall-app.notification-settings:read" }, + { "action": "grafana-oncall-app.user-settings:read" }, + { "action": "grafana-oncall-app.other-settings:read" } + ] + }, + "grants": ["Viewer"] + }, + { + "role": { + "name": "OnCaller", + "description": "Grants read access to everything in OnCall. In addition, grants edit access to Alert Groups and Schedules", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + + { "action": "grafana-oncall-app.alert-groups:read" }, + { "action": "grafana-oncall-app.alert-groups:write" }, + + { "action": "grafana-oncall-app.integrations:read" }, + { "action": "grafana-oncall-app.escalation-chains:read" }, + + { "action": "grafana-oncall-app.schedules:read" }, + { "action": "grafana-oncall-app.schedules:write" }, + + { "action": "grafana-oncall-app.chatops:read" }, + { "action": "grafana-oncall-app.outgoing-webhooks:read" }, + { "action": "grafana-oncall-app.maintenance:read" }, + { "action": "grafana-oncall-app.api-keys:read" }, + { "action": "grafana-oncall-app.notification-settings:read" }, + { "action": "grafana-oncall-app.user-settings:read" }, + { "action": "grafana-oncall-app.other-settings:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Alert Groups Reader", + "description": "Read-only access to OnCall Alert Groups", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.alert-groups:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Alert Groups Editor", + "description": "Read/write access to OnCall Alert Groups", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.alert-groups:read" }, + { "action": "grafana-oncall-app.alert-groups:write" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Integrations Reader", + "description": "Read-only access to OnCall Integrations", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.integrations:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Integrations Editor", + "description": "Read/write access to OnCall Integrations", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.integrations:read" }, + { "action": "grafana-oncall-app.integrations:write" }, + { "action": "grafana-oncall-app.integrations:test" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Escalation Chains Reader", + "description": "Read-only access to OnCall Escalation Chains", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.escalation-chains:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Escalation Chains Editor", + "description": "Read/write access to OnCall Escalation Chains", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.escalation-chains:read" }, + { "action": "grafana-oncall-app.escalation-chains:write" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Schedules Reader", + "description": "Read-only access to OnCall Schedules", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.schedules:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Schedules Editor", + "description": "Read/write access to OnCall Schedules", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.schedules:read" }, + { "action": "grafana-oncall-app.schedules:write" }, + { "action": "grafana-oncall-app.schedules:export" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "ChatOps Reader", + "description": "Read-only access to OnCall ChatOps", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.chatops:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "ChatOps Editor", + "description": "Read/write access to OnCall ChatOps", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.chatops:read" }, + { "action": "grafana-oncall-app.chatops:write" }, + { "action": "grafana-oncall-app.chatops:update-settings" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Outgoing Webhooks Reader", + "description": "Read-only access to OnCall Outgoing Webhooks", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.outgoing-webhooks:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Outgoing Webhooks Editor", + "description": "Read/write access to OnCall Outgoing Webhooks", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.outgoing-webhooks:read" }, + { "action": "grafana-oncall-app.outgoing-webhooks:write" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Maintenance Reader", + "description": "Read-only access to OnCall Maintenance", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.maintenance:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Maintenance Editor", + "description": "Read/write access to OnCall Maintenance", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.maintenance:read" }, + { "action": "grafana-oncall-app.maintenance:write" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "API Keys Reader", + "description": "Read-only access to OnCall API Keys", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.api-keys:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "API Keys Editor", + "description": "Read/write access to OnCall API Keys. Also grants access to be able to consume the API.", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.api-keys:read" }, + { "action": "grafana-oncall-app.api-keys:write" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Notification Settings Reader", + "description": "Read-only access to OnCall Notification Settings", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.notification-settings:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Notification Settings Editor", + "description": "Read/write access to OnCall Notification Settings", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.notification-settings:read" }, + { "action": "grafana-oncall-app.notification-settings:write" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "User Settings Reader", + "description": "Read-only access to OnCall User Settings", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.user-settings:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "User Settings Editor", + "description": "Read/write access to own OnCall User Settings", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.user-settings:read" }, + { "action": "grafana-oncall-app.user-settings:write" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "User Settings Admin", + "description": "Read/write access to your own, plus other's OnCall User Settings", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.user-settings:read" }, + { "action": "grafana-oncall-app.user-settings:write" }, + { "action": "grafana-oncall-app.user-settings:admin" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Settings Reader", + "description": "Read-only access to OnCall Settings", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.other-settings:read" } + ] + }, + "grants": [] + }, + { + "role": { + "name": "Settings Editor", + "description": "Read/write access to OnCall Settings", + "permissions": [ + { "action": "plugins.app:access", "scope": "plugins.app:id:grafana-oncall-app" }, + { "action": "grafana-oncall-app.other-settings:read" }, + { "action": "grafana-oncall-app.other-settings:write" } + ] + }, + "grants": [] + } + ], "dependencies": { "grafanaDependency": ">=8.3.2", "grafanaVersion": "8.3", diff --git a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx index 43288bb3..fadc91eb 100644 --- a/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx +++ b/grafana-plugin/src/plugin/GrafanaPluginRootPage.tsx @@ -15,6 +15,7 @@ import Header from 'navbar/Header/Header'; import LegacyNavTabsBar from 'navbar/LegacyNavTabsBar'; import { AppRootProps } from 'types'; +import Unauthorized from 'components/Unauthorized'; import DefaultPageLayout from 'containers/DefaultPageLayout/DefaultPageLayout'; import 'interceptors'; import { pages } from 'pages'; @@ -83,6 +84,9 @@ export const Root = observer((props: AppRootProps) => { return null; } + const { action: pagePermissionAction } = pages[page]; + const userHasAccess = pagePermissionAction ? store.isUserActionAllowed(pagePermissionAction) : true; + return ( {!isTopNavbar() && ( @@ -101,7 +105,11 @@ export const Root = observer((props: AppRootProps) => { 'u-position-relative' )} > - + {userHasAccess ? ( + + ) : ( + + )}
); diff --git a/grafana-plugin/src/state/rootBaseStore/index.ts b/grafana-plugin/src/state/rootBaseStore/index.ts index 17f2f48e..54c87b4c 100644 --- a/grafana-plugin/src/state/rootBaseStore/index.ts +++ b/grafana-plugin/src/state/rootBaseStore/index.ts @@ -1,5 +1,3 @@ -import { OrgRole } from '@grafana/data'; -import { contextSrv } from 'grafana/app/core/core'; import { action, observable } from 'mobx'; import moment from 'moment-timezone'; import qs from 'query-string'; @@ -32,7 +30,7 @@ import { makeRequest } from 'network'; import { NavMenuItem } from 'pages/routes'; import { AppFeature } from 'state/features'; import PluginState from 'state/plugin'; -import { UserAction } from 'state/userAction'; +import { UserActions, isUserActionAllowed } from 'utils/authorization'; // ------ Dashboard ------ // @@ -164,8 +162,10 @@ export class RootBaseStore { return this.setupPluginError('🚫 OnCall has temporarily disabled signup of new users. Please try again later.'); } - if (!contextSrv.hasRole(OrgRole.Admin)) { - return this.setupPluginError('🚫 Admin must sign on to setup OnCall before a Viewer can use it'); + if (!this.isUserActionAllowed(UserActions.PluginsInstall)) { + return this.setupPluginError( + '🚫 An Admin in your organization must sign on and setup OnCall before it can be used' + ); } try { @@ -199,9 +199,7 @@ export class RootBaseStore { this.appLoading = false; } - isUserActionAllowed(action: UserAction) { - return this.userStore.currentUser && this.userStore.currentUser.permissions.includes(action); - } + isUserActionAllowed = isUserActionAllowed; hasFeature(feature: string | AppFeature) { // todo use AppFeature only diff --git a/grafana-plugin/src/state/rootBaseStore/rootBaseStore.test.ts b/grafana-plugin/src/state/rootBaseStore/rootBaseStore.test.ts index 3cbc51e2..62c4f013 100644 --- a/grafana-plugin/src/state/rootBaseStore/rootBaseStore.test.ts +++ b/grafana-plugin/src/state/rootBaseStore/rootBaseStore.test.ts @@ -1,15 +1,14 @@ -import { OrgRole } from '@grafana/data'; -import { contextSrv as contextSrvOriginal } from 'grafana/app/core/core'; import { OnCallAppPluginMeta } from 'types'; import PluginState from 'state/plugin'; +import { UserActions } from 'utils/authorization'; import { RootBaseStore } from './'; -const contextSrv = contextSrvOriginal as { hasRole: jest.Mock> }; - jest.mock('state/plugin'); +const PluginInstallAction = UserActions.PluginsInstall; + const generatePluginData = ( onCallApiUrl: OnCallAppPluginMeta['jsonData']['onCallApiUrl'] = null ): OnCallAppPluginMeta => @@ -123,7 +122,7 @@ describe('rootBaseStore', () => { version: 'asdfasdf', license: 'asdfasdf', }); - contextSrv.hasRole.mockReturnValueOnce(false); + rootBaseStore.isUserActionAllowed = jest.fn().mockReturnValueOnce(false); PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null); // test @@ -133,14 +132,14 @@ describe('rootBaseStore', () => { expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledTimes(1); expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledWith(onCallApiUrl); - expect(contextSrv.hasRole).toHaveBeenCalledTimes(1); - expect(contextSrv.hasRole).toHaveBeenCalledWith(OrgRole.Admin); + expect(rootBaseStore.isUserActionAllowed).toHaveBeenCalledTimes(1); + expect(rootBaseStore.isUserActionAllowed).toHaveBeenCalledWith(PluginInstallAction); expect(PluginState.installPlugin).toHaveBeenCalledTimes(0); expect(rootBaseStore.appLoading).toBe(false); expect(rootBaseStore.initializationError).toEqual( - '🚫 Admin must sign on to setup OnCall before a Viewer can use it' + '🚫 An Admin in your organization must sign on and setup OnCall before it can be used' ); }); @@ -160,7 +159,7 @@ describe('rootBaseStore', () => { version: 'asdfasdf', license: 'asdfasdf', }); - contextSrv.hasRole.mockReturnValueOnce(true); + rootBaseStore.isUserActionAllowed = jest.fn().mockReturnValueOnce(true); PluginState.installPlugin = jest.fn().mockResolvedValueOnce(null); rootBaseStore.userStore.loadCurrentUser = mockedLoadCurrentUser; @@ -171,8 +170,8 @@ describe('rootBaseStore', () => { expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledTimes(1); expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledWith(onCallApiUrl); - expect(contextSrv.hasRole).toHaveBeenCalledTimes(1); - expect(contextSrv.hasRole).toHaveBeenCalledWith(OrgRole.Admin); + expect(rootBaseStore.isUserActionAllowed).toHaveBeenCalledTimes(1); + expect(rootBaseStore.isUserActionAllowed).toHaveBeenCalledWith(PluginInstallAction); expect(PluginState.installPlugin).toHaveBeenCalledTimes(1); expect(PluginState.installPlugin).toHaveBeenCalledWith(); @@ -199,7 +198,7 @@ describe('rootBaseStore', () => { version: 'asdfasdf', license: 'asdfasdf', }); - contextSrv.hasRole.mockReturnValueOnce(true); + rootBaseStore.isUserActionAllowed = jest.fn().mockReturnValueOnce(true); PluginState.installPlugin = jest.fn().mockRejectedValueOnce(installPluginError); PluginState.getHumanReadableErrorFromOnCallError = jest.fn().mockReturnValueOnce(humanReadableErrorMsg); @@ -210,8 +209,8 @@ describe('rootBaseStore', () => { expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledTimes(1); expect(PluginState.checkIfPluginIsConnected).toHaveBeenCalledWith(onCallApiUrl); - expect(contextSrv.hasRole).toHaveBeenCalledTimes(1); - expect(contextSrv.hasRole).toHaveBeenCalledWith(OrgRole.Admin); + expect(rootBaseStore.isUserActionAllowed).toHaveBeenCalledTimes(1); + expect(rootBaseStore.isUserActionAllowed).toHaveBeenCalledWith(PluginInstallAction); expect(PluginState.installPlugin).toHaveBeenCalledTimes(1); expect(PluginState.installPlugin).toHaveBeenCalledWith(); diff --git a/grafana-plugin/src/state/userAction.ts b/grafana-plugin/src/state/userAction.ts deleted file mode 100644 index d106d740..00000000 --- a/grafana-plugin/src/state/userAction.ts +++ /dev/null @@ -1,23 +0,0 @@ -export enum UserAction { - UpdateIncidents = 'update_incidents', - UpdateAlertReceiveChannels = 'update_alert_receive_channels', - UpdateEscalationPolicies = 'update_escalation_policies', - UpdateNotificationPolicies = 'update_notification_policies', - UpdateGeneralLogChannelId = 'update_general_log_channel_id', - UpdateGlobalSettings = 'update_global_settings', - UpdateOwnSettings = 'update_own_settings', - UpdateOtherUsersSettings = 'update_other_users_settings', - ViewOtherUsers = 'view_other_users', - UpdateIntegrations = 'update_integrations', - UpdateSchedules = 'update_schedules', - UpdateCustomActions = 'update_custom_actions', - UpdateApiTokens = 'update_api_tokens', - UpdateMaintenances = 'update_maintenances', - CreateTeam = 'create_team', - UpdateTeams = 'update_teams', - SendDemoAlert = 'send_demo_alert', - UpdateCurler = 'update_curler', - - // for testing purposes - Impossible = 'impossible', -} diff --git a/grafana-plugin/src/types.ts b/grafana-plugin/src/types.ts index 33e14637..b418f69d 100644 --- a/grafana-plugin/src/types.ts +++ b/grafana-plugin/src/types.ts @@ -1,4 +1,4 @@ -import { AppRootProps as BaseAppRootProps, AppPluginMeta, PluginConfigPageProps } from '@grafana/data'; +import { AppRootProps as BaseAppRootProps, AppPluginMeta, CurrentUserDTO, PluginConfigPageProps } from '@grafana/data'; export type OnCallPluginMetaJSONData = { stackId: number; @@ -21,7 +21,10 @@ export type OnCallPluginConfigPageProps = PluginConfigPageProps ({ + contextSrv: { + user: { + orgRole: null, + }, + hasAccess: (_action, _fallback): boolean => null, + }, +})); + +jest.mock('@grafana/runtime', () => ({ + config: { + featureToggles: { + accessControlOnCall: true, + }, + }, +})); + +describe('userHasMinimumRequiredRole', () => { + test.each([ + [OrgRole.Admin, OrgRole.Viewer, false], + [OrgRole.Admin, OrgRole.Editor, false], + [OrgRole.Admin, OrgRole.Admin, true], + [OrgRole.Editor, OrgRole.Viewer, false], + [OrgRole.Editor, OrgRole.Editor, true], + [OrgRole.Editor, OrgRole.Admin, true], + [OrgRole.Viewer, OrgRole.Viewer, true], + [OrgRole.Viewer, OrgRole.Editor, true], + [OrgRole.Viewer, OrgRole.Admin, true], + ])('Required role: %s Current role: %s', (requiredRole, mockCurrentRole, expected) => { + contextSrv.user.orgRole = mockCurrentRole; + expect(auth.userHasMinimumRequiredRole(requiredRole)).toBe(expected); + }); +}); + +describe('isUserActionAllowed', () => { + test('if RBAC is supported by the frontend, it uses the RBAC permission', () => { + // mocks + const permission = 'potato'; + contextSrv.user.permissions = { + [permission]: true, + }; + config.featureToggles.accessControlOnCall = true; + + // test + assertions + expect(auth.isUserActionAllowed({ permission, fallbackMinimumRoleRequired: OrgRole.Viewer })).toEqual(true); + }); + + test('if RBAC is not supported by the frontend, it uses the fallback role', () => { + // mocks + const permission = 'potato'; + contextSrv.user.orgRole = OrgRole.Editor; + config.featureToggles.accessControlOnCall = false; + + // test + assertions + expect(auth.isUserActionAllowed({ permission, fallbackMinimumRoleRequired: OrgRole.Viewer })).toEqual(true); + expect(auth.isUserActionAllowed({ permission, fallbackMinimumRoleRequired: OrgRole.Admin })).toEqual(false); + }); +}); + +describe('generatePermissionString', () => { + test('it properly builds permission strings with prefixes', () => { + expect(auth.generatePermissionString(auth.Resource.API_KEYS, auth.Action.READ, true)).toEqual( + 'grafana-oncall-app.api-keys:read' + ); + }); + + test('it properly builds permission strings without prefixes', () => { + expect(auth.generatePermissionString(auth.Resource.TEAMS, auth.Action.READ, false)).toEqual('teams:read'); + }); +}); diff --git a/grafana-plugin/src/utils/authorization/index.ts b/grafana-plugin/src/utils/authorization/index.ts new file mode 100644 index 00000000..4aa51b9b --- /dev/null +++ b/grafana-plugin/src/utils/authorization/index.ts @@ -0,0 +1,156 @@ +import { OrgRole } from '@grafana/data'; +import { config } from '@grafana/runtime'; +import { contextSrv } from 'grafana/app/core/core'; + +const ONCALL_PERMISSION_PREFIX = 'grafana-oncall-app'; + +export type UserAction = { + permission: string; + fallbackMinimumRoleRequired: OrgRole; +}; + +export enum Resource { + ALERT_GROUPS = 'alert-groups', + INTEGRATIONS = 'integrations', + ESCALATION_CHAINS = 'escalation-chains', + SCHEDULES = 'schedules', + CHATOPS = 'chatops', + OUTGOING_WEBHOOKS = 'outgoing-webhooks', + MAINTENANCE = 'maintenance', + API_KEYS = 'api-keys', + NOTIFICATIONS = 'notifications', + + NOTIFICATION_SETTINGS = 'notification-settings', + USER_SETTINGS = 'user-settings', + OTHER_SETTINGS = 'other-settings', + + TEAMS = 'teams', + PLUGINS = 'plugins', +} + +export enum Action { + READ = 'read', + WRITE = 'write', + ADMIN = 'admin', + TEST = 'test', + EXPORT = 'export', + UPDATE_SETTINGS = 'update-settings', + INSTALL = 'install', +} + +type Actions = + | 'AlertGroupsRead' + | 'AlertGroupsWrite' + | 'IntegrationsRead' + | 'IntegrationsWrite' + | 'IntegrationsTest' + | 'EscalationChainsRead' + | 'EscalationChainsWrite' + | 'SchedulesRead' + | 'SchedulesWrite' + | 'SchedulesExport' + | 'ChatOpsRead' + | 'ChatOpsWrite' + | 'ChatOpsUpdateSettings' + | 'OutgoingWebhooksRead' + | 'OutgoingWebhooksWrite' + | 'MaintenanceRead' + | 'MaintenanceWrite' + | 'APIKeysRead' + | 'APIKeysWrite' + | 'NotificationsRead' + | 'NotificationSettingsRead' + | 'NotificationSettingsWrite' + | 'UserSettingsRead' + | 'UserSettingsWrite' + | 'UserSettingsAdmin' + | 'OtherSettingsRead' + | 'OtherSettingsWrite' + | 'TeamsWrite' + | 'PluginsInstall'; + +const roleMapping: Record = { + [OrgRole.Admin]: 0, + [OrgRole.Editor]: 1, + [OrgRole.Viewer]: 2, +}; + +/** + * The logic here is: + * - an Admin should be able to do everything (including whatever an Editor and Viewer can do) + * - an Editor should be able to do things Editors and Viewers can do + * - a Viewer is only allowed to do things Viewers can do + */ +export const userHasMinimumRequiredRole = (minimumRoleRequired: OrgRole): boolean => + roleMapping[contextSrv.user.orgRole] <= roleMapping[minimumRoleRequired]; + +/** + * See here for more info on the hasAccess method + * https://github.com/grafana/grafana/blob/main/public/app/core/services/context_srv.ts#L165-L170 + * + * As a fallback (second argument), for cases where RBAC is not enabled for a grafana instance, rely on basic roles + */ +export const isUserActionAllowed = ({ permission, fallbackMinimumRoleRequired }: UserAction): boolean => { + if (config.featureToggles.accessControlOnCall) { + return !!contextSrv.user.permissions?.[permission]; + } + return userHasMinimumRequiredRole(fallbackMinimumRoleRequired); +}; + +export const generatePermissionString = (resource: Resource, action: Action, includePrefix: boolean): string => + `${includePrefix ? `${ONCALL_PERMISSION_PREFIX}.` : ''}${resource}:${action}`; + +const constructAction = ( + resource: Resource, + action: Action, + fallbackMinimumRoleRequired: OrgRole, + includePrefix = true +): UserAction => ({ + permission: generatePermissionString(resource, action, includePrefix), + fallbackMinimumRoleRequired, +}); + +export const UserActions: { [action in Actions]: UserAction } = { + AlertGroupsRead: constructAction(Resource.ALERT_GROUPS, Action.READ, OrgRole.Viewer), + AlertGroupsWrite: constructAction(Resource.ALERT_GROUPS, Action.WRITE, OrgRole.Editor), + + IntegrationsRead: constructAction(Resource.INTEGRATIONS, Action.READ, OrgRole.Viewer), + IntegrationsWrite: constructAction(Resource.INTEGRATIONS, Action.WRITE, OrgRole.Admin), + IntegrationsTest: constructAction(Resource.INTEGRATIONS, Action.TEST, OrgRole.Editor), + + EscalationChainsRead: constructAction(Resource.ESCALATION_CHAINS, Action.READ, OrgRole.Viewer), + EscalationChainsWrite: constructAction(Resource.ESCALATION_CHAINS, Action.WRITE, OrgRole.Admin), + + SchedulesRead: constructAction(Resource.SCHEDULES, Action.READ, OrgRole.Viewer), + SchedulesWrite: constructAction(Resource.SCHEDULES, Action.WRITE, OrgRole.Admin), + SchedulesExport: constructAction(Resource.SCHEDULES, Action.WRITE, OrgRole.Editor), + + ChatOpsRead: constructAction(Resource.CHATOPS, Action.READ, OrgRole.Viewer), + ChatOpsWrite: constructAction(Resource.CHATOPS, Action.WRITE, OrgRole.Editor), + ChatOpsUpdateSettings: constructAction(Resource.CHATOPS, Action.UPDATE_SETTINGS, OrgRole.Admin), + + OutgoingWebhooksRead: constructAction(Resource.OUTGOING_WEBHOOKS, Action.READ, OrgRole.Viewer), + OutgoingWebhooksWrite: constructAction(Resource.OUTGOING_WEBHOOKS, Action.WRITE, OrgRole.Admin), + + MaintenanceRead: constructAction(Resource.MAINTENANCE, Action.READ, OrgRole.Viewer), + MaintenanceWrite: constructAction(Resource.MAINTENANCE, Action.WRITE, OrgRole.Editor), + + APIKeysRead: constructAction(Resource.API_KEYS, Action.READ, OrgRole.Viewer), + APIKeysWrite: constructAction(Resource.API_KEYS, Action.WRITE, OrgRole.Editor), + + NotificationsRead: constructAction(Resource.NOTIFICATIONS, Action.READ, OrgRole.Editor), + + NotificationSettingsRead: constructAction(Resource.NOTIFICATION_SETTINGS, Action.READ, OrgRole.Viewer), + NotificationSettingsWrite: constructAction(Resource.NOTIFICATION_SETTINGS, Action.WRITE, OrgRole.Editor), + + UserSettingsRead: constructAction(Resource.USER_SETTINGS, Action.READ, OrgRole.Viewer), + UserSettingsWrite: constructAction(Resource.USER_SETTINGS, Action.WRITE, OrgRole.Editor), + UserSettingsAdmin: constructAction(Resource.USER_SETTINGS, Action.ADMIN, OrgRole.Admin), + + OtherSettingsRead: constructAction(Resource.OTHER_SETTINGS, Action.READ, OrgRole.Viewer), + OtherSettingsWrite: constructAction(Resource.OTHER_SETTINGS, Action.WRITE, OrgRole.Admin), + + // These are not oncall specific + TeamsWrite: constructAction(Resource.TEAMS, Action.WRITE, OrgRole.Admin, false), + PluginsInstall: constructAction(Resource.PLUGINS, Action.INSTALL, OrgRole.Admin, false), +}; diff --git a/grafana-plugin/yarn.lock b/grafana-plugin/yarn.lock index 1d3a045f..b09206b0 100644 --- a/grafana-plugin/yarn.lock +++ b/grafana-plugin/yarn.lock @@ -1141,7 +1141,7 @@ core-js-pure "^3.25.1" regenerator-runtime "^0.13.10" -"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.15.4", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": +"@babel/runtime@^7.0.0", "@babel/runtime@^7.1.2", "@babel/runtime@^7.10.1", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.12.0", "@babel/runtime@^7.12.13", "@babel/runtime@^7.12.5", "@babel/runtime@^7.13.10", "@babel/runtime@^7.14.5", "@babel/runtime@^7.15.4", "@babel/runtime@^7.17.2", "@babel/runtime@^7.18.3", "@babel/runtime@^7.2.0", "@babel/runtime@^7.20.0", "@babel/runtime@^7.5.5", "@babel/runtime@^7.6.2", "@babel/runtime@^7.8.4", "@babel/runtime@^7.8.7", "@babel/runtime@^7.9.2": version "7.20.1" resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.20.1.tgz#1148bb33ab252b165a06698fde7576092a78b4a9" integrity sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg== @@ -1192,6 +1192,11 @@ resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.0.tgz#fe364f025ba74f6de6c837a84ef44bdb1d61e68f" integrity sha512-mgmE7XBYY/21erpzhexk4Cj1cyTQ9LzvnTxtzM17BJ7ERMNE6W72mQRo0I1Ud8eFJ+RVVIcBNhLFZ3GX4XFz5w== +"@braintree/sanitize-url@6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@braintree/sanitize-url/-/sanitize-url-6.0.1.tgz#45ff061b9ded1c6e4474b33b336ebb1b986b825a" + integrity sha512-zr9Qs9KFQiEvMWdZesjcmRJlUck5NR+eKGS1uyKk+oYTWwlYrsoPEi6VmG6/TzBD1hKCGEimrhTgGS6hvn/xIQ== + "@csstools/postcss-color-function@^1.0.3": version "1.1.1" resolved "https://registry.yarnpkg.com/@csstools/postcss-color-function/-/postcss-color-function-1.1.1.tgz#2bd36ab34f82d0497cfacdc9b18d34b5e6f64b6b" @@ -1286,6 +1291,17 @@ "@emotion/weak-memoize" "^0.3.0" stylis "4.1.3" +"@emotion/css@11.10.5": + version "11.10.5" + resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.10.5.tgz#ca01bb83ce60517bc3a5c01d27ccf552fed84d9d" + integrity sha512-maJy0wG82hWsiwfJpc3WrYsyVwUbdu+sdIseKUB+/OLjB8zgc3tqkT6eO0Yt0AhIkJwGGnmMY/xmQwEAgQ4JHA== + dependencies: + "@emotion/babel-plugin" "^11.10.5" + "@emotion/cache" "^11.10.5" + "@emotion/serialize" "^1.1.1" + "@emotion/sheet" "^1.2.1" + "@emotion/utils" "^1.2.0" + "@emotion/css@11.9.0": version "11.9.0" resolved "https://registry.yarnpkg.com/@emotion/css/-/css-11.9.0.tgz#d5aeaca5ed19fc61cbdc9e032ad0b32fa6e366be" @@ -1307,20 +1323,7 @@ resolved "https://registry.yarnpkg.com/@emotion/memoize/-/memoize-0.8.0.tgz#f580f9beb67176fa57aae70b08ed510e1b18980f" integrity sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA== -"@emotion/react@11.9.3": - version "11.9.3" - resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.3.tgz#f4f4f34444f6654a2e550f5dab4f2d360c101df9" - integrity sha512-g9Q1GcTOlzOEjqwuLF/Zd9LC+4FljjPjDfxSM7KmEakm+hsHXk+bYZ2q+/hTJzr0OUNkujo72pXLQvXj6H+GJQ== - dependencies: - "@babel/runtime" "^7.13.10" - "@emotion/babel-plugin" "^11.7.1" - "@emotion/cache" "^11.9.3" - "@emotion/serialize" "^1.0.4" - "@emotion/utils" "^1.1.0" - "@emotion/weak-memoize" "^0.2.5" - hoist-non-react-statics "^3.3.1" - -"@emotion/react@^11.8.1": +"@emotion/react@11.10.5", "@emotion/react@^11.8.1": version "11.10.5" resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.10.5.tgz#95fff612a5de1efa9c0d535384d3cfa115fe175d" integrity sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A== @@ -1334,6 +1337,19 @@ "@emotion/weak-memoize" "^0.3.0" hoist-non-react-statics "^3.3.1" +"@emotion/react@11.9.3": + version "11.9.3" + resolved "https://registry.yarnpkg.com/@emotion/react/-/react-11.9.3.tgz#f4f4f34444f6654a2e550f5dab4f2d360c101df9" + integrity sha512-g9Q1GcTOlzOEjqwuLF/Zd9LC+4FljjPjDfxSM7KmEakm+hsHXk+bYZ2q+/hTJzr0OUNkujo72pXLQvXj6H+GJQ== + dependencies: + "@babel/runtime" "^7.13.10" + "@emotion/babel-plugin" "^11.7.1" + "@emotion/cache" "^11.9.3" + "@emotion/serialize" "^1.0.4" + "@emotion/utils" "^1.1.0" + "@emotion/weak-memoize" "^0.2.5" + hoist-non-react-statics "^3.3.1" + "@emotion/serialize@^1.0.3", "@emotion/serialize@^1.0.4", "@emotion/serialize@^1.1.1": version "1.1.1" resolved "https://registry.yarnpkg.com/@emotion/serialize/-/serialize-1.1.1.tgz#0595701b1902feded8a96d293b26be3f5c1a5cf0" @@ -1408,6 +1424,18 @@ minimatch "^3.1.2" strip-json-comments "^3.1.1" +"@floating-ui/core@^1.0.2": + version "1.0.2" + resolved "https://registry.yarnpkg.com/@floating-ui/core/-/core-1.0.2.tgz#d06a66d3ad8214186eda2432ac8b8d81868a571f" + integrity sha512-Skfy0YS3NJ5nV9us0uuPN0HDk1Q4edljaOhRBJGDWs9EBa7ZVMYBHRFlhLvvmwEoaIM9BlH6QJFn9/uZg0bACg== + +"@floating-ui/dom@^1.0.1": + version "1.0.6" + resolved "https://registry.yarnpkg.com/@floating-ui/dom/-/dom-1.0.6.tgz#e42393ec381a4fe96673fbcee137a95e86c93ebc" + integrity sha512-kt/tg1oip9OAH1xjCTcx1OpcUpu9rjDw3GKJ/rEhUqhO7QyJWfrHU0DpLTNsH67+JyFL5Kv9X1utsXwKFVtyEQ== + dependencies: + "@floating-ui/core" "^1.0.2" + "@formatjs/ecma402-abstract@1.13.0": version "1.13.0" resolved "https://registry.yarnpkg.com/@formatjs/ecma402-abstract/-/ecma402-abstract-1.13.0.tgz#df6db3cbee0182bbd2fd6217103781c802aee819" @@ -1447,25 +1475,6 @@ dependencies: tslib "2.4.0" -"@grafana/agent-core@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@grafana/agent-core/-/agent-core-0.4.0.tgz#0252a888ab16dea82d97c571ca765383a1d6b319" - integrity sha512-yFbTRWVZKwUTdZ3A1AAzinWhkY0UkmduOEmlr0EYT5DJUOS/vEnzev5oB3Mh00bUUvN+AUvlMx4Nvnju1ahmJg== - dependencies: - "@opentelemetry/api" "^1.1.0" - "@opentelemetry/api-metrics" "^0.29.1" - "@opentelemetry/otlp-transformer" "^0.29.1" - uuid "^8.3.2" - -"@grafana/agent-web@^0.4.0": - version "0.4.0" - resolved "https://registry.yarnpkg.com/@grafana/agent-web/-/agent-web-0.4.0.tgz#03c4da34e29b4ca9f40c3574b2e85a7127a070fd" - integrity sha512-rVjLmQ/+Q8j3klDVlgt2pb3fIeWMvn3UAQLSBTC0L53Z/snNGvKQBe8b14ndjO6+cxWXFMc2kMJpw6NxpSYL5Q== - dependencies: - "@grafana/agent-core" "^0.4.0" - ua-parser-js "^1.0.2" - web-vitals "^2.1.4" - "@grafana/data@9.2.4", "@grafana/data@^9.2.4": version "9.2.4" resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.2.4.tgz#38067f207006c07754c3ed5a8835dc1909df7e2d" @@ -1518,6 +1527,32 @@ uplot "1.6.22" xss "1.0.13" +"@grafana/data@9.3.0-beta1": + version "9.3.0-beta1" + resolved "https://registry.yarnpkg.com/@grafana/data/-/data-9.3.0-beta1.tgz#0c1d8da18b8f9a5c7e77312a1b36daf394bfc596" + integrity sha512-36ozpmsPjSW+yA/QFIX5j63aRV2sxQwros73YStV7x3aGM2P/vaxKm/Zux84uJxihwA9Ao8bf6RXTP3GBKcvSg== + dependencies: + "@braintree/sanitize-url" "6.0.1" + "@grafana/schema" "9.3.0-beta1" + "@types/d3-interpolate" "^1.4.0" + d3-interpolate "1.4.0" + date-fns "2.29.3" + eventemitter3 "4.0.7" + fast_array_intersect "1.1.0" + history "4.10.1" + lodash "4.17.21" + marked "4.2.0" + moment "2.29.4" + moment-timezone "0.5.38" + ol "7.1.0" + papaparse "5.3.2" + regenerator-runtime "0.13.10" + rxjs "7.5.7" + tinycolor2 "1.4.2" + tslib "2.4.1" + uplot "1.6.22" + xss "1.0.14" + "@grafana/e2e-selectors@9.2.4": version "9.2.4" resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-9.2.4.tgz#748539cc0313ee1c23055a100313235ef2fca64b" @@ -1536,6 +1571,15 @@ tslib "2.4.0" typescript "4.8.2" +"@grafana/e2e-selectors@9.3.0-beta1": + version "9.3.0-beta1" + resolved "https://registry.yarnpkg.com/@grafana/e2e-selectors/-/e2e-selectors-9.3.0-beta1.tgz#49ca6a4957763a8fee8560a5cd7f546a3f4853d3" + integrity sha512-0uG9eltmh/FPLk32+pfpw4Vz8WQNuVOy/E4pnIh2Wv9BlqHWxrABX7o6YlXzlCQMv8mxhCcey/OxJHC4AZxPzA== + dependencies: + "@grafana/tsconfig" "^1.2.0-rc1" + tslib "2.4.1" + typescript "4.8.4" + "@grafana/eslint-config@5.0.0", "@grafana/eslint-config@^5.0.0": version "5.0.0" resolved "https://registry.yarnpkg.com/@grafana/eslint-config/-/eslint-config-5.0.0.tgz#e08a89d378772340bc6cd1872ec4d15666269aba" @@ -1550,21 +1594,40 @@ eslint-plugin-react-hooks "4.3.0" typescript "4.6.4" -"@grafana/runtime@^9.2.4": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@grafana/runtime/-/runtime-9.2.4.tgz#f3d1a4e2ee51fed76ac31a37422e5978e3ff57a9" - integrity sha512-k6YLPBB8waRe5SqzwmhxwzEYduY3GvBsZTbERIwf+8/ep7kfRnqAhbbFFUujQU4+pgBd3N6HtXBg9KKB6eLqaA== +"@grafana/faro-core@^1.0.0-beta2": + version "1.0.0-beta2" + resolved "https://registry.yarnpkg.com/@grafana/faro-core/-/faro-core-1.0.0-beta2.tgz#97636677c1d687b0b238642a3978334652f263a5" + integrity sha512-htw6qrl4EsjxUrIugd+85H8voIxm+Vs8uOl4gGhsscb1/nUJoqTZmegUTXR+sYGyWZdHztoGV+rm5yerWrKCbQ== dependencies: - "@grafana/agent-web" "^0.4.0" - "@grafana/data" "9.2.4" - "@grafana/e2e-selectors" "9.2.4" - "@grafana/ui" "9.2.4" + "@opentelemetry/api" "^1.1.0" + "@opentelemetry/api-metrics" "^0.33.0" + "@opentelemetry/otlp-transformer" "^0.33.0" + fast-deep-equal "^3.1.3" + +"@grafana/faro-web-sdk@1.0.0-beta2": + version "1.0.0-beta2" + resolved "https://registry.yarnpkg.com/@grafana/faro-web-sdk/-/faro-web-sdk-1.0.0-beta2.tgz#d096a350d6366a108428a205753c797802eb480d" + integrity sha512-Z/ZbMpBG4/+ZHuPntVTANvStBP1pkDT3+oqKDYW3O4iP4wBhIUyXk7Pmr9LJZIjcStBizEFMH/N/F/gyD5DHjQ== + dependencies: + "@grafana/faro-core" "^1.0.0-beta2" + ua-parser-js "^1.0.32" + web-vitals "^3.0.4" + +"@grafana/runtime@9.3.0-beta1": + version "9.3.0-beta1" + resolved "https://registry.yarnpkg.com/@grafana/runtime/-/runtime-9.3.0-beta1.tgz#4bcd5d8c24c1e810b254f113598cbb1cb759ee16" + integrity sha512-Fd87OXQbf9IqGeOitwF8KBuyvw9Yv9VDmC30UKCvpQVtKTYoHngEYXMD1ZLUgmb4G18PYDsBqYfth4InfPAlSQ== + dependencies: + "@grafana/data" "9.3.0-beta1" + "@grafana/e2e-selectors" "9.3.0-beta1" + "@grafana/faro-web-sdk" "1.0.0-beta2" + "@grafana/ui" "9.3.0-beta1" "@sentry/browser" "6.19.7" history "4.10.1" lodash "4.17.21" - rxjs "7.5.6" + rxjs "7.5.7" systemjs "0.20.19" - tslib "2.4.0" + tslib "2.4.1" "@grafana/schema@9.2.4": version "9.2.4" @@ -1580,6 +1643,13 @@ dependencies: tslib "2.4.0" +"@grafana/schema@9.3.0-beta1": + version "9.3.0-beta1" + resolved "https://registry.yarnpkg.com/@grafana/schema/-/schema-9.3.0-beta1.tgz#0554d8a6c9de51e3f55f00da614d8c8f091980ab" + integrity sha512-/12NkJXGfbo3bWPUMsSGJXZiLOil3TX2xoiL86ssnziSdzN9b7uJ6xhdEUfZ3sdm4pXuiBq4tlJ9FUP6n6he8Q== + dependencies: + tslib "2.4.1" + "@grafana/toolkit@^9.2.4": version "9.2.6" resolved "https://registry.yarnpkg.com/@grafana/toolkit/-/toolkit-9.2.6.tgz#55d424321a65a027f3365c6e0df649bcc1d2c9d6" @@ -1675,16 +1745,16 @@ resolved "https://registry.yarnpkg.com/@grafana/tsconfig/-/tsconfig-1.2.0-rc1.tgz#10973c978ec95b0ea637511254b5f478bce04de7" integrity sha512-+SgQeBQ1pT6D/E3/dEdADqTrlgdIGuexUZ8EU+8KxQFKUeFeU7/3z/ayI2q/wpJ/Kr6WxBBNlrST6aOKia19Ag== -"@grafana/ui@9.2.4", "@grafana/ui@^9.2.4": - version "9.2.4" - resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-9.2.4.tgz#885b0f10bd700aa0dc094f2fcb554477fc47e410" - integrity sha512-V9sNQwcAkMAmWjM/DLMw9X+J0nqBmrwNV1uJ1kyS+3cRRwCNyJsZUz3NuOnzCbvCEl3bopLyY/WBSHONbLEoig== +"@grafana/ui@9.2.6": + version "9.2.6" + resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-9.2.6.tgz#1974edb48b5873c257278886b65ccb1d7e45a33a" + integrity sha512-NM+tNbpks218QHQo9hywr/00fMOjQt0shf3Sq2ywr8e172PXuPxh/AuTACKuVFgW2FY1z5StFUc2B40Dgvn0JQ== dependencies: "@emotion/css" "11.9.0" "@emotion/react" "11.9.3" - "@grafana/data" "9.2.4" - "@grafana/e2e-selectors" "9.2.4" - "@grafana/schema" "9.2.4" + "@grafana/data" "9.2.6" + "@grafana/e2e-selectors" "9.2.6" + "@grafana/schema" "9.2.6" "@monaco-editor/react" "4.4.5" "@popperjs/core" "2.11.5" "@react-aria/button" "3.6.1" @@ -1741,16 +1811,86 @@ uplot "1.6.22" uuid "8.3.2" -"@grafana/ui@9.2.6": - version "9.2.6" - resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-9.2.6.tgz#1974edb48b5873c257278886b65ccb1d7e45a33a" - integrity sha512-NM+tNbpks218QHQo9hywr/00fMOjQt0shf3Sq2ywr8e172PXuPxh/AuTACKuVFgW2FY1z5StFUc2B40Dgvn0JQ== +"@grafana/ui@9.3.0-beta1": + version "9.3.0-beta1" + resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-9.3.0-beta1.tgz#941db8fab3f570e1639257311c514cd6708fb297" + integrity sha512-40bQV7gHqONb18G7MmhueuvJcX+DGJYeKTiexZ+wLEW46/74iBIhRI5RyDQsqFntnZpOeVZuQQODWPlZZ7lYpw== + dependencies: + "@emotion/css" "11.10.5" + "@emotion/react" "11.10.5" + "@grafana/data" "9.3.0-beta1" + "@grafana/e2e-selectors" "9.3.0-beta1" + "@grafana/schema" "9.3.0-beta1" + "@leeoniya/ufuzzy" "0.8.0" + "@monaco-editor/react" "4.4.6" + "@popperjs/core" "2.11.6" + "@react-aria/button" "3.6.1" + "@react-aria/dialog" "3.3.1" + "@react-aria/focus" "3.8.0" + "@react-aria/menu" "3.6.1" + "@react-aria/overlays" "3.10.1" + "@react-aria/utils" "3.13.1" + "@react-stately/menu" "3.4.1" + "@sentry/browser" "6.19.7" + ansicolor "1.1.100" + calculate-size "1.1.1" + classnames "2.3.2" + core-js "3.26.0" + d3 "5.15.0" + date-fns "2.29.3" + hoist-non-react-statics "3.3.2" + i18next "^22.0.0" + immutable "4.1.0" + is-hotkey "0.2.0" + jquery "3.6.1" + lodash "4.17.21" + memoize-one "6.0.0" + moment "2.29.4" + monaco-editor "0.34.0" + ol "7.1.0" + prismjs "1.29.0" + rc-cascader "3.7.0" + rc-drawer "4.4.3" + rc-slider "10.0.1" + rc-time-picker "^3.7.3" + rc-tooltip "5.2.2" + react-beautiful-dnd "13.1.1" + react-calendar "3.9.0" + react-colorful "5.6.1" + react-custom-scrollbars-2 "4.5.0" + react-dropzone "14.2.3" + react-highlight-words "0.18.0" + react-hook-form "7.5.3" + react-i18next "^12.0.0" + react-inlinesvg "3.0.1" + react-popper "2.3.0" + react-popper-tooltip "^4.3.1" + react-router-dom "^5.2.0" + react-select "5.6.0" + react-select-event "^5.1.0" + react-table "7.8.0" + react-transition-group "4.4.5" + react-use "17.4.0" + react-window "1.8.8" + rxjs "7.5.7" + slate "0.47.9" + slate-plain-serializer "0.7.13" + slate-react "0.22.10" + tinycolor2 "1.4.2" + tslib "2.4.1" + uplot "1.6.22" + uuid "9.0.0" + +"@grafana/ui@^9.2.4": + version "9.2.4" + resolved "https://registry.yarnpkg.com/@grafana/ui/-/ui-9.2.4.tgz#885b0f10bd700aa0dc094f2fcb554477fc47e410" + integrity sha512-V9sNQwcAkMAmWjM/DLMw9X+J0nqBmrwNV1uJ1kyS+3cRRwCNyJsZUz3NuOnzCbvCEl3bopLyY/WBSHONbLEoig== dependencies: "@emotion/css" "11.9.0" "@emotion/react" "11.9.3" - "@grafana/data" "9.2.6" - "@grafana/e2e-selectors" "9.2.6" - "@grafana/schema" "9.2.6" + "@grafana/data" "9.2.4" + "@grafana/e2e-selectors" "9.2.4" + "@grafana/schema" "9.2.4" "@monaco-editor/react" "4.4.5" "@popperjs/core" "2.11.5" "@react-aria/button" "3.6.1" @@ -2135,6 +2275,11 @@ resolved "https://registry.yarnpkg.com/@kwsites/promise-deferred/-/promise-deferred-1.1.1.tgz#8ace5259254426ccef57f3175bc64ed7095ed919" integrity sha512-GaHYm+c0O9MjZRu0ongGBRbinu8gVAMd2UZjji6jVmqKtZluZnptXGWhz1E8j8D2HJ3f/yMxKAUC0b+57wncIw== +"@leeoniya/ufuzzy@0.8.0": + version "0.8.0" + resolved "https://registry.yarnpkg.com/@leeoniya/ufuzzy/-/ufuzzy-0.8.0.tgz#2ccfc29453e168ce5866bf6dee89771db404a7f7" + integrity sha512-EOc0fEsIqe6CDZxC14efhybnPcXyJi7VaZby40mWASZD0CI78ONoF+4+LGlcT58jsAIwEims5ARbRqo+BVHEAQ== + "@mapbox/jsonlint-lines-primitives@~2.0.2": version "2.0.2" resolved "https://registry.yarnpkg.com/@mapbox/jsonlint-lines-primitives/-/jsonlint-lines-primitives-2.0.2.tgz#ce56e539f83552b58d10d672ea4d6fc9adc7b234" @@ -2179,6 +2324,14 @@ "@monaco-editor/loader" "^1.3.2" prop-types "^15.7.2" +"@monaco-editor/react@4.4.6": + version "4.4.6" + resolved "https://registry.yarnpkg.com/@monaco-editor/react/-/react-4.4.6.tgz#8ae500b0edf85276d860ed702e7056c316548218" + integrity sha512-Gr3uz3LYf33wlFE3eRnta4RxP5FSNxiIV9ENn2D2/rN8KgGAD8ecvcITRtsbbyuOuNkwbuHYxfeaz2Vr+CtyFA== + dependencies: + "@monaco-editor/loader" "^1.3.2" + prop-types "^15.7.2" + "@nodelib/fs.scandir@2.1.5": version "2.1.5" resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" @@ -2200,10 +2353,10 @@ "@nodelib/fs.scandir" "2.1.5" fastq "^1.6.0" -"@opentelemetry/api-metrics@0.29.2", "@opentelemetry/api-metrics@^0.29.1": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/api-metrics/-/api-metrics-0.29.2.tgz#daa823e0965754222b49a6ae6133df8b39ff8fd2" - integrity sha512-yRdF5beqKuEdsPNoO7ijWCQ9HcyN0Tlgicf8RS6gzGOI54d6Hj7yKquJ6+X9XV+CSRbRWJYb+lOsXyso7uyX2g== +"@opentelemetry/api-metrics@0.33.0", "@opentelemetry/api-metrics@^0.33.0": + version "0.33.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/api-metrics/-/api-metrics-0.33.0.tgz#753d355289b7811ad254d6e5b0193bd1b9f23ab0" + integrity sha512-78evfPRRRnJA6uZ3xuBuS3VZlXTO/LRs+Ff1iv3O/7DgibCtq9k27T6Zlj8yRdJDFmcjcbQrvC0/CpDpWHaZYA== dependencies: "@opentelemetry/api" "^1.0.0" @@ -2212,55 +2365,55 @@ resolved "https://registry.yarnpkg.com/@opentelemetry/api/-/api-1.3.0.tgz#27c6f776ac3c1c616651e506a89f438a0ed6a055" integrity sha512-YveTnGNsFFixTKJz09Oi4zYkiLT5af3WpZDu4aIUM7xX+2bHAkOJayFTVQd6zB8kkWPpbua4Ha6Ql00grdLlJQ== -"@opentelemetry/core@1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.3.1.tgz#6eef5c5efca9a4cd7daa0cd4c7ff28ca2317c8d7" - integrity sha512-k7lOC86N7WIyUZsUuSKZfFIrUtINtlauMGQsC1r7jNmcr0vVJGqK1ROBvt7WWMxLbpMnt1q2pXJO8tKu0b9auA== +"@opentelemetry/core@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/core/-/core-1.7.0.tgz#83bdd1b7a4ceafcdffd6590420657caec5f7b34c" + integrity sha512-AVqAi5uc8DrKJBimCTFUT4iFI+5eXpo4sYmGbQ0CypG0piOTHE2g9c5aSoTGYXu3CzOmJZf7pT6Xh+nwm5d6yQ== dependencies: - "@opentelemetry/semantic-conventions" "1.3.1" + "@opentelemetry/semantic-conventions" "1.7.0" -"@opentelemetry/otlp-transformer@^0.29.1": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.29.2.tgz#61897d3d747182ab7e315a88a9a710a759c13390" - integrity sha512-Y6dJj+rhRGynxhLlgEJkdkXuLHdFG8igcSBv6oy3m3GHSSvZkyNV34dVjtZJ586mUXsbFuAf6uqjzteobewO1g== +"@opentelemetry/otlp-transformer@^0.33.0": + version "0.33.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/otlp-transformer/-/otlp-transformer-0.33.0.tgz#6fd3ddc944f017da08d445f142cad1779770e0e0" + integrity sha512-L4OpsUaki9/Fib17t44YkDvAz3RpMZTtl6hYBhcTqAnqY0wVBpQf0ra25GyHQTKj+oiA//ZxvOlmmM/dXCYxoQ== dependencies: - "@opentelemetry/api-metrics" "0.29.2" - "@opentelemetry/core" "1.3.1" - "@opentelemetry/resources" "1.3.1" - "@opentelemetry/sdk-metrics-base" "0.29.2" - "@opentelemetry/sdk-trace-base" "1.3.1" + "@opentelemetry/api-metrics" "0.33.0" + "@opentelemetry/core" "1.7.0" + "@opentelemetry/resources" "1.7.0" + "@opentelemetry/sdk-metrics" "0.33.0" + "@opentelemetry/sdk-trace-base" "1.7.0" -"@opentelemetry/resources@1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.3.1.tgz#9fd85ac4ffeefc35441404b384d5c1db8b243121" - integrity sha512-X8bl3X0YjlsHWy0Iv0KUETtZuRUznX4yr1iScKCtfy8AoRfZFc2xxWKMDJ0TrqYwSapgeg4YwpmRzUKmmnrbeA== +"@opentelemetry/resources@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/resources/-/resources-1.7.0.tgz#90ccd3a6a86b4dfba4e833e73944bd64958d78c5" + integrity sha512-u1M0yZotkjyKx8dj+46Sg5thwtOTBmtRieNXqdCRiWUp6SfFiIP0bI+1XK3LhuXqXkBXA1awJZaTqKduNMStRg== dependencies: - "@opentelemetry/core" "1.3.1" - "@opentelemetry/semantic-conventions" "1.3.1" + "@opentelemetry/core" "1.7.0" + "@opentelemetry/semantic-conventions" "1.7.0" -"@opentelemetry/sdk-metrics-base@0.29.2": - version "0.29.2" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics-base/-/sdk-metrics-base-0.29.2.tgz#bd515455f1d90e211458dcf957f0ae937772b155" - integrity sha512-7hhhZ/6YRRgAXOUTeCsbe6SIk3wZAdAHnEwGGp7aiVH5AOyioHyHInw4EHtowlD6dbLxUWURjh6k+Geht2zbxg== +"@opentelemetry/sdk-metrics@0.33.0": + version "0.33.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-metrics/-/sdk-metrics-0.33.0.tgz#c4e51decc6e3bb0e1e97c7b081955d357e46c2fe" + integrity sha512-ZXPixOlTd/FHLwpkmm5nTpJE7bZOPfmbSz8hBVFCEHkXE1aKEKaM38UFnZ+2xzOY1tDsDwyxEiiBiDX8y3039A== dependencies: - "@opentelemetry/api-metrics" "0.29.2" - "@opentelemetry/core" "1.3.1" - "@opentelemetry/resources" "1.3.1" + "@opentelemetry/api-metrics" "0.33.0" + "@opentelemetry/core" "1.7.0" + "@opentelemetry/resources" "1.7.0" lodash.merge "4.6.2" -"@opentelemetry/sdk-trace-base@1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.3.1.tgz#958083dbab928eefd17848959ac8810c787bec7f" - integrity sha512-Or95QZ+9QyvAiwqj+K68z8bDDuyWF50c37w17D10GV1dWzg4Ezcectsu/GB61QcBxm3Y4br0EN5F5TpIFfFliQ== +"@opentelemetry/sdk-trace-base@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/sdk-trace-base/-/sdk-trace-base-1.7.0.tgz#b498424e0c6340a9d80de63fd408c5c2130a60a5" + integrity sha512-Iz84C+FVOskmauh9FNnj4+VrA+hG5o+tkMzXuoesvSfunVSioXib0syVFeNXwOm4+M5GdWCuW632LVjqEXStIg== dependencies: - "@opentelemetry/core" "1.3.1" - "@opentelemetry/resources" "1.3.1" - "@opentelemetry/semantic-conventions" "1.3.1" + "@opentelemetry/core" "1.7.0" + "@opentelemetry/resources" "1.7.0" + "@opentelemetry/semantic-conventions" "1.7.0" -"@opentelemetry/semantic-conventions@1.3.1": - version "1.3.1" - resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.3.1.tgz#ba07b864a3c955f061aa30ea3ef7f4ae4449794a" - integrity sha512-wU5J8rUoo32oSef/rFpOT1HIjLjAv3qIDHkw1QIhODV3OpAVHi5oVzlouozg9obUmZKtbZ0qUe/m7FP0y0yBzA== +"@opentelemetry/semantic-conventions@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@opentelemetry/semantic-conventions/-/semantic-conventions-1.7.0.tgz#af80a1ef7cf110ea3a68242acd95648991bcd763" + integrity sha512-FGBx/Qd09lMaqQcogCHyYrFEpTx4cAjeS+48lMIR12z7LdH+zofGDVQSubN59nL6IpubfKqTeIDu9rNO28iHVA== "@petamoriken/float16@^3.4.7": version "3.6.6" @@ -2277,7 +2430,7 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.5.tgz#db5a11bf66bdab39569719555b0f76e138d7bd64" integrity sha512-9X2obfABZuDVLCgPK9aX0a/x4jaOEweTTWE2+9sr0Qqqevj2Uv5XorvusThmc9XGYpS9yI+fhh8RTafBtGposw== -"@popperjs/core@^2.11.5": +"@popperjs/core@2.11.6", "@popperjs/core@^2.11.5": version "2.11.6" resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.11.6.tgz#cee20bd55e68a1720bdab363ecf0c821ded4cd45" integrity sha512-50/17A98tWUfQ176raKiOGXuYpLyyVMkxxG6oylzL3BPOlA6ADGdK7EYunSa4I064xerltq9TGXs8HmOk5E+vw== @@ -4616,7 +4769,7 @@ classnames@2.3.1: resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.1.tgz#dfcfa3891e306ec1dad105d0e88f4417b8535e8e" integrity sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA== -classnames@2.x, classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1: +classnames@2.3.2, classnames@2.x, classnames@^2.2.1, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1: version "2.3.2" resolved "https://registry.yarnpkg.com/classnames/-/classnames-2.3.2.tgz#351d813bf0137fcc6a76a16b88208d2560a0d924" integrity sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw== @@ -4901,6 +5054,11 @@ core-js@3.25.1: resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.25.1.tgz#5818e09de0db8956e16bf10e2a7141e931b7c69c" integrity sha512-sr0FY4lnO1hkQ4gLDr24K0DGnweGO1QwSj5BpfQjpSJPdqWalja4cTps29Y/PJVG/P7FYlPDkH3hO+Tr0CvDgQ== +core-js@3.26.0: + version "3.26.0" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.26.0.tgz#a516db0ed0811be10eac5d94f3b8463d03faccfe" + integrity sha512-+DkDrhoR4Y0PxDz6rurahuB+I45OsEUv8E1maPTB6OuHRohMMcznBq9TMpdpDMm/hUPob/mJJS3PqgbHpMTQgw== + core-js@^2.4.0: version "2.6.12" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.6.12.tgz#d9333dfa7b065e347cc5682219d6f690859cc2ec" @@ -5425,6 +5583,11 @@ date-fns@2.29.1: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.1.tgz#9667c2615525e552b5135a3116b95b1961456e60" integrity sha512-dlLD5rKaKxpFdnjrs+5azHDFOPEu4ANy/LTh04A1DTzMM7qoajmKCBc8pkKRFT41CNzw+4gQh79X5C+Jq27HAw== +date-fns@2.29.3: + version "2.29.3" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.29.3.tgz#27402d2fc67eb442b511b70bbdf98e6411cd68a8" + integrity sha512-dDCnyH2WnnKusqvZZ6+jA1O51Ibt8ZMRNkDZdyAyK4YfbDwa/cEmuztzG5pk6hqlp9aSBPYcjOlktquahGwGeA== + dayjs@^1.11.5: version "1.11.6" resolved "https://registry.yarnpkg.com/dayjs/-/dayjs-1.11.6.tgz#2e79a226314ec3ec904e3ee1dd5a4f5e5b1c7afb" @@ -5754,6 +5917,11 @@ duplexer@^0.1.2: resolved "https://registry.yarnpkg.com/duplexer/-/duplexer-0.1.2.tgz#3abe43aef3835f8ae077d136ddce0f276b0400e6" integrity sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg== +earcut@^2.2.3: + version "2.2.4" + resolved "https://registry.yarnpkg.com/earcut/-/earcut-2.2.4.tgz#6d02fd4d68160c114825d06890a92ecaae60343a" + integrity sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ== + electron-to-chromium@^1.4.251: version "1.4.284" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592" @@ -7173,6 +7341,13 @@ html-minifier-terser@^6.0.2: relateurl "^0.2.7" terser "^5.10.0" +html-parse-stringify@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2" + integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg== + dependencies: + void-elements "3.1.0" + html-tags@^3.1.0: version "3.2.0" resolved "https://registry.yarnpkg.com/html-tags/-/html-tags-3.2.0.tgz#dbb3518d20b726524e4dd43de397eb0a95726961" @@ -7263,6 +7438,13 @@ hyphenate-style-name@^1.0.0, hyphenate-style-name@^1.0.2: resolved "https://registry.yarnpkg.com/hyphenate-style-name/-/hyphenate-style-name-1.0.4.tgz#691879af8e220aea5750e8827db4ef62a54e361d" integrity sha512-ygGZLjmXfPHj+ZWh6LwbC37l43MhfztxetbFCoYTM2VjkIUpeHgSNn7QIyVFj7YQ1Wl9Cbw5sholVJPzWvC2MQ== +i18next@^22.0.0: + version "22.0.6" + resolved "https://registry.yarnpkg.com/i18next/-/i18next-22.0.6.tgz#d7029912f8aa74ff295c0d9afd1b7dea45859b49" + integrity sha512-RlreNGoPIdDP4QG+qSA9PxZKGwlzmcozbI9ObI6+OyUa/Rp0EjZZA9ubyBjw887zVNZsC+7FI3sXX8oiTzAfig== + dependencies: + "@babel/runtime" "^7.17.2" + iconv-lite@0.4, iconv-lite@0.4.24, iconv-lite@^0.4.24: version "0.4.24" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b" @@ -8421,6 +8603,11 @@ jquery@3.6.0: resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.0.tgz#c72a09f15c1bdce142f49dbf1170bdf8adac2470" integrity sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw== +jquery@3.6.1: + version "3.6.1" + resolved "https://registry.yarnpkg.com/jquery/-/jquery-3.6.1.tgz#fab0408f8b45fc19f956205773b62b292c147a16" + integrity sha512-opJeO4nCucVnsjiXOE+/PcCgYw9Gwpvs/a6B1LL/lQhwWwpbVEVYDZ1FokFr8PRc7ghYlrFPuyHuiiDNTQxmcw== + js-cookie@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/js-cookie/-/js-cookie-2.2.1.tgz#69e106dc5d5806894562902aa5baec3744e9b2b8" @@ -8931,6 +9118,11 @@ marked@4.1.0: resolved "https://registry.yarnpkg.com/marked/-/marked-4.1.0.tgz#3fc6e7485f21c1ca5d6ec4a39de820e146954796" integrity sha512-+Z6KDjSPa6/723PQYyc1axYZpYYpDnECDaU6hkaf5gqBieBkMKYReL5hteF2QizhlMbgbo8umXl/clZ67+GlsA== +marked@4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/marked/-/marked-4.2.0.tgz#f1683b077626a6c53e28926b798a18184aa13a91" + integrity sha512-1qWHjHlBKwjnDfrkxd0L3Yx4LTad/WO7+d13YsXAC/ZfKj7p0xkLV3sDXJzfWgL7GfW4IBZwMAYWaz+ifyQouQ== + matchmediaquery@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/matchmediaquery/-/matchmediaquery-0.3.1.tgz#8247edc47e499ebb7c58f62a9ff9ccf5b815c6d7" @@ -8988,7 +9180,7 @@ memfs@^3.1.2, memfs@^3.4.1: dependencies: fs-monkey "^1.0.3" -memoize-one@6.0.0: +memoize-one@6.0.0, memoize-one@^6.0.0: version "6.0.0" resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045" integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw== @@ -9175,7 +9367,7 @@ moment-timezone@0.5.35: dependencies: moment ">= 2.9.0" -moment-timezone@^0.5.35: +moment-timezone@0.5.38, moment-timezone@^0.5.35: version "0.5.38" resolved "https://registry.yarnpkg.com/moment-timezone/-/moment-timezone-0.5.38.tgz#9674a5397b8be7c13de820fd387d8afa0f725aad" integrity sha512-nMIrzGah4+oYZPflDvLZUgoVUO4fvAqHstvG3xAUnMolWncuAiLDWNnJZj6EwJGMGfb1ZcuTFE6GI3hNOVWI/Q== @@ -9504,6 +9696,14 @@ object.values@^1.1.5: define-properties "^1.1.4" es-abstract "^1.20.4" +ol-mapbox-style@9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-9.1.0.tgz#1504b1a2c3cc23482c3c95cd55a1cf1d2ac8a451" + integrity sha512-R/XE6FdviaXNdnSw6ItHSEreMtQU68cwQCGv4Kl8yG0V1dZhnI5JWr8IOphJwffPVxfWTCnJb5aALGSB89MvhA== + dependencies: + "@mapbox/mapbox-gl-style-spec" "^13.23.1" + mapbox-to-css-font "^2.4.1" + ol-mapbox-style@^8.0.5: version "8.2.1" resolved "https://registry.yarnpkg.com/ol-mapbox-style/-/ol-mapbox-style-8.2.1.tgz#0f0c252b6495853a137d7e4dd3f915fab664b356" @@ -9522,6 +9722,17 @@ ol@6.15.1: pbf "3.2.1" rbush "^3.0.1" +ol@7.1.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/ol/-/ol-7.1.0.tgz#aab69a0539e59d6a4361cbc0f69f8b00c7298c9c" + integrity sha512-mAeV5Ca4mFhYaJoGWNZnIMN5VNnFTf63FgZjBiYu/DjQDGKNsD5QyvvqVziioVdOOgl6b8rPB/ypj2XNBinPwA== + dependencies: + earcut "^2.2.3" + geotiff "2.0.4" + ol-mapbox-style "9.1.0" + pbf "3.2.1" + rbush "^3.0.1" + once@^1.3.0, once@^1.3.1, once@^1.4.0: version "1.4.0" resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" @@ -10705,6 +10916,18 @@ rc-cascader@3.6.1: rc-tree "~5.6.3" rc-util "^5.6.1" +rc-cascader@3.7.0: + version "3.7.0" + resolved "https://registry.yarnpkg.com/rc-cascader/-/rc-cascader-3.7.0.tgz#98134df578ce1cca22be8fb4319b04df4f3dca36" + integrity sha512-SFtGpwmYN7RaWEAGTS4Rkc62ZV/qmQGg/tajr/7mfIkleuu8ro9Hlk6J+aA0x1YS4zlaZBtTcSaXM01QMiEV/A== + dependencies: + "@babel/runtime" "^7.12.5" + array-tree-filter "^2.1.0" + classnames "^2.3.1" + rc-select "~14.1.0" + rc-tree "~5.7.0" + rc-util "^5.6.1" + rc-drawer@4.4.3: version "4.4.3" resolved "https://registry.yarnpkg.com/rc-drawer/-/rc-drawer-4.4.3.tgz#2094937a844e55dc9644236a2d9fba79c344e321" @@ -10756,6 +10979,16 @@ rc-select@~14.1.0: rc-util "^5.16.1" rc-virtual-list "^3.2.0" +rc-slider@10.0.1: + version "10.0.1" + resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-10.0.1.tgz#7058c68ff1e1aa4e7c3536e5e10128bdbccb87f9" + integrity sha512-igTKF3zBet7oS/3yNiIlmU8KnZ45npmrmHlUUio8PNbIhzMcsh+oE/r2UD42Y6YD2D/s+kzCQkzQrPD6RY435Q== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "^2.2.5" + rc-util "^5.18.1" + shallowequal "^1.1.0" + rc-slider@9.7.5: version "9.7.5" resolved "https://registry.yarnpkg.com/rc-slider/-/rc-slider-9.7.5.tgz#193141c68e99b1dc3b746daeb6bf852946f5b7f4" @@ -10790,7 +11023,7 @@ rc-time-picker@^3.7.3: rc-trigger "^2.2.0" react-lifecycles-compat "^3.0.4" -rc-tooltip@^5.0.1: +rc-tooltip@5.2.2, rc-tooltip@^5.0.1: version "5.2.2" resolved "https://registry.yarnpkg.com/rc-tooltip/-/rc-tooltip-5.2.2.tgz#e5cafa8ecebf78108936a0bcb93c150fa81ac93b" integrity sha512-jtQzU/18S6EI3lhSGoDYhPqNpWajMtS5VV/ld1LwyfrDByQpYmw/LW6U7oFXXLukjfDHQ7Ju705A82PRNFWYhg== @@ -10810,6 +11043,17 @@ rc-tree@~5.6.3: rc-util "^5.16.1" rc-virtual-list "^3.4.8" +rc-tree@~5.7.0: + version "5.7.0" + resolved "https://registry.yarnpkg.com/rc-tree/-/rc-tree-5.7.0.tgz#d0e316eeeac2ba4a1c36b2b2201d84884f1c76a1" + integrity sha512-F+Ewkv/UcutshnVBMISP+lPdHDlcsL+YH/MQDVWbk+QdkfID7vXiwrHMEZn31+2Rbbm21z/HPceGS8PXGMmnQg== + dependencies: + "@babel/runtime" "^7.10.1" + classnames "2.x" + rc-motion "^2.0.1" + rc-util "^5.16.1" + rc-virtual-list "^3.4.8" + rc-trigger@^2.2.0: version "2.6.5" resolved "https://registry.yarnpkg.com/rc-trigger/-/rc-trigger-2.6.5.tgz#140a857cf28bd0fa01b9aecb1e26a50a700e9885" @@ -10845,7 +11089,7 @@ rc-util@^4.0.4, rc-util@^4.15.3, rc-util@^4.4.0: react-lifecycles-compat "^3.0.4" shallowequal "^1.1.0" -rc-util@^5.15.0, rc-util@^5.16.1, rc-util@^5.19.2, rc-util@^5.21.0, rc-util@^5.22.5, rc-util@^5.3.0, rc-util@^5.6.1, rc-util@^5.7.0: +rc-util@^5.15.0, rc-util@^5.16.1, rc-util@^5.18.1, rc-util@^5.19.2, rc-util@^5.21.0, rc-util@^5.22.5, rc-util@^5.3.0, rc-util@^5.6.1, rc-util@^5.7.0: version "5.24.4" resolved "https://registry.yarnpkg.com/rc-util/-/rc-util-5.24.4.tgz#a4126f01358c86f17c1bf380a1d83d6c9155ae65" integrity sha512-2a4RQnycV9eV7lVZPEJ7QwJRPlZNc06J7CwcwZo4vIHr3PfUqtYgl1EkUV9ETAc6VRRi8XZOMFhYG63whlIC9Q== @@ -10877,6 +11121,19 @@ react-beautiful-dnd@13.1.0: redux "^4.0.4" use-memo-one "^1.1.1" +react-beautiful-dnd@13.1.1: + version "13.1.1" + resolved "https://registry.yarnpkg.com/react-beautiful-dnd/-/react-beautiful-dnd-13.1.1.tgz#b0f3087a5840920abf8bb2325f1ffa46d8c4d0a2" + integrity sha512-0Lvs4tq2VcrEjEgDXHjT98r+63drkKEgqyxdA7qD3mvKwga6a5SscbdLPO2IExotU1jW8L0Ksdl0Cj2AF67nPQ== + dependencies: + "@babel/runtime" "^7.9.2" + css-box-model "^1.2.0" + memoize-one "^5.1.1" + raf-schd "^4.0.2" + react-redux "^7.2.0" + redux "^4.0.4" + use-memo-one "^1.1.1" + react-calendar@3.7.0: version "3.7.0" resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-3.7.0.tgz#951d56e91afb33b1c1e019cb790349fbffcc6894" @@ -10887,11 +11144,26 @@ react-calendar@3.7.0: merge-class-names "^1.1.1" prop-types "^15.6.0" +react-calendar@3.9.0: + version "3.9.0" + resolved "https://registry.yarnpkg.com/react-calendar/-/react-calendar-3.9.0.tgz#4dfe342ef61574c0e819e49847981076c7af58ea" + integrity sha512-g6RJCEaPovHTiV2bMhBUfm0a1YoMj4bOUpL8hQSLmR1Glhc7lgRLtZBd4mcC4jkoGsb+hv9uA/QH4pZcm5l9lQ== + dependencies: + "@wojtekmaj/date-utils" "^1.0.2" + get-user-locale "^1.2.0" + merge-class-names "^1.1.1" + prop-types "^15.6.0" + react-colorful@5.5.1: version "5.5.1" resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.5.1.tgz#29d9c4e496f2ca784dd2bb5053a3a4340cfaf784" integrity sha512-M1TJH2X3RXEt12sWkpa6hLc/bbYS0H6F4rIqjQZ+RxNBstpY67d9TrFXtqdZwhpmBXcCwEi7stKqFue3ZRkiOg== +react-colorful@5.6.1: + version "5.6.1" + resolved "https://registry.yarnpkg.com/react-colorful/-/react-colorful-5.6.1.tgz#7dc2aed2d7c72fac89694e834d179e32f3da563b" + integrity sha512-1exovf0uGTGyq5mXQT0zgQ80uvj2PCwvF8zY1RN9/vbJVSjSo3fsB/4L3ObbF7u70NduSiK4xu4Y6q1MHoUGEw== + react-copy-to-clipboard@^5.0.2: version "5.1.0" resolved "https://registry.yarnpkg.com/react-copy-to-clipboard/-/react-copy-to-clipboard-5.1.0.tgz#09aae5ec4c62750ccb2e6421a58725eabc41255c" @@ -10965,6 +11237,15 @@ react-dropzone@14.2.2: file-selector "^0.6.0" prop-types "^15.8.1" +react-dropzone@14.2.3: + version "14.2.3" + resolved "https://registry.yarnpkg.com/react-dropzone/-/react-dropzone-14.2.3.tgz#0acab68308fda2d54d1273a1e626264e13d4e84b" + integrity sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug== + dependencies: + attr-accept "^2.2.2" + file-selector "^0.6.0" + prop-types "^15.8.1" + react-emoji-render@^1.2.4: version "1.2.4" resolved "https://registry.yarnpkg.com/react-emoji-render/-/react-emoji-render-1.2.4.tgz#fa3542a692e1eed3236f0f12b8e3a61b2818e2c2" @@ -11005,6 +11286,14 @@ react-hook-form@7.5.3: resolved "https://registry.yarnpkg.com/react-hook-form/-/react-hook-form-7.5.3.tgz#9a624fa14ec153b154891c5ebddae02ec5c2e40f" integrity sha512-5T0mfJ4kCPKljd7t3Rgp7lML4Y2+kaZIeMdN6Zo/J7gBQ+WkrDBHOftdOtz4X+7/eqHGak5yL5evNpYdA9abVA== +react-i18next@^12.0.0: + version "12.0.0" + resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-12.0.0.tgz#634015a2c035779c5736ae4c2e5c34c1659753b1" + integrity sha512-/O7N6aIEAl1FaWZBNvhdIo9itvF/MO/nRKr9pYqRc9LhuC1u21SlfwpiYQqvaeNSEW3g3qUXLREOWMt+gxrWbg== + dependencies: + "@babel/runtime" "^7.14.5" + html-parse-stringify "^3.0.1" + react-immutable-proptypes@^2.1.0: version "2.2.0" resolved "https://registry.yarnpkg.com/react-immutable-proptypes/-/react-immutable-proptypes-2.2.0.tgz#cce96d68cc3c18e89617cbf3092d08e35126af4a" @@ -11020,6 +11309,14 @@ react-inlinesvg@3.0.0: exenv "^1.2.2" react-from-dom "^0.6.2" +react-inlinesvg@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/react-inlinesvg/-/react-inlinesvg-3.0.1.tgz#2133f5d2c770ac405060db2ce1c13eed30e7e83b" + integrity sha512-cBfoyfseNI2PkDA7ZKIlDoHq0eMfpoC3DhKBQNC+/X1M4ZQB+aXW+YiNPUDDDKXUsGDUIZWWiZWNFeauDIVdoA== + dependencies: + exenv "^1.2.2" + react-from-dom "^0.6.2" + react-is@^16.12.0, react-is@^16.13.1, react-is@^16.6.0, react-is@^16.7.0: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -11145,6 +11442,21 @@ react-select@5.4.0: prop-types "^15.6.0" react-transition-group "^4.3.0" +react-select@5.6.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/react-select/-/react-select-5.6.0.tgz#d987f4c86b3dcd32307a0104e503e4e8a9777a34" + integrity sha512-uUvP/72rA8NGhOL16RVBaeC12Wa4NUE0iXIa6hz0YRno9ZgxTmpuMeKzjR7vHcwmigpVCoe0prP+3NVb6Obq8Q== + dependencies: + "@babel/runtime" "^7.12.0" + "@emotion/cache" "^11.4.0" + "@emotion/react" "^11.8.1" + "@floating-ui/dom" "^1.0.1" + "@types/react-transition-group" "^4.4.0" + memoize-one "^6.0.0" + prop-types "^15.6.0" + react-transition-group "^4.3.0" + use-isomorphic-layout-effect "^1.1.2" + react-shallow-renderer@^16.13.1: version "16.15.0" resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" @@ -11194,7 +11506,7 @@ react-transition-group@4.4.2: loose-envify "^1.4.0" prop-types "^15.6.2" -react-transition-group@^4.3.0, react-transition-group@^4.4.5: +react-transition-group@4.4.5, react-transition-group@^4.3.0, react-transition-group@^4.4.5: version "4.4.5" resolved "https://registry.yarnpkg.com/react-transition-group/-/react-transition-group-4.4.5.tgz#e53d4e3f3344da8521489fbef8f2581d42becdd1" integrity sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g== @@ -11237,6 +11549,14 @@ react-window@1.8.7: "@babel/runtime" "^7.0.0" memoize-one ">=3.1.1 <6" +react-window@1.8.8: + version "1.8.8" + resolved "https://registry.yarnpkg.com/react-window/-/react-window-1.8.8.tgz#1b52919f009ddf91970cbdb2050a6c7be44df243" + integrity sha512-D4IiBeRtGXziZ1n0XklnFGu7h9gU684zepqyKzgPNzrsrk7xOCxni+TCckjg2Nr/DiaEEGVVmnhYSlT2rB47dQ== + dependencies: + "@babel/runtime" "^7.0.0" + memoize-one ">=3.1.1 <6" + react@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037" @@ -11321,6 +11641,11 @@ regenerate@^1.4.2: resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== +regenerator-runtime@0.13.10, regenerator-runtime@^0.13.10: + version "0.13.10" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" + integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== + regenerator-runtime@0.13.9: version "0.13.9" resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.9.tgz#8925742a98ffd90814988d7566ad30ca3b263b52" @@ -11331,11 +11656,6 @@ regenerator-runtime@^0.11.0: resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" integrity sha512-MguG95oij0fC3QV3URf4V2SDYGJhJnJGqvIIgdECeODCT98wSWDAJ94SSuVpYQUoTcGUIL6L4yNB7j1DFFHSBg== -regenerator-runtime@^0.13.10: - version "0.13.10" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz#ed07b19616bcbec5da6274ebc75ae95634bfc2ee" - integrity sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw== - regenerator-transform@^0.15.0: version "0.15.0" resolved "https://registry.yarnpkg.com/regenerator-transform/-/regenerator-transform-0.15.0.tgz#cbd9ead5d77fae1a48d957cf889ad0586adb6537" @@ -11602,6 +11922,13 @@ rxjs@7.5.6: dependencies: tslib "^2.1.0" +rxjs@7.5.7, rxjs@^7.5.1, rxjs@^7.5.5: + version "7.5.7" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" + integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== + dependencies: + tslib "^2.1.0" + rxjs@^6.4.0, rxjs@^6.6.0: version "6.6.7" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.6.7.tgz#90ac018acabf491bf65044235d5863c4dab804c9" @@ -11609,13 +11936,6 @@ rxjs@^6.4.0, rxjs@^6.6.0: dependencies: tslib "^1.9.0" -rxjs@^7.5.1, rxjs@^7.5.5: - version "7.5.7" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-7.5.7.tgz#2ec0d57fdc89ece220d2e702730ae8f1e49def39" - integrity sha512-z9MzKh/UcOqB3i20H6rtrlaE/CgjLOvheWK/9ILrbhROGTweAi1BaFsTT9FbwZi5Trr1qNRs+MXkhmR06awzQA== - dependencies: - tslib "^2.1.0" - safe-buffer@^5.1.0, safe-buffer@~5.2.0: version "5.2.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" @@ -11901,7 +12221,7 @@ slate-plain-serializer@0.7.11: resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.7.11.tgz#74ff6eb949e9fbd92ad98ed833d74d5082f2688b" integrity sha512-vzXQ68GiHHcTUcAB6ggf2qN/sX9BoLs77SMHacp5Gkg+oHAA/NxRzRH4efDAhpiJqfJZDrA3rQySK6+Y7KAuwg== -slate-plain-serializer@^0.7.11: +slate-plain-serializer@0.7.13, slate-plain-serializer@^0.7.11: version "0.7.13" resolved "https://registry.yarnpkg.com/slate-plain-serializer/-/slate-plain-serializer-0.7.13.tgz#6de8f5c645dd749f1b2e4426c20de74bfd213adf" integrity sha512-TtrlaslxQBEMV0LYdf3s7VAbTxRPe1xaW10WNNGAzGA855/0RhkaHjKkQiRjHv5rvbRleVf7Nxr9fH+4uErfxQ== @@ -12791,16 +13111,16 @@ tslib@2.4.0: resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.0.tgz#7cecaa7f073ce680a05847aa77be941098f36dc3" integrity sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ== +tslib@2.4.1, tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: + version "2.4.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" + integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== + tslib@^1.8.1, tslib@^1.9.0, tslib@^1.9.3: version "1.14.1" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.14.1.tgz#cf2d38bdc34a134bcaf1091c41f6619e2f672d00" integrity sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg== -tslib@^2.0.3, tslib@^2.1.0, tslib@^2.3.0, tslib@^2.4.0: - version "2.4.1" - resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.4.1.tgz#0d0bfbaac2880b91e22df0768e55be9753a5b17e" - integrity sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA== - tsutils@^3.21.0: version "3.21.0" resolved "https://registry.yarnpkg.com/tsutils/-/tsutils-3.21.0.tgz#b48717d394cea6c1e096983eed58e9d61715b623" @@ -12874,7 +13194,12 @@ typescript@4.8.2: resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.2.tgz#e3b33d5ccfb5914e4eeab6699cf208adee3fd790" integrity sha512-C0I1UsrrDHo2fYI5oaCGbSejwX4ch+9Y5jTQELvovfmFkK3HHSZJB8MSJcWLmCUBzQBchCrZ9rMRV6GuNrvGtw== -ua-parser-js@^1.0.2: +typescript@4.8.4: + version "4.8.4" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.8.4.tgz#c464abca159669597be5f96b8943500b238e60e6" + integrity sha512-QCh+85mCy+h0IGff8r5XWzOVSbBO+KfeYrMQh7NJ58QujwcE22u+NUSmUxqF+un70P9GXKxa2HCNiTTMJknyjQ== + +ua-parser-js@^1.0.32: version "1.0.32" resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-1.0.32.tgz#786bf17df97de159d5b1c9d5e8e9e89806f8a030" integrity sha512-dXVsz3M4j+5tTiovFVyVqssXBu5HM47//YSOeZ9fQkdDKkfzv2v3PP1jmH6FUyPW+yCSn7aBVK1fGGKNhowdDA== @@ -13040,6 +13365,11 @@ url-parse@^1.5.3: querystringify "^2.1.1" requires-port "^1.0.0" +use-isomorphic-layout-effect@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/use-isomorphic-layout-effect/-/use-isomorphic-layout-effect-1.1.2.tgz#497cefb13d863d687b08477d9e5a164ad8c1a6fb" + integrity sha512-49L8yCO3iGT/ZF9QttjwLF/ZD9Iwto5LnH5LmEdk/6cFmXddqi2ulF0edxTwjj+7mqvpVVGQWvbXZdn32wRSHA== + use-memo-one@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/use-memo-one/-/use-memo-one-1.1.3.tgz#2fd2e43a2169eabc7496960ace8c79efef975e99" @@ -13070,6 +13400,11 @@ uuid@8.3.2, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +uuid@9.0.0: + version "9.0.0" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5" + integrity sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg== + v8-compile-cache@^2.0.3, v8-compile-cache@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -13122,6 +13457,11 @@ vfile@^4.0.0: unist-util-stringify-position "^2.0.0" vfile-message "^2.0.0" +void-elements@3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09" + integrity sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w== + w3c-hr-time@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz#0a89cdf5cc15822df9c360543676963e0cc308cd" @@ -13165,10 +13505,10 @@ wcwidth@^1.0.1: dependencies: defaults "^1.0.3" -web-vitals@^2.1.4: - version "2.1.4" - resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-2.1.4.tgz#76563175a475a5e835264d373704f9dde718290c" - integrity sha512-sVWcwhU5mX6crfI5Vd2dC4qchyTqxV8URinzt25XqVh+bHEPGH4C3NPrNionCP7Obx59wrYEbNlw4Z8sjALzZg== +web-vitals@^3.0.4: + version "3.1.0" + resolved "https://registry.yarnpkg.com/web-vitals/-/web-vitals-3.1.0.tgz#a6f5156cb6c7fee562da46078540265ac2cd2d16" + integrity sha512-zCeQ+bOjWjJbXv5ZL0r8Py3XP2doCQMZXNKlBGfUjPAVZWokApdeF/kFlK1peuKlCt8sL9TFkKzyXE9/cmNJQA== web-worker@^1.2.0: version "1.2.0" @@ -13379,6 +13719,14 @@ xss@1.0.13: commander "^2.20.3" cssfilter "0.0.10" +xss@1.0.14: + version "1.0.14" + resolved "https://registry.yarnpkg.com/xss/-/xss-1.0.14.tgz#4f3efbde75ad0d82e9921cc3c95e6590dd336694" + integrity sha512-og7TEJhXvn1a7kzZGQ7ETjdQVS2UfZyTlsEdDOqvQF7GoxNfY+0YLCzBy1kPdsDDx4QuNAonQPddpsn6Xl/7sw== + dependencies: + commander "^2.20.3" + cssfilter "0.0.10" + y18n@^5.0.5: version "5.0.8" resolved "https://registry.yarnpkg.com/y18n/-/y18n-5.0.8.tgz#7f4934d0f7ca8c56f95314939ddcd2dd91ce1d55"