cleanup CustomButton backend code + add ngrok/express outgoing webhook e2e test (#2544)
# What this PR does - removes unused "custom button" backend code now that we've migrated to outgoing webhooks - adds new e2e test for webhooks asserting that an `ngrok`/`express` webhook handler receives the call as expected + payload is as expected (related to https://github.com/grafana/oncall/issues/2691) - skipped for now, the test passes locally but fails on GitHub Actions CI, seems to be networking related ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) --------- Co-authored-by: Michael Derynck <michael.derynck@grafana.com>
This commit is contained in:
parent
b544a4360a
commit
c5cd675738
47 changed files with 562 additions and 1609 deletions
|
|
@ -5,6 +5,9 @@ env:
|
|||
value: "False"
|
||||
- name: FEATURE_PROMETHEUS_EXPORTER_ENABLED
|
||||
value: "True"
|
||||
# enabled to be able to test docker.host.internal in the webhook e2e tests
|
||||
- name: DANGEROUS_WEBHOOKS_ENABLED
|
||||
value: "True"
|
||||
image:
|
||||
repository: localhost:63628/oncall/engine
|
||||
tag: dev
|
||||
|
|
|
|||
|
|
@ -34,10 +34,10 @@ The above command returns JSON structured in the following way:
|
|||
| ---------------------------------- | :--------------------------------------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
||||
| `escalation_chain_id` | Yes | Each escalation policy is assigned to a specific escalation chain. |
|
||||
| `position` | Optional | Escalation policies execute one after another starting from `position=0`. `Position=-1` will put the escalation policy to the end of the list. A new escalation policy created with a position of an existing escalation policy will move the old one (and all following) down in the list. |
|
||||
| `type` | Yes | One of: `wait`, `notify_persons`, `notify_person_next_each_time`, `notify_on_call_from_schedule`, `notify_user_group`, `trigger_action`, `resolve`, `notify_whole_channel`, `notify_if_time_from_to`. |
|
||||
| `type` | Yes | One of: `wait`, `notify_persons`, `notify_person_next_each_time`, `notify_on_call_from_schedule`, `notify_user_group`, `trigger_webhook`, `resolve`, `notify_whole_channel`, `notify_if_time_from_to`. |
|
||||
| `important` | Optional | Default is `false`. Will assign "important" to personal notification rules if `true`. This can be used to distinguish alerts on which you want to be notified immediately by phone. Applicable for types `notify_persons`, `notify_team_members`, `notify_on_call_from_schedule`, and `notify_user_group`. |
|
||||
| `duration` | If type = `wait` | The duration, in seconds, when type `wait` is chosen. Valid values are: `60`, `300`, `900`, `1800`, `3600`. |
|
||||
| `action_to_trigger` | If type = `trigger_action` | ID of a webhook. |
|
||||
| `action_to_trigger` | If type = `trigger_webhook` | ID of a webhook. |
|
||||
| `group_to_notify` | If type = `notify_user_group` | ID of a `User Group`. |
|
||||
| `persons_to_notify` | If type = `notify_persons` | List of user IDs. |
|
||||
| `persons_to_notify_next_each_time` | If type = `notify_person_next_each_time` | List of user IDs. |
|
||||
|
|
|
|||
|
|
@ -42,62 +42,6 @@ class EscalationSnapshotMixin:
|
|||
"""
|
||||
Builds new escalation chain in a json serializable format (dict).
|
||||
Use this method to prepare escalation chain data for saving to alert group before start new escalation.
|
||||
|
||||
Example result:
|
||||
{
|
||||
'channel_filter_snapshot': {
|
||||
'id': 1,
|
||||
'notify_in_slack': True,
|
||||
'str_for_clients': 'default',
|
||||
'notify_in_telegram': True
|
||||
},
|
||||
'escalation_chain_snapshot': {
|
||||
'id': 1,
|
||||
'name': 'Test'
|
||||
},
|
||||
'escalation_policies_snapshots': [
|
||||
{
|
||||
'id': 1,
|
||||
'step': 14,
|
||||
'order': 0,
|
||||
'to_time': None,
|
||||
'from_time': None,
|
||||
'num_alerts_in_window': None,
|
||||
'num_minutes_in_window': None,
|
||||
'wait_delay': None,
|
||||
'notify_schedule': None,
|
||||
'notify_to_group': None,
|
||||
'notify_to_team_members': None,
|
||||
'passed_last_time': None,
|
||||
'escalation_counter': 0,
|
||||
'last_notified_user': None,
|
||||
'custom_button_trigger': None,
|
||||
'notify_to_users_queue': [1,2,3]
|
||||
},
|
||||
{
|
||||
'id': 2,
|
||||
'step': 0,
|
||||
'order': 1,
|
||||
'to_time': None,
|
||||
'from_time': None,
|
||||
'num_alerts_in_window': None,
|
||||
'num_minutes_in_window': None,
|
||||
'wait_delay': '00:05:00',
|
||||
'notify_schedule': None,
|
||||
'notify_to_group': None,
|
||||
'notify_to_team_members': None,
|
||||
'passed_last_time': None,
|
||||
'escalation_counter': 0,
|
||||
'last_notified_user': None,
|
||||
'custom_button_trigger': None,
|
||||
'notify_to_users_queue': []
|
||||
},
|
||||
],
|
||||
'slack_channel_id': 'SLACK_CHANNEL_ID',
|
||||
'last_active_escalation_policy_order': None,
|
||||
'pause_escalation': False,
|
||||
'next_step_eta': '2021-10-18T10:28:28.890369Z
|
||||
}
|
||||
"""
|
||||
data = {}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from apps.alerts.models.custom_button import CustomButton
|
||||
from apps.alerts.models.escalation_policy import EscalationPolicy
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
from apps.user_management.models import User
|
||||
|
|
@ -58,7 +57,6 @@ class EscalationPolicySnapshotSerializer(serializers.ModelSerializer):
|
|||
)
|
||||
escalation_counter = serializers.IntegerField(default=0)
|
||||
passed_last_time = serializers.DateTimeField(allow_null=True, default=None)
|
||||
custom_button_trigger = PrimaryKeyRelatedFieldWithNoneValue(allow_null=True, queryset=CustomButton.objects)
|
||||
custom_webhook = PrimaryKeyRelatedFieldWithNoneValue(allow_null=True, queryset=Webhook.objects, default=None)
|
||||
notify_schedule = PrimaryKeyRelatedFieldWithNoneValue(allow_null=True, queryset=OnCallSchedule.objects)
|
||||
num_alerts_in_window = serializers.IntegerField(allow_null=True, default=None)
|
||||
|
|
@ -79,7 +77,6 @@ class EscalationPolicySnapshotSerializer(serializers.ModelSerializer):
|
|||
"to_time",
|
||||
"num_alerts_in_window",
|
||||
"num_minutes_in_window",
|
||||
"custom_button_trigger",
|
||||
"custom_webhook",
|
||||
"notify_schedule",
|
||||
"notify_to_group",
|
||||
|
|
|
|||
|
|
@ -11,7 +11,6 @@ from apps.alerts.escalation_snapshot.utils import eta_for_escalation_step_notify
|
|||
from apps.alerts.models.alert_group_log_record import AlertGroupLogRecord
|
||||
from apps.alerts.models.escalation_policy import EscalationPolicy
|
||||
from apps.alerts.tasks import (
|
||||
custom_button_result,
|
||||
custom_webhook_result,
|
||||
notify_all_task,
|
||||
notify_group_task,
|
||||
|
|
@ -37,7 +36,6 @@ class EscalationPolicySnapshot:
|
|||
"to_time",
|
||||
"num_alerts_in_window",
|
||||
"num_minutes_in_window",
|
||||
"custom_button_trigger",
|
||||
"custom_webhook",
|
||||
"notify_schedule",
|
||||
"notify_to_group",
|
||||
|
|
@ -66,7 +64,6 @@ class EscalationPolicySnapshot:
|
|||
to_time,
|
||||
num_alerts_in_window,
|
||||
num_minutes_in_window,
|
||||
custom_button_trigger,
|
||||
custom_webhook,
|
||||
notify_schedule,
|
||||
notify_to_group,
|
||||
|
|
@ -85,7 +82,6 @@ class EscalationPolicySnapshot:
|
|||
self.to_time = to_time
|
||||
self.num_alerts_in_window = num_alerts_in_window
|
||||
self.num_minutes_in_window = num_minutes_in_window
|
||||
self.custom_button_trigger = custom_button_trigger
|
||||
self.custom_webhook = custom_webhook
|
||||
self.notify_schedule = notify_schedule
|
||||
self.notify_to_group = notify_to_group
|
||||
|
|
@ -131,7 +127,6 @@ class EscalationPolicySnapshot:
|
|||
EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS_IMPORTANT: self._escalation_step_notify_team_members,
|
||||
EscalationPolicy.STEP_NOTIFY_SCHEDULE: self._escalation_step_notify_on_call_schedule,
|
||||
EscalationPolicy.STEP_NOTIFY_SCHEDULE_IMPORTANT: self._escalation_step_notify_on_call_schedule,
|
||||
EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON: self._escalation_step_trigger_custom_button,
|
||||
EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK: self._escalation_step_trigger_custom_webhook,
|
||||
EscalationPolicy.STEP_NOTIFY_USERS_QUEUE: self._escalation_step_notify_users_queue,
|
||||
EscalationPolicy.STEP_NOTIFY_IF_TIME: self._escalation_step_notify_if_time,
|
||||
|
|
@ -474,29 +469,6 @@ class EscalationPolicySnapshot:
|
|||
return self._get_result_tuple(pause_escalation=True)
|
||||
return None
|
||||
|
||||
def _escalation_step_trigger_custom_button(self, alert_group: "AlertGroup", _reason: str) -> None:
|
||||
tasks = []
|
||||
custom_button = self.custom_button_trigger
|
||||
if custom_button is not None:
|
||||
custom_button_task = custom_button_result.signature(
|
||||
(custom_button.pk, alert_group.pk),
|
||||
{
|
||||
"escalation_policy_pk": self.id,
|
||||
},
|
||||
immutable=True,
|
||||
)
|
||||
tasks.append(custom_button_task)
|
||||
else:
|
||||
log_record = AlertGroupLogRecord(
|
||||
type=AlertGroupLogRecord.TYPE_ESCALATION_FAILED,
|
||||
alert_group=alert_group,
|
||||
escalation_policy=self.escalation_policy,
|
||||
escalation_error_code=AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_CUSTOM_BUTTON_STEP_IS_NOT_CONFIGURED,
|
||||
escalation_policy_step=self.step,
|
||||
)
|
||||
log_record.save()
|
||||
self._execute_tasks(tasks)
|
||||
|
||||
def _escalation_step_trigger_custom_webhook(self, alert_group: "AlertGroup", _reason: str) -> None:
|
||||
tasks = []
|
||||
webhook = self.custom_webhook
|
||||
|
|
@ -514,7 +486,7 @@ class EscalationPolicySnapshot:
|
|||
type=AlertGroupLogRecord.TYPE_ESCALATION_FAILED,
|
||||
alert_group=alert_group,
|
||||
escalation_policy=self.escalation_policy,
|
||||
escalation_error_code=AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_CUSTOM_BUTTON_STEP_IS_NOT_CONFIGURED,
|
||||
escalation_error_code=AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_WEBHOOK_STEP_IS_NOT_CONFIGURED,
|
||||
escalation_policy_step=self.step,
|
||||
)
|
||||
log_record.save()
|
||||
|
|
|
|||
|
|
@ -554,18 +554,7 @@ class IncidentLogBuilder:
|
|||
# notification_plan_dict structure - {timedelta: [{"user_id": user.pk, "plan_lines": []}]
|
||||
for timedelta, notification_plan in notification_plan_dict.items():
|
||||
escalation_plan_dict.setdefault(timedelta, []).extend(notification_plan)
|
||||
elif escalation_policy_snapshot.step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON:
|
||||
if future_step:
|
||||
custom_button = escalation_policy_snapshot.custom_button_trigger
|
||||
if custom_button is not None:
|
||||
plan_line = f"trigger outgoing webhook `{custom_button.name}`"
|
||||
else:
|
||||
plan_line = (
|
||||
f'escalation step "{escalation_policy_snapshot.step_display}", '
|
||||
f"but outgoing webhook is unspecified. Skipping"
|
||||
)
|
||||
plan = {"plan_lines": [plan_line]}
|
||||
escalation_plan_dict.setdefault(timedelta, []).append(plan)
|
||||
# TODO: should we add logic here for new webhooks?
|
||||
elif escalation_policy_snapshot.step == EscalationPolicy.STEP_NOTIFY_IF_TIME:
|
||||
if future_step:
|
||||
if escalation_policy_snapshot.from_time is not None and escalation_policy_snapshot.to_time is not None:
|
||||
|
|
|
|||
|
|
@ -0,0 +1,18 @@
|
|||
# Generated by Django 4.2.10 on 2024-03-07 15:34
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('alerts', '0049_alter_alertgrouplogrecord_action_source'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='alertgrouplogrecord',
|
||||
name='type',
|
||||
field=models.IntegerField(choices=[(0, 'Acknowledged'), (1, 'Unacknowledged'), (2, 'Invite'), (3, 'Stop invitation'), (4, 'Re-invite'), (5, 'Escalation triggered'), (6, 'Invitation triggered'), (16, 'Escalation finished'), (7, 'Silenced'), (15, 'Unsilenced'), (8, 'Attached'), (9, 'Unattached'), (10, 'Custom webhook triggered'), (11, 'Unacknowledged by timeout'), (12, 'Failed attachment'), (13, 'Incident resolved'), (14, 'Incident unresolved'), (17, 'Escalation failed'), (18, 'Acknowledge reminder triggered'), (19, 'Wiped'), (20, 'Deleted'), (21, 'Incident registered'), (22, 'A route is assigned to the incident'), (23, 'Trigger direct paging escalation'), (24, 'Unpage a user'), (25, 'Restricted')]),
|
||||
),
|
||||
]
|
||||
|
|
@ -43,7 +43,7 @@ class AlertGroupLogRecord(models.Model):
|
|||
TYPE_SILENCE,
|
||||
TYPE_ATTACHED,
|
||||
TYPE_UNATTACHED,
|
||||
TYPE_CUSTOM_BUTTON_TRIGGERED,
|
||||
TYPE_CUSTOM_WEBHOOK_TRIGGERED,
|
||||
TYPE_AUTO_UN_ACK,
|
||||
TYPE_FAILED_ATTACHMENT,
|
||||
TYPE_RESOLVED,
|
||||
|
|
@ -77,7 +77,7 @@ class AlertGroupLogRecord(models.Model):
|
|||
TYPE_SILENCE,
|
||||
TYPE_ATTACHED,
|
||||
TYPE_UNATTACHED,
|
||||
TYPE_CUSTOM_BUTTON_TRIGGERED,
|
||||
TYPE_CUSTOM_WEBHOOK_TRIGGERED,
|
||||
TYPE_FAILED_ATTACHMENT,
|
||||
TYPE_RESOLVED,
|
||||
TYPE_UN_RESOLVED,
|
||||
|
|
@ -98,7 +98,7 @@ class AlertGroupLogRecord(models.Model):
|
|||
(TYPE_UN_SILENCE, "Unsilenced"),
|
||||
(TYPE_ATTACHED, "Attached"),
|
||||
(TYPE_UNATTACHED, "Unattached"),
|
||||
(TYPE_CUSTOM_BUTTON_TRIGGERED, "Custom button triggered"),
|
||||
(TYPE_CUSTOM_WEBHOOK_TRIGGERED, "Custom webhook triggered"),
|
||||
(TYPE_AUTO_UN_ACK, "Unacknowledged by timeout"),
|
||||
(TYPE_FAILED_ATTACHMENT, "Failed attachment"),
|
||||
(TYPE_RESOLVED, "Incident resolved"),
|
||||
|
|
@ -127,7 +127,7 @@ class AlertGroupLogRecord(models.Model):
|
|||
TYPE_UN_SILENCE: "un_silence",
|
||||
TYPE_ATTACHED: "attach",
|
||||
TYPE_UNATTACHED: "un_attach",
|
||||
TYPE_CUSTOM_BUTTON_TRIGGERED: "custom_button_triggered",
|
||||
TYPE_CUSTOM_WEBHOOK_TRIGGERED: "custom_webhook_triggered",
|
||||
TYPE_AUTO_UN_ACK: "auto_un_acknowledge",
|
||||
TYPE_FAILED_ATTACHMENT: "fail_attach",
|
||||
TYPE_RESOLVED: "resolve",
|
||||
|
|
@ -155,7 +155,7 @@ class AlertGroupLogRecord(models.Model):
|
|||
ERROR_ESCALATION_NOTIFY_GROUP_STEP_IS_NOT_CONFIGURED,
|
||||
ERROR_ESCALATION_USER_GROUP_IS_EMPTY,
|
||||
ERROR_ESCALATION_USER_GROUP_DOES_NOT_EXIST,
|
||||
ERROR_ESCALATION_TRIGGER_CUSTOM_BUTTON_STEP_IS_NOT_CONFIGURED,
|
||||
ERROR_ESCALATION_TRIGGER_WEBHOOK_STEP_IS_NOT_CONFIGURED,
|
||||
ERROR_ESCALATION_NOTIFY_IN_SLACK,
|
||||
ERROR_ESCALATION_NOTIFY_IF_NUM_ALERTS_IN_WINDOW_STEP_IS_NOT_CONFIGURED,
|
||||
ERROR_ESCALATION_TRIGGER_CUSTOM_WEBHOOK_ERROR,
|
||||
|
|
@ -471,7 +471,7 @@ class AlertGroupLogRecord(models.Model):
|
|||
f"{self.dependent_alert_group.long_verbose_name} has been unattached from this alert"
|
||||
f"{f' by {author_name}' if author_name else ''}"
|
||||
)
|
||||
elif self.type == AlertGroupLogRecord.TYPE_CUSTOM_BUTTON_TRIGGERED:
|
||||
elif self.type == AlertGroupLogRecord.TYPE_CUSTOM_WEBHOOK_TRIGGERED:
|
||||
webhook_name = ""
|
||||
trigger = None
|
||||
if step_specific_info is not None:
|
||||
|
|
@ -529,7 +529,7 @@ class AlertGroupLogRecord(models.Model):
|
|||
result += 'skipped escalation step "Notify Team Members" because it is not configured'
|
||||
elif (
|
||||
self.escalation_error_code
|
||||
== AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_CUSTOM_BUTTON_STEP_IS_NOT_CONFIGURED
|
||||
== AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_WEBHOOK_STEP_IS_NOT_CONFIGURED
|
||||
):
|
||||
result += 'skipped escalation step "Trigger Outgoing Webhook" because it is not configured'
|
||||
elif self.escalation_error_code == AlertGroupLogRecord.ERROR_ESCALATION_TRIGGER_CUSTOM_WEBHOOK_ERROR:
|
||||
|
|
|
|||
|
|
@ -1,18 +1,11 @@
|
|||
import json
|
||||
import logging
|
||||
import re
|
||||
import typing
|
||||
from json import JSONDecodeError
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.db.models import F
|
||||
from django.utils import timezone
|
||||
from requests.auth import HTTPBasicAuth
|
||||
|
||||
from common.jinja_templater import apply_jinja_template
|
||||
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
|
||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
|
|
@ -21,10 +14,6 @@ if typing.TYPE_CHECKING:
|
|||
from apps.alerts.models import EscalationPolicy
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
logger.setLevel(logging.DEBUG)
|
||||
|
||||
|
||||
def generate_public_primary_key_for_custom_button():
|
||||
prefix = "K"
|
||||
new_public_primary_key = generate_public_primary_key(prefix)
|
||||
|
|
@ -48,15 +37,11 @@ class CustomButtonManager(models.Manager):
|
|||
def get_queryset(self):
|
||||
return CustomButtonQueryset(self.model, using=self._db).filter(deleted_at=None)
|
||||
|
||||
def hard_delete(self):
|
||||
return self.get_queryset().hard_delete()
|
||||
|
||||
|
||||
class CustomButton(models.Model):
|
||||
escalation_policies: "RelatedManager['EscalationPolicy']"
|
||||
|
||||
objects = CustomButtonManager()
|
||||
objects_with_deleted = models.Manager()
|
||||
|
||||
public_primary_key = models.CharField(
|
||||
max_length=20,
|
||||
|
|
@ -90,132 +75,3 @@ class CustomButton(models.Model):
|
|||
|
||||
def __str__(self):
|
||||
return str(self.name)
|
||||
|
||||
def delete(self):
|
||||
logger.info(f"Soft delete of custom button {self}")
|
||||
self.escalation_policies.all().delete()
|
||||
self.deleted_at = timezone.now()
|
||||
# 100 - 22 = 78. 100 is max len of name field, and 22 is len of suffix _deleted_<public_primary_key>
|
||||
# So for case when user created button with maximum length name it is needed to trim it to 78 chars to be
|
||||
# able to add suffix.
|
||||
self.name = f"{self.name[:78]}_deleted_{self.public_primary_key}"
|
||||
self.save()
|
||||
|
||||
def hard_delete(self):
|
||||
super().delete()
|
||||
|
||||
def build_post_kwargs(self, alert):
|
||||
post_kwargs = {}
|
||||
if self.user and self.password:
|
||||
post_kwargs["auth"] = HTTPBasicAuth(self.user, self.password)
|
||||
if self.authorization_header:
|
||||
post_kwargs["headers"] = {"Authorization": self.authorization_header}
|
||||
if self.forward_whole_payload:
|
||||
post_kwargs["json"] = alert.raw_request_data
|
||||
elif self.data:
|
||||
try:
|
||||
rendered_data = apply_jinja_template(
|
||||
self.data,
|
||||
alert_payload=self._escape_alert_payload(alert.raw_request_data),
|
||||
alert_group_id=alert.group.public_primary_key,
|
||||
)
|
||||
try:
|
||||
post_kwargs["json"] = json.loads(rendered_data)
|
||||
except JSONDecodeError:
|
||||
post_kwargs["data"] = rendered_data
|
||||
except (JinjaTemplateError, JinjaTemplateWarning) as e:
|
||||
post_kwargs["json"] = {"error": e.fallback_message}
|
||||
return post_kwargs
|
||||
|
||||
def _escape_alert_payload(self, payload: dict):
|
||||
if isinstance(payload, dict):
|
||||
escaped_data = EscapeDoubleQuotesDict()
|
||||
for key in payload.keys():
|
||||
escaped_data[key] = self._escape_alert_payload(payload[key])
|
||||
elif isinstance(payload, list):
|
||||
escaped_data = []
|
||||
for value in payload:
|
||||
escaped_data.append(self._escape_alert_payload(value))
|
||||
elif isinstance(payload, str):
|
||||
escaped_data = self._escape_string(payload)
|
||||
else:
|
||||
escaped_data = payload
|
||||
return escaped_data
|
||||
|
||||
def _escape_string(self, 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.
|
||||
"""
|
||||
return json.dumps(string)[1:-1]
|
||||
|
||||
# Insight logs
|
||||
@property
|
||||
def insight_logs_type_verbal(self):
|
||||
return "outgoing_webhook"
|
||||
|
||||
@property
|
||||
def insight_logs_verbal(self):
|
||||
return self.name
|
||||
|
||||
@property
|
||||
def insight_logs_serialized(self):
|
||||
result = {
|
||||
"name": self.name,
|
||||
"webhook": self.webhook,
|
||||
"user": self.user,
|
||||
"password": self.password,
|
||||
"authorization_header": self.authorization_header,
|
||||
"data": self.data,
|
||||
"forward_whole_payload": self.forward_whole_payload,
|
||||
}
|
||||
|
||||
if self.team:
|
||||
result["team"] = self.team.name
|
||||
result["team_id"] = self.team.public_primary_key
|
||||
else:
|
||||
result["team"] = "General"
|
||||
return result
|
||||
|
||||
@property
|
||||
def insight_logs_metadata(self):
|
||||
result = {}
|
||||
if self.team:
|
||||
result["team"] = self.team.name
|
||||
result["team_id"] = self.team.public_primary_key
|
||||
else:
|
||||
result["team"] = "General"
|
||||
return result
|
||||
|
||||
|
||||
class EscapeDoubleQuotesDict(dict):
|
||||
"""
|
||||
Warning: Please, do not use this dict anywhere except CustomButton._escape_alert_payload.
|
||||
This custom dict escapes double quotes to produce string which is safe to pass to json.loads()
|
||||
It fixes case when CustomButton.build_post_kwargs failing on payloads which contains string with single quote.
|
||||
In this case built-in dict's str method will surround value with double quotes.
|
||||
|
||||
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
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ import datetime
|
|||
from django.conf import settings
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django_deprecate_fields import deprecate_field
|
||||
|
||||
from common.ordered_model.ordered_model import OrderedModel
|
||||
from common.public_primary_keys import generate_public_primary_key, increase_public_primary_key_length
|
||||
|
|
@ -38,7 +39,7 @@ class EscalationPolicy(OrderedModel):
|
|||
STEP_NOTIFY_IMPORTANT,
|
||||
STEP_NOTIFY_GROUP_IMPORTANT,
|
||||
STEP_NOTIFY_SCHEDULE_IMPORTANT,
|
||||
STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
_DEPRECATED_STEP_TRIGGER_CUSTOM_BUTTON, # only here to keep range intact
|
||||
STEP_NOTIFY_USERS_QUEUE,
|
||||
STEP_NOTIFY_IF_TIME,
|
||||
STEP_NOTIFY_MULTIPLE_USERS,
|
||||
|
|
@ -61,7 +62,7 @@ class EscalationPolicy(OrderedModel):
|
|||
(STEP_NOTIFY_IMPORTANT, "Notify User (Important)"),
|
||||
(STEP_NOTIFY_GROUP_IMPORTANT, "Notify Group (Important)"),
|
||||
(STEP_NOTIFY_SCHEDULE_IMPORTANT, "Notify Schedule (Important)"),
|
||||
(STEP_TRIGGER_CUSTOM_BUTTON, "Trigger Outgoing Webhook"),
|
||||
(_DEPRECATED_STEP_TRIGGER_CUSTOM_BUTTON, "Trigger Outgoing Webhook"),
|
||||
(STEP_NOTIFY_USERS_QUEUE, "Notify User (next each time)"),
|
||||
(STEP_NOTIFY_IF_TIME, "Continue escalation only if time is from"),
|
||||
(STEP_NOTIFY_MULTIPLE_USERS, "Notify multiple Users"),
|
||||
|
|
@ -85,7 +86,6 @@ class EscalationPolicy(OrderedModel):
|
|||
STEP_FINAL_NOTIFYALL,
|
||||
STEP_NOTIFY_GROUP,
|
||||
# Other
|
||||
STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
STEP_TRIGGER_CUSTOM_WEBHOOK,
|
||||
STEP_NOTIFY_USERS_QUEUE,
|
||||
STEP_NOTIFY_IF_TIME,
|
||||
|
|
@ -109,7 +109,6 @@ class EscalationPolicy(OrderedModel):
|
|||
STEP_NOTIFY_TEAM_MEMBERS_IMPORTANT,
|
||||
STEP_NOTIFY_MULTIPLE_USERS,
|
||||
STEP_NOTIFY_MULTIPLE_USERS_IMPORTANT,
|
||||
STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
STEP_TRIGGER_CUSTOM_WEBHOOK,
|
||||
STEP_REPEAT_ESCALATION_N_TIMES,
|
||||
]
|
||||
|
|
@ -136,7 +135,6 @@ class EscalationPolicy(OrderedModel):
|
|||
"Notify Slack User Group",
|
||||
),
|
||||
# Other
|
||||
STEP_TRIGGER_CUSTOM_BUTTON: ("Trigger outgoing webhook {{custom_action}}", "Trigger outgoing webhook"),
|
||||
STEP_TRIGGER_CUSTOM_WEBHOOK: ("Trigger webhook {{custom_webhook}}", "Trigger webhook"),
|
||||
STEP_NOTIFY_USERS_QUEUE: ("Round robin notification for {{users}}", "Notify users one by one (round-robin)"),
|
||||
STEP_NOTIFY_IF_TIME: (
|
||||
|
|
@ -157,7 +155,6 @@ class EscalationPolicy(OrderedModel):
|
|||
STEP_WAIT,
|
||||
STEP_FINAL_NOTIFYALL,
|
||||
STEP_FINAL_RESOLVE,
|
||||
STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
STEP_TRIGGER_CUSTOM_WEBHOOK,
|
||||
STEP_NOTIFY_USERS_QUEUE,
|
||||
STEP_NOTIFY_IF_TIME,
|
||||
|
|
@ -207,7 +204,6 @@ class EscalationPolicy(OrderedModel):
|
|||
STEP_NOTIFY_GROUP,
|
||||
STEP_FINAL_RESOLVE,
|
||||
STEP_FINAL_NOTIFYALL,
|
||||
STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
STEP_TRIGGER_CUSTOM_WEBHOOK,
|
||||
STEP_NOTIFY_IF_TIME,
|
||||
STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW,
|
||||
|
|
@ -224,7 +220,6 @@ class EscalationPolicy(OrderedModel):
|
|||
STEP_NOTIFY_IMPORTANT: "notify_one_person",
|
||||
STEP_NOTIFY_SCHEDULE: "notify_on_call_from_schedule",
|
||||
STEP_NOTIFY_SCHEDULE_IMPORTANT: "notify_on_call_from_schedule",
|
||||
STEP_TRIGGER_CUSTOM_BUTTON: "trigger_action",
|
||||
STEP_TRIGGER_CUSTOM_WEBHOOK: "trigger_webhook",
|
||||
STEP_NOTIFY_USERS_QUEUE: "notify_person_next_each_time",
|
||||
STEP_NOTIFY_MULTIPLE_USERS: "notify_persons",
|
||||
|
|
@ -286,12 +281,15 @@ class EscalationPolicy(OrderedModel):
|
|||
default=None,
|
||||
)
|
||||
|
||||
custom_button_trigger = models.ForeignKey(
|
||||
"alerts.CustomButton",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="escalation_policies",
|
||||
default=None,
|
||||
null=True,
|
||||
# TODO: remove this in a subsequent release
|
||||
custom_button_trigger = deprecate_field(
|
||||
models.ForeignKey(
|
||||
"alerts.CustomButton",
|
||||
on_delete=models.CASCADE,
|
||||
related_name="escalation_policies",
|
||||
default=None,
|
||||
null=True,
|
||||
)
|
||||
)
|
||||
|
||||
custom_webhook = models.ForeignKey(
|
||||
|
|
@ -405,12 +403,8 @@ class EscalationPolicy(OrderedModel):
|
|||
if self.notify_schedule:
|
||||
result["on-call_schedule"] = self.notify_schedule.insight_logs_verbal
|
||||
result["on-call_schedule_id"] = self.notify_schedule.public_primary_key
|
||||
elif self.step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON:
|
||||
if self.custom_button_trigger:
|
||||
result["outgoing_webhook"] = self.custom_button_trigger.insight_logs_verbal
|
||||
result["outgoing_webhook_id"] = self.custom_button_trigger.public_primary_key
|
||||
elif self.step == EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK:
|
||||
if self.custom_button_trigger:
|
||||
if self.custom_webhook:
|
||||
result["outgoing_webhook"] = self.custom_webhook.insight_logs_verbal
|
||||
result["outgoing_webhook_id"] = self.custom_webhook.public_primary_key
|
||||
elif self.step in [
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ from .alert_group_web_title_cache import ( # noqa:F401
|
|||
update_web_title_cache_for_alert_receive_channel,
|
||||
)
|
||||
from .check_escalation_finished import check_escalation_finished_task # noqa: F401
|
||||
from .custom_button_result import custom_button_result # noqa: F401
|
||||
from .custom_webhook_result import custom_webhook_result # noqa: F401
|
||||
from .delete_alert_group import delete_alert_group # noqa: F401
|
||||
from .delete_alert_group import finish_delete_alert_group # noqa: F401
|
||||
|
|
|
|||
|
|
@ -1,84 +0,0 @@
|
|||
import json
|
||||
import logging
|
||||
from functools import partial
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import transaction
|
||||
from jinja2 import TemplateError
|
||||
|
||||
from apps.alerts.utils import request_outgoing_webhook
|
||||
from common.custom_celery_tasks import shared_dedicated_queue_retry_task
|
||||
|
||||
from .send_alert_group_signal import send_alert_group_signal
|
||||
from .task_logger import task_logger
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@shared_dedicated_queue_retry_task(
|
||||
autoretry_for=(Exception,), retry_backoff=True, max_retries=1 if settings.DEBUG else None
|
||||
)
|
||||
def custom_button_result(custom_button_pk, alert_group_pk, user_pk=None, escalation_policy_pk=None):
|
||||
from apps.alerts.models import AlertGroup, AlertGroupLogRecord, CustomButton, EscalationPolicy
|
||||
from apps.user_management.models import User
|
||||
|
||||
task_logger.debug(
|
||||
f"Start custom_button_result for alert_group {alert_group_pk}, " f"custom_button {custom_button_pk}"
|
||||
)
|
||||
try:
|
||||
custom_button = CustomButton.objects.get(pk=custom_button_pk)
|
||||
except CustomButton.DoesNotExist:
|
||||
task_logger.info(f"Custom_button {custom_button_pk} for alert_group {alert_group_pk} does not exist")
|
||||
return
|
||||
|
||||
alert_group = AlertGroup.objects.filter(pk=alert_group_pk)[0]
|
||||
escalation_policy = EscalationPolicy.objects.filter(pk=escalation_policy_pk).first()
|
||||
task_logger.debug(
|
||||
f"Start getting data for request in custom_button_result task for alert_group {alert_group_pk}, "
|
||||
f"custom_button {custom_button_pk}"
|
||||
)
|
||||
|
||||
first_alert = alert_group.alerts.first()
|
||||
|
||||
try:
|
||||
post_kwargs = custom_button.build_post_kwargs(first_alert)
|
||||
except TemplateError:
|
||||
is_request_successful = False
|
||||
result_message = "Template error"
|
||||
except json.JSONDecodeError:
|
||||
is_request_successful = False
|
||||
result_message = "JSON decoding error"
|
||||
else:
|
||||
is_request_successful, result_message = request_outgoing_webhook(
|
||||
custom_button.webhook, "POST", post_kwargs=post_kwargs
|
||||
)
|
||||
|
||||
task_logger.debug(
|
||||
f"Send post request in custom_button_result task for alert_group {alert_group_pk}, "
|
||||
f"custom_button {custom_button_pk}"
|
||||
)
|
||||
with transaction.atomic():
|
||||
user = None
|
||||
if user_pk:
|
||||
user = User.objects.get(pk=user_pk)
|
||||
|
||||
log_record = AlertGroupLogRecord(
|
||||
type=AlertGroupLogRecord.TYPE_CUSTOM_BUTTON_TRIGGERED,
|
||||
alert_group=alert_group,
|
||||
custom_button=custom_button,
|
||||
author=user,
|
||||
reason=result_message,
|
||||
step_specific_info={
|
||||
"custom_button_name": custom_button.name,
|
||||
"is_request_successful": is_request_successful,
|
||||
},
|
||||
escalation_policy=escalation_policy,
|
||||
escalation_policy_step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
)
|
||||
log_record.save()
|
||||
task_logger.debug(
|
||||
f"call send_alert_group_signal for alert_group {alert_group_pk}, "
|
||||
f"log record {log_record.pk} with type '{log_record.get_type_display()}'"
|
||||
)
|
||||
transaction.on_commit(partial(send_alert_group_signal.apply_async, (log_record.pk,)))
|
||||
task_logger.debug(f"Finish custom_button_result for alert_group {alert_group_pk}, custom_button {custom_button_pk}")
|
||||
|
|
@ -1,61 +0,0 @@
|
|||
import pytest
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escaping_payload_with_double_quotes(
|
||||
make_organization,
|
||||
make_custom_action,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
alert_payload = {
|
||||
"text": '"Hello world"',
|
||||
}
|
||||
|
||||
alert = make_alert(alert_group=alert_group, raw_request_data=alert_payload)
|
||||
|
||||
custom_button = make_custom_action(
|
||||
name="github_button",
|
||||
webhook="https://github.com/",
|
||||
user="Chris Vanstras",
|
||||
password="qwerty",
|
||||
data='{\n "text" : "{{ alert_payload.text }}"\n}',
|
||||
authorization_header="auth_token",
|
||||
organization=organization,
|
||||
)
|
||||
|
||||
custom_button.build_post_kwargs(alert)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_escaping_payload_with_single_quote_in_string(
|
||||
make_organization,
|
||||
make_custom_action,
|
||||
make_alert_receive_channel,
|
||||
make_alert_group,
|
||||
make_alert,
|
||||
):
|
||||
organization = make_organization()
|
||||
alert_receive_channel = make_alert_receive_channel(organization)
|
||||
alert_group = make_alert_group(alert_receive_channel)
|
||||
alert_payload = {
|
||||
"text": "Hi, it's alert",
|
||||
}
|
||||
|
||||
alert = make_alert(alert_group=alert_group, raw_request_data=alert_payload)
|
||||
|
||||
custom_button = make_custom_action(
|
||||
name="github_button",
|
||||
webhook="https://github.com/",
|
||||
user="Chris Vanstras",
|
||||
password="qwerty",
|
||||
data='{"data" : "{{ alert_payload }}"}',
|
||||
authorization_header="auth_token",
|
||||
organization=organization,
|
||||
)
|
||||
|
||||
custom_button.build_post_kwargs(alert)
|
||||
|
|
@ -448,37 +448,6 @@ def test_escalation_step_notify_if_num_alerts_in_window_deleted_escalation_polic
|
|||
).exists()
|
||||
|
||||
|
||||
@patch("apps.alerts.escalation_snapshot.snapshot_classes.EscalationPolicySnapshot._execute_tasks", return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_step_trigger_custom_button(
|
||||
mocked_execute_tasks,
|
||||
escalation_step_test_setup,
|
||||
make_custom_action,
|
||||
make_escalation_policy,
|
||||
):
|
||||
organization, _, _, channel_filter, alert_group, reason = escalation_step_test_setup
|
||||
|
||||
custom_button = make_custom_action(organization=organization)
|
||||
|
||||
trigger_custom_button_step = make_escalation_policy(
|
||||
escalation_chain=channel_filter.escalation_chain,
|
||||
escalation_policy_step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
custom_button_trigger=custom_button,
|
||||
)
|
||||
escalation_policy_snapshot = get_escalation_policy_snapshot_from_model(trigger_custom_button_step)
|
||||
expected_eta = timezone.now() + timezone.timedelta(seconds=NEXT_ESCALATION_DELAY)
|
||||
result = escalation_policy_snapshot.execute(alert_group, reason)
|
||||
expected_result = EscalationPolicySnapshot.StepExecutionResultData(
|
||||
eta=result.eta,
|
||||
stop_escalation=False,
|
||||
pause_escalation=False,
|
||||
start_from_beginning=False,
|
||||
)
|
||||
assert expected_eta + timezone.timedelta(seconds=15) > result.eta > expected_eta - timezone.timedelta(seconds=15)
|
||||
assert result == expected_result
|
||||
assert mocked_execute_tasks.called
|
||||
|
||||
|
||||
@patch("apps.alerts.escalation_snapshot.snapshot_classes.EscalationPolicySnapshot._execute_tasks", return_value=None)
|
||||
@pytest.mark.django_db
|
||||
def test_escalation_step_trigger_custom_webhook(
|
||||
|
|
@ -493,7 +462,7 @@ def test_escalation_step_trigger_custom_webhook(
|
|||
|
||||
trigger_custom_webhook_step = make_escalation_policy(
|
||||
escalation_chain=channel_filter.escalation_chain,
|
||||
escalation_policy_step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
escalation_policy_step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
|
||||
custom_webhook=custom_webhook,
|
||||
)
|
||||
escalation_policy_snapshot = get_escalation_policy_snapshot_from_model(trigger_custom_webhook_step)
|
||||
|
|
@ -509,6 +478,13 @@ def test_escalation_step_trigger_custom_webhook(
|
|||
assert result == expected_result
|
||||
assert mocked_execute_tasks.called
|
||||
|
||||
with patch(
|
||||
"apps.alerts.escalation_snapshot.snapshot_classes.EscalationPolicySnapshot._escalation_step_trigger_custom_webhook"
|
||||
) as mock_webhook_escalation_step:
|
||||
escalation_policy_snapshot.execute(alert_group, reason)
|
||||
|
||||
mock_webhook_escalation_step.assert_called_once_with(alert_group, reason)
|
||||
|
||||
|
||||
@patch("apps.alerts.escalation_snapshot.snapshot_classes.EscalationPolicySnapshot._execute_tasks", return_value=None)
|
||||
@pytest.mark.django_db
|
||||
|
|
@ -626,7 +602,6 @@ def test_escalation_step_with_deleted_user(
|
|||
"to_time": None,
|
||||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"custom_button_trigger": None,
|
||||
"notify_schedule": None,
|
||||
"notify_to_group": None,
|
||||
"escalation_counter": 0,
|
||||
|
|
|
|||
|
|
@ -45,7 +45,6 @@ def test_raw_escalation_snapshot(escalation_snapshot_test_setup):
|
|||
"to_time": None,
|
||||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"escalation_counter": 0,
|
||||
"passed_last_time": None,
|
||||
|
|
@ -65,7 +64,6 @@ def test_raw_escalation_snapshot(escalation_snapshot_test_setup):
|
|||
"to_time": None,
|
||||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"escalation_counter": 0,
|
||||
"passed_last_time": None,
|
||||
|
|
@ -85,7 +83,6 @@ def test_raw_escalation_snapshot(escalation_snapshot_test_setup):
|
|||
"to_time": notify_if_time_step.to_time.isoformat(),
|
||||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"escalation_counter": 0,
|
||||
"passed_last_time": None,
|
||||
|
|
|
|||
|
|
@ -1,22 +0,0 @@
|
|||
import socket
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
from apps.alerts.utils import request_outgoing_webhook
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_request_outgoing_webhook_cannot_resolve_name():
|
||||
with patch("apps.alerts.utils.socket.gethostbyname", side_effect=socket.gaierror):
|
||||
success, err = request_outgoing_webhook("http://something.something/webhook", "GET")
|
||||
assert success is False
|
||||
assert err == "Cannot resolve name in url"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_request_outgoing_webhook_resolve_name_without_port():
|
||||
with patch("apps.alerts.utils.socket.gethostbyname") as mock_gethostbyname:
|
||||
mock_gethostbyname.return_value = "127.0.0.1"
|
||||
request_outgoing_webhook("http://something.something:9000/webhook", "GET")
|
||||
assert mock_gethostbyname.call_args_list[0].args[0] == "something.something"
|
||||
|
|
@ -1,14 +1,3 @@
|
|||
import ipaddress
|
||||
import json
|
||||
import socket
|
||||
from typing import Tuple
|
||||
from urllib.parse import urlparse
|
||||
|
||||
import requests
|
||||
|
||||
from apps.base.utils import live_settings
|
||||
|
||||
|
||||
def render_relative_timeline(log_created_at, alert_group_started_at):
|
||||
time_delta = log_created_at - alert_group_started_at
|
||||
seconds = int(time_delta.total_seconds())
|
||||
|
|
@ -23,77 +12,3 @@ def render_relative_timeline(log_created_at, alert_group_started_at):
|
|||
return "%dm%ds" % (minutes, seconds)
|
||||
else:
|
||||
return "%ds" % (seconds,)
|
||||
|
||||
|
||||
# TODO: remove this function when we remove CustomButton model
|
||||
def render_curl_command(webhook_url, http_request_type, post_kwargs):
|
||||
if http_request_type == "POST":
|
||||
curl_request = "curl -X POST"
|
||||
if "auth" in post_kwargs:
|
||||
curl_request += "\n-u ****"
|
||||
if "headers" in post_kwargs:
|
||||
curl_request += "\n-H ****"
|
||||
if "json" in post_kwargs:
|
||||
curl_request += "\n-d '{}'".format(json.dumps(post_kwargs["json"], indent=2, sort_keys=True))
|
||||
curl_request += "\n{}".format(webhook_url)
|
||||
elif http_request_type == "GET":
|
||||
curl_request = f"curl -X GET {webhook_url}"
|
||||
else:
|
||||
raise Exception("Unsupported http method")
|
||||
return curl_request
|
||||
|
||||
|
||||
# TODO: remove this function when we remove CustomButton model
|
||||
def request_outgoing_webhook(webhook_url, http_request_type, post_kwargs=None) -> Tuple[bool, str]:
|
||||
OUTGOING_WEBHOOK_TIMEOUT = 10
|
||||
if http_request_type not in ["POST", "GET"]:
|
||||
raise Exception(f"Wrong http_method parameter: {http_request_type}")
|
||||
|
||||
parsed_url = urlparse(webhook_url)
|
||||
# ensure the url looks like url
|
||||
if parsed_url.scheme not in ["http", "https"]:
|
||||
return False, "Malformed url"
|
||||
if not parsed_url.netloc:
|
||||
return False, "Malformed url"
|
||||
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:
|
||||
return False, "Cannot resolve name in url"
|
||||
if not live_settings.DANGEROUS_WEBHOOKS_ENABLED:
|
||||
if ipaddress.ip_address(socket.gethostbyname(webhook_url_ip_address)).is_private:
|
||||
return False, "This url is not supported for outgoing webhooks"
|
||||
|
||||
if post_kwargs is None:
|
||||
post_kwargs = {}
|
||||
|
||||
try:
|
||||
if http_request_type == "POST":
|
||||
r = requests.post(webhook_url, timeout=OUTGOING_WEBHOOK_TIMEOUT, **post_kwargs)
|
||||
elif http_request_type == "GET":
|
||||
r = requests.get(webhook_url, timeout=OUTGOING_WEBHOOK_TIMEOUT)
|
||||
else:
|
||||
raise Exception()
|
||||
r.raise_for_status()
|
||||
return True, "OK 200"
|
||||
except requests.exceptions.HTTPError:
|
||||
return False, "HTTP error {}".format(r.status_code)
|
||||
except requests.exceptions.SSLError:
|
||||
return False, "ssl certificate error"
|
||||
except requests.exceptions.ConnectionError:
|
||||
return False, "Connection error happened. Probably that's because of network or proxy."
|
||||
except requests.exceptions.MissingSchema:
|
||||
return False, "Url {} is incorrect. http:// or https:// might be missing.".format(webhook_url)
|
||||
except requests.exceptions.ChunkedEncodingError:
|
||||
return False, "File content or headers might be wrong."
|
||||
except requests.exceptions.InvalidURL:
|
||||
return False, "Url {} is incorrect".format(webhook_url)
|
||||
except requests.exceptions.TooManyRedirects:
|
||||
return False, "Multiple redirects happened. That's suspicious!"
|
||||
except requests.exceptions.Timeout:
|
||||
return False, f"Request timeout {OUTGOING_WEBHOOK_TIMEOUT} secs exceeded."
|
||||
except requests.exceptions.RequestException: # This is the correct syntax
|
||||
return False, "Failed to call outgoing webhook"
|
||||
except Exception:
|
||||
return False, "Failed to call outgoing webhook"
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
from rest_framework import serializers
|
||||
|
||||
from apps.api.serializers.custom_button import CustomButtonFastSerializer
|
||||
from apps.api.serializers.escalation_policy import EscalationPolicySerializer
|
||||
from apps.api.serializers.schedule_base import ScheduleFastSerializer
|
||||
from apps.api.serializers.user import FastUserSerializer
|
||||
|
|
@ -14,7 +13,6 @@ class EscalationPolicySnapshotAPISerializer(EscalationPolicySerializer):
|
|||
notify_to_users_queue = FastUserSerializer(many=True, read_only=True)
|
||||
notify_schedule = ScheduleFastSerializer(read_only=True)
|
||||
notify_to_group = UserGroupSerializer(read_only=True)
|
||||
custom_button_trigger = CustomButtonFastSerializer(read_only=True)
|
||||
custom_webhook = WebhookFastSerializer(read_only=True)
|
||||
|
||||
class Meta(EscalationPolicySerializer.Meta):
|
||||
|
|
@ -27,7 +25,6 @@ class EscalationPolicySnapshotAPISerializer(EscalationPolicySerializer):
|
|||
"num_alerts_in_window",
|
||||
"num_minutes_in_window",
|
||||
"slack_integration_required",
|
||||
"custom_button_trigger",
|
||||
"custom_webhook",
|
||||
"notify_schedule",
|
||||
"notify_to_group",
|
||||
|
|
|
|||
|
|
@ -1,79 +0,0 @@
|
|||
from collections import defaultdict
|
||||
|
||||
from django.core.validators import URLValidator, ValidationError
|
||||
from rest_framework import serializers
|
||||
from rest_framework.validators import UniqueTogetherValidator
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
from apps.base.utils import live_settings
|
||||
from common.api_helpers.custom_fields import TeamPrimaryKeyRelatedField
|
||||
from common.api_helpers.utils import CurrentOrganizationDefault, CurrentTeamDefault, URLValidatorWithoutTLD
|
||||
from common.jinja_templater import apply_jinja_template
|
||||
from common.jinja_templater.apply_jinja_template import JinjaTemplateError, JinjaTemplateWarning
|
||||
|
||||
|
||||
class CustomButtonSerializer(serializers.ModelSerializer):
|
||||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
organization = serializers.HiddenField(default=CurrentOrganizationDefault())
|
||||
team = TeamPrimaryKeyRelatedField(allow_null=True, default=CurrentTeamDefault())
|
||||
forward_whole_payload = serializers.BooleanField(allow_null=True, required=False)
|
||||
|
||||
class Meta:
|
||||
model = CustomButton
|
||||
fields = [
|
||||
"id",
|
||||
"name",
|
||||
"team",
|
||||
"webhook",
|
||||
"data",
|
||||
"user",
|
||||
"password",
|
||||
"authorization_header",
|
||||
"organization",
|
||||
"forward_whole_payload",
|
||||
]
|
||||
extra_kwargs = {
|
||||
"name": {"required": True, "allow_null": False, "allow_blank": False},
|
||||
"webhook": {"required": True, "allow_null": False, "allow_blank": False},
|
||||
}
|
||||
|
||||
validators = [UniqueTogetherValidator(queryset=CustomButton.objects.all(), fields=["name", "organization"])]
|
||||
|
||||
def validate_webhook(self, webhook):
|
||||
if webhook:
|
||||
try:
|
||||
if live_settings.DANGEROUS_WEBHOOKS_ENABLED:
|
||||
URLValidatorWithoutTLD()(webhook)
|
||||
else:
|
||||
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:
|
||||
apply_jinja_template(data, alert_payload=defaultdict(str), alert_group_id="abcd")
|
||||
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 data
|
||||
|
||||
def validate_forward_whole_payload(self, data):
|
||||
if data is None:
|
||||
return False
|
||||
return data
|
||||
|
||||
|
||||
class CustomButtonFastSerializer(serializers.ModelSerializer):
|
||||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
|
||||
class Meta:
|
||||
model = CustomButton
|
||||
fields = ["id", "name"]
|
||||
|
|
@ -3,7 +3,7 @@ from datetime import timedelta
|
|||
|
||||
from rest_framework import serializers
|
||||
|
||||
from apps.alerts.models import CustomButton, EscalationChain, EscalationPolicy
|
||||
from apps.alerts.models import EscalationChain, EscalationPolicy
|
||||
from apps.schedules.models import OnCallSchedule
|
||||
from apps.slack.models import SlackUserGroup
|
||||
from apps.user_management.models import Team, User
|
||||
|
|
@ -23,7 +23,6 @@ FROM_TIME = "from_time"
|
|||
TO_TIME = "to_time"
|
||||
NUM_ALERTS_IN_WINDOW = "num_alerts_in_window"
|
||||
NUM_MINUTES_IN_WINDOW = "num_minutes_in_window"
|
||||
CUSTOM_BUTTON_TRIGGER = "custom_button_trigger"
|
||||
CUSTOM_WEBHOOK_TRIGGER = "custom_webhook"
|
||||
|
||||
STEP_TYPE_TO_RELATED_FIELD_MAP = {
|
||||
|
|
@ -35,7 +34,6 @@ STEP_TYPE_TO_RELATED_FIELD_MAP = {
|
|||
EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS: [NOTIFY_TEAM_MEMBERS],
|
||||
EscalationPolicy.STEP_NOTIFY_IF_TIME: [FROM_TIME, TO_TIME],
|
||||
EscalationPolicy.STEP_NOTIFY_IF_NUM_ALERTS_IN_TIME_WINDOW: [NUM_ALERTS_IN_WINDOW, NUM_MINUTES_IN_WINDOW],
|
||||
EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON: [CUSTOM_BUTTON_TRIGGER],
|
||||
EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK: [CUSTOM_WEBHOOK_TRIGGER],
|
||||
}
|
||||
|
||||
|
|
@ -75,12 +73,6 @@ class EscalationPolicySerializer(EagerLoadingMixin, serializers.ModelSerializer)
|
|||
allow_null=True,
|
||||
filter_field="slack_team_identity__organizations",
|
||||
)
|
||||
custom_button_trigger = OrganizationFilteredPrimaryKeyRelatedField(
|
||||
queryset=CustomButton.objects,
|
||||
required=False,
|
||||
allow_null=True,
|
||||
filter_field="organization",
|
||||
)
|
||||
custom_webhook = OrganizationFilteredPrimaryKeyRelatedField(
|
||||
queryset=Webhook.objects,
|
||||
required=False,
|
||||
|
|
@ -101,7 +93,6 @@ class EscalationPolicySerializer(EagerLoadingMixin, serializers.ModelSerializer)
|
|||
"num_alerts_in_window",
|
||||
"num_minutes_in_window",
|
||||
"slack_integration_required",
|
||||
"custom_button_trigger",
|
||||
"custom_webhook",
|
||||
"notify_schedule",
|
||||
"notify_to_group",
|
||||
|
|
@ -114,7 +105,6 @@ class EscalationPolicySerializer(EagerLoadingMixin, serializers.ModelSerializer)
|
|||
"notify_schedule",
|
||||
"notify_to_group",
|
||||
"notify_to_team_members",
|
||||
"custom_button_trigger",
|
||||
"custom_webhook",
|
||||
]
|
||||
PREFETCH_RELATED = ["notify_to_users_queue"]
|
||||
|
|
@ -130,7 +120,6 @@ class EscalationPolicySerializer(EagerLoadingMixin, serializers.ModelSerializer)
|
|||
TO_TIME,
|
||||
NUM_ALERTS_IN_WINDOW,
|
||||
NUM_MINUTES_IN_WINDOW,
|
||||
CUSTOM_BUTTON_TRIGGER,
|
||||
CUSTOM_WEBHOOK_TRIGGER,
|
||||
]
|
||||
|
||||
|
|
@ -239,7 +228,6 @@ class EscalationPolicyUpdateSerializer(EscalationPolicySerializer):
|
|||
TO_TIME,
|
||||
NUM_ALERTS_IN_WINDOW,
|
||||
NUM_MINUTES_IN_WINDOW,
|
||||
CUSTOM_BUTTON_TRIGGER,
|
||||
CUSTOM_WEBHOOK_TRIGGER,
|
||||
]
|
||||
|
||||
|
|
|
|||
|
|
@ -48,7 +48,6 @@ def test_alert_group_escalation_snapshot_with_important(
|
|||
"to_time": None,
|
||||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"notify_schedule": None,
|
||||
"notify_to_group": None,
|
||||
|
|
@ -62,7 +61,6 @@ def test_alert_group_escalation_snapshot_with_important(
|
|||
"to_time": None,
|
||||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"notify_schedule": None,
|
||||
"notify_to_group": None,
|
||||
|
|
|
|||
|
|
@ -1,499 +0,0 @@
|
|||
import json
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
from apps.api.permissions import LegacyAccessControlRole
|
||||
|
||||
TEST_URL = "https://amixr.io"
|
||||
URL_WITH_TLD = "http://www.google.com"
|
||||
URL_WITHOUT_TLD = "http://container:8080"
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def custom_button_internal_api_setup(make_organization_and_user_with_plugin_token, make_custom_action):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
custom_button = make_custom_action(
|
||||
name="github_button",
|
||||
webhook="https://github.com/",
|
||||
user="Chris Vanstras",
|
||||
password="qwerty",
|
||||
data='{"name": "{{ alert_payload }}"}',
|
||||
authorization_header="auth_token",
|
||||
organization=organization,
|
||||
)
|
||||
return user, token, custom_button
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_list_custom_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
expected_payload = [
|
||||
{
|
||||
"id": custom_button.public_primary_key,
|
||||
"name": "github_button",
|
||||
"team": None,
|
||||
"webhook": "https://github.com/",
|
||||
"data": '{"name": "{{ alert_payload }}"}',
|
||||
"user": "Chris Vanstras",
|
||||
"password": "qwerty",
|
||||
"authorization_header": "auth_token",
|
||||
"forward_whole_payload": False,
|
||||
}
|
||||
]
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_detail_custom_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key})
|
||||
|
||||
expected_payload = {
|
||||
"id": custom_button.public_primary_key,
|
||||
"name": "github_button",
|
||||
"team": None,
|
||||
"webhook": "https://github.com/",
|
||||
"data": '{"name": "{{ alert_payload }}"}',
|
||||
"user": "Chris Vanstras",
|
||||
"password": "qwerty",
|
||||
"authorization_header": "auth_token",
|
||||
"forward_whole_payload": False,
|
||||
}
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert response.json() == expected_payload
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_custom_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
data = {
|
||||
"name": "amixr_button",
|
||||
"webhook": TEST_URL,
|
||||
"team": None,
|
||||
}
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
custom_button = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
expected_response = data | {
|
||||
"id": custom_button.public_primary_key,
|
||||
"user": None,
|
||||
"password": None,
|
||||
"data": None,
|
||||
"authorization_header": None,
|
||||
"forward_whole_payload": False,
|
||||
}
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.data == expected_response
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_valid_data_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
data = {
|
||||
"name": "amixr_button_with_valid_data",
|
||||
"webhook": TEST_URL,
|
||||
"data": '{"name": "{{ alert_payload }}"}',
|
||||
"team": None,
|
||||
}
|
||||
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
# modify initial data by adding id and None for optional fields
|
||||
custom_button = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
expected_response = data | {
|
||||
"id": custom_button.public_primary_key,
|
||||
"user": None,
|
||||
"password": None,
|
||||
"authorization_header": None,
|
||||
"forward_whole_payload": False,
|
||||
}
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_valid_nested_data_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
data = {
|
||||
"name": "amixr_button_with_valid_data",
|
||||
"webhook": TEST_URL,
|
||||
# Assert that nested field access still works as long as the variable
|
||||
# is quoted, making it valid JSON.
|
||||
# This ensures backwards compatibility from when templates were required
|
||||
# to be JSON.
|
||||
"data": '{"nested_item": "{{ alert_payload.foo.bar }}"}',
|
||||
"team": None,
|
||||
}
|
||||
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
# modify initial data by adding id and None for optional fields
|
||||
custom_button = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
expected_response = data | {
|
||||
"id": custom_button.public_primary_key,
|
||||
"user": None,
|
||||
"password": None,
|
||||
"authorization_header": None,
|
||||
"forward_whole_payload": False,
|
||||
}
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_valid_data_after_render_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
data = {
|
||||
"name": "amixr_button_with_valid_data",
|
||||
"webhook": TEST_URL,
|
||||
"data": '{"name": "{{ alert_payload.name }}", "labels": {{ alert_payload.labels | tojson }}}',
|
||||
"team": None,
|
||||
}
|
||||
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
# modify initial data by adding id and None for optional fields
|
||||
custom_button = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
expected_response = data | {
|
||||
"id": custom_button.public_primary_key,
|
||||
"user": None,
|
||||
"password": None,
|
||||
"authorization_header": None,
|
||||
"forward_whole_payload": False,
|
||||
}
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_valid_data_after_render_use_all_data_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
data = {
|
||||
"name": "amixr_button_with_valid_data",
|
||||
"webhook": TEST_URL,
|
||||
"data": "{{ alert_payload | tojson }}",
|
||||
"team": None,
|
||||
}
|
||||
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
# modify initial data by adding id and None for optional fields
|
||||
custom_button = CustomButton.objects.get(public_primary_key=response.data["id"])
|
||||
expected_response = data | {
|
||||
"id": custom_button.public_primary_key,
|
||||
"user": None,
|
||||
"password": None,
|
||||
"authorization_header": None,
|
||||
"forward_whole_payload": False,
|
||||
}
|
||||
assert response.status_code == status.HTTP_201_CREATED
|
||||
assert response.json() == expected_response
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_invalid_url_custom_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
data = {
|
||||
"name": "amixr_button_invalid_url",
|
||||
"webhook": "invalid_url",
|
||||
}
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_invalid_data_custom_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
data = {
|
||||
"name": "amixr_button_invalid_data",
|
||||
"webhook": TEST_URL,
|
||||
"data": "{{%",
|
||||
}
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_custom_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key})
|
||||
|
||||
data = {
|
||||
"name": "github_button_updated",
|
||||
"webhook": "https://github.com/",
|
||||
"team": None,
|
||||
}
|
||||
response = client.put(
|
||||
url, data=json.dumps(data), content_type="application/json", **make_user_auth_headers(user, token)
|
||||
)
|
||||
updated_instance = CustomButton.objects.get(public_primary_key=custom_button.public_primary_key)
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
assert updated_instance.name == "github_button_updated"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_custom_button(custom_button_internal_api_setup, make_user_auth_headers):
|
||||
user, token, custom_button = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key})
|
||||
|
||||
response = client.delete(url, **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_204_NO_CONTENT
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"role,expected_status",
|
||||
[
|
||||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_create_permissions(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_user_auth_headers,
|
||||
role,
|
||||
expected_status,
|
||||
):
|
||||
_, user, token = make_organization_and_user_with_plugin_token(role)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
with patch(
|
||||
"apps.api.views.custom_button.CustomButtonView.create",
|
||||
return_value=Response(
|
||||
status=status.HTTP_200_OK,
|
||||
),
|
||||
):
|
||||
response = client.post(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"role,expected_status",
|
||||
[
|
||||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_update_permissions(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_custom_action,
|
||||
make_user_auth_headers,
|
||||
role,
|
||||
expected_status,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
||||
custom_button = make_custom_action(organization=organization)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key})
|
||||
|
||||
with patch(
|
||||
"apps.api.views.custom_button.CustomButtonView.update",
|
||||
return_value=Response(
|
||||
status=status.HTTP_200_OK,
|
||||
),
|
||||
):
|
||||
response = client.put(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
response = client.patch(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"role,expected_status",
|
||||
[
|
||||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_list_permissions(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_custom_action,
|
||||
make_user_auth_headers,
|
||||
role,
|
||||
expected_status,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
||||
make_custom_action(organization=organization)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
with patch(
|
||||
"apps.api.views.custom_button.CustomButtonView.list",
|
||||
return_value=Response(
|
||||
status=status.HTTP_200_OK,
|
||||
),
|
||||
):
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"role,expected_status",
|
||||
[
|
||||
(LegacyAccessControlRole.ADMIN, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_200_OK),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_retrieve_permissions(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_custom_action,
|
||||
make_user_auth_headers,
|
||||
role,
|
||||
expected_status,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
||||
custom_button = make_custom_action(organization=organization)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key})
|
||||
|
||||
with patch(
|
||||
"apps.api.views.custom_button.CustomButtonView.retrieve",
|
||||
return_value=Response(
|
||||
status=status.HTTP_200_OK,
|
||||
),
|
||||
):
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"role,expected_status",
|
||||
[
|
||||
(LegacyAccessControlRole.ADMIN, status.HTTP_204_NO_CONTENT),
|
||||
(LegacyAccessControlRole.EDITOR, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.VIEWER, status.HTTP_403_FORBIDDEN),
|
||||
(LegacyAccessControlRole.NONE, status.HTTP_403_FORBIDDEN),
|
||||
],
|
||||
)
|
||||
def test_custom_button_delete_permissions(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_custom_action,
|
||||
make_user_auth_headers,
|
||||
role,
|
||||
expected_status,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token(role)
|
||||
custom_button = make_custom_action(organization=organization)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key})
|
||||
|
||||
with patch(
|
||||
"apps.api.views.custom_button.CustomButtonView.destroy",
|
||||
return_value=Response(
|
||||
status=status.HTTP_204_NO_CONTENT,
|
||||
),
|
||||
):
|
||||
response = client.delete(url, format="json", **make_user_auth_headers(user, token))
|
||||
|
||||
assert response.status_code == expected_status
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_custom_button_from_other_team_with_flag(
|
||||
make_organization_and_user_with_plugin_token,
|
||||
make_team,
|
||||
make_user_auth_headers,
|
||||
make_custom_action,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_plugin_token()
|
||||
|
||||
team = make_team(organization)
|
||||
|
||||
custom_button = make_custom_action(organization=organization, team=team)
|
||||
client = APIClient()
|
||||
|
||||
url = reverse("api-internal:custom_button-detail", kwargs={"pk": custom_button.public_primary_key})
|
||||
url = f"{url}?from_organization=true"
|
||||
|
||||
response = client.get(url, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"dangerous_webhooks,webhook_url,expected_status",
|
||||
[
|
||||
(True, URL_WITH_TLD, status.HTTP_201_CREATED),
|
||||
(True, URL_WITHOUT_TLD, status.HTTP_201_CREATED),
|
||||
(False, URL_WITH_TLD, status.HTTP_201_CREATED),
|
||||
(False, URL_WITHOUT_TLD, status.HTTP_400_BAD_REQUEST),
|
||||
],
|
||||
)
|
||||
def test_url_without_tld_custom_button(
|
||||
custom_button_internal_api_setup,
|
||||
make_user_auth_headers,
|
||||
settings,
|
||||
dangerous_webhooks,
|
||||
webhook_url,
|
||||
expected_status,
|
||||
):
|
||||
settings.DANGEROUS_WEBHOOKS_ENABLED = dangerous_webhooks
|
||||
|
||||
user, token, _ = custom_button_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
|
||||
data = {
|
||||
"name": "amixr_button",
|
||||
"webhook": webhook_url,
|
||||
"team": None,
|
||||
}
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == expected_status
|
||||
|
|
@ -713,7 +713,6 @@ def test_escalation_policy_can_not_create_with_non_step_type_related_data(
|
|||
(EscalationPolicy.STEP_NOTIFY_USERS_QUEUE, ["notify_to_users_queue"]),
|
||||
(EscalationPolicy.STEP_NOTIFY_IF_TIME, ["from_time", "to_time"]),
|
||||
(EscalationPolicy.STEP_NOTIFY_MULTIPLE_USERS, ["notify_to_users_queue"]),
|
||||
(EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON, ["custom_button_trigger"]),
|
||||
(EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK, ["custom_webhook"]),
|
||||
],
|
||||
)
|
||||
|
|
@ -753,7 +752,6 @@ def test_escalation_policy_update_drop_non_step_type_related_data(
|
|||
"notify_to_team_members",
|
||||
"from_time",
|
||||
"to_time",
|
||||
"custom_button_trigger",
|
||||
"custom_webhook",
|
||||
]
|
||||
for f in related_fields:
|
||||
|
|
@ -804,7 +802,6 @@ def test_escalation_policy_switch_importance(
|
|||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"slack_integration_required": escalation_policy.slack_integration_required,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"notify_schedule": None,
|
||||
"notify_to_group": None,
|
||||
|
|
@ -862,7 +859,6 @@ def test_escalation_policy_filter_by_user(
|
|||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"slack_integration_required": False,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"notify_schedule": None,
|
||||
"notify_to_group": None,
|
||||
|
|
@ -880,7 +876,6 @@ def test_escalation_policy_filter_by_user(
|
|||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"slack_integration_required": False,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"notify_schedule": None,
|
||||
"notify_to_group": None,
|
||||
|
|
@ -946,7 +941,6 @@ def test_escalation_policy_filter_by_slack_channel(
|
|||
"num_alerts_in_window": None,
|
||||
"num_minutes_in_window": None,
|
||||
"slack_integration_required": False,
|
||||
"custom_button_trigger": None,
|
||||
"custom_webhook": None,
|
||||
"notify_schedule": None,
|
||||
"notify_to_group": None,
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@ from django.urls import reverse
|
|||
from django.utils import timezone
|
||||
from rest_framework import status
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.serializers import ValidationError
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from apps.alerts.models import EscalationPolicy
|
||||
|
|
@ -725,25 +724,6 @@ def test_create_web_schedule(schedule_internal_api_setup, make_user_auth_headers
|
|||
assert response.data == data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_invalid_ical_schedule(schedule_internal_api_setup, make_user_auth_headers):
|
||||
user, token, _, _, _, _ = schedule_internal_api_setup
|
||||
client = APIClient()
|
||||
url = reverse("api-internal:custom_button-list")
|
||||
with patch(
|
||||
"apps.api.serializers.schedule_ical.ScheduleICalSerializer.validate_ical_url_primary",
|
||||
side_effect=ValidationError("Ical download failed"),
|
||||
):
|
||||
data = {
|
||||
"ical_url_primary": ICAL_URL,
|
||||
"ical_url_overrides": None,
|
||||
"name": "created_ical_schedule",
|
||||
"type": 1,
|
||||
}
|
||||
response = client.post(url, data, format="json", **make_user_auth_headers(user, token))
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize("calendar_type", [0, 2])
|
||||
def test_create_schedule_invalid_time_zone(schedule_internal_api_setup, make_user_auth_headers, calendar_type):
|
||||
|
|
|
|||
|
|
@ -340,7 +340,6 @@ def test_team_permissions_wrong_team(
|
|||
make_user,
|
||||
make_escalation_chain,
|
||||
make_schedule,
|
||||
make_custom_action,
|
||||
make_token_for_organization,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
|
|
@ -361,14 +360,12 @@ def test_team_permissions_wrong_team(
|
|||
|
||||
escalation_chain = make_escalation_chain(organization, team=team_without_user)
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team_without_user)
|
||||
webhook = make_custom_action(organization, team=team_without_user)
|
||||
|
||||
for endpoint, instance in (
|
||||
("alertgroup", alert_group),
|
||||
("alert_receive_channel", alert_receive_channel),
|
||||
("escalation_chain", escalation_chain),
|
||||
("schedule", schedule),
|
||||
("custom_button", webhook),
|
||||
):
|
||||
url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key})
|
||||
|
||||
|
|
@ -387,7 +384,6 @@ def test_team_permissions_not_in_team(
|
|||
make_user,
|
||||
make_escalation_chain,
|
||||
make_schedule,
|
||||
make_custom_action,
|
||||
make_token_for_organization,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
|
|
@ -410,14 +406,12 @@ def test_team_permissions_not_in_team(
|
|||
|
||||
escalation_chain = make_escalation_chain(organization, team=team)
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team)
|
||||
webhook = make_custom_action(organization, team=team)
|
||||
|
||||
for endpoint, instance in (
|
||||
("alertgroup", alert_group),
|
||||
("alert_receive_channel", alert_receive_channel),
|
||||
("escalation_chain", escalation_chain),
|
||||
("schedule", schedule),
|
||||
("custom_button", webhook),
|
||||
):
|
||||
url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key})
|
||||
|
||||
|
|
@ -447,7 +441,6 @@ def test_team_permissions_right_team(
|
|||
make_user,
|
||||
make_escalation_chain,
|
||||
make_schedule,
|
||||
make_custom_action,
|
||||
make_token_for_organization,
|
||||
make_user_auth_headers,
|
||||
):
|
||||
|
|
@ -472,14 +465,12 @@ def test_team_permissions_right_team(
|
|||
|
||||
escalation_chain = make_escalation_chain(organization, team=team)
|
||||
schedule = make_schedule(organization, schedule_class=OnCallScheduleCalendar, team=team)
|
||||
webhook = make_custom_action(organization, team=team)
|
||||
|
||||
for endpoint, instance in (
|
||||
("alertgroup", alert_group),
|
||||
("alert_receive_channel", alert_receive_channel),
|
||||
("escalation_chain", escalation_chain),
|
||||
("schedule", schedule),
|
||||
("custom_button", webhook),
|
||||
("user", another_user),
|
||||
):
|
||||
url = reverse(f"api-internal:{endpoint}-detail", kwargs={"pk": instance.public_primary_key})
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ from .views.alert_receive_channel import AlertReceiveChannelView
|
|||
from .views.alert_receive_channel_template import AlertReceiveChannelTemplateView
|
||||
from .views.alerts import AlertDetailView
|
||||
from .views.channel_filter import ChannelFilterView
|
||||
from .views.custom_button import CustomButtonView
|
||||
from .views.escalation_chain import EscalationChainViewSet
|
||||
from .views.escalation_policy import EscalationPolicyView
|
||||
from .views.features import FeaturesAPIView
|
||||
|
|
@ -57,7 +56,6 @@ router.register(
|
|||
)
|
||||
router.register(r"channel_filters", ChannelFilterView, basename="channel_filter")
|
||||
router.register(r"schedules", ScheduleView, basename="schedule")
|
||||
router.register(r"custom_buttons", CustomButtonView, basename="custom_button")
|
||||
router.register(r"webhooks", WebhooksView, basename="webhooks")
|
||||
router.register(r"resolution_notes", ResolutionNoteView, basename="resolution_note")
|
||||
router.register(r"telegram_channels", TelegramChannelViewSet, basename="telegram_channel")
|
||||
|
|
|
|||
|
|
@ -1,125 +0,0 @@
|
|||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django_filters import rest_framework as filters
|
||||
from rest_framework.decorators import action
|
||||
from rest_framework.exceptions import NotFound
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework.permissions import IsAuthenticated
|
||||
from rest_framework.response import Response
|
||||
from rest_framework.viewsets import ModelViewSet
|
||||
|
||||
from apps.alerts.models import CustomButton
|
||||
from apps.api.permissions import RBACPermission
|
||||
from apps.api.serializers.custom_button import CustomButtonSerializer
|
||||
from apps.auth_token.auth import PluginAuthentication
|
||||
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
|
||||
|
||||
|
||||
class CustomButtonFilter(ByTeamModelFieldFilterMixin, ModelFieldFilterMixin, filters.FilterSet):
|
||||
team = TeamModelMultipleChoiceFilter()
|
||||
|
||||
|
||||
class CustomButtonView(TeamFilteringMixin, PublicPrimaryKeyMixin[CustomButton], ModelViewSet):
|
||||
authentication_classes = (PluginAuthentication,)
|
||||
permission_classes = (IsAuthenticated, RBACPermission)
|
||||
|
||||
rbac_permissions = {
|
||||
"metadata": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ],
|
||||
"filters": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ],
|
||||
"list": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ],
|
||||
"retrieve": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_READ],
|
||||
"create": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
|
||||
"update": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
|
||||
"partial_update": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
|
||||
"destroy": [RBACPermission.Permissions.OUTGOING_WEBHOOKS_WRITE],
|
||||
}
|
||||
|
||||
model = CustomButton
|
||||
serializer_class = CustomButtonSerializer
|
||||
|
||||
filter_backends = [SearchFilter, filters.DjangoFilterBackend]
|
||||
search_fields = ["public_primary_key", "name"]
|
||||
filterset_class = CustomButtonFilter
|
||||
|
||||
def get_queryset(self, ignore_filtering_by_available_teams=False):
|
||||
queryset = CustomButton.objects.filter(
|
||||
organization=self.request.auth.organization,
|
||||
)
|
||||
if not ignore_filtering_by_available_teams:
|
||||
queryset = queryset.filter(*self.available_teams_lookup_args).distinct()
|
||||
|
||||
return queryset
|
||||
|
||||
def get_object(self):
|
||||
# get the object from the whole organization if there is a flag `get_from_organization=true`
|
||||
# otherwise get the object from the current team
|
||||
get_from_organization = self.request.query_params.get("from_organization", "false") == "true"
|
||||
if get_from_organization:
|
||||
return self.get_object_from_organization()
|
||||
return super().get_object()
|
||||
|
||||
def get_object_from_organization(self):
|
||||
# use this method to get the object from the whole organization instead of the current team
|
||||
pk = self.kwargs["pk"]
|
||||
organization = self.request.auth.organization
|
||||
try:
|
||||
obj = (
|
||||
organization.custom_buttons.filter(*self.available_teams_lookup_args)
|
||||
.distinct()
|
||||
.get(public_primary_key=pk)
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
raise NotFound
|
||||
|
||||
# May raise a permission denied
|
||||
self.check_object_permissions(self.request, obj)
|
||||
|
||||
return obj
|
||||
|
||||
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=False)
|
||||
def filters(self, request):
|
||||
filter_name = request.query_params.get("search", None)
|
||||
api_root = "/api/internal/v1/"
|
||||
|
||||
filter_options = [
|
||||
{
|
||||
"name": "team",
|
||||
"type": "team_select",
|
||||
"href": api_root + "teams/",
|
||||
"global": True,
|
||||
},
|
||||
]
|
||||
|
||||
if filter_name is not None:
|
||||
filter_options = list(filter(lambda f: filter_name in f["name"], filter_options))
|
||||
|
||||
return Response(filter_options)
|
||||
|
|
@ -118,9 +118,6 @@ class EscalationPolicyView(
|
|||
def escalation_options(self, request):
|
||||
choices = []
|
||||
for step in EscalationPolicy.INTERNAL_API_STEPS:
|
||||
if step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON:
|
||||
continue
|
||||
|
||||
verbal = EscalationPolicy.INTERNAL_API_STEPS_TO_VERBAL_MAP[step]
|
||||
can_change_importance = (
|
||||
step in EscalationPolicy.IMPORTANT_STEPS_SET or step in EscalationPolicy.DEFAULT_STEPS_SET
|
||||
|
|
|
|||
|
|
@ -36,15 +36,6 @@ class EscalationPolicyTypeField(fields.CharField):
|
|||
return step_type
|
||||
|
||||
|
||||
class WebhookTransitionField(OrganizationFilteredPrimaryKeyRelatedField):
|
||||
def get_attribute(self, instance):
|
||||
value = super().get_attribute(instance)
|
||||
if value is None:
|
||||
# fallback to the custom button old value
|
||||
value = instance.custom_button_trigger
|
||||
return value
|
||||
|
||||
|
||||
class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer):
|
||||
id = serializers.CharField(read_only=True, source="public_primary_key")
|
||||
escalation_chain_id = OrganizationFilteredPrimaryKeyRelatedField(
|
||||
|
|
@ -76,7 +67,7 @@ class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer):
|
|||
source="notify_to_group",
|
||||
filter_field="slack_team_identity__organizations",
|
||||
)
|
||||
action_to_trigger = WebhookTransitionField(
|
||||
action_to_trigger = OrganizationFilteredPrimaryKeyRelatedField(
|
||||
queryset=Webhook.objects,
|
||||
required=False,
|
||||
source="custom_webhook",
|
||||
|
|
@ -99,6 +90,7 @@ class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer):
|
|||
"type",
|
||||
"duration",
|
||||
"important",
|
||||
"action_to_trigger",
|
||||
"persons_to_notify",
|
||||
"team_to_notify",
|
||||
"persons_to_notify_next_each_time",
|
||||
|
|
@ -190,7 +182,7 @@ class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer):
|
|||
fields_to_remove.remove("persons_to_notify_next_each_time")
|
||||
elif step in [EscalationPolicy.STEP_NOTIFY_GROUP, EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT]:
|
||||
fields_to_remove.remove("group_to_notify")
|
||||
elif step in (EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON, EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK):
|
||||
elif step == EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK:
|
||||
fields_to_remove.remove("action_to_trigger")
|
||||
elif step == EscalationPolicy.STEP_NOTIFY_IF_TIME:
|
||||
fields_to_remove.remove("notify_if_time_from")
|
||||
|
|
@ -216,7 +208,6 @@ class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer):
|
|||
"notify_schedule",
|
||||
"notify_to_group",
|
||||
"notify_to_team_members",
|
||||
"custom_button_trigger",
|
||||
"custom_webhook",
|
||||
"from_time",
|
||||
"to_time",
|
||||
|
|
@ -226,7 +217,7 @@ class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer):
|
|||
step = validated_data.get("step")
|
||||
important = validated_data.pop("important", None)
|
||||
|
||||
if step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON and validated_data.get("custom_webhook"):
|
||||
if step == EscalationPolicy._DEPRECATED_STEP_TRIGGER_CUSTOM_BUTTON and validated_data.get("custom_webhook"):
|
||||
# migrate step to webhook
|
||||
step = validated_data["step"] = EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK
|
||||
|
||||
|
|
@ -244,8 +235,6 @@ class EscalationPolicySerializer(EagerLoadingMixin, OrderedModelSerializer):
|
|||
validated_data_fields_to_remove.remove("notify_to_group")
|
||||
elif step in [EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS, EscalationPolicy.STEP_NOTIFY_TEAM_MEMBERS_IMPORTANT]:
|
||||
validated_data_fields_to_remove.remove("notify_to_team_members")
|
||||
elif step == EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON:
|
||||
validated_data_fields_to_remove.remove("custom_button_trigger")
|
||||
elif step == EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK:
|
||||
validated_data_fields_to_remove.remove("custom_webhook")
|
||||
elif step == EscalationPolicy.STEP_NOTIFY_IF_TIME:
|
||||
|
|
@ -302,8 +291,6 @@ class EscalationPolicyUpdateSerializer(EscalationPolicySerializer):
|
|||
instance.notify_to_team_members = None
|
||||
if step not in [EscalationPolicy.STEP_NOTIFY_GROUP, EscalationPolicy.STEP_NOTIFY_GROUP_IMPORTANT]:
|
||||
instance.notify_to_group = None
|
||||
if step != EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON:
|
||||
instance.custom_button_trigger = None
|
||||
if step != EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK:
|
||||
instance.custom_webhook = None
|
||||
if step != EscalationPolicy.STEP_NOTIFY_IF_TIME:
|
||||
|
|
|
|||
|
|
@ -322,7 +322,7 @@ def test_create_escalation_policy_using_webhooks(
|
|||
|
||||
data_for_create = {
|
||||
"escalation_chain_id": escalation_chain.public_primary_key,
|
||||
"type": "trigger_action",
|
||||
"type": "trigger_webhook",
|
||||
"position": 0,
|
||||
"action_to_trigger": webhook.public_primary_key,
|
||||
}
|
||||
|
|
@ -338,88 +338,6 @@ def test_create_escalation_policy_using_webhooks(
|
|||
assert response.data == serializer.data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_retrieve_escalation_policy_using_button(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
escalation_policies_setup,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
action = make_custom_action(organization)
|
||||
escalation_chain, _, _ = escalation_policies_setup(organization, user)
|
||||
|
||||
escalation_policy_action = escalation_chain.escalation_policies.create(
|
||||
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
custom_button_trigger=action,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
url = reverse("api-public:escalation_policies-detail", kwargs={"pk": escalation_policy_action.public_primary_key})
|
||||
response = client.get(url, format="json", HTTP_AUTHORIZATION=token)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
escalation_policy = EscalationPolicy.objects.get(public_primary_key=response.data["id"])
|
||||
serializer = EscalationPolicySerializer(escalation_policy)
|
||||
assert response.data == serializer.data
|
||||
assert response.data["action_to_trigger"] == action.public_primary_key
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_escalation_policy_using_button_disabled(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
escalation_policies_setup,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
action = make_custom_action(organization)
|
||||
other_action = make_custom_action(organization)
|
||||
escalation_chain, _, _ = escalation_policies_setup(organization, user)
|
||||
|
||||
escalation_policy_action = escalation_chain.escalation_policies.create(
|
||||
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
custom_button_trigger=action,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
data_to_change = {"action_to_trigger": other_action.public_primary_key}
|
||||
url = reverse("api-public:escalation_policies-detail", kwargs={"pk": escalation_policy_action.public_primary_key})
|
||||
response = client.put(url, data=data_to_change, format="json", HTTP_AUTHORIZATION=token)
|
||||
|
||||
assert response.status_code == status.HTTP_400_BAD_REQUEST
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_escalation_policy_using_button_to_webhook(
|
||||
make_organization_and_user_with_token,
|
||||
make_custom_action,
|
||||
make_custom_webhook,
|
||||
escalation_policies_setup,
|
||||
):
|
||||
organization, user, token = make_organization_and_user_with_token()
|
||||
action = make_custom_action(organization)
|
||||
webhook = make_custom_webhook(organization)
|
||||
escalation_chain, _, _ = escalation_policies_setup(organization, user)
|
||||
|
||||
escalation_policy_action = escalation_chain.escalation_policies.create(
|
||||
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
custom_button_trigger=action,
|
||||
)
|
||||
|
||||
client = APIClient()
|
||||
data_to_change = {"action_to_trigger": webhook.public_primary_key}
|
||||
url = reverse("api-public:escalation_policies-detail", kwargs={"pk": escalation_policy_action.public_primary_key})
|
||||
response = client.put(url, data=data_to_change, format="json", HTTP_AUTHORIZATION=token)
|
||||
|
||||
assert response.status_code == status.HTTP_200_OK
|
||||
|
||||
escalation_policy = EscalationPolicy.objects.get(public_primary_key=response.data["id"])
|
||||
serializer = EscalationPolicySerializer(escalation_policy)
|
||||
assert response.data == serializer.data
|
||||
# step is migrated
|
||||
assert escalation_policy.step == EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@pytest.mark.parametrize(
|
||||
"value,expected_status",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,11 @@ from common.insight_log import EntityEvent, write_resource_insight_log
|
|||
|
||||
|
||||
class ActionView(RateLimitHeadersMixin, PublicPrimaryKeyMixin, UpdateSerializerMixin, ModelViewSet):
|
||||
"""
|
||||
This endpoint is deprecated and webhooks should be used instead. This view should remain in the
|
||||
codebase in order to support terraform configurations that are still referencing it.
|
||||
"""
|
||||
|
||||
authentication_classes = (ApiTokenAuthentication,)
|
||||
permission_classes = (IsAuthenticated,)
|
||||
pagination_class = FiftyPageSizePaginator
|
||||
|
|
|
|||
|
|
@ -278,11 +278,6 @@ class AlertGroupSlackRepresentative(AlertGroupAbstractRepresentative):
|
|||
step = AcknowledgeConfirmationStep(self.log_record.alert_group.channel.organization.slack_team_identity)
|
||||
step.process_signal(self.log_record)
|
||||
|
||||
def on_custom_button_triggered(self):
|
||||
CustomButtonProcessStep = ScenarioStep.get_step("distribute_alerts", "CustomButtonProcessStep")
|
||||
step = CustomButtonProcessStep(self.log_record.alert_group.channel.organization.slack_team_identity)
|
||||
step.process_signal(self.log_record)
|
||||
|
||||
def on_wiped(self):
|
||||
WipeGroupStep = ScenarioStep.get_step("distribute_alerts", "WipeGroupStep")
|
||||
step = WipeGroupStep(self.log_record.alert_group.channel.organization.slack_team_identity)
|
||||
|
|
|
|||
|
|
@ -1,19 +1,15 @@
|
|||
import json
|
||||
import logging
|
||||
import typing
|
||||
from contextlib import suppress
|
||||
from datetime import datetime
|
||||
|
||||
from django.core.cache import cache
|
||||
from django.utils import timezone
|
||||
from jinja2 import TemplateError
|
||||
|
||||
from apps.alerts.constants import ActionSource
|
||||
from apps.alerts.incident_appearance.renderers.constants import DEFAULT_BACKUP_TITLE
|
||||
from apps.alerts.incident_appearance.renderers.slack_renderer import AlertSlackRenderer
|
||||
from apps.alerts.models import Alert, AlertGroup, AlertGroupLogRecord, AlertReceiveChannel, Invitation
|
||||
from apps.alerts.tasks import custom_button_result
|
||||
from apps.alerts.utils import render_curl_command
|
||||
from apps.api.permissions import RBACPermission
|
||||
from apps.slack.constants import CACHE_UPDATE_INCIDENT_SLACK_MESSAGE_LIFETIME
|
||||
from apps.slack.errors import (
|
||||
|
|
@ -565,69 +561,6 @@ class StopInvitationProcess(AlertGroupActionsMixin, scenario_step.ScenarioStep):
|
|||
self.alert_group_slack_service.update_alert_group_slack_message(log_record.invitation.alert_group)
|
||||
|
||||
|
||||
class CustomButtonProcessStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
|
||||
|
||||
def process_scenario(
|
||||
self,
|
||||
slack_user_identity: "SlackUserIdentity",
|
||||
slack_team_identity: "SlackTeamIdentity",
|
||||
payload: EventPayload,
|
||||
) -> None:
|
||||
from apps.alerts.models import CustomButton
|
||||
|
||||
alert_group = self.get_alert_group(slack_team_identity, payload)
|
||||
if not self.is_authorized(alert_group):
|
||||
self.open_unauthorized_warning(payload)
|
||||
return
|
||||
|
||||
custom_button_pk = payload["actions"][0]["name"].split("_")[1]
|
||||
alert_group_pk = payload["actions"][0]["name"].split("_")[2]
|
||||
try:
|
||||
CustomButton.objects.get(pk=custom_button_pk)
|
||||
except CustomButton.DoesNotExist:
|
||||
warning_text = "Oops! This button was deleted"
|
||||
self.open_warning_window(payload, warning_text=warning_text)
|
||||
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
|
||||
else:
|
||||
custom_button_result.apply_async(
|
||||
args=(
|
||||
custom_button_pk,
|
||||
alert_group_pk,
|
||||
),
|
||||
kwargs={"user_pk": self.user.pk},
|
||||
)
|
||||
|
||||
def process_signal(self, log_record: AlertGroupLogRecord) -> None:
|
||||
alert_group = log_record.alert_group
|
||||
result_message = log_record.reason
|
||||
custom_button = log_record.custom_button
|
||||
debug_message = ""
|
||||
if not log_record.step_specific_info["is_request_successful"]:
|
||||
with suppress(TemplateError, json.JSONDecodeError):
|
||||
post_kwargs = custom_button.build_post_kwargs(log_record.alert_group.alerts.first())
|
||||
curl_request = render_curl_command(log_record.custom_button.webhook, "POST", post_kwargs)
|
||||
debug_message = f"```{curl_request}```"
|
||||
|
||||
if log_record.author is not None:
|
||||
user_verbal = log_record.author.get_username_with_slack_verbal(mention=True)
|
||||
text = (
|
||||
f"{user_verbal} sent a request from an outgoing webhook `{log_record.custom_button.name}` "
|
||||
f"with the result `{result_message}`"
|
||||
)
|
||||
else:
|
||||
text = (
|
||||
f"A request from an outgoing webhook `{log_record.custom_button.name}` was sent "
|
||||
f"according to escalation policy with the result `{result_message}`"
|
||||
)
|
||||
attachments = [
|
||||
{"callback_id": "alert", "text": debug_message},
|
||||
]
|
||||
self.alert_group_slack_service.publish_message_to_alert_group_thread(
|
||||
alert_group, attachments=attachments, text=text
|
||||
)
|
||||
|
||||
|
||||
class ResolveGroupStep(AlertGroupActionsMixin, scenario_step.ScenarioStep):
|
||||
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.ALERT_GROUPS_WRITE]
|
||||
|
||||
|
|
@ -1162,10 +1095,4 @@ STEPS_ROUTING: ScenarioRoute.RoutingSteps = [
|
|||
"action_name": StopInvitationProcess.routing_uid(),
|
||||
"step": StopInvitationProcess,
|
||||
},
|
||||
{
|
||||
"payload_type": PayloadType.INTERACTIVE_MESSAGE,
|
||||
"action_type": InteractiveMessageActionType.BUTTON,
|
||||
"action_name": CustomButtonProcessStep.routing_uid(),
|
||||
"step": CustomButtonProcessStep,
|
||||
},
|
||||
]
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ LEGACY_SUFFIX = " (Legacy)"
|
|||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def convert_custom_button_to_webhook(apps, schema_editor):
|
||||
CustomButton = apps.get_model("alerts", "CustomButton")
|
||||
Webhooks = apps.get_model("webhooks", "Webhook")
|
||||
|
|
@ -36,10 +37,10 @@ def convert_custom_button_to_webhook(apps, schema_editor):
|
|||
)
|
||||
# migrate related escalation policies
|
||||
EscalationPolicies.objects.filter(
|
||||
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
step=EscalationPolicy._DEPRECATED_STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
custom_button_trigger=cb,
|
||||
).update(
|
||||
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
|
||||
step=EscalationPolicy._DEPRECATED_STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
custom_webhook=webhook,
|
||||
)
|
||||
|
||||
|
|
@ -60,7 +61,7 @@ def undo_custom_button_to_webhook(apps, schema_editor):
|
|||
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_WEBHOOK,
|
||||
custom_webhook=webhook,
|
||||
).update(
|
||||
step=EscalationPolicy.STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
step=EscalationPolicy._DEPRECATED_STEP_TRIGGER_CUSTOM_BUTTON,
|
||||
custom_button_trigger=cb,
|
||||
custom_webhook=None,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -251,8 +251,7 @@ def execute_webhook(webhook_pk, alert_group_id, user_id, escalation_policy_id, t
|
|||
|
||||
# create log record
|
||||
error_code = None
|
||||
# reuse existing webhooks record type (TODO: rename after migration)
|
||||
log_type = AlertGroupLogRecord.TYPE_CUSTOM_BUTTON_TRIGGERED
|
||||
log_type = AlertGroupLogRecord.TYPE_CUSTOM_WEBHOOK_TRIGGERED
|
||||
reason = str(status["status_code"])
|
||||
if error is not None:
|
||||
log_type = AlertGroupLogRecord.TYPE_ESCALATION_FAILED
|
||||
|
|
|
|||
|
|
@ -249,7 +249,7 @@ def test_execute_webhook_ok(
|
|||
assert log.url == templated_url
|
||||
# check log record
|
||||
log_record = alert_group.log_records.last()
|
||||
assert log_record.type == AlertGroupLogRecord.TYPE_CUSTOM_BUTTON_TRIGGERED
|
||||
assert log_record.type == AlertGroupLogRecord.TYPE_CUSTOM_WEBHOOK_TRIGGERED
|
||||
expected_info = {
|
||||
"trigger": "acknowledge",
|
||||
"webhook_id": webhook.public_primary_key,
|
||||
|
|
@ -306,7 +306,7 @@ def test_execute_webhook_via_escalation_ok(
|
|||
assert mock_requests.post.called
|
||||
# check log record
|
||||
log_record = alert_group.log_records.last()
|
||||
assert log_record.type == AlertGroupLogRecord.TYPE_CUSTOM_BUTTON_TRIGGERED
|
||||
assert log_record.type == AlertGroupLogRecord.TYPE_CUSTOM_WEBHOOK_TRIGGERED
|
||||
expected_info = {
|
||||
"trigger": "escalation",
|
||||
"webhook_id": webhook.public_primary_key,
|
||||
|
|
|
|||
|
|
@ -85,10 +85,10 @@ def escape_string(string: str):
|
|||
|
||||
class EscapeDoubleQuotesDict(dict):
|
||||
"""
|
||||
Warning: Please, do not use this dict anywhere except CustomButton._escape_alert_payload.
|
||||
This custom dict escapes double quotes to produce string which is safe to pass to json.loads()
|
||||
It fixes case when CustomButton.build_post_kwargs failing on payloads which contains string with single quote.
|
||||
In this case built-in dict's str method will surround value with double quotes.
|
||||
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.
|
||||
|
||||
For example:
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,7 @@ from urllib.parse import urljoin
|
|||
|
||||
import requests
|
||||
from django.conf import settings
|
||||
from django.core.validators import URLValidator
|
||||
from django.utils import dateparse, timezone
|
||||
from django.utils.regex_helper import _lazy_re_compile
|
||||
from icalendar import Calendar
|
||||
from rest_framework import serializers
|
||||
from rest_framework.request import Request
|
||||
|
|
@ -52,34 +50,6 @@ class CurrentTeamDefault:
|
|||
return "%s()" % self.__class__.__name__
|
||||
|
||||
|
||||
class URLValidatorWithoutTLD(URLValidator):
|
||||
"""
|
||||
Overrides Django URLValidator Regex. It removes the tld part because
|
||||
most of the time, containers don't have any TLD in their urls and such outgoing webhooks
|
||||
can't be registered.
|
||||
"""
|
||||
|
||||
host_re = (
|
||||
"("
|
||||
+ URLValidator.hostname_re
|
||||
+ URLValidator.domain_re
|
||||
+ URLValidator.tld_re
|
||||
+ "|"
|
||||
+ URLValidator.hostname_re
|
||||
+ "|localhost)"
|
||||
)
|
||||
|
||||
regex = _lazy_re_compile(
|
||||
r"^(?:[a-z0-9.+-]*)://" # scheme is validated separately
|
||||
r"(?:[^\s:@/]+(?::[^\s:@/]*)?@)?" # user:pass authentication
|
||||
r"(?:" + URLValidator.ipv4_re + "|" + URLValidator.ipv6_re + "|" + host_re + ")"
|
||||
r"(?::[0-9]{1,5})?" # port
|
||||
r"(?:[/?#][^\s]*)?" # resource path
|
||||
r"\Z",
|
||||
re.IGNORECASE,
|
||||
)
|
||||
|
||||
|
||||
class CurrentUserDefault:
|
||||
"""
|
||||
Utility class to get the current user right from the serializer field.
|
||||
|
|
|
|||
|
|
@ -1,20 +0,0 @@
|
|||
import pytest
|
||||
from django.core.validators import ValidationError
|
||||
|
||||
from common.api_helpers.utils import URLValidatorWithoutTLD
|
||||
|
||||
valid_urls = ["https://www.google.com", "https://www.google", "http://conatainer1"]
|
||||
invalid_urls = ["https:/www.google.com", "htt://www.google.com/"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", valid_urls)
|
||||
def test_urlvalidator_without_tld_valid_urls(url):
|
||||
# Test valid URLs
|
||||
URLValidatorWithoutTLD()(url)
|
||||
|
||||
|
||||
@pytest.mark.parametrize("url", invalid_urls)
|
||||
def test_urlvalidator_without_tld_invalid_urls(url):
|
||||
# Test an invalid URL
|
||||
with pytest.raises(ValidationError):
|
||||
URLValidatorWithoutTLD()(url)
|
||||
|
|
@ -177,7 +177,6 @@ CELERY_TASK_ROUTES = {
|
|||
"apps.telegram.tasks.send_log_and_actions_message": {"queue": "telegram"},
|
||||
"apps.telegram.tasks.on_alert_group_action_triggered_async": {"queue": "telegram"},
|
||||
# WEBHOOK
|
||||
"apps.alerts.tasks.custom_button_result.custom_button_result": {"queue": "webhook"},
|
||||
"apps.alerts.tasks.custom_webhook_result.custom_webhook_result": {"queue": "webhook"},
|
||||
"apps.webhooks.tasks.trigger_webhook.execute_webhook": {"queue": "webhook"},
|
||||
"apps.webhooks.tasks.trigger_webhook.send_webhook_event": {"queue": "webhook"},
|
||||
|
|
|
|||
|
|
@ -1,15 +1,13 @@
|
|||
import { Locator, expect, test } from '../fixtures';
|
||||
import { createEscalationChain, EscalationStep, selectEscalationStepValue } from '../utils/escalationChain';
|
||||
import { createEscalationChain, EscalationStep } from '../utils/escalationChain';
|
||||
import { generateRandomValue } from '../utils/forms';
|
||||
|
||||
test('escalation policy does not go back to "Default" after adding users to notify', async ({ adminRolePage }) => {
|
||||
const { page, userName } = adminRolePage;
|
||||
const escalationChainName = generateRandomValue();
|
||||
|
||||
// create important escalation step
|
||||
await createEscalationChain(page, escalationChainName, EscalationStep.NotifyUsers, null, true);
|
||||
// add user to notify
|
||||
await selectEscalationStepValue(page, EscalationStep.NotifyUsers, userName);
|
||||
// create important escalation step + add user to notif
|
||||
await createEscalationChain(page, escalationChainName, EscalationStep.NotifyUsers, userName, true);
|
||||
|
||||
// reload and check if important is still selected
|
||||
await page.reload();
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
import { test } from '../fixtures';
|
||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
import { checkWebhookPresenceInTable } from '../utils/outgoingWebhooks';
|
||||
|
||||
test('create simple webhook and check it is displayed on the list correctly', async ({ adminRolePage: { page } }) => {
|
||||
const WEBHOOK_NAME = generateRandomValue();
|
||||
const WEBHOOK_URL = 'https://example.com';
|
||||
await goToOnCallPage(page, 'outgoing_webhooks');
|
||||
|
||||
await clickButton({ page, buttonText: 'New Outgoing Webhook' });
|
||||
|
||||
await page.getByText('Simple').first().click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.keyboard.insertText(WEBHOOK_URL);
|
||||
await page.locator('[name=name]').fill(WEBHOOK_NAME);
|
||||
await page.getByLabel('New Outgoing Webhook').getByRole('img').nth(1).click(); // Open team dropdown
|
||||
await page.getByLabel('Select options menu').getByText('No team').click();
|
||||
await clickButton({ page, buttonText: 'Create' });
|
||||
|
||||
await checkWebhookPresenceInTable({ page, webhookName: WEBHOOK_NAME, expectedTriggerType: 'Escalation step' });
|
||||
});
|
||||
|
|
@ -0,0 +1,82 @@
|
|||
import express from 'express';
|
||||
|
||||
import { expect, test } from '../fixtures';
|
||||
import { EscalationStep, createEscalationChain } from '../utils/escalationChain';
|
||||
import { clickButton, generateRandomValue } from '../utils/forms';
|
||||
import { createIntegrationAndSendDemoAlert } from '../utils/integrations';
|
||||
import { goToOnCallPage } from '../utils/navigation';
|
||||
import { checkWebhookPresenceInTable } from '../utils/outgoingWebhooks';
|
||||
|
||||
const createWebhook = async ({ page, webhookName, webhookUrl }) => {
|
||||
await goToOnCallPage(page, 'outgoing_webhooks');
|
||||
|
||||
await clickButton({ page, buttonText: 'New Outgoing Webhook' });
|
||||
|
||||
await page.getByText('Simple').first().click();
|
||||
|
||||
await page.waitForTimeout(2000);
|
||||
|
||||
await page.keyboard.insertText(webhookUrl);
|
||||
await page.locator('[name=name]').fill(webhookName);
|
||||
await page.getByLabel('New Outgoing Webhook').getByRole('img').nth(1).click(); // Open team dropdown
|
||||
await page.getByLabel('Select options menu').getByText('No team').click();
|
||||
await clickButton({ page, buttonText: 'Create' });
|
||||
};
|
||||
|
||||
test.describe('simple webhook', () => {
|
||||
test('Create and check it is displayed on the list correctly', async ({ adminRolePage: { page } }) => {
|
||||
const webhookName = generateRandomValue();
|
||||
await createWebhook({ page, webhookName, webhookUrl: 'https://example.com' });
|
||||
await checkWebhookPresenceInTable({ page, webhookName, expectedTriggerType: 'Escalation step' });
|
||||
});
|
||||
|
||||
/**
|
||||
* TODO: will finalize this test in a separate PR. It passes locally but something about the networking on
|
||||
* GitHub Actions causes the test to fail on CI
|
||||
*/
|
||||
test.skip('Create and check that our webhook actually receives the payload', async ({ adminRolePage: { page } }) => {
|
||||
const escalationChainName = generateRandomValue();
|
||||
const integrationName = generateRandomValue();
|
||||
const PORT = 5050;
|
||||
|
||||
/**
|
||||
* This is a simple express server that listens for outgoing webhook requests
|
||||
* The backend is able to communicate with it via host.docker.internal (docker -> host communication)
|
||||
*/
|
||||
let resolveRequest: (value: unknown) => void;
|
||||
const requestPromise = new Promise<any>((resolve) => {
|
||||
resolveRequest = resolve;
|
||||
});
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
app.post('/', (req, res) => {
|
||||
resolveRequest(req.body); // Resolve the promise with the request body
|
||||
res.send('ok');
|
||||
});
|
||||
app.listen(PORT);
|
||||
|
||||
/**
|
||||
* TODO: this might need to be parametrized to be read from an env var
|
||||
* rather than hardcoding the hostname to be host.docker.internal
|
||||
*/
|
||||
const webhookUrl = `http://host.docker.internal:${PORT}`;
|
||||
const webhookName = generateRandomValue();
|
||||
|
||||
await createWebhook({ page, webhookName, webhookUrl });
|
||||
|
||||
await createEscalationChain(page, escalationChainName, EscalationStep.TriggerWebhook, webhookName);
|
||||
await createIntegrationAndSendDemoAlert(page, integrationName, escalationChainName);
|
||||
|
||||
/**
|
||||
* Wait for the request to be received on our express server's endpoint handler
|
||||
* when the request is received, the promise will be resolved w/ the request body
|
||||
*/
|
||||
const payload = await requestPromise;
|
||||
|
||||
expect(payload.alert_group.state).toEqual('firing');
|
||||
expect(payload.alert_payload.message).toEqual('This alert was sent by user for demonstration purposes');
|
||||
expect(payload.integration.name).toEqual(`${integrationName} - Webhook`);
|
||||
expect(payload.integration.type).toEqual('webhook');
|
||||
});
|
||||
});
|
||||
|
|
@ -7,11 +7,13 @@ export enum EscalationStep {
|
|||
NotifyUsers = 'Notify users',
|
||||
NotifyUsersFromOnCallSchedule = 'Notify users from on-call schedule',
|
||||
ContinueEscalationIfCurrentUTCTimeIsIn = 'Continue escalation if current UTC time is in range',
|
||||
TriggerWebhook = 'Trigger webhook',
|
||||
}
|
||||
|
||||
const escalationStepValuePlaceholder: Partial<Record<EscalationStep, string>> = {
|
||||
[EscalationStep.NotifyUsers]: 'Select User',
|
||||
[EscalationStep.NotifyUsersFromOnCallSchedule]: 'Select Schedule',
|
||||
[EscalationStep.TriggerWebhook]: 'Select Webhook',
|
||||
};
|
||||
|
||||
export const createEscalationChain = async (
|
||||
|
|
|
|||
|
|
@ -61,6 +61,7 @@
|
|||
"@testing-library/react": "14.0.0",
|
||||
"@testing-library/user-event": "^14.4.3",
|
||||
"@types/dompurify": "^2.3.4",
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/jest": "^29.5.0",
|
||||
"@types/lodash": "^4.14.194",
|
||||
"@types/lodash-es": "^4.17.6",
|
||||
|
|
@ -87,6 +88,7 @@
|
|||
"eslint-plugin-rulesdir": "^0.2.1",
|
||||
"eslint-plugin-unused-imports": "^3.1.0",
|
||||
"eslint-webpack-plugin": "^4.0.1",
|
||||
"express": "^4.18.3",
|
||||
"fork-ts-checker-webpack-plugin": "^8.0.0",
|
||||
"glob": "^10.2.7",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
|
|
|
|||
|
|
@ -3121,6 +3121,21 @@
|
|||
dependencies:
|
||||
"@babel/types" "^7.3.0"
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.19.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.19.5.tgz#04ce9a3b677dc8bd681a17da1ab9835dc9d3ede4"
|
||||
integrity sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==
|
||||
dependencies:
|
||||
"@types/connect" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/connect@*":
|
||||
version "3.4.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.38.tgz#5ba7f3bc4fbbdeaff8dded952e5ff2cc53f8d858"
|
||||
integrity sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/d3-color@*":
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/d3-color/-/d3-color-3.1.0.tgz#6594da178ded6c7c3842f3cc0ac84b156f12f2d4"
|
||||
|
|
@ -3174,6 +3189,26 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4"
|
||||
integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==
|
||||
|
||||
"@types/express-serve-static-core@^4.17.33":
|
||||
version "4.17.43"
|
||||
resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.17.43.tgz#10d8444be560cb789c4735aea5eac6e5af45df54"
|
||||
integrity sha512-oaYtiBirUOPQGSWNGPWnzyAFJ0BP3cwvN4oWZQY+zUBwpVIGsKUkpBpSztp74drYcjavs7SKFZ4DX1V2QeN8rg==
|
||||
dependencies:
|
||||
"@types/node" "*"
|
||||
"@types/qs" "*"
|
||||
"@types/range-parser" "*"
|
||||
"@types/send" "*"
|
||||
|
||||
"@types/express@^4.17.21":
|
||||
version "4.17.21"
|
||||
resolved "https://registry.yarnpkg.com/@types/express/-/express-4.17.21.tgz#c26d4a151e60efe0084b23dc3369ebc631ed192d"
|
||||
integrity sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==
|
||||
dependencies:
|
||||
"@types/body-parser" "*"
|
||||
"@types/express-serve-static-core" "^4.17.33"
|
||||
"@types/qs" "*"
|
||||
"@types/serve-static" "*"
|
||||
|
||||
"@types/fined@*":
|
||||
version "1.1.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/fined/-/fined-1.1.3.tgz#83f03e8f0a8d3673dfcafb18fce3571f6250e1bc"
|
||||
|
|
@ -3207,6 +3242,11 @@
|
|||
"@types/react" "*"
|
||||
hoist-non-react-statics "^3.3.0"
|
||||
|
||||
"@types/http-errors@*":
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/http-errors/-/http-errors-2.0.4.tgz#7eb47726c391b7345a6ec35ad7f4de469cf5ba4f"
|
||||
integrity sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==
|
||||
|
||||
"@types/inquirer@^6.5.0":
|
||||
version "6.5.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/inquirer/-/inquirer-6.5.0.tgz#b83b0bf30b88b8be7246d40e51d32fe9d10e09be"
|
||||
|
|
@ -3318,6 +3358,16 @@
|
|||
dependencies:
|
||||
"@types/unist" "*"
|
||||
|
||||
"@types/mime@*":
|
||||
version "3.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.4.tgz#2198ac274de6017b44d941e00261d5bc6a0e0a45"
|
||||
integrity sha512-iJt33IQnVRkqeqC7PzBHPTC6fDlRNRW8vjrgqtScAhrmMwe8c4Eo7+fUGTa+XdWrpEgpyKWMYmi2dIwMAYRzPw==
|
||||
|
||||
"@types/mime@^1":
|
||||
version "1.3.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/mime/-/mime-1.3.5.tgz#1ef302e01cf7d2b5a0fa526790c9123bf1d06690"
|
||||
integrity sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==
|
||||
|
||||
"@types/minimatch@*":
|
||||
version "5.1.2"
|
||||
resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-5.1.2.tgz#07508b45797cb81ec3f273011b054cd0755eddca"
|
||||
|
|
@ -3365,6 +3415,11 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/prop-types/-/prop-types-15.7.5.tgz#5f19d2b85a98e9558036f6a3cacc8819420f05cf"
|
||||
integrity sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==
|
||||
|
||||
"@types/qs@*":
|
||||
version "6.9.12"
|
||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.12.tgz#afa96b383a3a6fdc859453a1892d41b607fc7756"
|
||||
integrity sha512-bZcOkJ6uWrL0Qb2NAWKa7TBU+mJHPzhx9jjLL1KHF+XpzEcR7EXHvjbHlGtR/IsP1vyPrehuS6XqkmaePy//mg==
|
||||
|
||||
"@types/query-string@^6.3.0":
|
||||
version "6.3.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/query-string/-/query-string-6.3.0.tgz#b6fa172a01405abcaedac681118e78429d62ea39"
|
||||
|
|
@ -3372,6 +3427,11 @@
|
|||
dependencies:
|
||||
query-string "*"
|
||||
|
||||
"@types/range-parser@*":
|
||||
version "1.2.7"
|
||||
resolved "https://registry.yarnpkg.com/@types/range-parser/-/range-parser-1.2.7.tgz#50ae4353eaaddc04044279812f52c8c65857dbcb"
|
||||
integrity sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==
|
||||
|
||||
"@types/react-copy-to-clipboard@^5.0.4":
|
||||
version "5.0.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/react-copy-to-clipboard/-/react-copy-to-clipboard-5.0.4.tgz#558f2c38a97f53693e537815f6024f1e41e36a7e"
|
||||
|
|
@ -3465,6 +3525,23 @@
|
|||
resolved "https://registry.yarnpkg.com/@types/semver/-/semver-7.5.6.tgz#c65b2bfce1bec346582c07724e3f8c1017a20339"
|
||||
integrity sha512-dn1l8LaMea/IjDoHNd9J52uBbInB796CDffS6VdIxvqYCPSG0V0DzHp76GpaWnlhg88uYyPbXCDIowa86ybd5A==
|
||||
|
||||
"@types/send@*":
|
||||
version "0.17.4"
|
||||
resolved "https://registry.yarnpkg.com/@types/send/-/send-0.17.4.tgz#6619cd24e7270793702e4e6a4b958a9010cfc57a"
|
||||
integrity sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==
|
||||
dependencies:
|
||||
"@types/mime" "^1"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/serve-static@*":
|
||||
version "1.15.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.15.5.tgz#15e67500ec40789a1e8c9defc2d32a896f05b033"
|
||||
integrity sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==
|
||||
dependencies:
|
||||
"@types/http-errors" "*"
|
||||
"@types/mime" "*"
|
||||
"@types/node" "*"
|
||||
|
||||
"@types/shimmer@^1.0.2":
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/@types/shimmer/-/shimmer-1.0.5.tgz#491d8984d4510e550bfeb02d518791d7f59d2b88"
|
||||
|
|
@ -3911,6 +3988,14 @@ abab@^2.0.6:
|
|||
resolved "https://registry.yarnpkg.com/abab/-/abab-2.0.6.tgz#41b80f2c871d19686216b82309231cfd3cb3d291"
|
||||
integrity sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==
|
||||
|
||||
accepts@~1.3.8:
|
||||
version "1.3.8"
|
||||
resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.8.tgz#0bf0be125b67014adcb0b0921e62db7bffe16b2e"
|
||||
integrity sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==
|
||||
dependencies:
|
||||
mime-types "~2.1.34"
|
||||
negotiator "0.6.3"
|
||||
|
||||
acorn-globals@^7.0.0:
|
||||
version "7.0.1"
|
||||
resolved "https://registry.yarnpkg.com/acorn-globals/-/acorn-globals-7.0.1.tgz#0dbf05c44fa7c94332914c02066d5beff62c40c3"
|
||||
|
|
@ -4187,6 +4272,11 @@ array-each@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/array-each/-/array-each-1.0.1.tgz#a794af0c05ab1752846ee753a1f211a05ba0c44f"
|
||||
integrity sha512-zHjL5SZa68hkKHBFBK6DJCTtr9sfTCPCaph/L7tMSLcTFgy+zX7E+6q5UArbtOtMBCtxdICpfTCspRse+ywyXA==
|
||||
|
||||
array-flatten@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2"
|
||||
integrity sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==
|
||||
|
||||
array-includes@^3.1.5:
|
||||
version "3.1.6"
|
||||
resolved "https://registry.yarnpkg.com/array-includes/-/array-includes-3.1.6.tgz#9e9e720e194f198266ba9e18c29e6a9b0e4b225f"
|
||||
|
|
@ -4535,6 +4625,24 @@ binary-extensions@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.2.0.tgz#75f502eeaf9ffde42fc98829645be4ea76bd9e2d"
|
||||
integrity sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==
|
||||
|
||||
body-parser@1.20.2:
|
||||
version "1.20.2"
|
||||
resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.20.2.tgz#6feb0e21c4724d06de7ff38da36dad4f57a747fd"
|
||||
integrity sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
content-type "~1.0.5"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
http-errors "2.0.0"
|
||||
iconv-lite "0.4.24"
|
||||
on-finished "2.4.1"
|
||||
qs "6.11.0"
|
||||
raw-body "2.5.2"
|
||||
type-is "~1.6.18"
|
||||
unpipe "1.0.0"
|
||||
|
||||
body@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/body/-/body-5.1.0.tgz#e4ba0ce410a46936323367609ecb4e6553125069"
|
||||
|
|
@ -4647,6 +4755,11 @@ bytes@1:
|
|||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-1.0.0.tgz#3569ede8ba34315fab99c3e92cb04c7220de1fa8"
|
||||
integrity sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==
|
||||
|
||||
bytes@3.1.2:
|
||||
version "3.1.2"
|
||||
resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.1.2.tgz#8b0beeb98605adf1b128fa4386403c009e0221a5"
|
||||
integrity sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==
|
||||
|
||||
cache-base@^1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/cache-base/-/cache-base-1.0.1.tgz#0a7f46416831c8b662ee36fe4e7c59d76f666ab2"
|
||||
|
|
@ -5144,6 +5257,18 @@ constant-case@^3.0.4:
|
|||
tslib "^2.0.3"
|
||||
upper-case "^2.0.2"
|
||||
|
||||
content-disposition@0.5.4:
|
||||
version "0.5.4"
|
||||
resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.4.tgz#8b82b4efac82512a02bb0b1dcec9d2c5e8eb5bfe"
|
||||
integrity sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==
|
||||
dependencies:
|
||||
safe-buffer "5.2.1"
|
||||
|
||||
content-type@~1.0.4, content-type@~1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.5.tgz#8b773162656d1d1086784c8f23a54ce6d73d7918"
|
||||
integrity sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==
|
||||
|
||||
continuable-cache@^0.3.1:
|
||||
version "0.3.1"
|
||||
resolved "https://registry.yarnpkg.com/continuable-cache/-/continuable-cache-0.3.1.tgz#bd727a7faed77e71ff3985ac93351a912733ad0f"
|
||||
|
|
@ -5159,6 +5284,16 @@ convert-source-map@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a"
|
||||
integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==
|
||||
|
||||
cookie-signature@1.0.6:
|
||||
version "1.0.6"
|
||||
resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c"
|
||||
integrity sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==
|
||||
|
||||
cookie@0.5.0:
|
||||
version "0.5.0"
|
||||
resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.5.0.tgz#d1f5d71adec6558c58f389987c366aa47e994f8b"
|
||||
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
|
||||
|
||||
copy-descriptor@^0.1.0:
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d"
|
||||
|
|
@ -5631,6 +5766,13 @@ debounce@^1.2.1:
|
|||
resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.1.tgz#38881d8f4166a5c5848020c11827b834bcb3e0a5"
|
||||
integrity sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==
|
||||
|
||||
debug@2.6.9, debug@^2.2.0, debug@^2.3.3:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
|
||||
version "4.3.4"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865"
|
||||
|
|
@ -5638,13 +5780,6 @@ debug@4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.2.0, debug@^4.3.1, d
|
|||
dependencies:
|
||||
ms "2.1.2"
|
||||
|
||||
debug@^2.2.0, debug@^2.3.3:
|
||||
version "2.6.9"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f"
|
||||
integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
|
||||
dependencies:
|
||||
ms "2.0.0"
|
||||
|
||||
debug@^3.1.0, debug@^3.2.7:
|
||||
version "3.2.7"
|
||||
resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.7.tgz#72580b7e9145fb39b6676f9c5e5fb100b934179a"
|
||||
|
|
@ -5816,6 +5951,16 @@ delayed-stream@~1.0.0:
|
|||
resolved "https://registry.yarnpkg.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
|
||||
integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
|
||||
|
||||
depd@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df"
|
||||
integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==
|
||||
|
||||
destroy@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015"
|
||||
integrity sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==
|
||||
|
||||
detect-file@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/detect-file/-/detect-file-1.0.0.tgz#f0d66d03672a825cb1b73bdb3fe62310c8e552b7"
|
||||
|
|
@ -5988,6 +6133,11 @@ easy-table@1.2.0:
|
|||
optionalDependencies:
|
||||
wcwidth "^1.0.1"
|
||||
|
||||
ee-first@1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d"
|
||||
integrity sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==
|
||||
|
||||
electron-to-chromium@^1.4.251:
|
||||
version "1.4.284"
|
||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.284.tgz#61046d1e4cab3a25238f6bf7413795270f125592"
|
||||
|
|
@ -6030,6 +6180,11 @@ encode-registry@^3.0.1:
|
|||
dependencies:
|
||||
mem "^8.0.0"
|
||||
|
||||
encodeurl@~1.0.2:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59"
|
||||
integrity sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==
|
||||
|
||||
end-of-stream@^1.1.0:
|
||||
version "1.4.4"
|
||||
resolved "https://registry.yarnpkg.com/end-of-stream/-/end-of-stream-1.4.4.tgz#5ae64a5f45057baf3626ec14da0ca5e4b2431eb0"
|
||||
|
|
@ -6307,6 +6462,11 @@ escalade@^3.1.1:
|
|||
resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.1.1.tgz#d8cfdc7000965c5a0174b4a82eaa5c0552742e40"
|
||||
integrity sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==
|
||||
|
||||
escape-html@~1.0.3:
|
||||
version "1.0.3"
|
||||
resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988"
|
||||
integrity sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==
|
||||
|
||||
escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5:
|
||||
version "1.0.5"
|
||||
resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4"
|
||||
|
|
@ -6690,6 +6850,11 @@ esutils@^2.0.2:
|
|||
resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64"
|
||||
integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
|
||||
|
||||
etag@~1.8.1:
|
||||
version "1.8.1"
|
||||
resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887"
|
||||
integrity sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==
|
||||
|
||||
eventemitter3@5.0.0:
|
||||
version "5.0.0"
|
||||
resolved "https://registry.yarnpkg.com/eventemitter3/-/eventemitter3-5.0.0.tgz#084eb7f5b5388df1451e63f4c2aafd71b217ccb3"
|
||||
|
|
@ -6793,6 +6958,43 @@ expect@^29.0.0, expect@^29.7.0:
|
|||
jest-message-util "^29.7.0"
|
||||
jest-util "^29.7.0"
|
||||
|
||||
express@^4.18.3:
|
||||
version "4.18.3"
|
||||
resolved "https://registry.yarnpkg.com/express/-/express-4.18.3.tgz#6870746f3ff904dee1819b82e4b51509afffb0d4"
|
||||
integrity sha512-6VyCijWQ+9O7WuVMTRBTl+cjNNIzD5cY5mQ1WM8r/LEkI2u8EYpOotESNwzNlyCn3g+dmjKYI6BmNneSr/FSRw==
|
||||
dependencies:
|
||||
accepts "~1.3.8"
|
||||
array-flatten "1.1.1"
|
||||
body-parser "1.20.2"
|
||||
content-disposition "0.5.4"
|
||||
content-type "~1.0.4"
|
||||
cookie "0.5.0"
|
||||
cookie-signature "1.0.6"
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
finalhandler "1.2.0"
|
||||
fresh "0.5.2"
|
||||
http-errors "2.0.0"
|
||||
merge-descriptors "1.0.1"
|
||||
methods "~1.1.2"
|
||||
on-finished "2.4.1"
|
||||
parseurl "~1.3.3"
|
||||
path-to-regexp "0.1.7"
|
||||
proxy-addr "~2.0.7"
|
||||
qs "6.11.0"
|
||||
range-parser "~1.2.1"
|
||||
safe-buffer "5.2.1"
|
||||
send "0.18.0"
|
||||
serve-static "1.15.0"
|
||||
setprototypeof "1.2.0"
|
||||
statuses "2.0.1"
|
||||
type-is "~1.6.18"
|
||||
utils-merge "1.0.1"
|
||||
vary "~1.1.2"
|
||||
|
||||
extend-shallow@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/extend-shallow/-/extend-shallow-2.0.1.tgz#51af7d614ad9a9f610ea1bafbb989d6b1c56890f"
|
||||
|
|
@ -6984,6 +7186,19 @@ filter-obj@^1.1.0:
|
|||
resolved "https://registry.yarnpkg.com/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b"
|
||||
integrity sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==
|
||||
|
||||
finalhandler@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.2.0.tgz#7d23fe5731b207b4640e4fcd00aec1f9207a7b32"
|
||||
integrity sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
on-finished "2.4.1"
|
||||
parseurl "~1.3.3"
|
||||
statuses "2.0.1"
|
||||
unpipe "~1.0.0"
|
||||
|
||||
find-cache-dir@^4.0.0:
|
||||
version "4.0.0"
|
||||
resolved "https://registry.yarnpkg.com/find-cache-dir/-/find-cache-dir-4.0.0.tgz#a30ee0448f81a3990708f6453633c733e2f6eec2"
|
||||
|
|
@ -7124,6 +7339,11 @@ form-data@^4.0.0:
|
|||
combined-stream "^1.0.8"
|
||||
mime-types "^2.1.12"
|
||||
|
||||
forwarded@0.2.0:
|
||||
version "0.2.0"
|
||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.2.0.tgz#2269936428aad4c15c7ebe9779a84bf0b2a81811"
|
||||
integrity sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
|
||||
|
||||
fragment-cache@^0.2.1:
|
||||
version "0.2.1"
|
||||
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
|
||||
|
|
@ -7131,6 +7351,11 @@ fragment-cache@^0.2.1:
|
|||
dependencies:
|
||||
map-cache "^0.2.2"
|
||||
|
||||
fresh@0.5.2:
|
||||
version "0.5.2"
|
||||
resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7"
|
||||
integrity sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==
|
||||
|
||||
fs-extra@10.1.0, fs-extra@^10.0.0:
|
||||
version "10.1.0"
|
||||
resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-10.1.0.tgz#02873cfbc4084dde127eaa5f9905eef2325d1abf"
|
||||
|
|
@ -7752,6 +7977,17 @@ htmlparser2@^3.10.0:
|
|||
inherits "^2.0.1"
|
||||
readable-stream "^3.1.1"
|
||||
|
||||
http-errors@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-2.0.0.tgz#b7774a1486ef73cf7667ac9ae0858c012c57b9d3"
|
||||
integrity sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==
|
||||
dependencies:
|
||||
depd "2.0.0"
|
||||
inherits "2.0.4"
|
||||
setprototypeof "1.2.0"
|
||||
statuses "2.0.1"
|
||||
toidentifier "1.0.1"
|
||||
|
||||
http-parser-js@>=0.5.1:
|
||||
version "0.5.8"
|
||||
resolved "https://registry.yarnpkg.com/http-parser-js/-/http-parser-js-0.5.8.tgz#af23090d9ac4e24573de6f6aecc9d84a48bf20e3"
|
||||
|
|
@ -7803,6 +8039,13 @@ i18next@^22.0.0:
|
|||
dependencies:
|
||||
"@babel/runtime" "^7.17.2"
|
||||
|
||||
iconv-lite@0.4.24, iconv-lite@^0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
iconv-lite@0.6, iconv-lite@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.6.3.tgz#a52f80bf38da1952eb5c681790719871a1a72501"
|
||||
|
|
@ -7810,13 +8053,6 @@ iconv-lite@0.6, iconv-lite@0.6.3:
|
|||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3.0.0"
|
||||
|
||||
iconv-lite@^0.4.24:
|
||||
version "0.4.24"
|
||||
resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.24.tgz#2022b4b25fbddc21d2f524974a474aafe733908b"
|
||||
integrity sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
|
||||
dependencies:
|
||||
safer-buffer ">= 2.1.2 < 3"
|
||||
|
||||
icss-utils@^5.0.0, icss-utils@^5.1.0:
|
||||
version "5.1.0"
|
||||
resolved "https://registry.yarnpkg.com/icss-utils/-/icss-utils-5.1.0.tgz#c6be6858abd013d768e98366ae47e25d5887b1ae"
|
||||
|
|
@ -7918,7 +8154,7 @@ inflight@^1.0.4:
|
|||
once "^1.3.0"
|
||||
wrappy "1"
|
||||
|
||||
inherits@2, inherits@^2.0.1, inherits@^2.0.3:
|
||||
inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3:
|
||||
version "2.0.4"
|
||||
resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
|
||||
integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
|
||||
|
|
@ -8013,6 +8249,11 @@ invariant@^2.2.1, invariant@^2.2.2, invariant@^2.2.4:
|
|||
dependencies:
|
||||
loose-envify "^1.0.0"
|
||||
|
||||
ipaddr.js@1.9.1:
|
||||
version "1.9.1"
|
||||
resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.9.1.tgz#bff38543eeb8984825079ff3a2a8e6cbd46781b3"
|
||||
integrity sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
|
||||
|
||||
is-absolute@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/is-absolute/-/is-absolute-1.0.0.tgz#395e1ae84b11f26ad1795e73c17378e48a301576"
|
||||
|
|
@ -9691,6 +9932,11 @@ mdn-data@2.0.14:
|
|||
resolved "https://registry.yarnpkg.com/mdn-data/-/mdn-data-2.0.14.tgz#7113fc4281917d63ce29b43446f701e68c25ba50"
|
||||
integrity sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==
|
||||
|
||||
media-typer@0.3.0:
|
||||
version "0.3.0"
|
||||
resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748"
|
||||
integrity sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==
|
||||
|
||||
mem@^6.0.1:
|
||||
version "6.1.1"
|
||||
resolved "https://registry.yarnpkg.com/mem/-/mem-6.1.1.tgz#ea110c2ebc079eca3022e6b08c85a795e77f6318"
|
||||
|
|
@ -9747,6 +9993,11 @@ meow@^9.0.0:
|
|||
type-fest "^0.18.0"
|
||||
yargs-parser "^20.2.3"
|
||||
|
||||
merge-descriptors@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61"
|
||||
integrity sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==
|
||||
|
||||
merge-stream@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"
|
||||
|
|
@ -9757,6 +10008,11 @@ merge2@^1.2.3, merge2@^1.3.0, merge2@^1.4.1:
|
|||
resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae"
|
||||
integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
|
||||
|
||||
methods@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee"
|
||||
integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==
|
||||
|
||||
micro-memoize@^4.1.2:
|
||||
version "4.1.2"
|
||||
resolved "https://registry.yarnpkg.com/micro-memoize/-/micro-memoize-4.1.2.tgz#ce719c1ba1e41592f1cd91c64c5f41dcbf135f36"
|
||||
|
|
@ -9802,13 +10058,18 @@ mime-db@1.52.0:
|
|||
resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
|
||||
integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
|
||||
|
||||
mime-types@^2.1.12, mime-types@^2.1.27:
|
||||
mime-types@^2.1.12, mime-types@^2.1.27, mime-types@~2.1.24, mime-types@~2.1.34:
|
||||
version "2.1.35"
|
||||
resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
|
||||
integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
|
||||
dependencies:
|
||||
mime-db "1.52.0"
|
||||
|
||||
mime@1.6.0:
|
||||
version "1.6.0"
|
||||
resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1"
|
||||
integrity sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
|
||||
|
||||
mimic-fn@^1.0.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022"
|
||||
|
|
@ -9972,7 +10233,7 @@ ms@2.1.2:
|
|||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009"
|
||||
integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
|
||||
|
||||
ms@^2.1.1:
|
||||
ms@2.1.3, ms@^2.1.1:
|
||||
version "2.1.3"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2"
|
||||
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
|
||||
|
|
@ -10044,6 +10305,11 @@ ndjson@^2.0.0:
|
|||
split2 "^3.0.0"
|
||||
through2 "^4.0.0"
|
||||
|
||||
negotiator@0.6.3:
|
||||
version "0.6.3"
|
||||
resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.3.tgz#58e323a72fedc0d6f9cd4d31fe49f51479590ccd"
|
||||
integrity sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==
|
||||
|
||||
neo-async@^2.6.0, neo-async@^2.6.2:
|
||||
version "2.6.2"
|
||||
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
|
||||
|
|
@ -10433,6 +10699,13 @@ ol@^7.3.0:
|
|||
pbf "3.2.1"
|
||||
rbush "^3.0.1"
|
||||
|
||||
on-finished@2.4.1:
|
||||
version "2.4.1"
|
||||
resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.4.1.tgz#58c8c44116e54845ad57f14ab10b03533184ac3f"
|
||||
integrity sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==
|
||||
dependencies:
|
||||
ee-first "1.1.1"
|
||||
|
||||
once@^1.3.0, once@^1.3.1, once@^1.4.0:
|
||||
version "1.4.0"
|
||||
resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1"
|
||||
|
|
@ -10698,6 +10971,11 @@ parse5@^7.0.0, parse5@^7.1.1:
|
|||
dependencies:
|
||||
entities "^4.4.0"
|
||||
|
||||
parseurl@~1.3.3:
|
||||
version "1.3.3"
|
||||
resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4"
|
||||
integrity sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
|
||||
|
||||
pascal-case@^2.0.0:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/pascal-case/-/pascal-case-2.0.1.tgz#2d578d3455f660da65eca18ef95b4e0de912761e"
|
||||
|
|
@ -10786,6 +11064,11 @@ path-temp@^2.1.0:
|
|||
dependencies:
|
||||
unique-string "^2.0.0"
|
||||
|
||||
path-to-regexp@0.1.7:
|
||||
version "0.1.7"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c"
|
||||
integrity sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==
|
||||
|
||||
path-to-regexp@^1.7.0:
|
||||
version "1.8.0"
|
||||
resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-1.8.0.tgz#887b3ba9d84393e87a0a0b9f4cb756198b53548a"
|
||||
|
|
@ -11132,6 +11415,14 @@ protocol-buffers-schema@^3.3.1:
|
|||
resolved "https://registry.yarnpkg.com/protocol-buffers-schema/-/protocol-buffers-schema-3.6.0.tgz#77bc75a48b2ff142c1ad5b5b90c94cd0fa2efd03"
|
||||
integrity sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==
|
||||
|
||||
proxy-addr@~2.0.7:
|
||||
version "2.0.7"
|
||||
resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.7.tgz#f19fe69ceab311eeb94b42e70e8c2070f9ba1025"
|
||||
integrity sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
|
||||
dependencies:
|
||||
forwarded "0.2.0"
|
||||
ipaddr.js "1.9.1"
|
||||
|
||||
proxy-from-env@^1.1.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
|
||||
|
|
@ -11170,6 +11461,13 @@ qrcode.react@^3.1.0:
|
|||
resolved "https://registry.yarnpkg.com/qrcode.react/-/qrcode.react-3.1.0.tgz#5c91ddc0340f768316fbdb8fff2765134c2aecd8"
|
||||
integrity sha512-oyF+Urr3oAMUG/OiOuONL3HXM+53wvuH3mtIWQrYmsXoAq0DkvZp2RYUWFSMFtbdOpuS++9v+WAkzNVkMlNW6Q==
|
||||
|
||||
qs@6.11.0:
|
||||
version "6.11.0"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.0.tgz#fd0d963446f7a65e1367e01abd85429453f0c37a"
|
||||
integrity sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==
|
||||
dependencies:
|
||||
side-channel "^1.0.4"
|
||||
|
||||
qs@^6.4.0:
|
||||
version "6.11.2"
|
||||
resolved "https://registry.yarnpkg.com/qs/-/qs-6.11.2.tgz#64bea51f12c1f5da1bc01496f48ffcff7c69d7d9"
|
||||
|
|
@ -11241,6 +11539,21 @@ randombytes@^2.1.0:
|
|||
dependencies:
|
||||
safe-buffer "^5.1.0"
|
||||
|
||||
range-parser@~1.2.1:
|
||||
version "1.2.1"
|
||||
resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.1.tgz#3cf37023d199e1c24d1a55b84800c2f3e6468031"
|
||||
integrity sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
|
||||
|
||||
raw-body@2.5.2:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.5.2.tgz#99febd83b90e08975087e8f1f9419a149366b68a"
|
||||
integrity sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==
|
||||
dependencies:
|
||||
bytes "3.1.2"
|
||||
http-errors "2.0.0"
|
||||
iconv-lite "0.4.24"
|
||||
unpipe "1.0.0"
|
||||
|
||||
raw-body@~1.1.0:
|
||||
version "1.1.7"
|
||||
resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-1.1.7.tgz#1d027c2bfa116acc6623bca8f00016572a87d425"
|
||||
|
|
@ -12392,7 +12705,7 @@ safe-array-concat@^1.0.1, safe-array-concat@^1.1.0:
|
|||
has-symbols "^1.0.3"
|
||||
isarray "^2.0.5"
|
||||
|
||||
safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
|
||||
safe-buffer@5.2.1, safe-buffer@>=5.1.0, safe-buffer@^5.1.0, safe-buffer@~5.2.0:
|
||||
version "5.2.1"
|
||||
resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6"
|
||||
integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
|
||||
|
|
@ -12540,6 +12853,25 @@ semver@^6.0.0, semver@^6.1.0, semver@^6.3.0, semver@^6.3.1:
|
|||
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4"
|
||||
integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==
|
||||
|
||||
send@0.18.0:
|
||||
version "0.18.0"
|
||||
resolved "https://registry.yarnpkg.com/send/-/send-0.18.0.tgz#670167cc654b05f5aa4a767f9113bb371bc706be"
|
||||
integrity sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==
|
||||
dependencies:
|
||||
debug "2.6.9"
|
||||
depd "2.0.0"
|
||||
destroy "1.2.0"
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
etag "~1.8.1"
|
||||
fresh "0.5.2"
|
||||
http-errors "2.0.0"
|
||||
mime "1.6.0"
|
||||
ms "2.1.3"
|
||||
on-finished "2.4.1"
|
||||
range-parser "~1.2.1"
|
||||
statuses "2.0.1"
|
||||
|
||||
sentence-case@^2.1.0:
|
||||
version "2.1.1"
|
||||
resolved "https://registry.yarnpkg.com/sentence-case/-/sentence-case-2.1.1.tgz#1f6e2dda39c168bf92d13f86d4a918933f667ed4"
|
||||
|
|
@ -12571,6 +12903,16 @@ serialize-javascript@^6.0.1:
|
|||
dependencies:
|
||||
randombytes "^2.1.0"
|
||||
|
||||
serve-static@1.15.0:
|
||||
version "1.15.0"
|
||||
resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.15.0.tgz#faaef08cffe0a1a62f60cad0c4e513cff0ac9540"
|
||||
integrity sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==
|
||||
dependencies:
|
||||
encodeurl "~1.0.2"
|
||||
escape-html "~1.0.3"
|
||||
parseurl "~1.3.3"
|
||||
send "0.18.0"
|
||||
|
||||
set-function-length@^1.1.1:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.0.tgz#2f81dc6c16c7059bda5ab7c82c11f03a515ed8e1"
|
||||
|
|
@ -12628,6 +12970,11 @@ set-value@^2.0.0, set-value@^2.0.1:
|
|||
is-plain-object "^2.0.3"
|
||||
split-string "^3.0.1"
|
||||
|
||||
setprototypeof@1.2.0:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.2.0.tgz#66c9a24a73f9fc28cbe66b09fed3d33dcaf1b424"
|
||||
integrity sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==
|
||||
|
||||
shallow-clone@^3.0.0:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3"
|
||||
|
|
@ -13046,6 +13393,11 @@ static-extend@^0.1.1:
|
|||
define-property "^0.2.5"
|
||||
object-copy "^0.1.0"
|
||||
|
||||
statuses@2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/statuses/-/statuses-2.0.1.tgz#55cb000ccf1d48728bd23c685a063998cf1a1b63"
|
||||
integrity sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==
|
||||
|
||||
strict-uri-encode@^2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546"
|
||||
|
|
@ -13633,6 +13985,11 @@ toggle-selection@^1.0.6:
|
|||
resolved "https://registry.yarnpkg.com/toggle-selection/-/toggle-selection-1.0.6.tgz#6e45b1263f2017fa0acc7d89d78b15b8bf77da32"
|
||||
integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
|
||||
|
||||
toidentifier@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/toidentifier/-/toidentifier-1.0.1.tgz#3be34321a88a820ed1bd80dfaa33e479fbb8dd35"
|
||||
integrity sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==
|
||||
|
||||
totalist@^1.0.0:
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/totalist/-/totalist-1.1.0.tgz#a4d65a3e546517701e3e5c37a47a70ac97fe56df"
|
||||
|
|
@ -13823,6 +14180,14 @@ type-fest@^0.8.1:
|
|||
resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.8.1.tgz#09e249ebde851d3b1e48d27c105444667f17b83d"
|
||||
integrity sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==
|
||||
|
||||
type-is@~1.6.18:
|
||||
version "1.6.18"
|
||||
resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.18.tgz#4e552cd05df09467dcbc4ef739de89f2cf37c131"
|
||||
integrity sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
|
||||
dependencies:
|
||||
media-typer "0.3.0"
|
||||
mime-types "~2.1.24"
|
||||
|
||||
type-of@^2.0.1:
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/type-of/-/type-of-2.0.1.tgz#e72a1741896568e9f628378d816d6912f7f23972"
|
||||
|
|
@ -14023,6 +14388,11 @@ universalify@^2.0.0:
|
|||
resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717"
|
||||
integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==
|
||||
|
||||
unpipe@1.0.0, unpipe@~1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
|
||||
integrity sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==
|
||||
|
||||
unset-value@^1.0.0:
|
||||
version "1.0.0"
|
||||
resolved "https://registry.yarnpkg.com/unset-value/-/unset-value-1.0.0.tgz#8376873f7d2335179ffb1e6fc3a8ed0dfc8ab559"
|
||||
|
|
@ -14146,6 +14516,11 @@ util-deprecate@^1.0.1, util-deprecate@^1.0.2:
|
|||
resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf"
|
||||
integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
|
||||
|
||||
utils-merge@1.0.1:
|
||||
version "1.0.1"
|
||||
resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
|
||||
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
|
||||
|
||||
uuid@9.0.0:
|
||||
version "9.0.0"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-9.0.0.tgz#592f550650024a38ceb0c562f2f6aa435761efb5"
|
||||
|
|
@ -14209,6 +14584,11 @@ value-equal@^1.0.1:
|
|||
resolved "https://registry.yarnpkg.com/value-equal/-/value-equal-1.0.1.tgz#1e0b794c734c5c0cade179c437d356d931a34d6c"
|
||||
integrity sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==
|
||||
|
||||
vary@~1.1.2:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc"
|
||||
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==
|
||||
|
||||
version-selector-type@^3.0.0:
|
||||
version "3.0.0"
|
||||
resolved "https://registry.yarnpkg.com/version-selector-type/-/version-selector-type-3.0.0.tgz#47c365fb4d9ca4a54e6dabcad6fb7a46265f7955"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue