Merge pull request #106 from grafana/update-outgoing-webhooks-public-endpoint
Update public api endpoint for outgoing webhooks
This commit is contained in:
commit
08f0d99572
3 changed files with 286 additions and 8 deletions
|
|
@ -1,17 +1,88 @@
|
|||
import json
|
||||
|
||||
from django.core.validators import URLValidator, ValidationError
|
||||
from jinja2 import Template, TemplateError
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
|
||||
from common.api_helpers.utils import CurrentOrganizationDefault
|
||||
|
||||
|
||||
class ActionSerializer(serializers.ModelSerializer):
|
||||
class ActionCreateSerializer(serializers.ModelSerializer):
|
||||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
team_id = TeamPrimaryKeyRelatedField(allow_null=True, source="team")
|
||||
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
|
||||
team_id = TeamPrimaryKeyRelatedField(required=False, allow_null=True, source="team")
|
||||
|
||||
class Meta:
|
||||
model = CustomButton
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"organization",
|
||||
"team_id",
|
||||
"webhook",
|
||||
"data",
|
||||
"user",
|
||||
"password",
|
||||
"authorization_header",
|
||||
"forward_whole_payload",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"required": True, "allow_null": False, "allow_blank": False},
|
||||
"webhook": {"required": True, "allow_null": False, "allow_blank": False},
|
||||
"data": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"user": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"password": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"authorization_header": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"forward_whole_payload": {"required": False, "allow_null": True},
|
||||
}
|
||||
|
||||
validators = [UniqueTogetherValidator(queryset=CustomButton.objects.all(), fields=["name", "organization"])]
|
||||
|
||||
def validate_webhook(self, webhook):
|
||||
if webhook:
|
||||
try:
|
||||
URLValidator()(webhook)
|
||||
except ValidationError:
|
||||
raise serializers.ValidationError("Webhook is incorrect")
|
||||
return webhook
|
||||
return None
|
||||
|
||||
def validate_data(self, data):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
try:
|
||||
json.loads(data)
|
||||
except ValueError:
|
||||
raise serializers.ValidationError("Data has incorrect format")
|
||||
|
||||
try:
|
||||
Template(data)
|
||||
except TemplateError:
|
||||
raise serializers.ValidationError("Data has incorrect template")
|
||||
|
||||
return data
|
||||
|
||||
def validate_forward_whole_payload(self, data):
|
||||
if data is None:
|
||||
return False
|
||||
return data
|
||||
|
||||
|
||||
class ActionUpdateSerializer(ActionCreateSerializer):
|
||||
team_id = TeamPrimaryKeyRelatedField(source="team", read_only=True)
|
||||
|
||||
class Meta(ActionCreateSerializer.Meta):
|
||||
|
||||
extra_kwargs = {
|
||||
"name": {"required": False, "allow_null": False, "allow_blank": False},
|
||||
"webhook": {"required": False, "allow_null": False, "allow_blank": False},
|
||||
"data": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"user": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"password": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"authorization_header": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"forward_whole_payload": {"required": False, "allow_null": True},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ from django.urls import reverse
|
|||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_custom_actions(
|
||||
|
|
@ -28,6 +30,12 @@ def test_get_custom_actions(
|
|||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"webhook": custom_action.webhook,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -60,6 +68,12 @@ def test_get_custom_actions_filter_by_name(
|
|||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"webhook": custom_action.webhook,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
}
|
||||
],
|
||||
}
|
||||
|
|
@ -87,3 +101,171 @@ def test_get_custom_actions_filter_by_name_empty_result(
|
|||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_custom_action(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
):
|
||||
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
custom_action = make_custom_action(organization=organization)
|
||||
|
||||
url = reverse("api-public:actions-detail", kwargs={"pk": custom_action.public_primary_key})
|
||||
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_payload = {
|
||||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"webhook": custom_action.webhook,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_custom_action(make_organization_and_user_with_token):
|
||||
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-public:actions-list")
|
||||
|
||||
data = {
|
||||
"name": "Test outgoing webhook",
|
||||
"webhook": "https://example.com",
|
||||
}
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
custom_action = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
|
||||
expected_result = {
|
||||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"webhook": custom_action.webhook,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data == expected_result
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_custom_action_invalid_data(
|
||||
make_organization_and_user_with_token,
|
||||
):
|
||||
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-public:actions-list")
|
||||
|
||||
data = {
|
||||
"name": "Test outgoing webhook",
|
||||
"webhook": "invalid_url",
|
||||
}
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.data["webhook"][0] == "Webhook is incorrect"
|
||||
|
||||
data = {
|
||||
"name": "Test outgoing webhook",
|
||||
}
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.data["webhook"][0] == "This field is required."
|
||||
|
||||
data = {
|
||||
"webhook": "https://example.com",
|
||||
}
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.data["name"][0] == "This field is required."
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_custom_action(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
):
|
||||
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
custom_action = make_custom_action(organization=organization)
|
||||
|
||||
url = reverse("api-public:actions-detail", kwargs={"pk": custom_action.public_primary_key})
|
||||
|
||||
data = {
|
||||
"name": "RENAMED",
|
||||
}
|
||||
|
||||
assert custom_action.name != data["name"]
|
||||
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_result = {
|
||||
"id": custom_action.public_primary_key,
|
||||
"name": data["name"],
|
||||
"team_id": None,
|
||||
"webhook": custom_action.webhook,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
custom_action.refresh_from_db()
|
||||
assert custom_action.name == expected_result["name"]
|
||||
assert response.data == expected_result
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_custom_action(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
):
|
||||
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
custom_action = make_custom_action(organization=organization)
|
||||
url = reverse("api-public:actions-detail", kwargs={"pk": custom_action.public_primary_key})
|
||||
|
||||
assert custom_action.deleted_at is None
|
||||
|
||||
response = client.delete(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
custom_action.refresh_from_db()
|
||||
assert custom_action.deleted_at is not None
|
||||
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
assert response.status_code == status.HTTP_404_NOT_FOUND
|
||||
assert response.data["detail"] == "Not found."
|
||||
|
|
|
|||
|
|
@ -1,25 +1,26 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from rest_framework import mixins
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.viewsets import GenericViewSet
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
from apps.auth_token.auth import ApiTokenAuthentication
|
||||
from apps.public_api.serializers.action import ActionSerializer
|
||||
from apps.public_api.serializers.action import ActionCreateSerializer, ActionUpdateSerializer
|
||||
from apps.public_api.throttlers.user_throttle import UserThrottle
|
||||
from apps.user_management.organization_log_creator import OrganizationLogType, create_organization_log
|
||||
from common.api_helpers.filters import ByTeamFilter
|
||||
from common.api_helpers.mixins import RateLimitHeadersMixin
|
||||
from common.api_helpers.mixins import PublicPrimaryKeyMixin, RateLimitHeadersMixin, UpdateSerializerMixin
|
||||
from common.api_helpers.paginators import FiftyPageSizePaginator
|
||||
|
||||
|
||||
class ActionView(RateLimitHeadersMixin, mixins.ListModelMixin, GenericViewSet):
|
||||
class ActionView(RateLimitHeadersMixin, PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet):
|
||||
authentication_classes = (ApiTokenAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
pagination_class = FiftyPageSizePaginator
|
||||
throttle_classes = [UserThrottle]
|
||||
|
||||
model = CustomButton
|
||||
serializer_class = ActionSerializer
|
||||
serializer_class = ActionCreateSerializer
|
||||
update_serializer_class = ActionUpdateSerializer
|
||||
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = ByTeamFilter
|
||||
|
|
@ -32,3 +33,27 @@ class ActionView(RateLimitHeadersMixin, mixins.ListModelMixin, GenericViewSet):
|
|||
queryset = queryset.filter(name=action_name)
|
||||
|
||||
return queryset
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
instance = serializer.instance
|
||||
organization = self.request.auth.organization
|
||||
user = self.request.user
|
||||
description = f"Custom action {instance.name} was created"
|
||||
create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_CREATED, description)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
organization = self.request.auth.organization
|
||||
user = self.request.user
|
||||
old_state = serializer.instance.repr_settings_for_client_side_logging
|
||||
serializer.save()
|
||||
new_state = serializer.instance.repr_settings_for_client_side_logging
|
||||
description = f"Custom action {serializer.instance.name} was changed " f"from:\n{old_state}\nto:\n{new_state}"
|
||||
create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_CHANGED, description)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
organization = self.request.auth.organization
|
||||
user = self.request.user
|
||||
description = f"Custom action {instance.name} was deleted"
|
||||
create_organization_log(organization, user, OrganizationLogType.TYPE_CUSTOM_ACTION_DELETED, description)
|
||||
instance.delete()
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue