Webhooks Public API (#2790)
# What this PR does - Add public API for Webhooks CRUD, and GET webhook responses - Add insight resource logs for internal and public webhook API calls - Change public actions API to wrap Webhooks to maintain compatibility with existing callers ## Which issue(s) this PR fixes #2792 #2793 ## 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)
This commit is contained in:
parent
0dfa882a06
commit
7440a836da
16 changed files with 986 additions and 198 deletions
|
|
@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Public API for webhooks @mderynck ([#2790](https://github.com/grafana/oncall/pull/2790))
|
||||
|
||||
### Changed
|
||||
|
||||
- Public API for actions now wraps webhooks @mderynck ([#2790](https://github.com/grafana/oncall/pull/2790))
|
||||
- Allow mobile app to access status endpoint @mderynck ([#2791](https://github.com/grafana/oncall/pull/2791))
|
||||
|
||||
## v1.3.26 (2023-08-22)
|
||||
|
|
|
|||
|
|
@ -1,17 +1,21 @@
|
|||
---
|
||||
canonical: https://grafana.com/docs/oncall/latest/oncall-api-reference/outgoing_webhooks/
|
||||
title: Outgoing webhooks HTTP API
|
||||
title: Outgoing Webhooks HTTP API
|
||||
weight: 700
|
||||
---
|
||||
|
||||
# Outgoing webhooks (actions)
|
||||
# Outgoing Webhooks
|
||||
|
||||
Used in escalation policies with type `trigger_action`.
|
||||
> ⚠️ A note about actions: Before version **v1.3.11** webhooks existed as actions within the API, the /actions
|
||||
> endpoint remains available and is compatible with previous callers but under the hood it will interact with the
|
||||
> new webhooks objects. It is recommended to use the /webhooks endpoint going forward which has more features.
|
||||
|
||||
## List actions
|
||||
For more details about specific fields of a webhook see [outgoing webhooks][outgoing-webhooks] documentation.
|
||||
|
||||
## List webhooks
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/actions/" \
|
||||
curl "{{API_URL}}/api/v1/webhooks/" \
|
||||
--request GET \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
|
|
@ -21,21 +25,210 @@ The above command returns JSON structured in the following way:
|
|||
|
||||
```json
|
||||
{
|
||||
"count": 1,
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"id": "KGEFG74LU1D8L",
|
||||
"name": "Publish alert group notification to JIRA"
|
||||
"id": "{{WEBHOOK_UID}}",
|
||||
"name": "Demo Webhook",
|
||||
"is_webhook_enabled": true,
|
||||
"team": null,
|
||||
"data": "{\"labels\" : {{ alert_payload.commonLabels | tojson()}}}",
|
||||
"username": null,
|
||||
"password": null,
|
||||
"authorization_header": "****************",
|
||||
"trigger_template": null,
|
||||
"headers": null,
|
||||
"url": "https://example.com",
|
||||
"forward_all": false,
|
||||
"http_method": "POST",
|
||||
"trigger_type": "acknowledge",
|
||||
"integration_filter": [
|
||||
"CRV8A5MXC751A"
|
||||
]
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"count": 1,
|
||||
"current_page_number": 1,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
||||
**HTTP request**
|
||||
## Get webhook
|
||||
|
||||
`GET {{API_URL}}/api/v1/actions/`
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
|
||||
--request GET \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
```
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "{{WEBHOOK_UID}}",
|
||||
"name": "Demo Webhook",
|
||||
"is_webhook_enabled": true,
|
||||
"team": null,
|
||||
"data": "{\"labels\" : {{ alert_payload.commonLabels | tojson()}}}",
|
||||
"username": null,
|
||||
"password": null,
|
||||
"authorization_header": "****************",
|
||||
"trigger_template": null,
|
||||
"headers": null,
|
||||
"url": "https://example.com",
|
||||
"forward_all": false,
|
||||
"http_method": "POST",
|
||||
"trigger_type": "acknowledge",
|
||||
"integration_filter": [
|
||||
"CRV8A5MXC751A"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Create webhook
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/webhooks/" \
|
||||
--request POST \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"name": "New Webhook",
|
||||
"url": "https://example.com",
|
||||
"http_method": "POST",
|
||||
"trigger_type" : "resolve"
|
||||
}'
|
||||
```
|
||||
|
||||
### Trigger Types
|
||||
|
||||
See [here](outgoing-webhooks#event-types) for details
|
||||
|
||||
- `escalation`
|
||||
- `alert group created`
|
||||
- `acknowledge`
|
||||
- `resolve`
|
||||
- `silence`
|
||||
- `unsilence`
|
||||
- `unresolve`
|
||||
- `unacknowledge`
|
||||
|
||||
### HTTP Methods
|
||||
|
||||
- `POST`
|
||||
- `GET`
|
||||
- `PUT`
|
||||
- `DELETE`
|
||||
- `OPTIONS`
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "{{WEBHOOK_UID}}",
|
||||
"name": "New Webhook",
|
||||
"is_webhook_enabled": true,
|
||||
"team": null,
|
||||
"data": null,
|
||||
"username": null,
|
||||
"password": null,
|
||||
"authorization_header": null,
|
||||
"trigger_template": null,
|
||||
"headers": null,
|
||||
"url": "https://example.com",
|
||||
"forward_all": true,
|
||||
"http_method": "POST",
|
||||
"trigger_type": "resolve",
|
||||
"integration_filter": null
|
||||
}
|
||||
```
|
||||
|
||||
## Update webhook
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
|
||||
--request PUT \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json" \
|
||||
--data '{
|
||||
"is_webhook_enabled": false
|
||||
}'
|
||||
```
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "{{WEBHOOK_UID}}",
|
||||
"name": "New Webhook",
|
||||
"is_webhook_enabled": false,
|
||||
"team": null,
|
||||
"data": null,
|
||||
"username": null,
|
||||
"password": null,
|
||||
"authorization_header": null,
|
||||
"trigger_template": null,
|
||||
"headers": null,
|
||||
"url": "https://example.com",
|
||||
"forward_all": true,
|
||||
"http_method": "POST",
|
||||
"trigger_type": "resolve",
|
||||
"integration_filter": null
|
||||
}
|
||||
```
|
||||
|
||||
## Delete webhook
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/" \
|
||||
--request DELETE \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
```
|
||||
|
||||
## Get webhook responses
|
||||
|
||||
```shell
|
||||
curl "{{API_URL}}/api/v1/webhooks/{{WEBHOOK_UID}}/responses" \
|
||||
--request GET \
|
||||
--header "Authorization: meowmeowmeow" \
|
||||
--header "Content-Type: application/json"
|
||||
```
|
||||
|
||||
The above command returns JSON structured in the following way:
|
||||
|
||||
```json
|
||||
{
|
||||
"next": null,
|
||||
"previous": null,
|
||||
"results": [
|
||||
{
|
||||
"timestamp": "2023-08-18T16:38:23.106015Z",
|
||||
"url": "https://example.com",
|
||||
"request_trigger": "",
|
||||
"request_headers": "{\"Authorization\": \"****************\"}",
|
||||
"request_data": "{\"labels\": {\"alertname\": \"InstanceDown\", \"job\": \"node\", \"severity\": \"critical\"}}",
|
||||
"status_code": 200,
|
||||
"content": "",
|
||||
"event_data": "{\"event\": {\"type\": \"acknowledge\", \"time\": \"2023-08-18T16:38:21.442981+00:00\"}, \"user\": {\"id\": \"UK49JJNPZMFLJ\", \"username\": \"oncall\", \"email\": \"admin@localhost\"}, \"alert_group\": {\"id\": \"IZQERPWKWCGH1\", \"integration_id\": \"CRV8A5MXC751A\", \"route_id\": \"RWNCT6C77M3WM\", \"alerts_count\": 1, \"state\": \"acknowledged\", \"created_at\": \"2023-08-18T16:34:27.678406Z\", \"resolved_at\": null, \"acknowledged_at\": \"2023-08-18T16:38:21.442981Z\", \"title\": \"[firing:2] InstanceDown \", \"permalinks\": {\"slack\": null, \"telegram\": null, \"web\": \"http://localhost:3000/a/grafana-oncall-app/alert-groups/IZQERPWKWCGH1\"}}, \"alert_group_id\": \"IZQERPWKWCGH1\", \"alert_payload\": {\"alerts\": [{\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"production\", \"instance\": \"localhost:8081\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8081 down\", \"description\": \"localhost:8081 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f404ecabc8dd5cd7\", \"generatorURL\": \"\"}, {\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"canary\", \"instance\": \"localhost:8082\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8082 down\", \"description\": \"localhost:8082 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f8f08d4e32c61a9d\", \"generatorURL\": \"\"}], \"status\": \"firing\", \"version\": \"4\", \"groupKey\": \"{}:{alertname=\\\"InstanceDown\\\"}\", \"receiver\": \"combo\", \"numFiring\": 2, \"externalURL\": \"\", \"groupLabels\": {\"alertname\": \"InstanceDown\"}, \"numResolved\": 0, \"commonLabels\": {\"job\": \"node\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"truncatedAlerts\": 0, \"commonAnnotations\": {}}, \"integration\": {\"id\": \"CRV8A5MXC751A\", \"type\": \"alertmanager\", \"name\": \"One - Alertmanager\", \"team\": null}, \"notified_users\": [], \"users_to_be_notified\": []}"
|
||||
},
|
||||
{
|
||||
"timestamp": "2023-08-18T16:34:38.580574Z",
|
||||
"url": "https://example.com",
|
||||
"request_trigger": "",
|
||||
"request_headers": null,
|
||||
"request_data": "Data - Template Warning: Object of type Undefined is not JSON serializable",
|
||||
"status_code": null,
|
||||
"content": null,
|
||||
"event_data": "{\"event\": {\"type\": \"acknowledge\", \"time\": \"2023-08-18T16:34:37.940655+00:00\"}, \"user\": {\"id\": \"UK49JJNPZMFLJ\", \"username\": \"oncall\", \"email\": \"admin@localhost\"}, \"alert_group\": {\"id\": \"IZQERPWKWCGH1\", \"integration_id\": \"CRV8A5MXC751A\", \"route_id\": \"RWNCT6C77M3WM\", \"alerts_count\": 1, \"state\": \"acknowledged\", \"created_at\": \"2023-08-18T16:34:27.678406Z\", \"resolved_at\": null, \"acknowledged_at\": \"2023-08-18T16:34:37.940655Z\", \"title\": \"[firing:2] InstanceDown \", \"permalinks\": {\"slack\": null, \"telegram\": null, \"web\": \"http://localhost:3000/a/grafana-oncall-app/alert-groups/IZQERPWKWCGH1\"}}, \"alert_group_id\": \"IZQERPWKWCGH1\", \"alert_payload\": {\"alerts\": [{\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"production\", \"instance\": \"localhost:8081\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8081 down\", \"description\": \"localhost:8081 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f404ecabc8dd5cd7\", \"generatorURL\": \"\"}, {\"endsAt\": \"0001-01-01T00:00:00Z\", \"labels\": {\"job\": \"node\", \"group\": \"canary\", \"instance\": \"localhost:8082\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"status\": \"firing\", \"startsAt\": \"2023-06-12T08:24:38.326Z\", \"annotations\": {\"title\": \"Instance localhost:8082 down\", \"description\": \"localhost:8082 of job node has been down for more than 1 minute.\"}, \"fingerprint\": \"f8f08d4e32c61a9d\", \"generatorURL\": \"\"}], \"status\": \"firing\", \"version\": \"4\", \"groupKey\": \"{}:{alertname=\\\"InstanceDown\\\"}\", \"receiver\": \"combo\", \"numFiring\": 2, \"externalURL\": \"\", \"groupLabels\": {\"alertname\": \"InstanceDown\"}, \"numResolved\": 0, \"commonLabels\": {\"job\": \"node\", \"severity\": \"critical\", \"alertname\": \"InstanceDown\"}, \"truncatedAlerts\": 0, \"commonAnnotations\": {}}, \"integration\": {\"id\": \"CRV8A5MXC751A\", \"type\": \"alertmanager\", \"name\": \"One - Alertmanager\", \"team\": null}, \"notified_users\": [], \"users_to_be_notified\": []}"
|
||||
}
|
||||
],
|
||||
"page_size": 50,
|
||||
"count": 2,
|
||||
"current_page_number": 1,
|
||||
"total_pages": 1
|
||||
}
|
||||
```
|
||||
|
|
|
|||
|
|
@ -371,7 +371,7 @@ To fix change the template to:
|
|||
|
||||
```json
|
||||
{
|
||||
"labels": "{{ alert_payload.labels | tojson()}}"
|
||||
"labels": {{ alert_payload.labels | tojson()}}
|
||||
}
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -44,7 +44,6 @@ class WebhookSerializer(serializers.ModelSerializer):
|
|||
"is_webhook_enabled",
|
||||
"is_legacy",
|
||||
"team",
|
||||
"data",
|
||||
"user",
|
||||
"username",
|
||||
"password",
|
||||
|
|
|
|||
|
|
@ -64,8 +64,8 @@ def test_get_list_webhooks(webhook_internal_api_setup, make_user_auth_headers):
|
|||
"event_data": "",
|
||||
},
|
||||
"trigger_template": None,
|
||||
"trigger_type": None,
|
||||
"trigger_type_name": "",
|
||||
"trigger_type": "0",
|
||||
"trigger_type_name": "Escalation step",
|
||||
}
|
||||
]
|
||||
|
||||
|
|
@ -106,8 +106,8 @@ def test_get_detail_webhook(webhook_internal_api_setup, make_user_auth_headers):
|
|||
"event_data": "",
|
||||
},
|
||||
"trigger_template": None,
|
||||
"trigger_type": None,
|
||||
"trigger_type_name": "",
|
||||
"trigger_type": "0",
|
||||
"trigger_type_name": "Escalation step",
|
||||
}
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ from apps.webhooks.utils import apply_jinja_template_for_json
|
|||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.filters import ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, TeamModelMultipleChoiceFilter
|
||||
from common.api_helpers.mixins import PublicPrimaryKeyMixin, TeamFilteringMixin
|
||||
from common.insight_log import EntityEvent, write_resource_insight_log
|
||||
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
|
||||
|
||||
NEW_WEBHOOK_PK = "new"
|
||||
|
|
@ -60,6 +61,30 @@ class WebhooksView(TeamFilteringMixin, PublicPrimaryKeyMixin, ModelViewSet):
|
|||
search_fields = ["public_primary_key", "name"]
|
||||
filterset_class = WebhooksFilter
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
write_resource_insight_log(instance=serializer.instance, author=self.request.user, event=EntityEvent.CREATED)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
prev_state = serializer.instance.insight_logs_serialized
|
||||
serializer.save()
|
||||
new_state = serializer.instance.insight_logs_serialized
|
||||
write_resource_insight_log(
|
||||
instance=serializer.instance,
|
||||
author=self.request.user,
|
||||
event=EntityEvent.UPDATED,
|
||||
prev_state=prev_state,
|
||||
new_state=new_state,
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
write_resource_insight_log(
|
||||
instance=instance,
|
||||
author=self.request.user,
|
||||
event=EntityEvent.DELETED,
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
def get_queryset(self, ignore_filtering_by_available_teams=False):
|
||||
queryset = Webhook.objects.filter(
|
||||
organization=self.request.auth.organization,
|
||||
|
|
|
|||
|
|
@ -1,106 +1,64 @@
|
|||
import json
|
||||
from collections import defaultdict
|
||||
|
||||
from django.core.validators import URLValidator, ValidationError
|
||||
from jinja2 import TemplateError
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
from apps.public_api.serializers.webhooks import WebhookCreateSerializer, WebhookTriggerTypeField
|
||||
from apps.webhooks.models import Webhook
|
||||
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
|
||||
from common.api_helpers.utils import CurrentOrganizationDefault
|
||||
from common.jinja_templater import jinja_template_env
|
||||
from common.api_helpers.utils import CurrentTeamDefault
|
||||
|
||||
|
||||
class ActionCreateSerializer(serializers.ModelSerializer):
|
||||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
|
||||
team_id = TeamPrimaryKeyRelatedField(required=False, allow_null=True, source="team")
|
||||
url = serializers.CharField(required=True, allow_null=False, allow_blank=False, source="webhook")
|
||||
class ActionCreateSerializer(WebhookCreateSerializer):
|
||||
team_id = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault(), source="team")
|
||||
user = serializers.CharField(required=False, source="username")
|
||||
trigger_type = WebhookTriggerTypeField(required=False)
|
||||
forward_whole_payload = serializers.BooleanField(required=False, source="forward_all")
|
||||
|
||||
class Meta:
|
||||
model = CustomButton
|
||||
model = Webhook
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"is_webhook_enabled",
|
||||
"organization",
|
||||
"team_id",
|
||||
"url",
|
||||
"user",
|
||||
"data",
|
||||
"user",
|
||||
"password",
|
||||
"authorization_header",
|
||||
"trigger_template",
|
||||
"headers",
|
||||
"url",
|
||||
"forward_whole_payload",
|
||||
"http_method",
|
||||
"trigger_type",
|
||||
"integration_filter",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"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},
|
||||
"url": {"required": True, "allow_null": False, "allow_blank": False},
|
||||
}
|
||||
|
||||
validators = [UniqueTogetherValidator(queryset=CustomButton.objects.all(), fields=["name", "organization"])]
|
||||
|
||||
def validate_url(self, url):
|
||||
if url:
|
||||
try:
|
||||
URLValidator()(url)
|
||||
except ValidationError:
|
||||
raise serializers.ValidationError("URL is incorrect")
|
||||
return url
|
||||
return None
|
||||
|
||||
def validate_data(self, data):
|
||||
if not data:
|
||||
return None
|
||||
|
||||
try:
|
||||
template = jinja_template_env.from_string(data)
|
||||
except TemplateError:
|
||||
raise serializers.ValidationError("Data has incorrect template")
|
||||
|
||||
try:
|
||||
rendered = template.render(
|
||||
{
|
||||
# Validate that the template can be rendered with a JSON-ish alert payload.
|
||||
# We don't know what the actual payload will be, so we use a defaultdict
|
||||
# so that attribute access within a template will never fail
|
||||
# (provided it's only one level deep - we won't accept templates that attempt
|
||||
# to do nested attribute access).
|
||||
# Every attribute access should return a string to ensure that users are
|
||||
# correctly using `tojson` or wrapping fields in strings.
|
||||
# If we instead used a `defaultdict(dict)` or `defaultdict(lambda: 1)` we
|
||||
# would accidentally accept templates such as `{"name": {{ alert_payload.name }}}`
|
||||
# which would then fail at the true render time due to the
|
||||
# lack of explicit quotes around the template variable; this would render
|
||||
# as `{"name": some_alert_name}` which is not valid JSON.
|
||||
"alert_payload": defaultdict(str),
|
||||
"alert_group_id": "abcd",
|
||||
}
|
||||
)
|
||||
json.loads(rendered)
|
||||
except ValueError:
|
||||
raise serializers.ValidationError("Data has incorrect format")
|
||||
|
||||
return data
|
||||
|
||||
def validate_forward_whole_payload(self, data):
|
||||
if data is None:
|
||||
return False
|
||||
return data
|
||||
validators = [UniqueTogetherValidator(queryset=Webhook.objects.all(), fields=["name", "organization"])]
|
||||
|
||||
|
||||
class ActionUpdateSerializer(ActionCreateSerializer):
|
||||
url = serializers.CharField(required=False, allow_null=False, allow_blank=False, source="webhook")
|
||||
user = serializers.CharField(required=False, source="username")
|
||||
trigger_type = WebhookTriggerTypeField(required=False)
|
||||
forward_whole_payload = serializers.BooleanField(required=False, source="forward_all")
|
||||
|
||||
class Meta(ActionCreateSerializer.Meta):
|
||||
extra_kwargs = {
|
||||
"name": {"required": False, "allow_null": False, "allow_blank": False},
|
||||
"data": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"is_webhook_enabled": {"required": False, "allow_null": 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},
|
||||
"trigger_template": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"headers": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"url": {"required": False, "allow_null": False, "allow_blank": False},
|
||||
"data": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"forward_whole_payload": {"required": False, "allow_null": False},
|
||||
"http_method": {"required": False, "allow_null": False, "allow_blank": False},
|
||||
"integration_filter": {"required": False, "allow_null": True},
|
||||
}
|
||||
|
|
|
|||
170
engine/apps/public_api/serializers/webhooks.py
Normal file
170
engine/apps/public_api/serializers/webhooks.py
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from rest_framework import fields, serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from apps.alerts.models import AlertReceiveChannel
|
||||
from apps.webhooks.models import Webhook, WebhookResponse
|
||||
from apps.webhooks.models.webhook import PUBLIC_WEBHOOK_HTTP_METHODS, WEBHOOK_FIELD_PLACEHOLDER
|
||||
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
|
||||
from common.api_helpers.exceptions import BadRequest
|
||||
from common.api_helpers.utils import CurrentOrganizationDefault, CurrentTeamDefault, CurrentUserDefault
|
||||
from common.jinja_templater import apply_jinja_template
|
||||
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
|
||||
|
||||
INTEGRATION_FILTER_MESSAGE = "integration_filter must be a list of valid integration ids"
|
||||
|
||||
|
||||
class WebhookTriggerTypeField(fields.CharField):
|
||||
def to_representation(self, value):
|
||||
return Webhook.PUBLIC_TRIGGER_TYPES_MAP[value]
|
||||
|
||||
def to_internal_value(self, data):
|
||||
try:
|
||||
trigger_type = [
|
||||
key
|
||||
for key, value in Webhook.PUBLIC_TRIGGER_TYPES_MAP.items()
|
||||
if value == data and key in Webhook.PUBLIC_TRIGGER_TYPES_MAP
|
||||
][0]
|
||||
except IndexError:
|
||||
raise BadRequest(detail=f"trigger_type must one of {Webhook.PUBLIC_ALL_TRIGGER_TYPES}")
|
||||
return trigger_type
|
||||
|
||||
|
||||
class WebhookResponseSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = WebhookResponse
|
||||
fields = [
|
||||
"timestamp",
|
||||
"url",
|
||||
"request_trigger",
|
||||
"request_headers",
|
||||
"request_data",
|
||||
"status_code",
|
||||
"content",
|
||||
"event_data",
|
||||
]
|
||||
|
||||
|
||||
class WebhookCreateSerializer(serializers.ModelSerializer):
|
||||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
|
||||
team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault())
|
||||
user = serializers.HiddenField(default=CurrentUserDefault())
|
||||
trigger_type = WebhookTriggerTypeField()
|
||||
|
||||
class Meta:
|
||||
model = Webhook
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"is_webhook_enabled",
|
||||
"organization",
|
||||
"team",
|
||||
"user",
|
||||
"data",
|
||||
"username",
|
||||
"password",
|
||||
"authorization_header",
|
||||
"trigger_template",
|
||||
"headers",
|
||||
"url",
|
||||
"forward_all",
|
||||
"http_method",
|
||||
"trigger_type",
|
||||
"integration_filter",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"required": True, "allow_null": False, "allow_blank": False},
|
||||
"url": {"required": True, "allow_null": False, "allow_blank": False},
|
||||
"http_method": {"required": True, "allow_null": False, "allow_blank": False},
|
||||
}
|
||||
|
||||
validators = [UniqueTogetherValidator(queryset=Webhook.objects.all(), fields=["name", "organization"])]
|
||||
|
||||
def to_representation(self, instance):
|
||||
result = super().to_representation(instance)
|
||||
if instance.password:
|
||||
result["password"] = WEBHOOK_FIELD_PLACEHOLDER
|
||||
if instance.authorization_header:
|
||||
result["authorization_header"] = WEBHOOK_FIELD_PLACEHOLDER
|
||||
return result
|
||||
|
||||
def to_internal_value(self, data):
|
||||
webhook = self.instance
|
||||
if data.get("password") == WEBHOOK_FIELD_PLACEHOLDER:
|
||||
data["password"] = webhook.password
|
||||
if data.get("authorization_header") == WEBHOOK_FIELD_PLACEHOLDER:
|
||||
data["authorization_header"] = webhook.authorization_header
|
||||
return super().to_internal_value(data)
|
||||
|
||||
def _validate_template_field(self, template):
|
||||
try:
|
||||
apply_jinja_template(template, alert_payload=defaultdict(str), alert_group_id="alert_group_1")
|
||||
except JinjaTemplateError as e:
|
||||
raise serializers.ValidationError(e.fallback_message)
|
||||
except JinjaTemplateWarning:
|
||||
# Suppress render exceptions since we do not have a representative payload to test with
|
||||
pass
|
||||
return template
|
||||
|
||||
def validate_trigger_template(self, trigger_template):
|
||||
if not trigger_template:
|
||||
return None
|
||||
return self._validate_template_field(trigger_template)
|
||||
|
||||
def validate_headers(self, headers):
|
||||
if not headers:
|
||||
return None
|
||||
return self._validate_template_field(headers)
|
||||
|
||||
def validate_url(self, url):
|
||||
if not url:
|
||||
return None
|
||||
return self._validate_template_field(url)
|
||||
|
||||
def validate_data(self, data):
|
||||
if not data:
|
||||
return None
|
||||
return self._validate_template_field(data)
|
||||
|
||||
def validate_forward_all(self, data):
|
||||
if data is None:
|
||||
return False
|
||||
return data
|
||||
|
||||
def validate_http_method(self, http_method):
|
||||
if http_method not in PUBLIC_WEBHOOK_HTTP_METHODS:
|
||||
raise serializers.ValidationError(f"Must be one of {PUBLIC_WEBHOOK_HTTP_METHODS}")
|
||||
return http_method
|
||||
|
||||
def validate_integration_filter(self, integration_filter):
|
||||
if integration_filter:
|
||||
if type(integration_filter) is not list:
|
||||
raise serializers.ValidationError(INTEGRATION_FILTER_MESSAGE)
|
||||
integrations = AlertReceiveChannel.objects.filter(
|
||||
organization=self.context["request"].auth.organization, public_primary_key__in=integration_filter
|
||||
)
|
||||
if len(integrations) != len(integration_filter):
|
||||
raise serializers.ValidationError(INTEGRATION_FILTER_MESSAGE)
|
||||
return integration_filter
|
||||
|
||||
|
||||
class WebhookUpdateSerializer(WebhookCreateSerializer):
|
||||
trigger_type = WebhookTriggerTypeField(required=False)
|
||||
|
||||
class Meta(WebhookCreateSerializer.Meta):
|
||||
extra_kwargs = {
|
||||
"name": {"required": False, "allow_null": False, "allow_blank": False},
|
||||
"is_webhook_enabled": {"required": False, "allow_null": False},
|
||||
"username": {"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},
|
||||
"trigger_template": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"headers": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"url": {"required": False, "allow_null": False, "allow_blank": False},
|
||||
"data": {"required": False, "allow_null": True, "allow_blank": False},
|
||||
"forward_all": {"required": False, "allow_null": False},
|
||||
"http_method": {"required": False, "allow_null": False, "allow_blank": False},
|
||||
"integration_filter": {"required": False, "allow_null": True},
|
||||
}
|
||||
|
|
@ -3,18 +3,15 @@ from django.urls import reverse
|
|||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
from apps.webhooks.models import Webhook
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_custom_actions(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
):
|
||||
def test_get_custom_actions(make_organization_and_user_with_token, make_custom_webhook):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
custom_action = make_custom_action(organization=organization)
|
||||
custom_action = make_custom_webhook(organization=organization)
|
||||
|
||||
url = reverse("api-public:actions-list")
|
||||
|
||||
|
|
@ -29,12 +26,18 @@ def test_get_custom_actions(
|
|||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"url": custom_action.webhook,
|
||||
"url": custom_action.url,
|
||||
"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,
|
||||
"forward_whole_payload": custom_action.forward_all,
|
||||
"is_webhook_enabled": custom_action.is_webhook_enabled,
|
||||
"trigger_template": custom_action.trigger_template,
|
||||
"headers": custom_action.headers,
|
||||
"http_method": custom_action.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[custom_action.trigger_type],
|
||||
"integration_filter": custom_action.integration_filter,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
|
|
@ -49,13 +52,13 @@ def test_get_custom_actions(
|
|||
@pytest.mark.django_db
|
||||
def test_get_custom_actions_filter_by_name(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
custom_action = make_custom_action(organization=organization)
|
||||
make_custom_action(organization=organization)
|
||||
custom_action = make_custom_webhook(organization=organization)
|
||||
make_custom_webhook(organization=organization)
|
||||
url = reverse("api-public:actions-list")
|
||||
|
||||
response = client.get(f"{url}?name={custom_action.name}", format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
|
@ -69,12 +72,18 @@ def test_get_custom_actions_filter_by_name(
|
|||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"url": custom_action.webhook,
|
||||
"url": custom_action.url,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"user": custom_action.username,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
"forward_whole_payload": custom_action.forward_all,
|
||||
"is_webhook_enabled": custom_action.is_webhook_enabled,
|
||||
"trigger_template": custom_action.trigger_template,
|
||||
"headers": custom_action.headers,
|
||||
"http_method": custom_action.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[custom_action.trigger_type],
|
||||
"integration_filter": custom_action.integration_filter,
|
||||
}
|
||||
],
|
||||
"current_page_number": 1,
|
||||
|
|
@ -89,12 +98,12 @@ def test_get_custom_actions_filter_by_name(
|
|||
@pytest.mark.django_db
|
||||
def test_get_custom_actions_filter_by_name_empty_result(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
make_custom_action(organization=organization)
|
||||
make_custom_webhook(organization=organization)
|
||||
|
||||
url = reverse("api-public:actions-list")
|
||||
|
||||
|
|
@ -117,12 +126,12 @@ def test_get_custom_actions_filter_by_name_empty_result(
|
|||
@pytest.mark.django_db
|
||||
def test_get_custom_action(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
custom_action = make_custom_action(organization=organization)
|
||||
custom_action = make_custom_webhook(organization=organization)
|
||||
|
||||
url = reverse("api-public:actions-detail", kwargs={"pk": custom_action.public_primary_key})
|
||||
|
||||
|
|
@ -132,12 +141,18 @@ def test_get_custom_action(
|
|||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"url": custom_action.webhook,
|
||||
"url": custom_action.url,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"user": custom_action.username,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
"forward_whole_payload": custom_action.forward_all,
|
||||
"is_webhook_enabled": custom_action.is_webhook_enabled,
|
||||
"trigger_template": custom_action.trigger_template,
|
||||
"headers": custom_action.headers,
|
||||
"http_method": custom_action.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[custom_action.trigger_type],
|
||||
"integration_filter": custom_action.integration_filter,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -158,18 +173,24 @@ def test_create_custom_action(make_organization_and_user_with_token):
|
|||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
custom_action = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
custom_action = Webhook.objects.get(public_primary_key=response.data["id"])
|
||||
|
||||
expected_result = {
|
||||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"url": custom_action.webhook,
|
||||
"url": custom_action.url,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"user": custom_action.username,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
"forward_whole_payload": custom_action.forward_all,
|
||||
"is_webhook_enabled": custom_action.is_webhook_enabled,
|
||||
"trigger_template": custom_action.trigger_template,
|
||||
"headers": custom_action.headers,
|
||||
"http_method": custom_action.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[custom_action.trigger_type],
|
||||
"integration_filter": custom_action.integration_filter,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
|
@ -195,18 +216,24 @@ def test_create_custom_action_nested_data(make_organization_and_user_with_token)
|
|||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
custom_action = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
custom_action = Webhook.objects.get(public_primary_key=response.data["id"])
|
||||
|
||||
expected_result = {
|
||||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"url": custom_action.webhook,
|
||||
"url": custom_action.url,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"user": custom_action.username,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
"forward_whole_payload": custom_action.forward_all,
|
||||
"is_webhook_enabled": custom_action.is_webhook_enabled,
|
||||
"trigger_template": custom_action.trigger_template,
|
||||
"headers": custom_action.headers,
|
||||
"http_method": custom_action.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[custom_action.trigger_type],
|
||||
"integration_filter": custom_action.integration_filter,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
|
@ -232,18 +259,24 @@ def test_create_custom_action_valid_after_render(make_organization_and_user_with
|
|||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
custom_action = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
custom_action = Webhook.objects.get(public_primary_key=response.data["id"])
|
||||
|
||||
expected_result = {
|
||||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"url": custom_action.webhook,
|
||||
"url": custom_action.url,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"user": custom_action.username,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
"forward_whole_payload": custom_action.forward_all,
|
||||
"is_webhook_enabled": custom_action.is_webhook_enabled,
|
||||
"trigger_template": custom_action.trigger_template,
|
||||
"headers": custom_action.headers,
|
||||
"http_method": custom_action.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[custom_action.trigger_type],
|
||||
"integration_filter": custom_action.integration_filter,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
|
|
@ -269,94 +302,39 @@ def test_create_custom_action_valid_after_render_use_all_data(make_organization_
|
|||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
custom_action = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
custom_action = Webhook.objects.get(public_primary_key=response.data["id"])
|
||||
|
||||
expected_result = {
|
||||
"id": custom_action.public_primary_key,
|
||||
"name": custom_action.name,
|
||||
"team_id": None,
|
||||
"url": custom_action.webhook,
|
||||
"url": custom_action.url,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"user": custom_action.username,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
"forward_whole_payload": custom_action.forward_all,
|
||||
"is_webhook_enabled": custom_action.is_webhook_enabled,
|
||||
"trigger_template": custom_action.trigger_template,
|
||||
"headers": custom_action.headers,
|
||||
"http_method": custom_action.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[custom_action.trigger_type],
|
||||
"integration_filter": custom_action.integration_filter,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == 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",
|
||||
"url": "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["url"][0] == "URL 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["url"][0] == "This field is required."
|
||||
|
||||
data = {
|
||||
"url": "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."
|
||||
|
||||
data = {
|
||||
"name": "Test outgoing webhook",
|
||||
"url": "https://example.com",
|
||||
"data": "invalid_json",
|
||||
}
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.data["data"][0] == "Data has incorrect format"
|
||||
|
||||
data = {
|
||||
"name": "Test outgoing webhook",
|
||||
"url": "https://example.com",
|
||||
# This would need a `| tojson` or some double quotes around it to pass validation.
|
||||
"data": "{{ alert_payload.name }}",
|
||||
}
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
assert response.data["data"][0] == "Data has incorrect format"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_custom_action(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
custom_action = make_custom_action(organization=organization)
|
||||
custom_action = make_custom_webhook(organization=organization)
|
||||
|
||||
url = reverse("api-public:actions-detail", kwargs={"pk": custom_action.public_primary_key})
|
||||
|
||||
|
|
@ -372,12 +350,18 @@ def test_update_custom_action(
|
|||
"id": custom_action.public_primary_key,
|
||||
"name": data["name"],
|
||||
"team_id": None,
|
||||
"url": custom_action.webhook,
|
||||
"url": custom_action.url,
|
||||
"data": custom_action.data,
|
||||
"user": custom_action.user,
|
||||
"user": custom_action.username,
|
||||
"password": custom_action.password,
|
||||
"authorization_header": custom_action.authorization_header,
|
||||
"forward_whole_payload": custom_action.forward_whole_payload,
|
||||
"forward_whole_payload": custom_action.forward_all,
|
||||
"is_webhook_enabled": custom_action.is_webhook_enabled,
|
||||
"trigger_template": custom_action.trigger_template,
|
||||
"headers": custom_action.headers,
|
||||
"http_method": custom_action.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[custom_action.trigger_type],
|
||||
"integration_filter": custom_action.integration_filter,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
|
@ -389,12 +373,12 @@ def test_update_custom_action(
|
|||
@pytest.mark.django_db
|
||||
def test_delete_custom_action(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
custom_action = make_custom_action(organization=organization)
|
||||
custom_action = make_custom_webhook(organization=organization)
|
||||
url = reverse("api-public:actions-detail", kwargs={"pk": custom_action.public_primary_key})
|
||||
|
||||
assert custom_action.deleted_at is None
|
||||
|
|
|
|||
320
engine/apps/public_api/tests/test_webhooks.py
Normal file
320
engine/apps/public_api/tests/test_webhooks.py
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
import json
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.webhooks.models import Webhook
|
||||
|
||||
|
||||
def _get_expected_result(webhook):
|
||||
return {
|
||||
"id": webhook.public_primary_key,
|
||||
"name": webhook.name,
|
||||
"team": webhook.team,
|
||||
"url": webhook.url,
|
||||
"data": webhook.data,
|
||||
"username": webhook.username,
|
||||
"password": webhook.password,
|
||||
"authorization_header": webhook.authorization_header,
|
||||
"forward_all": webhook.forward_all,
|
||||
"is_webhook_enabled": webhook.is_webhook_enabled,
|
||||
"trigger_template": webhook.trigger_template,
|
||||
"headers": webhook.headers,
|
||||
"http_method": webhook.http_method,
|
||||
"trigger_type": Webhook.PUBLIC_TRIGGER_TYPES_MAP[webhook.trigger_type],
|
||||
"integration_filter": webhook.integration_filter,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_webhooks(make_organization_and_user_with_token, make_custom_webhook):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
webhook = make_custom_webhook(organization=organization)
|
||||
|
||||
url = reverse("api-public:webhooks-list")
|
||||
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_payload = {
|
||||
"count": 1,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [_get_expected_result(webhook)],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_webhooks_filter_by_name(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
webhook = make_custom_webhook(organization=organization)
|
||||
make_custom_webhook(organization=organization)
|
||||
url = reverse("api-public:webhooks-list")
|
||||
|
||||
response = client.get(f"{url}?name={webhook.name}", format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_payload = {
|
||||
"count": 1,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [_get_expected_result(webhook)],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_webhooks_filter_by_name_empty_result(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
make_custom_webhook(organization=organization)
|
||||
|
||||
url = reverse("api-public:webhooks-list")
|
||||
|
||||
response = client.get(f"{url}?name=NonExistentName", format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_payload = {
|
||||
"count": 0,
|
||||
"next": None,
|
||||
"previous": None,
|
||||
"results": [],
|
||||
"current_page_number": 1,
|
||||
"page_size": 50,
|
||||
"total_pages": 1,
|
||||
}
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_webhook(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
webhook = make_custom_webhook(organization=organization)
|
||||
|
||||
url = reverse("api-public:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
||||
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_payload = _get_expected_result(webhook)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.data == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_webhook(make_organization_and_user_with_token):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-public:webhooks-list")
|
||||
|
||||
data = {}
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
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
|
||||
data["url"] = "https://example.com"
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data["trigger_type"] = "escalation"
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data["http_method"] = "POST"
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
webhook = Webhook.objects.get(public_primary_key=response.data["id"])
|
||||
|
||||
expected_result = _get_expected_result(webhook)
|
||||
|
||||
assert response.data == expected_result
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_webhook_nested_data(make_organization_and_user_with_token):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-public:webhooks-list")
|
||||
|
||||
data = {
|
||||
"name": "Test outgoing webhook with nested data",
|
||||
"url": "https://example.com",
|
||||
"data": '{"nested_item": "{{ alert_payload.foo.bar | to_json }}"}',
|
||||
"http_method": "POST",
|
||||
"trigger_type": "acknowledge",
|
||||
}
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
data["data"] = '{"nested_item": "{{ alert_payload.foo.bar | tojson() }}"}'
|
||||
|
||||
response = client.post(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
webhook = Webhook.objects.get(public_primary_key=response.data["id"])
|
||||
|
||||
expected_result = _get_expected_result(webhook)
|
||||
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == expected_result
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_webhook(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
webhook = make_custom_webhook(organization=organization)
|
||||
url = reverse("api-public:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
||||
data = {
|
||||
"name": "RENAMED",
|
||||
}
|
||||
assert webhook.name != data["name"]
|
||||
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
|
||||
expected_result = _get_expected_result(webhook)
|
||||
expected_result["name"] = data["name"]
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
webhook.refresh_from_db()
|
||||
assert webhook.name == expected_result["name"]
|
||||
assert response.data == expected_result
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_webhook(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_webhook,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
webhook = make_custom_webhook(organization=organization)
|
||||
url = reverse("api-public:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
||||
|
||||
assert webhook.deleted_at is None
|
||||
|
||||
response = client.delete(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
webhook.refresh_from_db()
|
||||
assert webhook.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."
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_webhook_responses(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_webhook,
|
||||
make_webhook_response,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
client = APIClient()
|
||||
|
||||
webhook = make_custom_webhook(organization=organization)
|
||||
webhook.refresh_from_db()
|
||||
|
||||
response_count = 20
|
||||
for i in range(0, response_count):
|
||||
make_webhook_response(
|
||||
webhook=webhook,
|
||||
trigger_type=webhook.trigger_type,
|
||||
status_code=200,
|
||||
content=json.dumps({"id": "third-party-id"}),
|
||||
event_data=json.dumps({"test": "abc"}),
|
||||
)
|
||||
|
||||
url = reverse("api-public:webhooks-responses", kwargs={"pk": webhook.public_primary_key})
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
webhook_response = response.data["results"][0]
|
||||
assert webhook_response["status_code"] == 200
|
||||
assert webhook_response["content"] == '{"id": "third-party-id"}'
|
||||
assert webhook_response["event_data"] == '{"test": "abc"}'
|
||||
assert response.data["count"] == 20
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_webhook_validate_integration_filters(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_webhook,
|
||||
make_alert_receive_channel,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
webhook = make_custom_webhook(organization=organization)
|
||||
url = reverse("api-public:webhooks-detail", kwargs={"pk": webhook.public_primary_key})
|
||||
data = {"integration_filter": alert_receive_channel.public_primary_key}
|
||||
|
||||
client = APIClient()
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == 400
|
||||
|
||||
data["integration_filter"] = ["abc"]
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == 400
|
||||
|
||||
data["integration_filter"] = [alert_receive_channel.public_primary_key, alert_receive_channel.public_primary_key]
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
assert response.status_code == 400
|
||||
|
||||
data["integration_filter"] = [alert_receive_channel.public_primary_key]
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
webhook.refresh_from_db()
|
||||
assert response.status_code == 200
|
||||
assert response.data["integration_filter"] == data["integration_filter"]
|
||||
assert webhook.integration_filter == data["integration_filter"]
|
||||
|
||||
data["integration_filter"] = []
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
webhook.refresh_from_db()
|
||||
assert response.status_code == 200
|
||||
assert response.data["integration_filter"] == data["integration_filter"]
|
||||
assert webhook.integration_filter == data["integration_filter"]
|
||||
|
||||
data["integration_filter"] = None
|
||||
response = client.put(url, data=data, format="json", HTTP_AUTHORIZATION=f"{token}")
|
||||
webhook.refresh_from_db()
|
||||
assert response.status_code == 200
|
||||
assert response.data["integration_filter"] == data["integration_filter"]
|
||||
assert webhook.integration_filter == data["integration_filter"]
|
||||
|
|
@ -26,6 +26,7 @@ router.register(r"user_groups", views.UserGroupView, basename="user_groups")
|
|||
router.register(r"on_call_shifts", views.CustomOnCallShiftView, basename="on_call_shifts")
|
||||
router.register(r"teams", views.TeamView, basename="teams")
|
||||
router.register(r"shift_swaps", views.ShiftSwapViewSet, basename="shift_swap")
|
||||
router.register(r"webhooks", views.WebhooksView, basename="webhooks")
|
||||
|
||||
|
||||
urlpatterns = [
|
||||
|
|
|
|||
|
|
@ -17,3 +17,4 @@ from .slack_channels import SlackChannelView # noqa: F401
|
|||
from .teams import TeamView # noqa: F401
|
||||
from .user_groups import UserGroupView # noqa: F401
|
||||
from .users import UserView # noqa: F401
|
||||
from .webhooks import WebhooksView # noqa: F401
|
||||
|
|
|
|||
|
|
@ -2,10 +2,11 @@ from django_filters import rest_framework as filters
|
|||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
from apps.api.serializers.webhook import WebhookSerializer
|
||||
from apps.auth_token.auth import ApiTokenAuthentication
|
||||
from apps.public_api.serializers.action import ActionCreateSerializer, ActionUpdateSerializer
|
||||
from apps.public_api.throttlers.user_throttle import UserThrottle
|
||||
from apps.webhooks.models import Webhook
|
||||
from common.api_helpers.filters import ByTeamFilter
|
||||
from common.api_helpers.mixins import PublicPrimaryKeyMixin, RateLimitHeadersMixin, UpdateSerializerMixin
|
||||
from common.api_helpers.paginators import FiftyPageSizePaginator
|
||||
|
|
@ -18,7 +19,7 @@ class ActionView(RateLimitHeadersMixin, PublicPrimaryKeyMixin, UpdateSerializerM
|
|||
pagination_class = FiftyPageSizePaginator
|
||||
throttle_classes = [UserThrottle]
|
||||
|
||||
model = CustomButton
|
||||
model = WebhookSerializer
|
||||
serializer_class = ActionCreateSerializer
|
||||
update_serializer_class = ActionUpdateSerializer
|
||||
|
||||
|
|
@ -27,7 +28,7 @@ class ActionView(RateLimitHeadersMixin, PublicPrimaryKeyMixin, UpdateSerializerM
|
|||
|
||||
def get_queryset(self):
|
||||
action_name = self.request.query_params.get("name", None)
|
||||
queryset = CustomButton.objects.filter(organization=self.request.auth.organization)
|
||||
queryset = Webhook.objects.filter(organization=self.request.auth.organization)
|
||||
|
||||
if action_name:
|
||||
queryset = queryset.filter(name=action_name)
|
||||
|
|
|
|||
94
engine/apps/public_api/views/webhooks.py
Normal file
94
engine/apps/public_api/views/webhooks.py
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
from django_filters import rest_framework as filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from apps.auth_token.auth import ApiTokenAuthentication
|
||||
from apps.public_api.serializers.webhooks import (
|
||||
WebhookCreateSerializer,
|
||||
WebhookResponseSerializer,
|
||||
WebhookUpdateSerializer,
|
||||
)
|
||||
from apps.public_api.throttlers import UserThrottle
|
||||
from apps.webhooks.models import Webhook, WebhookResponse
|
||||
from common.api_helpers.filters import ByTeamFilter
|
||||
from common.api_helpers.mixins import RateLimitHeadersMixin, UpdateSerializerMixin
|
||||
from common.api_helpers.paginators import FiftyPageSizePaginator
|
||||
from common.insight_log import EntityEvent, write_resource_insight_log
|
||||
|
||||
|
||||
class WebhooksView(RateLimitHeadersMixin, UpdateSerializerMixin, ModelViewSet):
|
||||
authentication_classes = (ApiTokenAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
pagination_class = FiftyPageSizePaginator
|
||||
throttle_classes = [UserThrottle]
|
||||
|
||||
model = Webhook
|
||||
serializer_class = WebhookCreateSerializer
|
||||
update_serializer_class = WebhookUpdateSerializer
|
||||
|
||||
filter_backends = (filters.DjangoFilterBackend,)
|
||||
filterset_class = ByTeamFilter
|
||||
|
||||
def get_queryset(self):
|
||||
webhook_name = self.request.query_params.get("name", None)
|
||||
queryset = Webhook.objects.filter(organization=self.request.auth.organization)
|
||||
|
||||
if webhook_name:
|
||||
queryset = queryset.filter(name=webhook_name)
|
||||
|
||||
return queryset.order_by("id")
|
||||
|
||||
def get_object(self):
|
||||
public_primary_key = self.kwargs["pk"]
|
||||
|
||||
try:
|
||||
return Webhook.objects.filter(organization=self.request.auth.organization).get(
|
||||
public_primary_key=public_primary_key
|
||||
)
|
||||
except Webhook.DoesNotExist:
|
||||
raise NotFound
|
||||
|
||||
def perform_create(self, serializer):
|
||||
serializer.save()
|
||||
write_resource_insight_log(
|
||||
instance=serializer.instance,
|
||||
author=self.request.user,
|
||||
event=EntityEvent.CREATED,
|
||||
)
|
||||
|
||||
def perform_update(self, serializer):
|
||||
prev_state = serializer.instance.insight_logs_serialized
|
||||
serializer.save()
|
||||
new_state = serializer.instance.insight_logs_serialized
|
||||
write_resource_insight_log(
|
||||
instance=serializer.instance,
|
||||
author=self.request.user,
|
||||
event=EntityEvent.UPDATED,
|
||||
prev_state=prev_state,
|
||||
new_state=new_state,
|
||||
)
|
||||
|
||||
def perform_destroy(self, instance):
|
||||
write_resource_insight_log(
|
||||
instance=instance,
|
||||
author=self.request.user,
|
||||
event=EntityEvent.DELETED,
|
||||
)
|
||||
instance.delete()
|
||||
|
||||
@action(methods=["get"], detail=True)
|
||||
def responses(self, request, pk):
|
||||
webhook = self.get_object()
|
||||
queryset = WebhookResponse.objects.filter(webhook_id=webhook.id, trigger_type=webhook.trigger_type).order_by(
|
||||
"-timestamp"
|
||||
)
|
||||
page = self.paginate_queryset(queryset)
|
||||
if page is not None:
|
||||
response_serializer = WebhookResponseSerializer(page, many=True)
|
||||
return self.get_paginated_response(response_serializer.data)
|
||||
|
||||
response_serializer = WebhookResponseSerializer(queryset, many=True)
|
||||
return Response(response_serializer.data)
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 3.2.20 on 2023-08-14 22:50
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('webhooks', '0009_alter_webhook_authorization_header'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='webhook',
|
||||
name='trigger_type',
|
||||
field=models.IntegerField(choices=[(0, 'Escalation step'), (1, 'Alert Group Created'), (2, 'Acknowledged'), (3, 'Resolved'), (4, 'Silenced'), (5, 'Unsilenced'), (6, 'Unresolved'), (7, 'Unacknowledged')], default=0, null=True),
|
||||
),
|
||||
]
|
||||
|
|
@ -1,8 +1,10 @@
|
|||
import json
|
||||
import logging
|
||||
import typing
|
||||
from json import JSONDecodeError
|
||||
|
||||
import requests
|
||||
from celery.utils.log import get_task_logger
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
|
|
@ -30,6 +32,10 @@ if typing.TYPE_CHECKING:
|
|||
from apps.alerts.models import EscalationPolicy
|
||||
|
||||
WEBHOOK_FIELD_PLACEHOLDER = "****************"
|
||||
PUBLIC_WEBHOOK_HTTP_METHODS = ["GET", "POST", "PUT", "DELETE", "OPTIONS"]
|
||||
|
||||
logger = get_task_logger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def generate_public_primary_key_for_webhook():
|
||||
|
|
@ -88,6 +94,19 @@ class Webhook(models.Model):
|
|||
(TRIGGER_UNACKNOWLEDGE, "Unacknowledged"),
|
||||
)
|
||||
|
||||
PUBLIC_TRIGGER_TYPES_MAP = {
|
||||
TRIGGER_ESCALATION_STEP: "escalation",
|
||||
TRIGGER_ALERT_GROUP_CREATED: "alert group created",
|
||||
TRIGGER_ACKNOWLEDGE: "acknowledge",
|
||||
TRIGGER_RESOLVE: "resolve",
|
||||
TRIGGER_SILENCE: "silence",
|
||||
TRIGGER_UNSILENCE: "unsilence",
|
||||
TRIGGER_UNRESOLVE: "unresolve",
|
||||
TRIGGER_UNACKNOWLEDGE: "unacknowledge",
|
||||
}
|
||||
|
||||
PUBLIC_ALL_TRIGGER_TYPES = [i for i in PUBLIC_TRIGGER_TYPES_MAP.values()]
|
||||
|
||||
public_primary_key = models.CharField(
|
||||
max_length=20,
|
||||
validators=[MinLengthValidator(settings.PUBLIC_PRIMARY_KEY_MIN_LENGTH + 1)],
|
||||
|
|
@ -119,7 +138,7 @@ class Webhook(models.Model):
|
|||
data = models.TextField(null=True, default=None)
|
||||
forward_all = models.BooleanField(default=True)
|
||||
http_method = models.CharField(max_length=32, default="POST")
|
||||
trigger_type = models.IntegerField(choices=TRIGGER_TYPES, default=None, null=True)
|
||||
trigger_type = models.IntegerField(choices=TRIGGER_TYPES, default=TRIGGER_ESCALATION_STEP, null=True)
|
||||
is_webhook_enabled = models.BooleanField(null=True, default=True)
|
||||
integration_filter = models.JSONField(default=None, null=True, blank=True)
|
||||
is_legacy = models.BooleanField(null=True, default=False)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue