Merge pull request #2084 from grafana/dev

v1.2.35
This commit is contained in:
Matias Bordese 2023-06-01 15:29:43 -03:00 committed by GitHub
commit 00d7e8679b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
30 changed files with 1661 additions and 291 deletions

View file

@ -5,6 +5,14 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## v1.2.35 (2023-06-01)
### Fixed
- Fix a bug with permissions for telegram user settings by @alexintech ([#2075](https://github.com/grafana/oncall/pull/2075))
- Fix orphaned messages in Slack by @vadimkerr ([#2023](https://github.com/grafana/oncall/pull/2023))
- Fix duplicated slack shift-changed notifications ([#2080](https://github.com/grafana/oncall/pull/2080))
## v1.2.34 (2023-05-31)
### Added

View file

@ -16,6 +16,7 @@ ENGINE_PROFILE = engine
UI_PROFILE = oncall_ui
REDIS_PROFILE = redis
RABBITMQ_PROFILE = rabbitmq
PROMETHEUS_PROFILE = prometheus
GRAFANA_PROFILE = grafana
DEV_ENV_DIR = ./dev

1
dev/.gitignore vendored
View file

@ -7,4 +7,5 @@
!.env.postgres.dev
!.env.sqlite.dev
!add_env_var.sh
!prometheus.yml
!README.md

View file

@ -68,6 +68,7 @@ make start COMPOSE_PROFILES=postgres,engine,grafana,rabbitmq
The possible profiles values are:
- `grafana`
- `prometheus`
- `engine`
- `oncall_ui`
- `redis`
@ -138,6 +139,13 @@ license_text = <content-of-the-license-jwt-that-you-downloaded>
(_Note_: you may need to restart your `grafana` container after modifying its configuration)
### Enabling OnCall prometheus exporter for local development
Add `prometheus` to your `COMPOSE_PROFILES` and set `FEATURE_PROMETHEUS_EXPORTER_ENABLED=True` in your
`dev/.env.dev` file. You may need to restart your `grafana` container to make sure the new datasource
is added (or add it manually using the UI; Prometheus will be running in `host.docker.internal:9090`
by default, using default settings).
### Django Silk Profiling
In order to setup [`django-silk`](https://github.com/jazzband/django-silk) for local profiling, perform the following

8
dev/prometheus.yml Normal file
View file

@ -0,0 +1,8 @@
global:
scrape_interval: 15s
evaluation_interval: 15s
scrape_configs:
- job_name: prometheus
static_configs:
- targets: ["host.docker.internal:8080"]

View file

@ -270,6 +270,17 @@ services:
profiles:
- postgres
prometheus:
container_name: prometheus
labels: *oncall-labels
image: prom/prometheus
volumes:
- ./dev/prometheus.yml:/etc/prometheus/prometheus.yml
ports:
- "9090:9090"
profiles:
- prometheus
grafana:
container_name: grafana
labels: *oncall-labels
@ -294,6 +305,7 @@ services:
volumes:
- grafanadata_dev:/var/lib/grafana
- ./grafana-plugin:/var/lib/grafana/plugins/grafana-plugin
- ./provisioning:/etc/grafana/provisioning
- ${GRAFANA_DEV_PROVISIONING:-/dev/null}:/etc/grafana/grafana.ini
depends_on:
postgres:

View file

@ -107,7 +107,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
"name": ScenarioStep.get_step("distribute_alerts", "UnAttachGroupStep").routing_uid(),
"text": "Unattach",
"type": "button",
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
"value": self._alert_group_action_value(),
}
],
}
@ -180,7 +180,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
"emoji": True,
},
"type": "button",
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
"value": self._alert_group_action_value(),
"action_id": ScenarioStep.get_step(
"distribute_alerts",
"AcknowledgeGroupStep",
@ -196,7 +196,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
"emoji": True,
},
"type": "button",
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
"value": self._alert_group_action_value(),
"action_id": ScenarioStep.get_step(
"distribute_alerts",
"UnAcknowledgeGroupStep",
@ -208,7 +208,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
"text": {"type": "plain_text", "text": "Resolve", "emoji": True},
"type": "button",
"style": "primary",
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
"value": self._alert_group_action_value(),
"action_id": ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep").routing_uid(),
},
)
@ -221,7 +221,10 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
if not self.alert_group.silenced:
silence_options = [
{"text": {"type": "plain_text", "text": text, "emoji": True}, "value": str(value)}
{
"text": {"type": "plain_text", "text": text, "emoji": True},
"value": self._alert_group_action_value(delay=value),
}
for value, text in AlertGroup.SILENCE_DELAY_OPTIONS
]
buttons.append(
@ -230,7 +233,6 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
"type": "static_select",
"options": silence_options,
"action_id": ScenarioStep.get_step("distribute_alerts", "SilenceGroupStep").routing_uid(),
# "value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
}
)
else:
@ -238,7 +240,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
{
"text": {"type": "plain_text", "text": "Unsilence", "emoji": True},
"type": "button",
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
"value": self._alert_group_action_value(),
"action_id": ScenarioStep.get_step("distribute_alerts", "UnSilenceGroupStep").routing_uid(),
},
)
@ -247,12 +249,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
"text": {"type": "plain_text", "text": "Attach to ...", "emoji": True},
"type": "button",
"action_id": ScenarioStep.get_step("distribute_alerts", "SelectAttachGroupStep").routing_uid(),
"value": json.dumps(
{
"alert_group_pk": self.alert_group.pk,
"organization_id": self.alert_group.channel.organization_id,
}
),
"value": self._alert_group_action_value(),
}
buttons.append(attach_button)
else:
@ -260,7 +257,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
{
"text": {"type": "plain_text", "text": "Unresolve", "emoji": True},
"type": "button",
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
"value": self._alert_group_action_value(),
"action_id": ScenarioStep.get_step("distribute_alerts", "UnResolveGroupStep").routing_uid(),
},
)
@ -270,12 +267,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
{
"text": {"type": "plain_text", "text": ":mag: Format Alert", "emoji": True},
"type": "button",
"value": json.dumps(
{
"alert_group_pk": str(self.alert_group.pk),
"organization_id": self.alert_group.channel.organization_id,
}
),
"value": self._alert_group_action_value(),
"action_id": ScenarioStep.get_step(
"alertgroup_appearance", "OpenAlertAppearanceDialogStep"
).routing_uid(),
@ -292,13 +284,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
},
"type": "button",
"action_id": ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep").routing_uid(),
"value": json.dumps(
{
"resolution_note_window_action": "edit",
"alert_group_pk": self.alert_group.pk,
"organization_id": self.alert_group.channel.organization_id,
}
),
"value": self._alert_group_action_value(resolution_note_window_action="edit"),
}
if resolution_notes_count == 0:
resolution_notes_button["style"] = "primary"
@ -322,7 +308,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
"text": {"type": "plain_text", "text": "Resolve", "emoji": True},
"type": "button",
"style": "primary",
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
"value": self._alert_group_action_value(),
"action_id": ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep").routing_uid(),
},
)
@ -339,13 +325,11 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
invitee_name = invitation.invitee.get_username_with_slack_verbal()
buttons.append(
{
"name": "{}_{}".format(
ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess").routing_uid(), invitation.pk
),
"name": ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess").routing_uid(),
"text": "Stop inviting {}".format(invitee_name),
"type": "button",
"style": "primary",
"value": json.dumps({"organization_id": self.alert_group.channel.organization_id}),
"value": self._alert_group_action_value(invitation_id=invitation.pk),
},
)
return [
@ -359,6 +343,13 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
def _get_select_user_element(
self, action_id, multi_select=False, initial_user=None, initial_users_list=None, text=None
):
def get_action_value(user_id):
"""
In contrast to other buttons and select menus, self._alert_group_action_value is not used here.
It's because there could be a lot of users, and we don't want to increase the payload size too much.
"""
return json.dumps({"user_id": user_id})
MAX_STATIC_SELECT_OPTIONS = 100
if not text:
@ -382,7 +373,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
user_verbal = f"{user.get_username_with_slack_verbal()}"
if len(user_verbal) > 75:
user_verbal = user_verbal[:72] + "..."
option = {"text": {"type": "plain_text", "text": user_verbal}, "value": json.dumps({"user_id": user.pk})}
option = {"text": {"type": "plain_text", "text": user_verbal}, "value": get_action_value(user.pk)}
options.append(option)
if users_count > MAX_STATIC_SELECT_OPTIONS:
@ -397,7 +388,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
elif users_count == 0: # strange case when there are no users to select
option = {
"text": {"type": "plain_text", "text": "No users to select"},
"value": json.dumps({"user_id": None}),
"value": get_action_value(None),
}
options.append(option)
element["options"] = options
@ -413,7 +404,7 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
user_verbal = f"{user.get_username_with_slack_verbal()}"
option = {
"text": {"type": "plain_text", "text": user_verbal},
"value": json.dumps({"user_id": user.pk}),
"value": get_action_value(user.pk),
}
initial_options.append(option)
element["initial_options"] = initial_options
@ -421,8 +412,22 @@ class AlertGroupSlackRenderer(AlertGroupBaseRenderer):
user_verbal = f"{initial_user.get_username_with_slack_verbal()}"
initial_option = {
"text": {"type": "plain_text", "text": user_verbal},
"value": json.dumps({"user_id": initial_user.pk}),
"value": get_action_value(initial_user.pk),
}
element["initial_option"] = initial_option
return element
def _alert_group_action_value(self, **kwargs):
"""
Store organization and alert group IDs in Slack button or select menu values.
alert_group_pk is used in apps.slack.scenarios.step_mixins.AlertGroupActionsMixin to get the right alert group
when handling AG actions in Slack.
"""
data = {
"organization_id": self.alert_group.channel.organization_id,
"alert_group_pk": self.alert_group.pk,
**kwargs,
}
return json.dumps(data) # Slack block elements allow to pass value as string only (max 2000 chars)

View file

@ -2,7 +2,6 @@ import datetime
import json
from copy import copy
import icalendar
from django.apps import apps
from django.utils import timezone
@ -12,7 +11,6 @@ from apps.schedules.ical_utils import (
event_start_end_all_day_with_respect_to_type,
get_icalendar_tz_or_utc,
get_usernames_from_ical_event,
is_icals_equal,
memoized_users_in_ical,
)
from apps.slack.scenarios import scenario_step
@ -191,8 +189,6 @@ def notify_ical_schedule_shift(schedule_pk):
MIN_DAYS_TO_LOOKUP_FOR_THE_END_OF_EVENT = 3
ical_changed = False
now = timezone.datetime.now(timezone.utc)
# get list of iCalendars from current iCal files. If there is more than one calendar, primary calendar will always
# be the first
@ -240,60 +236,15 @@ def notify_ical_schedule_shift(schedule_pk):
for item in drop:
current_shifts.pop(item)
is_prev_ical_diff = False
prev_overrides_priority = 0
prev_shifts = {}
prev_users = {}
# compare events from prev and current shifts
prev_shifts = json.loads(schedule.current_shifts) if not schedule.empty_oncall else {}
# convert datetimes which was dumped to str back to datetime to calculate shift diff correct
str_format = "%Y-%m-%d %X%z"
for prev_shift in prev_shifts.values():
prev_shift["start"] = datetime.datetime.strptime(prev_shift["start"], str_format)
prev_shift["end"] = datetime.datetime.strptime(prev_shift["end"], str_format)
# Get list of tuples with prev and current ical file for each calendar. If there is more than one calendar, primary
# calendar will be the first.
# example result for ical calendar:
# [(prev_ical_file_primary, current_ical_file_primary), (prev_ical_file_overrides, current_ical_file_overrides)]
# example result for calendar with custom events:
# [(prev_ical_file, current_ical_file)]
prev_and_current_ical_files = schedule.get_prev_and_current_ical_files()
for prev_ical_file, current_ical_file in prev_and_current_ical_files:
if prev_ical_file and (not current_ical_file or not is_icals_equal(current_ical_file, prev_ical_file)):
task_logger.info(f"ical files are different")
# If icals are not equal then compare current_events from them
is_prev_ical_diff = True
prev_calendar = icalendar.Calendar.from_ical(prev_ical_file)
prev_shifts_result, prev_users_result = get_current_shifts_from_ical(
prev_calendar,
schedule,
prev_overrides_priority,
)
if prev_overrides_priority == 0 and prev_shifts_result:
prev_overrides_priority = max([prev_shifts_result[uid]["priority"] for uid in prev_shifts_result]) + 1
prev_shifts.update(prev_shifts_result)
prev_users.update(prev_users_result)
recalculate_shifts_with_respect_to_priority(prev_shifts, prev_users)
if is_prev_ical_diff:
# drop events that don't intersection with current time
drop = []
for uid, prev_shift in prev_shifts.items():
if not prev_shift["start"] < now < prev_shift["end"]:
drop.append(uid)
for item in drop:
prev_shifts.pop(item)
shift_changed, diff_uids = calculate_shift_diff(current_shifts, prev_shifts)
else:
# Else comparing events from prev and current shifts
prev_shifts = json.loads(schedule.current_shifts) if not schedule.empty_oncall else {}
# convert datetimes which was dumped to str back to datetime to calculate shift diff correct
str_format = "%Y-%m-%d %X%z"
for prev_shift in prev_shifts.values():
prev_shift["start"] = datetime.datetime.strptime(prev_shift["start"], str_format)
prev_shift["end"] = datetime.datetime.strptime(prev_shift["end"], str_format)
shift_changed, diff_uids = calculate_shift_diff(current_shifts, prev_shifts)
shift_changed, diff_uids = calculate_shift_diff(current_shifts, prev_shifts)
if shift_changed:
task_logger.info(f"shifts_changed: {diff_uids}")
@ -370,11 +321,6 @@ def notify_ical_schedule_shift(schedule_pk):
if schedule.notify_oncall_shift_freq != OnCallSchedule.NotifyOnCallShiftFreq.NEVER:
try:
if ical_changed:
slack_client.api_call(
"chat.postMessage", channel=schedule.channel, text=f"Schedule {schedule.name} was changed"
)
slack_client.api_call(
"chat.postMessage",
channel=schedule.channel,

View file

@ -1,13 +1,16 @@
import json
import textwrap
from datetime import datetime
from unittest.mock import Mock, patch
import icalendar
import pytest
import pytz
from django.utils import timezone
from apps.alerts.tasks.notify_ical_schedule_shift import notify_ical_schedule_shift
from apps.alerts.tasks.notify_ical_schedule_shift import get_current_shifts_from_ical, notify_ical_schedule_shift
from apps.schedules.ical_utils import memoized_users_in_ical
from apps.schedules.models import OnCallScheduleICal
from apps.schedules.models import CustomOnCallShift, OnCallScheduleCalendar, OnCallScheduleICal
ICAL_DATA = """
BEGIN:VCALENDAR
@ -105,3 +108,199 @@ def test_next_shift_notification_long_shifts(
notification = slack_blocks[0]["text"]["text"]
assert "*New on-call shift:*\nuser2" in notification
assert "*Next on-call shift:*\nuser1" in notification
@pytest.mark.django_db
def test_overrides_changes_no_current_no_triggering_notification(
make_organization_and_user_with_slack_identities,
make_user,
make_schedule,
make_on_call_shift,
):
organization, _, _, _ = make_organization_and_user_with_slack_identities()
user1 = make_user(organization=organization, username="user1")
# clear users pks <-> organization cache (persisting between tests)
memoized_users_in_ical.cache_clear()
ical_before = textwrap.dedent(
"""
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
DTSTART:20230101T020000
DTEND:20230101T170000
DTSTAMP:20230101T000000
UID:id1@google.com
CREATED:20230101T000000
DESCRIPTION:
LAST-MODIFIED:20230101T000000
LOCATION:
SEQUENCE:1
STATUS:CONFIRMED
SUMMARY:user1
TRANSP:TRANSPARENT
END:VEVENT
END:VCALENDAR"""
)
# event outside current time is changed
ical_after = textwrap.dedent(
"""
BEGIN:VCALENDAR
PRODID:-//Google Inc//Google Calendar 70.9054//EN
VERSION:2.0
CALSCALE:GREGORIAN
METHOD:PUBLISH
BEGIN:VEVENT
DTSTART:20230101T020000
DTEND:20230101T210000
DTSTAMP:20230101T000000
UID:id1@google.com
CREATED:20230101T000000
DESCRIPTION:
LAST-MODIFIED:20230101T000000
LOCATION:
SEQUENCE:2
STATUS:CONFIRMED
SUMMARY:user1
TRANSP:TRANSPARENT
END:VEVENT
END:VCALENDAR"""
)
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleCalendar,
name="test_schedule",
channel="channel",
prev_ical_file_overrides=ical_before,
cached_ical_file_overrides=ical_after,
)
now = timezone.now().replace(microsecond=0)
start_date = now - timezone.timedelta(days=7)
data = {
"start": start_date,
"rotation_start": start_date,
"duration": timezone.timedelta(seconds=3600 * 24),
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
}
on_call_shift = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
)
on_call_shift.add_rolling_users([[user1]])
on_call_shift.schedules.add(schedule)
# setup current shifts before checking/triggering for notifications
calendar = icalendar.Calendar.from_ical(schedule._ical_file_primary)
current_shifts, _ = get_current_shifts_from_ical(calendar, schedule, 0)
schedule.current_shifts = json.dumps(current_shifts, default=str)
schedule.empty_oncall = False
schedule.save()
with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call:
notify_ical_schedule_shift(schedule.oncallschedule_ptr_id)
assert not mock_slack_api_call.called
@pytest.mark.django_db
def test_no_changes_no_triggering_notification(
make_organization_and_user_with_slack_identities,
make_user,
make_schedule,
make_on_call_shift,
):
organization, _, _, _ = make_organization_and_user_with_slack_identities()
user1 = make_user(organization=organization, username="user1")
# clear users pks <-> organization cache (persisting between tests)
memoized_users_in_ical.cache_clear()
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleCalendar,
name="test_schedule",
channel="channel",
prev_ical_file_overrides=None,
cached_ical_file_overrides=None,
)
now = timezone.now().replace(microsecond=0)
start_date = now - timezone.timedelta(days=7)
data = {
"start": start_date,
"rotation_start": start_date,
"duration": timezone.timedelta(seconds=3600 * 24),
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
}
on_call_shift = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
)
on_call_shift.add_rolling_users([[user1]])
on_call_shift.schedules.add(schedule)
# setup current shifts before checking/triggering for notifications
calendar = icalendar.Calendar.from_ical(schedule._ical_file_primary)
current_shifts, _ = get_current_shifts_from_ical(calendar, schedule, 0)
schedule.current_shifts = json.dumps(current_shifts, default=str)
schedule.empty_oncall = False
schedule.save()
with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call:
notify_ical_schedule_shift(schedule.oncallschedule_ptr_id)
assert not mock_slack_api_call.called
@pytest.mark.django_db
def test_current_shift_changes_trigger_notification(
make_organization_and_user_with_slack_identities,
make_user,
make_schedule,
make_on_call_shift,
):
organization, _, _, _ = make_organization_and_user_with_slack_identities()
user1 = make_user(organization=organization, username="user1")
# clear users pks <-> organization cache (persisting between tests)
memoized_users_in_ical.cache_clear()
schedule = make_schedule(
organization,
schedule_class=OnCallScheduleCalendar,
name="test_schedule",
channel="channel",
prev_ical_file_overrides=None,
cached_ical_file_overrides=None,
)
now = timezone.now().replace(microsecond=0)
start_date = now - timezone.timedelta(days=7)
data = {
"start": start_date,
"rotation_start": start_date,
"duration": timezone.timedelta(seconds=3600 * 24),
"priority_level": 1,
"frequency": CustomOnCallShift.FREQUENCY_DAILY,
}
on_call_shift = make_on_call_shift(
organization=organization, shift_type=CustomOnCallShift.TYPE_ROLLING_USERS_EVENT, **data
)
on_call_shift.add_rolling_users([[user1]])
on_call_shift.schedules.add(schedule)
schedule.refresh_ical_file()
# setup empty current shifts before checking/triggering for notifications
schedule.current_shifts = json.dumps({}, default=str)
schedule.empty_oncall = False
schedule.save()
with patch("apps.slack.slack_client.SlackClientWithErrorHandling.api_call") as mock_slack_api_call:
notify_ical_schedule_shift(schedule.oncallschedule_ptr_id)
assert mock_slack_api_call.called

View file

@ -527,9 +527,9 @@ def test_channel_filter_convert_from_regex_to_jinja2(
make_channel_filter(alert_receive_channel, is_default=True)
# r"..." used to keep this string as raw string
regex_filtering_term = r"\".*\": \"This alert was sent by user for the demonstration purposes\""
final_filtering_term = r'{{ payload | json_dumps | regex_search("\".*\": \"This alert was sent by user for the demonstration purposes\"") }}'
payload = {"description": "This alert was sent by user for the demonstration purposes"}
regex_filtering_term = r"\".*\": \"This alert was sent by user for demonstration purposes\""
final_filtering_term = r'{{ payload | json_dumps | regex_search("\".*\": \"This alert was sent by user for demonstration purposes\"") }}'
payload = {"description": "This alert was sent by user for demonstration purposes"}
regex_channel_filter = make_channel_filter(
alert_receive_channel,

View file

@ -80,7 +80,8 @@ class SlackMessage(models.Model):
self.alert_group.slack_message = self
self.alert_group.save(update_fields=["slack_message"])
return self.alert_group
return self.alert.group
else:
raise
@property
def permalink(self):
@ -217,32 +218,3 @@ class SlackMessage(models.Model):
pass
else:
raise e
@classmethod
def get_alert_group_from_slack_message_payload(cls, slack_team_identity, payload):
message_ts = payload.get("message_ts") or payload["container"]["message_ts"] # interactive message or block
channel_id = payload["channel"]["id"]
try:
slack_message = cls.objects.get(
slack_id=message_ts,
_slack_team_identity=slack_team_identity,
channel_id=channel_id,
)
alert_group = slack_message.get_alert_group()
except cls.DoesNotExist as e:
logger.error(
f"Tried to get SlackMessage from message_ts:"
f"slack_team_identity_id={slack_team_identity.pk},"
f"message_ts={message_ts}"
)
raise e
except cls.alert.RelatedObjectDoesNotExist as e:
logger.error(
f"Tried to get AlertGroup from SlackMessage:"
f"slack_team_identity_id={slack_team_identity.pk},"
f"message_ts={message_ts}"
)
raise e
return alert_group

View file

@ -5,38 +5,25 @@ from django.apps import apps
from apps.api.permissions import RBACPermission
from apps.slack.scenarios import scenario_step
from .step_mixins import CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin
from .step_mixins import AlertGroupActionsMixin, CheckAlertIsUnarchivedMixin
class OpenAlertAppearanceDialogStep(
CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin, scenario_step.ScenarioStep
):
class OpenAlertAppearanceDialogStep(CheckAlertIsUnarchivedMixin, AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "open Alert Appearance"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
AlertGroup = apps.get_model("alerts", "AlertGroup")
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
try:
message_ts = payload["message_ts"]
except KeyError:
message_ts = payload["container"]["message_ts"]
try:
alert_group_pk = payload["actions"][0]["action_id"].split("_")[1]
except (KeyError, IndexError):
value = json.loads(payload["actions"][0]["value"])
alert_group_pk = value["alert_group_pk"]
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
return
blocks = []
private_metadata = {
"organization_id": self.organization.pk if self.organization else alert_group.organization.pk,
"alert_group_pk": alert_group_pk,
"message_ts": message_ts,
"alert_group_pk": alert_group.pk,
"message_ts": payload.get("message_ts") or payload["container"]["message_ts"],
}
alert_receive_channel = alert_group.channel

View file

@ -35,7 +35,7 @@ from apps.slack.tasks import (
from apps.slack.utils import get_cache_key_update_incident_slack_message
from common.utils import clean_markup, is_string_with_visible_characters
from .step_mixins import CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin
from .step_mixins import AlertGroupActionsMixin, CheckAlertIsUnarchivedMixin
ATTACH_TO_ALERT_GROUPS_LIMIT = 20
@ -218,17 +218,20 @@ class AlertShootingStep(scenario_step.ScenarioStep):
class InviteOtherPersonToIncident(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "invite to incident"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
User = apps.get_model("user_management", "User")
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
selected_user = None
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
@ -255,21 +258,24 @@ class InviteOtherPersonToIncident(
class SilenceGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "silence incident"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
value = payload["actions"][0]["selected_option"]["value"]
try:
silence_delay = int(payload["actions"][0]["selected_options"][0]["value"])
except KeyError:
silence_delay = int(payload["actions"][0]["selected_option"]["value"])
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
silence_delay = json.loads(value)["delay"]
except TypeError:
# Deprecated handler kept for backward compatibility (so older Slack messages can still be processed)
silence_delay = int(value)
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
alert_group.silence_by_user(self.user, silence_delay, action_source=ActionSource.SLACK)
@ -281,15 +287,17 @@ class SilenceGroupStep(
class UnSilenceGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "unsilence incident"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
alert_group.un_silence_by_user(self.user, action_source=ActionSource.SLACK)
@ -300,20 +308,20 @@ class UnSilenceGroupStep(
class SelectAttachGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "Select Alert Group for Attaching to"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
AlertGroup = apps.get_model("alerts", "AlertGroup")
value = json.loads(payload["actions"][0]["value"])
alert_group_pk = value.get("alert_group_pk")
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
return
blocks = []
view = {
"callback_id": AttachGroupStep.routing_uid(),
@ -326,7 +334,7 @@ class SelectAttachGroupStep(
"private_metadata": json.dumps(
{
"organization_id": self.organization.pk if self.organization else alert_group.organization.pk,
"alert_group_pk": alert_group_pk,
"alert_group_pk": alert_group.pk,
}
),
"close": {"type": "plain_text", "text": "Cancel", "emoji": True},
@ -335,8 +343,8 @@ class SelectAttachGroupStep(
if attached_incidents_exists:
attached_incidents = alert_group.dependent_alert_groups.all()
text = (
f"Oops! This incident cannot be attached to another one because it already has "
f"attached incidents ({attached_incidents.count()}):\n"
f"Oops! This Alert Group cannot be attached to another one because it already has "
f"attached Alert Group ({attached_incidents.count()}):\n"
)
for dependent_alert in attached_incidents:
if dependent_alert.slack_permalink:
@ -372,7 +380,7 @@ class SelectAttachGroupStep(
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Oops! There is no incidents, available to attach.",
"text": "Oops! There are no Alert Groups available to attach.",
},
}
)
@ -441,7 +449,7 @@ class SelectAttachGroupStep(
},
"label": {
"type": "plain_text",
"text": "Select incident:",
"text": "Select Alert Group:",
"emoji": True,
},
}
@ -451,11 +459,10 @@ class SelectAttachGroupStep(
class AttachGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "Attach incident"
REQUIRED_PERMISSIONS = [] # Permissions are handled in SelectAttachGroupStep
def process_signal(self, log_record):
alert_group = log_record.alert_group
@ -497,8 +504,7 @@ class AttachGroupStep(
root_alert_group_pk = int(payload["actions"][0]["selected_option"]["value"])
root_alert_group = AlertGroup.all_objects.get(pk=root_alert_group_pk)
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
alert_group = self.get_alert_group(slack_team_identity, payload)
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group) and self.check_alert_is_unarchived(
slack_team_identity, payload, root_alert_group
):
@ -509,14 +515,17 @@ class AttachGroupStep(
class UnAttachGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "Unattach incident"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
alert_group.un_attach_by_user(self.user, action_source=ActionSource.SLACK)
@ -525,17 +534,26 @@ class UnAttachGroupStep(
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
class StopInvitationProcess(CheckAlertIsUnarchivedMixin, IncidentActionsAccessControlMixin, scenario_step.ScenarioStep):
class StopInvitationProcess(CheckAlertIsUnarchivedMixin, AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "stop invitation"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
return
invitation_pk = payload["actions"][0]["name"].split("_")[1]
Invitation.stop_invitation(invitation_pk, self.user)
try:
value = json.loads(payload["actions"][0]["value"])
invitation_id = value["invitation_id"]
except KeyError:
# Deprecated handler kept for backward compatibility (so older Slack messages can still be processed)
invitation_id = payload["actions"][0]["name"].split("_")[1]
Invitation.stop_invitation(invitation_id, self.user)
def process_signal(self, log_record):
self.alert_group_slack_service.update_alert_group_slack_message(log_record.invitation.alert_group)
@ -543,16 +561,18 @@ class StopInvitationProcess(CheckAlertIsUnarchivedMixin, IncidentActionsAccessCo
class CustomButtonProcessStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
# TODO:
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "click custom button"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
CustomButtom = apps.get_model("alerts", "CustomButton")
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
custom_button_pk = payload["actions"][0]["name"].split("_")[1]
alert_group_pk = payload["actions"][0]["name"].split("_")[2]
@ -603,16 +623,18 @@ class CustomButtonProcessStep(
class ResolveGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "resolve incident"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
ResolutionNoteModalStep = scenario_step.ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep")
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
return
@ -645,14 +667,17 @@ class ResolveGroupStep(
class UnResolveGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "unresolve incident"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
alert_group.un_resolve_by_user(self.user, action_source=ActionSource.SLACK)
@ -663,36 +688,38 @@ class UnResolveGroupStep(
class AcknowledgeGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "acknowledge incident"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
logger.debug(f"process_scenario in AcknowledgeGroupStep for alert_group {alert_group.pk}")
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
alert_group.acknowledge_by_user(self.user, action_source=ActionSource.SLACK)
def process_signal(self, log_record):
alert_group = log_record.alert_group
logger.debug(f"Started process_signal in AcknowledgeGroupStep for alert_group {alert_group.pk}")
self.alert_group_slack_service.update_alert_group_slack_message(alert_group)
logger.debug(f"Finished process_signal in AcknowledgeGroupStep for alert_group {alert_group.pk}")
class UnAcknowledgeGroupStep(
CheckAlertIsUnarchivedMixin,
IncidentActionsAccessControlMixin,
AlertGroupActionsMixin,
scenario_step.ScenarioStep,
):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
ACTION_VERBOSE = "unacknowledge incident"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
alert_group = SlackMessage.get_alert_group_from_slack_message_payload(slack_team_identity, payload)
logger.debug(f"process_scenario in UnAcknowledgeGroupStep for alert_group {alert_group.pk}")
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
if self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
alert_group.un_acknowledge_by_user(self.user, action_source=ActionSource.SLACK)
@ -717,7 +744,7 @@ class UnAcknowledgeGroupStep(
]
text = (
f"{user_verbal} hasn't responded to an acknowledge timeout reminder."
f" Alert Group is unacknowledged automatically"
f" Alert Group is unacknowledged automatically."
)
if alert_group.slack_message.ack_reminder_message_ts:
try:
@ -749,8 +776,6 @@ class UnAcknowledgeGroupStep(
class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
ACTION_VERBOSE = "confirm acknowledge status"
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
AlertGroup = apps.get_model("alerts", "AlertGroup")
alert_group_id = payload["actions"][0]["value"].split("_")[1]
@ -762,7 +787,7 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
if alert_group.acknowledged_by == AlertGroup.USER:
if self.user == alert_group.acknowledged_by_user:
user_verbal = alert_group.acknowledged_by_user.get_username_with_slack_verbal()
text = f"{user_verbal} confirmed that the incident is still acknowledged"
text = f"{user_verbal} confirmed that the Alert Group is still acknowledged."
self._slack_client.api_call(
"chat.update",
channel=channel,
@ -776,11 +801,11 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
"chat.postEphemeral",
channel=channel,
user=slack_user_identity.slack_id,
text="This alert is acknowledged by another user. Acknowledge it yourself first.",
text="This Alert Group is acknowledged by another user. Acknowledge it yourself first.",
)
elif alert_group.acknowledged_by == AlertGroup.SOURCE:
user_verbal = self.user.get_username_with_slack_verbal()
text = f"{user_verbal} confirmed that the incident is still acknowledged"
text = f"{user_verbal} confirmed that the Alert Group is still acknowledged."
self._slack_client.api_call(
"chat.update",
channel=channel,
@ -799,7 +824,7 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
"chat.postEphemeral",
channel=channel,
user=slack_user_identity.slack_id,
text="This alert is already unacknowledged.",
text="This Alert Group is already unacknowledged.",
)
def process_signal(self, log_record):
@ -809,12 +834,12 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
alert_group = log_record.alert_group
channel_id = alert_group.slack_message.channel_id
user_verbal = log_record.author.get_username_with_slack_verbal(mention=True)
text = f"{user_verbal}, please confirm that you're still working on this incident."
text = f"{user_verbal}, please confirm that you're still working on this Alert Group."
if alert_group.channel.organization.unacknowledge_timeout != Organization.UNACKNOWLEDGE_TIMEOUT_NEVER:
attachments = [
{
"fallback": "Are you still working on this incident?",
"fallback": "Are you still working on this Alert Group?",
"text": text,
"callback_id": "alert",
"attachment_type": "default",
@ -883,8 +908,6 @@ class AcknowledgeConfirmationStep(AcknowledgeGroupStep):
class WipeGroupStep(scenario_step.ScenarioStep):
ACTION_VERBOSE = "wipe incident"
def process_signal(self, log_record):
alert_group = log_record.alert_group
user_verbal = log_record.author.get_username_with_slack_verbal()
@ -894,8 +917,6 @@ class WipeGroupStep(scenario_step.ScenarioStep):
class DeleteGroupStep(scenario_step.ScenarioStep):
ACTION_VERBOSE = "delete incident"
def process_signal(self, log_record):
alert_group = log_record.alert_group

View file

@ -5,12 +5,13 @@ from django.apps import apps
from django.db.models import Q
from django.utils import timezone
from apps.api.permissions import RBACPermission
from apps.slack.scenarios import scenario_step
from apps.slack.slack_client.exceptions import SlackAPIException
from apps.user_management.models import User
from common.api_helpers.utils import create_engine_url
from .step_mixins import CheckAlertIsUnarchivedMixin
from .step_mixins import AlertGroupActionsMixin, CheckAlertIsUnarchivedMixin
logger = logging.getLogger(__name__)
logger.setLevel(logging.DEBUG)
@ -372,18 +373,28 @@ class UpdateResolutionNoteStep(scenario_step.ScenarioStep):
return blocks
class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, scenario_step.ScenarioStep):
class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, AlertGroupActionsMixin, scenario_step.ScenarioStep):
REQUIRED_PERMISSIONS = [RBACPermission.Permissions.CHATOPS_WRITE]
RESOLUTION_NOTE_TEXT_BLOCK_ID = "resolution_note_text"
RESOLUTION_NOTE_MESSAGES_MAX_COUNT = 25
def process_scenario(self, slack_user_identity, slack_team_identity, payload, data=None):
AlertGroup = apps.get_model("alerts", "AlertGroup")
if data:
# Argument "data" is used when step is called from other step, e.g. AddRemoveThreadMessageStep
AlertGroup = apps.get_model("alerts", "AlertGroup")
alert_group = AlertGroup.all_objects.get(pk=data["alert_group_pk"])
else:
# Handle "Add Resolution notes" button click
alert_group = self.get_alert_group(slack_team_identity, payload)
if not self.is_authorized(alert_group):
self.open_unauthorized_warning(payload)
return
value = data or json.loads(payload["actions"][0]["value"])
resolution_note_window_action = value.get("resolution_note_window_action", "") or value.get("action_value", "")
alert_group_pk = value.get("alert_group_pk")
action_resolve = value.get("action_resolve", False)
channel_id = payload["channel"]["id"] if "channel" in payload else None
alert_group = AlertGroup.all_objects.get(pk=alert_group_pk)
if not self.check_alert_is_unarchived(slack_team_identity, payload, alert_group):
return
@ -413,7 +424,7 @@ class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari
"private_metadata": json.dumps(
{
"organization_id": self.organization.pk if self.organization else alert_group.organization.pk,
"alert_group_pk": alert_group_pk,
"alert_group_pk": alert_group.pk,
}
),
}
@ -431,7 +442,7 @@ class ResolutionNoteModalStep(CheckAlertIsUnarchivedMixin, scenario_step.Scenari
# Ignore "not_found" error, it means that the view was closed by user before the update request.
# It doesn't disrupt the user experience.
logger.debug(
f"API call to views.update failed for alert group {alert_group_pk}, error: not_found. "
f"API call to views.update failed for alert group {alert_group.pk}, error: not_found. "
f"Most likely the view was closed by user before the request was processed. "
)
else:

View file

@ -1,72 +1,164 @@
import json
import logging
from abc import ABC, abstractmethod
from apps.alerts.models import AlertGroup
from apps.api.permissions import user_is_authorized
from apps.slack.models import SlackMessage, SlackTeamIdentity
logger = logging.getLogger(__name__)
class AccessControl(ABC):
class AlertGroupActionsMixin:
"""
Mixin for alert group actions (ack, resolve, etc.). Intended to be used as a mixin along with ScenarioStep.
"""
REQUIRED_PERMISSIONS = []
ACTION_VERBOSE = ""
def process_scenario(self, slack_user_identity, slack_team_identity, payload):
if self.check_membership():
return super().process_scenario(slack_user_identity, slack_team_identity, payload)
else:
self.send_denied_message(payload)
def get_alert_group(self, slack_team_identity: SlackTeamIdentity, payload: dict) -> AlertGroup:
"""
Get AlertGroup instance on Slack message button click or select menu change.
"""
def check_membership(self):
return user_is_authorized(self.user, self.REQUIRED_PERMISSIONS)
alert_group = (
self._get_alert_group_from_action(payload) # Try to get alert_group_pk from PRESSED button
or self._get_alert_group_from_message(payload) # Try to use alert_group_pk from ANY button in message
or self._get_alert_group_from_slack_message_in_db(slack_team_identity, payload) # Fetch message from DB
)
@abstractmethod
def send_denied_message(self, payload):
pass
# Repair alert group if Slack message is orphaned
if alert_group.slack_message is None:
self._repair_alert_group(slack_team_identity, alert_group, payload)
return alert_group
class IncidentActionsAccessControlMixin(AccessControl):
"""
Mixin for auth in incident actions
"""
def is_authorized(self, alert_group: AlertGroup) -> bool:
"""
Check that user has required permissions to perform an action.
"""
def send_denied_message_to_channel(self, payload=None):
# Send denied message to thread by default
return False
return (
self.user is not None
and self.user.organization == alert_group.channel.organization
and user_is_authorized(self.user, self.REQUIRED_PERMISSIONS)
)
def send_denied_message(self, payload):
def open_unauthorized_warning(self, payload: dict) -> None:
self.open_warning_window(
payload,
warning_text="You do not have permission to perform this action. Ask an admin to upgrade your permissions.",
title="Permission denied",
)
def _repair_alert_group(
self, slack_team_identity: SlackTeamIdentity, alert_group: AlertGroup, payload: dict
) -> None:
"""
There's a possibility that OnCall failed to create a SlackMessage instance for an AlertGroup, but the message
was sent to Slack. This method creates SlackMessage instance for such orphaned messages.
"""
channel_id = payload["channel"]["id"]
try:
thread_ts = payload["message_ts"]
message_id = payload["message"]["ts"]
except KeyError:
thread_ts = payload["message"]["ts"]
message_id = payload["original_message"]["ts"]
text = "Attempted to {} by {}, but failed due to a lack of permissions.".format(
self.ACTION_VERBOSE,
self.user.get_username_with_slack_verbal(),
slack_message = SlackMessage.objects.create(
slack_id=message_id,
organization=alert_group.channel.organization,
_slack_team_identity=slack_team_identity,
channel_id=channel_id,
alert_group=alert_group,
)
self._slack_client.api_call(
"chat.postMessage",
channel=payload["channel"]["id"],
text=text,
blocks=[
{
"type": "section",
"block_id": "alert",
"text": {
"type": "mrkdwn",
"text": text,
},
},
],
thread_ts=None if self.send_denied_message_to_channel(payload) else thread_ts,
unfurl_links=True,
alert_group.slack_message = slack_message
alert_group.save(update_fields=["slack_message"])
def _get_alert_group_from_action(self, payload: dict) -> AlertGroup | None:
"""
Get AlertGroup instance from action data in payload. Action data is data encoded into buttons and select
menus in apps.alerts.incident_appearance.renderers.slack_renderer.AlertGroupSlackRenderer._get_buttons_blocks.
"""
action = payload["actions"][0]
action_type = action["type"]
if action_type == "button":
value_string = action["value"]
elif action_type == "static_select":
value_string = action["selected_option"]["value"]
else:
raise ValueError(f"Unexpected action type: {action_type}")
try:
value = json.loads(value_string)
except (TypeError, json.JSONDecodeError):
return None
try:
alert_group_pk = value["alert_group_pk"]
except (KeyError, TypeError):
return None
return AlertGroup.all_objects.get(pk=alert_group_pk)
def _get_alert_group_from_message(self, payload: dict) -> AlertGroup | None:
"""
Get AlertGroup instance from message data in payload. It's similar to _get_alert_group_from_action,
but it tries to get alert_group_pk from ANY button in the message, not just the one that was clicked.
"""
try:
# sometimes message is in "original_message" field, not "message"
message = payload.get("message") or payload["original_message"]
elements = message["attachments"][0]["blocks"][0]["elements"]
except (KeyError, IndexError):
return None
for element in elements:
value_string = element.get("value")
if not value_string:
continue
try:
value = json.loads(value_string)
except (TypeError, json.JSONDecodeError):
continue
try:
alert_group_pk = value["alert_group_pk"]
except (KeyError, TypeError):
continue
return AlertGroup.all_objects.get(pk=alert_group_pk)
def _get_alert_group_from_slack_message_in_db(
self, slack_team_identity: SlackTeamIdentity, payload: dict
) -> AlertGroup:
"""
Get AlertGroup instance from SlackMessage instance.
Old messages may not have alert_group_pk encoded into buttons, so we need to query SlackMessage to figure out
the AlertGroup.
"""
message_ts = payload.get("message_ts") or payload["container"]["message_ts"] # interactive message or block
channel_id = payload["channel"]["id"]
# All Slack messages from OnCall should have alert_group_pk encoded into buttons, so reaching this point means
# something probably went wrong.
logger.warning(f"alert_group_pk not found in payload, fetching SlackMessage from DB. message_ts: {message_ts}")
# Get SlackMessage from DB
slack_message = SlackMessage.objects.get(
slack_id=message_ts,
_slack_team_identity=slack_team_identity,
channel_id=channel_id,
)
return slack_message.get_alert_group()
class CheckAlertIsUnarchivedMixin(object):
REQUIRED_PERMISSIONS = []
ACTION_VERBOSE = ""
class CheckAlertIsUnarchivedMixin:
def check_alert_is_unarchived(self, slack_team_identity, payload, alert_group, warning=True):
alert_group_is_unarchived = alert_group.started_at.date() > self.organization.archive_alerts_from
if not alert_group_is_unarchived:

View file

@ -0,0 +1,852 @@
import json
from unittest.mock import patch
import pytest
from apps.api.permissions import LegacyAccessControlRole
from apps.slack.scenarios.scenario_step import ScenarioStep
from apps.slack.scenarios.step_mixins import AlertGroupActionsMixin
class TestScenario(AlertGroupActionsMixin, ScenarioStep):
pass
# List of steps to be tested for alert group actions (getting alert group from Slack payload + user permissions check)
ALERT_GROUP_ACTIONS_STEPS = [
# Acknowledge / Unacknowledge buttons
ScenarioStep.get_step("distribute_alerts", "AcknowledgeGroupStep"),
ScenarioStep.get_step("distribute_alerts", "UnAcknowledgeGroupStep"),
# Resolve / Unresolve buttons
ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep"),
ScenarioStep.get_step("distribute_alerts", "UnResolveGroupStep"),
# Invite / Stop inviting buttons
ScenarioStep.get_step("distribute_alerts", "InviteOtherPersonToIncident"),
ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess"),
# Silence / Unsilence buttons
ScenarioStep.get_step("distribute_alerts", "SilenceGroupStep"),
ScenarioStep.get_step("distribute_alerts", "UnSilenceGroupStep"),
# Attach / Unattach buttons
ScenarioStep.get_step("distribute_alerts", "SelectAttachGroupStep"),
ScenarioStep.get_step("distribute_alerts", "UnAttachGroupStep"),
# Format alert button
ScenarioStep.get_step("alertgroup_appearance", "OpenAlertAppearanceDialogStep"),
# Add resolution notes button
ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep"),
]
# Constants to simplify parametrized tests
ORGANIZATION_ID = 42
ALERT_GROUP_ID = 24
SLACK_MESSAGE_TS = "RANDOM_MESSAGE_TS"
SLACK_CHANNEL_ID = "RANDOM_CHANNEL_ID"
USER_ID = 56
INVITATION_ID = 78
def _get_payload(action_type="button", **kwargs):
"""
Utility function to generate payload to be used by scenario steps.
"""
if action_type == "button":
return {
"actions": [
{
"type": "button",
"value": json.dumps(
{"organization_id": ORGANIZATION_ID, "alert_group_pk": ALERT_GROUP_ID, **kwargs}
),
}
],
}
elif action_type == "static_select":
return {
"actions": [
{
"type": "static_select",
"selected_option": {
"value": json.dumps(
{"organization_id": ORGANIZATION_ID, "alert_group_pk": ALERT_GROUP_ID, **kwargs}
)
},
}
],
}
@pytest.mark.parametrize("step_class", ALERT_GROUP_ACTIONS_STEPS)
@pytest.mark.django_db
def test_alert_group_actions_unauthorized(
step_class, make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
):
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities(
role=LegacyAccessControlRole.VIEWER
)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
payload = {
"actions": [
{
"type": "button",
"value": json.dumps({"organization_id": organization.pk, "alert_group_pk": alert_group.pk}),
}
],
"channel": {"id": "RANDOM_CHANNEL_ID"},
"message": {"ts": "RANDOM_MESSAGE_TS"},
"trigger_id": "RANDOM_TRIGGER_ID",
}
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
with patch.object(step, "open_unauthorized_warning") as mock_open_unauthorized_warning:
step.process_scenario(slack_user_identity, slack_team_identity, payload)
mock_open_unauthorized_warning.assert_called_once()
@pytest.mark.django_db
def test_get_alert_group_button(
make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
):
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
payload = {
"actions": [
{
"type": "button",
"value": json.dumps({"organization_id": organization.pk, "alert_group_pk": alert_group.pk}),
}
],
"channel": {"id": "RANDOM_CHANNEL_ID"},
"message": {"ts": "RANDOM_MESSAGE_TS"},
}
step = TestScenario(organization=organization, user=user, slack_team_identity=slack_team_identity)
result = step.get_alert_group(slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group == result # check it's the right alert group
assert alert_group.slack_message is not None # check that orphaned Slack message is repaired
@pytest.mark.django_db
def test_get_alert_group_static_select(
make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
):
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
payload = {
"actions": [
{
"type": "static_select",
"selected_option": {
"value": json.dumps({"organization_id": organization.pk, "alert_group_pk": alert_group.pk})
},
}
],
"channel": {"id": "RANDOM_CHANNEL_ID"},
"message": {"ts": "RANDOM_MESSAGE_TS"},
}
step = TestScenario(organization=organization, user=user, slack_team_identity=slack_team_identity)
result = step.get_alert_group(slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group == result # check it's the right alert group
assert alert_group.slack_message is not None # check that orphaned Slack message is repaired
@pytest.mark.django_db
def test_get_alert_group_from_message(
make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
):
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
payload = {
"actions": [
{
"type": "button",
"value": "no alert_group_pk",
}
],
"message": {
"ts": "RANDOM_MESSAGE_TS",
"attachments": [{"blocks": [{"elements": [{"value": json.dumps({"alert_group_pk": alert_group.pk})}]}]}],
},
"channel": {"id": "RANDOM_CHANNEL_ID"},
}
step = TestScenario(organization=organization, user=user, slack_team_identity=slack_team_identity)
result = step.get_alert_group(slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group == result # check it's the right alert group
assert alert_group.slack_message is not None # check that orphaned Slack message is repaired
@pytest.mark.django_db
def test_get_alert_group_from_slack_message_in_db(
make_organization_and_user_with_slack_identities,
make_alert_receive_channel,
make_alert_group,
make_slack_channel,
make_slack_message,
):
organization, user, slack_team_identity, _ = make_organization_and_user_with_slack_identities()
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
slack_channel = make_slack_channel(slack_team_identity)
slack_message = make_slack_message(alert_group=alert_group, channel_id=slack_channel.slack_id)
payload = {
"message_ts": slack_message.slack_id,
"channel": {"id": slack_channel.slack_id},
"actions": [{"type": "button", "value": "RANDOM_VALUE"}],
}
step = TestScenario(organization=organization, user=user, slack_team_identity=slack_team_identity)
result = step.get_alert_group(slack_team_identity, payload)
assert alert_group == result
@pytest.mark.parametrize(
"payload",
[
_get_payload(),
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"actions": [{"type": "button", "value": json.dumps({"organization_id": ORGANIZATION_ID})}],
},
],
)
@pytest.mark.django_db
def test_step_acknowledge(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, acknowledged=False, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "AcknowledgeGroupStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group.acknowledged is True
@pytest.mark.parametrize(
"payload",
[
_get_payload(),
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"actions": [{"type": "button", "value": json.dumps({"organization_id": ORGANIZATION_ID})}],
},
],
)
@pytest.mark.django_db
def test_step_unacknowledge(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, acknowledged=True, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "UnAcknowledgeGroupStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group.acknowledged is False
@pytest.mark.parametrize(
"payload",
[
_get_payload(),
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"actions": [{"type": "button", "value": json.dumps({"organization_id": ORGANIZATION_ID})}],
},
],
)
@pytest.mark.django_db
def test_step_resolve(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, resolved=False, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "ResolveGroupStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group.resolved is True
@pytest.mark.parametrize(
"payload",
[
_get_payload(),
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"actions": [{"type": "button", "value": json.dumps({"organization_id": ORGANIZATION_ID})}],
},
],
)
@pytest.mark.django_db
def test_step_unresolve(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, resolved=True, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "UnResolveGroupStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group.resolved is False
@pytest.mark.parametrize(
"payload",
[
# Usual data such as alert_group_pk is not passed to InviteOtherPersonToIncident, so it doesn't increase
# payload size too much.
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"actions": [
{
"type": "static_select",
"selected_option": {"value": json.dumps({"user_id": USER_ID})},
}
],
},
],
)
@pytest.mark.django_db
def test_step_invite(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
second_user = make_user(organization=organization, pk=USER_ID)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, resolved=True, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "InviteOtherPersonToIncident")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group.invitations.count() == 1
invitation = alert_group.invitations.first()
assert invitation.author == user
assert invitation.invitee == second_user
@pytest.mark.parametrize(
"payload",
[
_get_payload(invitation_id=INVITATION_ID),
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"actions": [
{
"name": f"StopInvitationProcess_{INVITATION_ID}",
"type": "button",
"value": json.dumps({"organization_id": ORGANIZATION_ID}),
}
],
},
],
)
@pytest.mark.django_db
def test_step_stop_invite(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
make_invitation,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
second_user = make_user(organization=organization, pk=USER_ID)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, resolved=True, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
invitation = make_invitation(alert_group, user, second_user, pk=INVITATION_ID)
step_class = ScenarioStep.get_step("distribute_alerts", "StopInvitationProcess")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
invitation.refresh_from_db()
assert invitation.is_active is False
@pytest.mark.parametrize(
"payload",
[
_get_payload(action_type="static_select", delay=1800),
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"actions": [
{
"type": "static_select",
"selected_option": {"value": "1800"},
}
],
},
],
)
@pytest.mark.django_db
def test_step_silence(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, silenced=False, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "SilenceGroupStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group.silenced is True
@pytest.mark.parametrize(
"payload",
[
_get_payload(action_type="static_select", delay=1800),
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"actions": [
{
"type": "button",
"value": json.dumps({"organization_id": ORGANIZATION_ID}),
}
],
},
],
)
@pytest.mark.django_db
def test_step_unsilence(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, silenced=True, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "UnSilenceGroupStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group.silenced is False
@pytest.mark.parametrize(
"payload",
[
_get_payload() | {"trigger_id": "RANDOM_TRIGGER_ID"},
],
)
@pytest.mark.django_db
def test_step_select_attach(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "SelectAttachGroupStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
step.process_scenario(slack_user_identity, slack_team_identity, payload)
assert mock_slack_api_call.call_args.args == ("views.open",)
@pytest.mark.parametrize(
"payload",
[
_get_payload() | {"trigger_id": "RANDOM_TRIGGER_ID"},
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"trigger_id": "RANDOM_TRIGGER_ID",
"actions": [
{
"type": "button",
"value": json.dumps({"organization_id": ORGANIZATION_ID}),
}
],
},
],
)
@pytest.mark.django_db
def test_step_unattach(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
root_alert_group = make_alert_group(alert_receive_channel)
alert_group = make_alert_group(alert_receive_channel, root_alert_group=root_alert_group, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("distribute_alerts", "UnAttachGroupStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
alert_group.refresh_from_db()
assert alert_group.root_alert_group is None
@pytest.mark.parametrize(
"payload",
[
_get_payload() | {"message_ts": "RANDOM_TS", "trigger_id": "RANDOM_TRIGGER_ID"},
# deprecated payload shape, but still supported to handle older Slack messages
{
"message_ts": SLACK_MESSAGE_TS,
"channel": {"id": SLACK_CHANNEL_ID},
"trigger_id": "RANDOM_TRIGGER_ID",
"actions": [
{
"type": "button",
"value": json.dumps({"organization_id": ORGANIZATION_ID, "alert_group_pk": str(ALERT_GROUP_ID)}),
}
],
},
],
)
@pytest.mark.django_db
def test_step_format_alert(
payload,
make_organization,
make_slack_team_identity,
make_user,
make_slack_user_identity,
make_alert_receive_channel,
make_alert_group,
make_alert,
make_slack_channel,
make_slack_message,
):
slack_team_identity = make_slack_team_identity()
slack_user_identity = make_slack_user_identity(slack_team_identity=slack_team_identity)
slack_channel = make_slack_channel(slack_team_identity, slack_id=SLACK_CHANNEL_ID)
organization = make_organization(pk=ORGANIZATION_ID, slack_team_identity=slack_team_identity)
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
user = make_user(organization=organization, slack_user_identity=slack_user_identity)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel, pk=ALERT_GROUP_ID)
make_alert(alert_group, raw_request_data={})
slack_message = make_slack_message(
alert_group=alert_group, channel_id=slack_channel.slack_id, slack_id=SLACK_MESSAGE_TS
)
slack_message.get_alert_group() # fix FKs
step_class = ScenarioStep.get_step("alertgroup_appearance", "OpenAlertAppearanceDialogStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
step.process_scenario(slack_user_identity, slack_team_identity, payload)
assert mock_slack_api_call.call_args.args == ("views.open",)
@pytest.mark.django_db
def test_step_resolution_note(
make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group, make_alert
):
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities()
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_alert(alert_group, raw_request_data={})
payload = {
"trigger_id": "RANDOM_TRIGGER_ID",
"actions": [
{
"type": "button",
"value": json.dumps(
{
"organization_id": organization.pk,
"alert_group_pk": alert_group.pk,
"resolution_note_window_action": "edit",
}
),
}
],
"channel": {"id": "RANDOM_CHANNEL_ID"},
"message": {"ts": "RANDOM_MESSAGE_TS"},
}
step_class = ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep")
step = step_class(organization=organization, user=user, slack_team_identity=slack_team_identity)
with patch.object(step._slack_client, "api_call") as mock_slack_api_call:
step.process_scenario(slack_user_identity, slack_team_identity, payload)
assert mock_slack_api_call.call_args.args == ("views.open",)

View file

@ -189,26 +189,43 @@ def test_get_resolution_notes_blocks_latest_limit(
side_effect=SlackAPIException(response={"ok": False, "error": "not_found"}),
)
def test_resolution_notes_modal_closed_before_update(
mock_slack_api_call, make_organization_and_user_with_slack_identities, make_alert_receive_channel, make_alert_group
mock_slack_api_call,
make_organization_and_user_with_slack_identities,
make_alert_receive_channel,
make_alert_group,
make_slack_message,
):
ResolutionNoteModalStep = ScenarioStep.get_step("resolution_note", "ResolutionNoteModalStep")
organization, _, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities()
organization, user, slack_team_identity, slack_user_identity = make_organization_and_user_with_slack_identities()
organization.refresh_from_db() # without this there's something weird with organization.archive_alerts_from
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
slack_message = make_slack_message(
alert_group=alert_group, channel_id="RANDOM_CHANNEL_ID", slack_id="RANDOM_MESSAGE_ID"
)
slack_message.get_alert_group() # fix FKs
payload = {
"trigger_id": "TEST",
"view": {"id": "TEST"},
"actions": [
{"value": json.dumps({"alert_group_pk": alert_group.pk, "resolution_note_window_action": "update"})}
{
"type": "button",
"value": json.dumps(
{
"organization_id": organization.pk,
"alert_group_pk": alert_group.pk,
"resolution_note_window_action": "update",
}
),
}
],
}
# Check that no error is raised even if the Slack API call fails
step = ResolutionNoteModalStep(organization=organization, slack_team_identity=slack_team_identity)
step = ResolutionNoteModalStep(organization=organization, user=user, slack_team_identity=slack_team_identity)
step.process_scenario(slack_user_identity, slack_team_identity, payload)
# Check that "views.update" API call was made

View file

@ -0,0 +1,213 @@
import json
import pytest
from apps.alerts.incident_appearance.renderers.slack_renderer import AlertGroupSlackRenderer
from apps.alerts.models import AlertGroup
@pytest.mark.django_db
def test_slack_renderer_acknowledge_button(make_organization, 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)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[0]
assert button["text"]["text"] == "Acknowledge"
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
@pytest.mark.django_db
def test_slack_renderer_unacknowledge_button(
make_organization, 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, acknowledged=True)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[0]
assert button["text"]["text"] == "Unacknowledge"
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
@pytest.mark.django_db
def test_slack_renderer_resolve_button(make_organization, 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)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[1]
assert button["text"]["text"] == "Resolve"
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
@pytest.mark.django_db
def test_slack_renderer_unresolve_button(make_organization, 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, resolved=True)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[0]
assert button["text"]["text"] == "Unresolve"
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
@pytest.mark.django_db
def test_slack_renderer_invite_action(
make_organization, make_user, make_alert_receive_channel, make_alert_group, make_alert
):
organization = make_organization()
user = make_user(organization=organization)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
ack_button = elements[2]
assert ack_button["placeholder"]["text"] == "Invite..."
# Check only user_id is passed. Otherwise, if there are a lot of users, the payload could be unnecessarily large.
assert json.loads(ack_button["options"][0]["value"]) == {"user_id": user.pk}
@pytest.mark.django_db
def test_slack_renderer_stop_invite_button(
make_organization, make_user, make_alert_receive_channel, make_alert_group, make_alert, make_invitation
):
organization = make_organization()
user = make_user(organization=organization)
alert_receive_channel = make_alert_receive_channel(organization)
alert_group = make_alert_group(alert_receive_channel)
make_alert(alert_group=alert_group, raw_request_data={})
invitation = make_invitation(alert_group, user, user)
action = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[1]["actions"][0]
assert action["text"] == f"Stop inviting {user.username}"
assert json.loads(action["value"]) == {
"organization_id": organization.pk,
"alert_group_pk": alert_group.pk,
"invitation_id": invitation.pk,
}
@pytest.mark.django_db
def test_slack_renderer_silence_button(make_organization, 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)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[3]
assert button["placeholder"]["text"] == "Silence"
values = [json.loads(option["value"]) for option in button["options"]]
assert values == [
{"organization_id": organization.pk, "alert_group_pk": alert_group.pk, "delay": delay}
for delay, _ in AlertGroup.SILENCE_DELAY_OPTIONS
]
@pytest.mark.django_db
def test_slack_renderer_unsilence_button(make_organization, 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, silenced=True)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[3]
assert button["text"]["text"] == "Unsilence"
assert json.loads(button["value"]) == {
"organization_id": organization.pk,
"alert_group_pk": alert_group.pk,
}
@pytest.mark.django_db
def test_slack_renderer_attach_button(make_organization, 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, silenced=True)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[4]
assert button["text"]["text"] == "Attach to ..."
assert json.loads(button["value"]) == {
"organization_id": organization.pk,
"alert_group_pk": alert_group.pk,
}
@pytest.mark.django_db
def test_slack_renderer_unattach_button(make_organization, make_alert_receive_channel, make_alert_group, make_alert):
organization = make_organization()
alert_receive_channel = make_alert_receive_channel(organization)
root_alert_group = make_alert_group(alert_receive_channel)
make_alert(alert_group=root_alert_group, raw_request_data={})
alert_group = make_alert_group(alert_receive_channel, root_alert_group=root_alert_group)
make_alert(alert_group=alert_group, raw_request_data={})
action = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["actions"][0]
assert action["text"] == "Unattach"
assert json.loads(action["value"]) == {
"organization_id": organization.pk,
"alert_group_pk": alert_group.pk,
}
@pytest.mark.django_db
def test_slack_renderer_format_alert_button(
make_organization, 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)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[5]
assert button["text"]["text"] == ":mag: Format Alert"
assert json.loads(button["value"]) == {"organization_id": organization.pk, "alert_group_pk": alert_group.pk}
@pytest.mark.django_db
def test_slack_renderer_resolution_notes_button(
make_organization, 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)
make_alert(alert_group=alert_group, raw_request_data={})
elements = AlertGroupSlackRenderer(alert_group).render_alert_group_attachments()[0]["blocks"][0]["elements"]
button = elements[6]
assert button["text"]["text"] == "Add Resolution notes"
assert json.loads(button["value"]) == {
"organization_id": organization.pk,
"alert_group_pk": alert_group.pk,
"resolution_note_window_action": "edit",
}

View file

@ -276,7 +276,7 @@ example_payload = {
"labels": {"alertname": "TestAlert", "region": "eu-1", "severity": "critical"},
"annotations": {
"message": "This is test alert",
"description": "This alert was sent by user for the demonstration purposes",
"description": "This alert was sent by user for demonstration purposes",
"runbook_url": "https://grafana.com/",
},
"startsAt": "2018-12-25T15:47:47.377363608Z",

View file

@ -57,4 +57,4 @@ resolve_condition = """\
acknowledge_condition = None
example_payload = {"message": "This alert was sent by user for the demonstration purposes"}
example_payload = {"message": "This alert was sent by user for demonstration purposes"}

View file

@ -52,5 +52,5 @@ example_payload = {
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
"state": "alerting",
"link_to_upstream_details": "https://en.wikipedia.org/wiki/Downtime",
"message": "This alert was sent by user for the demonstration purposes\nSmth happened. Oh no!",
"message": "This alert was sent by user for demonstration purposes\nSmth happened. Oh no!",
}

View file

@ -224,7 +224,7 @@ example_payload = {
"alertname": "TestAlert",
"region": "eu-1",
},
"annotations": {"description": "This alert was sent by user for the demonstration purposes"},
"annotations": {"description": "This alert was sent by user for demonstration purposes"},
"startsAt": "2018-12-25T15:47:47.377363608Z",
"endsAt": "0001-01-01T00:00:00Z",
"generatorURL": "",

View file

@ -115,7 +115,7 @@ example_payload = {
"alertname": "TestAlert",
"region": "eu-1",
},
"annotations": {"description": "This alert was sent by user for the demonstration purposes"},
"annotations": {"description": "This alert was sent by user for demonstration purposes"},
"startsAt": "2018-12-25T15:47:47.377363608Z",
"endsAt": "0001-01-01T00:00:00Z",
"generatorURL": "",

View file

@ -54,6 +54,6 @@ acknowledge_condition = None
example_payload = {
"id": "TestAlert",
"message": "This alert was sent by user for the demonstration purposes",
"message": "This alert was sent by user for demonstration purposes",
"data": "{foo: bar}",
}

View file

@ -57,4 +57,4 @@ resolve_condition = """\
{%- endif %}"""
acknowledge_condition = None
example_payload = {"message": "This alert was sent by user for the demonstration purposes"}
example_payload = {"message": "This alert was sent by user for demonstration purposes"}

View file

@ -54,5 +54,5 @@ example_payload = {
"image_url": "https://upload.wikimedia.org/wikipedia/commons/e/ee/Grumpy_Cat_by_Gage_Skidmore.jpg",
"state": "alerting",
"link_to_upstream_details": "https://en.wikipedia.org/wiki/Downtime",
"message": "This alert was sent by user for the demonstration purposes\nSmth happened. Oh no!",
"message": "This alert was sent by user for demonstration purposes\nSmth happened. Oh no!",
}

View file

@ -44,6 +44,11 @@ urlpatterns = [
path("api/internal/v1/mobile_app/", include("apps.mobile_app.urls", namespace="mobile_app_tmp")),
] + static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
if settings.FEATURE_PROMETHEUS_EXPORTER_ENABLED:
urlpatterns += [
path("metrics/", include("apps.metrics_exporter.urls")),
]
if settings.FEATURE_SLACK_INTEGRATION_ENABLED:
urlpatterns += [
path("api/internal/v1/slack/", include("apps.slack.urls")),

View file

@ -62,6 +62,7 @@ FEATURE_SLACK_INTEGRATION_ENABLED = getenv_boolean("FEATURE_SLACK_INTEGRATION_EN
FEATURE_WEB_SCHEDULES_ENABLED = getenv_boolean("FEATURE_WEB_SCHEDULES_ENABLED", default=False)
FEATURE_MULTIREGION_ENABLED = getenv_boolean("FEATURE_MULTIREGION_ENABLED", default=False)
FEATURE_INBOUND_EMAIL_ENABLED = getenv_boolean("FEATURE_INBOUND_EMAIL_ENABLED", default=False)
FEATURE_PROMETHEUS_EXPORTER_ENABLED = getenv_boolean("FEATURE_PROMETHEUS_EXPORTER_ENABLED", default=False)
GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED = getenv_boolean("GRAFANA_CLOUD_ONCALL_HEARTBEAT_ENABLED", default=True)
GRAFANA_CLOUD_NOTIFICATIONS_ENABLED = getenv_boolean("GRAFANA_CLOUD_NOTIFICATIONS_ENABLED", default=True)

View file

@ -40,7 +40,7 @@ const TelegramInfo = observer((_props: TelegramInfoProps) => {
return (
<WithPermissionControlDisplay
userAction={UserActions.OtherSettingsWrite}
userAction={UserActions.UserSettingsWrite}
message="You do not have permission to perform this action. Ask an admin to upgrade your permissions."
>
{telegramConfigured || !store.hasFeature(AppFeature.LiveSettings) ? (

View file

@ -0,0 +1,11 @@
apiVersion: 1
datasources:
- name: Prometheus
type: prometheus
access: proxy
url: http://host.docker.internal:9090
jsonData:
httpMethod: POST
manageAlerts: true
prometheusType: Prometheus