Disallow creating and deleting direct paging integrations (#3475)

# What this PR does

Disallows creating and deleting direct paging integrations via both
internal and public APIs. It also hides the direct paging option in the
UI when creating a new integration.

## Which issue(s) this PR fixes

Related to https://github.com/grafana/oncall-private/issues/2302

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)

---------

Co-authored-by: Dominik <dominik.broj@grafana.com>
This commit is contained in:
Vadim Stepanov 2023-12-04 13:13:53 +00:00 committed by GitHub
parent 9eb09c0272
commit 4ccfda58e5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 103 additions and 85 deletions

View file

@ -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

View file

@ -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

View file

@ -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,

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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()

View file

@ -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_')

View file

@ -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<IntegrationActionsProps> = ({
</HorizontalGroup>
</div>
</CopyToClipboard>
<div className={cx('thin-line-break')} />
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<div className={cx('integration__actionItem')}>
<div
onClick={() => {
setConfirmModal({
isOpen: true,
title: 'Delete Integration?',
body: (
<Text type="primary">
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} /> ?
</Text>
),
onConfirm: deleteIntegration,
dismissText: 'Cancel',
confirmText: 'Delete',
});
}}
className="u-width-100"
>
<Text type="danger">
<HorizontalGroup spacing={'xs'}>
<Icon name="trash-alt" />
<span>Delete Integration</span>
</HorizontalGroup>
</Text>
<RenderConditionally shouldRender={alertReceiveChannel.allow_delete}>
<div className={cx('thin-line-break')} />
<WithPermissionControlTooltip userAction={UserActions.IntegrationsWrite}>
<div className={cx('integration__actionItem')}>
<div
onClick={() => {
setConfirmModal({
isOpen: true,
title: 'Delete Integration?',
body: (
<Text type="primary">
Are you sure you want to delete <Emoji text={alertReceiveChannel.verbal_name} /> ?
</Text>
),
onConfirm: deleteIntegration,
dismissText: 'Cancel',
confirmText: 'Delete',
});
}}
className="u-width-100"
>
<Text type="danger">
<HorizontalGroup spacing={'xs'}>
<Icon name="trash-alt" />
<span>Delete Integration</span>
</HorizontalGroup>
</Text>
</div>
</div>
</div>
</WithPermissionControlTooltip>
</WithPermissionControlTooltip>
</RenderConditionally>
</div>
)}
>