diff --git a/CHANGELOG.md b/CHANGELOG.md index 50c75e54..b768aaa7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Disallow creating and deleting direct paging integrations by @vadimkerr ([#3475](https://github.com/grafana/oncall/pull/3475)) + ## v1.3.70 (2023-12-01) Maintenance release diff --git a/engine/apps/api/serializers/alert_receive_channel.py b/engine/apps/api/serializers/alert_receive_channel.py index 0aae34b6..3e1ecc30 100644 --- a/engine/apps/api/serializers/alert_receive_channel.py +++ b/engine/apps/api/serializers/alert_receive_channel.py @@ -349,6 +349,10 @@ class AlertReceiveChannelSerializer( def validate_integration(integration): if integration is None or integration not in AlertReceiveChannel.WEB_INTEGRATION_CHOICES: raise BadRequest(detail="invalid integration") + + if integration == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING: + raise BadRequest(detail="Direct paging integrations can't be created") + return integration def validate_verbal_name(self, verbal_name): @@ -372,7 +376,8 @@ class AlertReceiveChannelSerializer( return IntegrationHeartBeatSerializer(heartbeat).data def get_allow_delete(self, obj: "AlertReceiveChannel"): - return True + # don't allow deleting direct paging integrations + return obj.integration != AlertReceiveChannel.INTEGRATION_DIRECT_PAGING def get_alert_count(self, obj: "AlertReceiveChannel"): return 0 diff --git a/engine/apps/api/tests/test_alert_receive_channel.py b/engine/apps/api/tests/test_alert_receive_channel.py index 8dac1b4b..3f8487c8 100644 --- a/engine/apps/api/tests/test_alert_receive_channel.py +++ b/engine/apps/api/tests/test_alert_receive_channel.py @@ -785,45 +785,17 @@ def test_get_alert_receive_channels_direct_paging_present_for_filters( @pytest.mark.django_db -def test_create_alert_receive_channels_direct_paging( +def test_cant_create_alert_receive_channels_direct_paging( make_organization_and_user_with_plugin_token, make_team, make_alert_receive_channel, make_user_auth_headers ): organization, user, token = make_organization_and_user_with_plugin_token() - team = make_team(organization) client = APIClient() url = reverse("api-internal:alert_receive_channel-list") - - response_1 = client.post( + response = client.post( url, data={"integration": "direct_paging"}, format="json", **make_user_auth_headers(user, token) ) - response_2 = client.post( - url, data={"integration": "direct_paging"}, format="json", **make_user_auth_headers(user, token) - ) - - response_3 = client.post( - url, - data={"integration": "direct_paging", "team": team.public_primary_key}, - format="json", - **make_user_auth_headers(user, token), - ) - response_4 = client.post( - url, - data={"integration": "direct_paging", "team": team.public_primary_key}, - format="json", - **make_user_auth_headers(user, token), - ) - - # Check direct paging integration for "No team" is created - assert response_1.status_code == status.HTTP_201_CREATED - # Check direct paging integration is not created, as it already exists for "No team" - assert response_2.status_code == status.HTTP_400_BAD_REQUEST - - # Check direct paging integration for team is created - assert response_3.status_code == status.HTTP_201_CREATED - # Check direct paging integration is not created, as it already exists for team - assert response_4.status_code == status.HTTP_400_BAD_REQUEST - assert response_4.json()["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL + assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.django_db @@ -852,6 +824,27 @@ def test_update_alert_receive_channels_direct_paging( assert response.json()["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL +@pytest.mark.django_db +def test_cant_delete_direct_paging_integration( + make_organization_and_user_with_plugin_token, make_alert_receive_channel, make_user_auth_headers +): + organization, user, token = make_organization_and_user_with_plugin_token() + integration = make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING) + + # check allow_delete is False (so the frontend can hide the delete button) + client = APIClient() + url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": integration.public_primary_key}) + response = client.get(url, **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_200_OK + assert response.json()["allow_delete"] is False + + # check delete is not allowed + client = APIClient() + url = reverse("api-internal:alert_receive_channel-detail", kwargs={"pk": integration.public_primary_key}) + response = client.delete(url, **make_user_auth_headers(user, token)) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.django_db def test_start_maintenance_integration( make_user_auth_headers, diff --git a/engine/apps/api/views/alert_receive_channel.py b/engine/apps/api/views/alert_receive_channel.py index 9edab2dc..b0309a7a 100644 --- a/engine/apps/api/views/alert_receive_channel.py +++ b/engine/apps/api/views/alert_receive_channel.py @@ -134,6 +134,14 @@ class AlertReceiveChannelView( new_state=new_state, ) + def destroy(self, request, *args, **kwargs): + # don't allow deleting direct paging integrations + instance = self.get_object() + if instance.integration == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING: + raise BadRequest(detail="Direct paging integrations can't be deleted") + + return super().destroy(request, *args, **kwargs) + def perform_destroy(self, instance): write_resource_insight_log( instance=instance, diff --git a/engine/apps/public_api/serializers/integrations.py b/engine/apps/public_api/serializers/integrations.py index 3dafad78..af612011 100644 --- a/engine/apps/public_api/serializers/integrations.py +++ b/engine/apps/public_api/serializers/integrations.py @@ -67,6 +67,8 @@ class IntegrationTypeField(fields.CharField): raise BadRequest(detail="Invalid integration type") if has_legacy_prefix(data): raise BadRequest("This integration type is deprecated") + if data == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING: + raise BadRequest(detail="Direct paging integrations can't be created") return data diff --git a/engine/apps/public_api/tests/test_integrations.py b/engine/apps/public_api/tests/test_integrations.py index 8e5dd150..0d7a3045 100644 --- a/engine/apps/public_api/tests/test_integrations.py +++ b/engine/apps/public_api/tests/test_integrations.py @@ -820,35 +820,15 @@ def test_update_integration_default_route( @pytest.mark.django_db -def test_create_integrations_direct_paging( +def test_cant_create_integrations_direct_paging( make_organization_and_user_with_token, make_team, make_alert_receive_channel, make_user_auth_headers ): organization, _, token = make_organization_and_user_with_token() - team = make_team(organization) client = APIClient() url = reverse("api-public:integrations-list") - - response_1 = client.post(url, data={"type": "direct_paging"}, format="json", HTTP_AUTHORIZATION=token) - response_2 = client.post(url, data={"type": "direct_paging"}, format="json", HTTP_AUTHORIZATION=token) - - response_3 = client.post( - url, data={"type": "direct_paging", "team_id": team.public_primary_key}, format="json", HTTP_AUTHORIZATION=token - ) - response_4 = client.post( - url, data={"type": "direct_paging", "team_id": team.public_primary_key}, format="json", HTTP_AUTHORIZATION=token - ) - - # Check direct paging integration for "No team" is created - assert response_1.status_code == status.HTTP_201_CREATED - # Check direct paging integration is not created, as it already exists for "No team" - assert response_2.status_code == status.HTTP_400_BAD_REQUEST - - # Check direct paging integration for team is created - assert response_3.status_code == status.HTTP_201_CREATED - # Check direct paging integration is not created, as it already exists for team - assert response_4.status_code == status.HTTP_400_BAD_REQUEST - assert response_4.data["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL + response = client.post(url, data={"type": "direct_paging"}, format="json", HTTP_AUTHORIZATION=token) + assert response.status_code == status.HTTP_400_BAD_REQUEST @pytest.mark.django_db @@ -873,6 +853,17 @@ def test_update_integrations_direct_paging( assert response.data["detail"] == AlertReceiveChannel.DuplicateDirectPagingError.DETAIL +@pytest.mark.django_db +def test_cant_delete_direct_paging_integration(make_organization_and_user_with_token, make_alert_receive_channel): + organization, user, token = make_organization_and_user_with_token() + integration = make_alert_receive_channel(organization, integration=AlertReceiveChannel.INTEGRATION_DIRECT_PAGING) + + client = APIClient() + url = reverse("api-public:integrations-detail", args=[integration.public_primary_key]) + response = client.delete(url, HTTP_AUTHORIZATION=token) + assert response.status_code == status.HTTP_400_BAD_REQUEST + + @pytest.mark.django_db def test_get_integration_type_legacy( make_organization_and_user_with_token, make_alert_receive_channel, make_channel_filter, make_integration_heartbeat diff --git a/engine/apps/public_api/views/integrations.py b/engine/apps/public_api/views/integrations.py index 5bcb92fb..358f5420 100644 --- a/engine/apps/public_api/views/integrations.py +++ b/engine/apps/public_api/views/integrations.py @@ -8,6 +8,7 @@ from apps.alerts.models import AlertReceiveChannel from apps.auth_token.auth import ApiTokenAuthentication from apps.public_api.serializers import IntegrationSerializer, IntegrationUpdateSerializer from apps.public_api.throttlers.user_throttle import UserThrottle +from common.api_helpers.exceptions import BadRequest from common.api_helpers.filters import ByTeamFilter from common.api_helpers.mixins import FilterSerializerMixin, RateLimitHeadersMixin, UpdateSerializerMixin from common.api_helpers.paginators import FiftyPageSizePaginator @@ -70,6 +71,14 @@ class IntegrationView( new_state=new_state, ) + def destroy(self, request, *args, **kwargs): + # don't allow deleting direct paging integrations + instance = self.get_object() + if instance.integration == AlertReceiveChannel.INTEGRATION_DIRECT_PAGING: + raise BadRequest(detail="Direct paging integrations can't be deleted") + + return super().destroy(request, *args, **kwargs) + def perform_destroy(self, instance): write_resource_insight_log(instance=instance, author=self.request.user, event=EntityEvent.DELETED) instance.delete() diff --git a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx index d2847e57..03d24f91 100644 --- a/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx +++ b/grafana-plugin/src/containers/IntegrationForm/IntegrationForm.tsx @@ -88,6 +88,11 @@ const IntegrationForm = observer((props: IntegrationFormProps) => { return false; } + // don't allow creating direct paging integrations + if (option.value === 'direct_paging') { + return false; + } + return ( option.display_name.toLowerCase().includes(filterValue.toLowerCase()) && !option.value.toLowerCase().startsWith('legacy_') diff --git a/grafana-plugin/src/pages/integration/Integration.tsx b/grafana-plugin/src/pages/integration/Integration.tsx index 17c7848c..ab32cbb4 100644 --- a/grafana-plugin/src/pages/integration/Integration.tsx +++ b/grafana-plugin/src/pages/integration/Integration.tsx @@ -34,6 +34,7 @@ import IntegrationBlock from 'components/Integrations/IntegrationBlock'; import PageErrorHandlingWrapper, { PageBaseState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper'; import { initErrorDataState } from 'components/PageErrorHandlingWrapper/PageErrorHandlingWrapper.helpers'; import PluginLink from 'components/PluginLink/PluginLink'; +import RenderConditionally from 'components/RenderConditionally/RenderConditionally'; import Tag from 'components/Tag/Tag'; import Text from 'components/Text/Text'; import TooltipBadge from 'components/TooltipBadge/TooltipBadge'; @@ -958,37 +959,37 @@ const IntegrationActions: React.FC = ({ - -
- - -
-
{ - setConfirmModal({ - isOpen: true, - title: 'Delete Integration?', - body: ( - - Are you sure you want to delete ? - - ), - onConfirm: deleteIntegration, - dismissText: 'Cancel', - confirmText: 'Delete', - }); - }} - className="u-width-100" - > - - - - Delete Integration - - + +
+ +
+
{ + setConfirmModal({ + isOpen: true, + title: 'Delete Integration?', + body: ( + + Are you sure you want to delete ? + + ), + onConfirm: deleteIntegration, + dismissText: 'Cancel', + confirmText: 'Delete', + }); + }} + className="u-width-100" + > + + + + Delete Integration + + +
-
- + +
)} >