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:
parent
9eb09c0272
commit
4ccfda58e5
9 changed files with 103 additions and 85 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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_')
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue