Fix orphaned messages in Slack (#2023)

# What this PR does
Reworks Slack handlers for buttons and select menus for AG Slack
messages.

<img width="602" alt="Screenshot 2023-05-31 at 19 34 05"
src="https://github.com/grafana/oncall/assets/20116910/857bf096-7bdd-427b-94b6-15aad873a8ac">


## Current implementation

- It's possible to end up with orphaned Slack messages that are posted
to Slack but have no `SlackMessage` instance in the DB. For such
messages, clicking buttons will result in an exception and HTTP 500. See
private repo
[issue](https://github.com/grafana/oncall-private/issues/1841) for more
info.
- Bug in authorization system, which effectively bypasses any permission
checks. For example, it's possible to resolve an alert group while being
a Viewer.
- No tests covering most buttons.

## Changes in this PR

- Make the system more robust, don't use `SlackMessage` model to figure
out the alert group being interacted on, instead embed `alert_group_pk`
to every button and use it when receiving interaction requests from
Slack.
- Existing orphaned Slack messages will be repaired. Clicking buttons
under orphaned messages will work (and missing `SlackMessage` instance
will be created on interaction). This is possible because some buttons
already have `alert_group_pk` embedded, and it's possible to get this
data on button clicks (even if the clicked button itself doesn't have
`alert_group_pk` embedded).
- Fix authorization. Show warning window when unauthorized:
<img width="511" alt="Screenshot 2023-05-31 at 19 40 02"
src="https://github.com/grafana/oncall/assets/20116910/5abeeaa7-1b61-4a47-b3af-0e21d5cd1907">

- Added tests for all the buttons under AG message. Add tests checking
authorization, actual execution of scenario steps, orphan message
repairing, backward compatibility, etc. Also add tests on
`AlertGroupSlackRenderer` checking that correct data is embedded into
buttons.
- Cosmetic changes such as renaming `incident`  to `Alert Group`.

## Which issue(s) this PR fixes
Related to https://github.com/grafana/oncall-private/issues/1841

## Checklist

- [x] Unit, integration, and e2e (if applicable) tests updated
- [x] Documentation added (or `pr:no public docs` PR label added if not
required)
- [x] `CHANGELOG.md` updated (or `pr:no changelog` PR label added if not
required)
This commit is contained in:
Vadim Stepanov 2023-06-01 11:21:30 +01:00 committed by GitHub
parent e66fe67174
commit d1373b58d2
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 1386 additions and 215 deletions

View file

@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### 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))
## v1.2.34 (2023-05-31)

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

@ -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",
}