2023-03-09 16:39:25 -03:00
|
|
|
import ipaddress
|
|
|
|
|
import json
|
|
|
|
|
import re
|
|
|
|
|
import socket
|
|
|
|
|
from urllib.parse import urlparse
|
|
|
|
|
|
|
|
|
|
from django.conf import settings
|
|
|
|
|
|
|
|
|
|
from apps.base.utils import live_settings
|
2023-11-28 20:47:57 +08:00
|
|
|
from apps.labels.utils import get_alert_group_labels_dict, get_labels_dict, is_labels_feature_enabled
|
2023-04-20 10:13:48 -06:00
|
|
|
from apps.schedules.ical_utils import list_users_to_notify_from_ical
|
2023-03-09 16:39:25 -03:00
|
|
|
from common.jinja_templater import apply_jinja_template
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidWebhookUrl(Exception):
|
|
|
|
|
def __init__(self, message):
|
|
|
|
|
self.message = f"URL - {message}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidWebhookTrigger(Exception):
|
|
|
|
|
def __init__(self, message):
|
|
|
|
|
self.message = f"Trigger - {message}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidWebhookHeaders(Exception):
|
|
|
|
|
def __init__(self, message):
|
|
|
|
|
self.message = f"Headers - {message}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class InvalidWebhookData(Exception):
|
|
|
|
|
def __init__(self, message):
|
|
|
|
|
self.message = f"Data - {message}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def parse_url(url):
|
|
|
|
|
parsed_url = urlparse(url)
|
|
|
|
|
# ensure the url looks like url
|
|
|
|
|
if parsed_url.scheme not in ["http", "https"] or not parsed_url.netloc:
|
|
|
|
|
raise InvalidWebhookUrl("Malformed url")
|
|
|
|
|
|
|
|
|
|
if settings.BASE_URL in url:
|
|
|
|
|
raise InvalidWebhookUrl("Potential self-reference")
|
|
|
|
|
|
|
|
|
|
if not live_settings.DANGEROUS_WEBHOOKS_ENABLED:
|
|
|
|
|
# Get the ip address of the webhook url and check if it belongs to the private network
|
|
|
|
|
try:
|
|
|
|
|
webhook_url_ip_address = socket.gethostbyname(parsed_url.hostname)
|
|
|
|
|
except socket.gaierror:
|
|
|
|
|
raise InvalidWebhookUrl("Cannot resolve name in url")
|
|
|
|
|
if ipaddress.ip_address(socket.gethostbyname(webhook_url_ip_address)).is_private:
|
|
|
|
|
raise InvalidWebhookUrl("This url is not supported for outgoing webhooks")
|
|
|
|
|
|
|
|
|
|
return parsed_url
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def apply_jinja_template_for_json(template, payload):
|
|
|
|
|
escaped_payload = escape_payload(payload)
|
|
|
|
|
return apply_jinja_template(template, **escaped_payload)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def escape_payload(payload: dict):
|
|
|
|
|
if isinstance(payload, dict):
|
|
|
|
|
escaped_payload = EscapeDoubleQuotesDict()
|
|
|
|
|
for key in payload.keys():
|
|
|
|
|
escaped_payload[key] = escape_payload(payload[key])
|
|
|
|
|
elif isinstance(payload, list):
|
|
|
|
|
escaped_payload = []
|
|
|
|
|
for value in payload:
|
|
|
|
|
escaped_payload.append(escape_payload(value))
|
|
|
|
|
elif isinstance(payload, str):
|
|
|
|
|
escaped_payload = escape_string(payload)
|
|
|
|
|
else:
|
|
|
|
|
escaped_payload = payload
|
|
|
|
|
return escaped_payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def escape_string(string: str):
|
|
|
|
|
"""
|
|
|
|
|
Escapes string to use in json.loads() method.
|
|
|
|
|
json.dumps is the simples way to escape all special characters in string.
|
|
|
|
|
First and last chars are quotes from json.dumps(), we don't need them, only escaping.
|
|
|
|
|
"""
|
2024-01-11 12:35:23 -07:00
|
|
|
return json.dumps(string, ensure_ascii=False)[1:-1]
|
2023-03-09 16:39:25 -03:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class EscapeDoubleQuotesDict(dict):
|
|
|
|
|
"""
|
2024-03-28 11:37:22 -04:00
|
|
|
Warning: Please, do not use this dict anywhere except `apps.webhooks.utils.escape_payload`.
|
|
|
|
|
This custom dict escapes double quotes to produce string which is safe to pass to `json.loads()`
|
|
|
|
|
It fixes issues originating from payloads which contains strings with single quote.
|
|
|
|
|
In this case, built-in `dict`'s `str` method will surround value with double quotes.
|
2023-03-09 16:39:25 -03:00
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
|
|
alert_payload = {
|
|
|
|
|
"text": "Hi, it's alert",
|
|
|
|
|
}
|
|
|
|
|
template = '{"data" : "{{ alert_payload }}"}'
|
|
|
|
|
rendered = '{"data" : "{\'text\': "Hi, it\'s alert"}"}'
|
|
|
|
|
# and json.loads(rendered) will fail due to unescaped double quotes
|
|
|
|
|
|
|
|
|
|
# Now with EscapeDoubleQuotesDict.
|
|
|
|
|
|
|
|
|
|
alert_payload = EscapeDoubleQuotesDict({
|
|
|
|
|
"text": "Hi, it's alert",
|
|
|
|
|
})
|
|
|
|
|
rendered = '{"data" : "{\'text\': \\"Hi, it\'s alert\\"}"}'
|
|
|
|
|
# and json.loads(rendered) works.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __str__(self):
|
|
|
|
|
original_str = super().__str__()
|
|
|
|
|
if '"' in original_str:
|
|
|
|
|
return re.sub('(?<!\\\\)"', '\\\\"', original_str)
|
|
|
|
|
return original_str
|
2023-03-13 18:19:22 -03:00
|
|
|
|
|
|
|
|
|
2023-04-06 14:52:23 -03:00
|
|
|
def _serialize_event_user(user):
|
|
|
|
|
if not user:
|
|
|
|
|
return None
|
|
|
|
|
return {
|
|
|
|
|
"id": user.public_primary_key,
|
|
|
|
|
"username": user.username,
|
|
|
|
|
"email": user.email,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2023-04-20 10:13:48 -06:00
|
|
|
def _extract_users_from_escalation_snapshot(escalation_snapshot):
|
|
|
|
|
from apps.alerts.models import EscalationPolicy
|
|
|
|
|
|
|
|
|
|
users = []
|
|
|
|
|
if escalation_snapshot:
|
|
|
|
|
for policy_snapshot in escalation_snapshot.escalation_policies_snapshots:
|
|
|
|
|
if policy_snapshot.step in [
|
|
|
|
|
EscalationPolicy.STEP_NOTIFY,
|
|
|
|
|
EscalationPolicy.STEP_NOTIFY_IMPORTANT,
|
|
|
|
|
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS,
|
|
|
|
|
EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT,
|
|
|
|
|
]:
|
|
|
|
|
for user in policy_snapshot.notify_to_users_queue:
|
|
|
|
|
users.append(_serialize_event_user(user))
|
|
|
|
|
elif policy_snapshot.step in [
|
|
|
|
|
EscalationPolicy.STEP_NOTIFY_SCHEDULE,
|
|
|
|
|
EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT,
|
|
|
|
|
]:
|
|
|
|
|
if policy_snapshot.notify_schedule:
|
|
|
|
|
for user in list_users_to_notify_from_ical(policy_snapshot.notify_schedule):
|
|
|
|
|
users.append(_serialize_event_user(user))
|
2023-04-26 15:55:08 -06:00
|
|
|
return list({u["id"]: u for u in users if u}.values())
|
2023-04-20 10:13:48 -06:00
|
|
|
|
|
|
|
|
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
def serialize_event(event, alert_group, user, webhook, responses=None):
|
2024-03-20 10:54:27 +00:00
|
|
|
from apps.alerts.models import AlertGroupExternalID
|
2023-03-13 18:19:22 -03:00
|
|
|
from apps.public_api.serializers import IncidentSerializer
|
|
|
|
|
|
|
|
|
|
alert_payload = alert_group.alerts.first()
|
|
|
|
|
alert_payload_raw = ""
|
|
|
|
|
if alert_payload:
|
|
|
|
|
alert_payload_raw = alert_payload.raw_request_data
|
|
|
|
|
|
|
|
|
|
data = {
|
|
|
|
|
"event": event,
|
2023-04-06 14:52:23 -03:00
|
|
|
"user": _serialize_event_user(user),
|
2023-03-13 18:19:22 -03:00
|
|
|
"alert_group": IncidentSerializer(alert_group).data,
|
|
|
|
|
"alert_group_id": alert_group.public_primary_key,
|
|
|
|
|
"alert_payload": alert_payload_raw,
|
2023-04-06 14:52:23 -03:00
|
|
|
"integration": {
|
|
|
|
|
"id": alert_group.channel.public_primary_key,
|
|
|
|
|
"type": alert_group.channel.integration,
|
|
|
|
|
"name": alert_group.channel.short_name,
|
|
|
|
|
"team": alert_group.channel.team.name if alert_group.channel.team else None,
|
|
|
|
|
},
|
|
|
|
|
"notified_users": [
|
|
|
|
|
_serialize_event_user(user)
|
|
|
|
|
for user in set(notification.author for notification in alert_group.sent_notifications)
|
|
|
|
|
],
|
2023-04-20 10:13:48 -06:00
|
|
|
"users_to_be_notified": _extract_users_from_escalation_snapshot(alert_group.escalation_snapshot),
|
2024-04-27 03:20:08 +05:30
|
|
|
"alert_group_acknowledged_by": _serialize_event_user(alert_group.acknowledged_by_user),
|
|
|
|
|
"alert_group_resolved_by": _serialize_event_user(alert_group.resolved_by_user),
|
2023-03-13 18:19:22 -03:00
|
|
|
}
|
2023-03-21 10:43:37 -03:00
|
|
|
if responses:
|
|
|
|
|
data["responses"] = responses
|
|
|
|
|
|
Webhook labels (#3383)
This PR add labels for webhooks.
1. Make webhook "labelable" with ability to filter by labels.
2. Add labels to the webhook payload. It contain new field webhook with
it's name, id and labels. Field integration and alert_group has a
corresponding label field as well. See example of a new payload below:
```
{
"event": {
"type": "escalation"
},
"user": null,
"alert_group": {
"id": "IRFN6ZD31N31B",
"integration_id": "CTWM7U4A2QG97",
"route_id": "RUE7U7Z46SKGY",
"alerts_count": 1,
"state": "firing",
"created_at": "2023-11-22T08:54:55.178243Z",
"resolved_at": null,
"acknowledged_at": null,
"title": "Incident",
"permalinks": {
"slack": null,
"telegram": null,
"web": "http://grafana:3000/a/grafana-oncall-app/alert-groups/IRFN6ZD31N31B"
},
"labels": {
"severity": "critical"
}
},
"alert_group_id": "IRFN6ZD31N31B",
"alert_payload": {
"message": "This alert was sent by user for demonstration purposes"
},
"integration": {
"id": "CTWM7U4A2QG97",
"type": "webhook",
"name": "hi - Webhook",
"team": null,
"labels": {
"hello": "world",
"severity": "critical"
}
},
"notified_users": [],
"users_to_be_notified": [],
"webhook": {
"id": "WHAXK4BTC7TAEQ",
"name": "test",
"labels": {
"hello": "kesha"
}
}
}
```
I feel that there is an opportunity to make code cleaner - remove all
label logic from serializers, views and utils to models or dedicated
LabelerService and introduce Labelable interface with something like
label_verbal, update_labels methods. However, I don't want to tie
webhook labels with a refactoring.
---------
Co-authored-by: Dominik <dominik.broj@grafana.com>
2023-11-22 19:17:41 +08:00
|
|
|
# Enrich webhook data with labels payloads if labels feature is enabled
|
|
|
|
|
# TODO: once feature flag will be removed this code should go to the 'data' dict declaration
|
|
|
|
|
if is_labels_feature_enabled(alert_group.channel.organization):
|
2023-11-28 20:47:57 +08:00
|
|
|
data["webhook"] = {"id": webhook.public_primary_key, "name": webhook.name, "labels": get_labels_dict(webhook)}
|
|
|
|
|
data["integration"]["labels"] = get_labels_dict(alert_group.channel)
|
|
|
|
|
data["alert_group"]["labels"] = get_alert_group_labels_dict(alert_group)
|
2024-03-20 10:54:27 +00:00
|
|
|
|
|
|
|
|
# Add additional webhook data if the integration has it
|
2024-04-10 08:37:11 -03:00
|
|
|
source_alert_receive_channel = webhook.get_source_alert_receive_channel()
|
2024-03-20 10:54:27 +00:00
|
|
|
if source_alert_receive_channel and hasattr(source_alert_receive_channel.config, "additional_webhook_data"):
|
|
|
|
|
data.update(source_alert_receive_channel.config.additional_webhook_data(source_alert_receive_channel))
|
|
|
|
|
|
|
|
|
|
# Add external ID (e.g. ServiceNow incident ID) to webhook data
|
|
|
|
|
if source_alert_receive_channel:
|
|
|
|
|
external_id = AlertGroupExternalID.objects.filter(
|
|
|
|
|
source_alert_receive_channel=source_alert_receive_channel, alert_group=alert_group
|
|
|
|
|
).first()
|
|
|
|
|
data["external_id"] = external_id.value if external_id else None
|
|
|
|
|
|
2023-03-13 18:19:22 -03:00
|
|
|
return data
|