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"