Skip to content

Commit 03e19b7

Browse files
committed
Revert "Revert slack org does not exist changes breaking escalate command (#2057)"
This reverts commit 835d267.
1 parent 5975b9d commit 03e19b7

File tree

3 files changed

+185
-68
lines changed

3 files changed

+185
-68
lines changed

CHANGELOG.md

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
### Fixed
1111

1212
- Fix templates when slack or telegram is disabled ([#2064](https://github.com/grafana/oncall/pull/2064))
13+
- Fix + revert [#2057](https://github.com/grafana/oncall/pull/2057) which reverted a change which properly handles
14+
`Organization.DoesNotExist` exceptions for Slack events by @joeyorlando ([#TBD](https://github.com/grafana/oncall/pull/TBD))
1315

1416
## v1.2.33 (2023-05-30)
1517

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import json
2+
from unittest.mock import call, patch
3+
4+
import pytest
5+
from rest_framework import status
6+
from rest_framework.test import APIClient
7+
8+
from apps.slack.scenarios.scenario_step import PAYLOAD_TYPE_BLOCK_ACTIONS
9+
10+
EVENT_TRIGGER_ID = "5333959822612.4122782784722.4734ff484b2ac4d36a185bb242ee9932"
11+
WARNING_TEXT = (
12+
"OnCall is not able to process this action because one of the following scenarios: \n"
13+
"1. The Slack chatops integration was disconnected from the instance that the Alert Group belongs "
14+
"to, BUT the Slack workspace is still connected to another instance as well. In this case, simply log "
15+
"in to the OnCall web interface and re-install the Slack Integration with this workspace again.\n"
16+
"2. (Less likely) The Grafana instance belonging to this Alert Group was deleted. In this case the Alert Group is orphaned and cannot be acted upon."
17+
)
18+
19+
20+
def _make_request(payload):
21+
return APIClient().post(
22+
"/slack/interactive_api_endpoint/",
23+
format="json",
24+
data=payload,
25+
**{
26+
"HTTP_X_SLACK_SIGNATURE": "asdfasdf",
27+
"HTTP_X_SLACK_REQUEST_TIMESTAMP": "xxcxcvx",
28+
},
29+
)
30+
31+
32+
@patch("apps.slack.views.SlackEventApiEndpointView.verify_signature", return_value=True)
33+
@patch("apps.slack.views.SlackEventApiEndpointView._open_warning_window_if_needed")
34+
@pytest.mark.django_db
35+
def test_organization_not_found_scenario_properly_handled(
36+
mock_open_warning_window_if_needed,
37+
_mock_verify_signature,
38+
make_organization,
39+
make_slack_user_identity,
40+
make_slack_team_identity,
41+
):
42+
slack_team_id = "T043LP0P2M8"
43+
slack_access_token = "asdfasdf"
44+
slack_bot_access_token = "cmncvmnvcnm"
45+
slack_bot_user_id = "mncvnmvcmnvcmncv,,cx,"
46+
47+
slack_user_id = "iurtiurituritu"
48+
49+
def _make_slack_team_identity():
50+
return make_slack_team_identity(
51+
slack_id=slack_team_id,
52+
detected_token_revoked=None,
53+
access_token=slack_access_token,
54+
bot_access_token=slack_bot_access_token,
55+
bot_user_id=slack_bot_user_id,
56+
)
57+
58+
# SCENARIO 1
59+
# two orgs connected to same slack workspace, the one belonging to the alert group/slack message
60+
# is no longer connected to the slack workspace, but another org still is
61+
slack_team_identity = _make_slack_team_identity()
62+
make_slack_user_identity(slack_team_identity=slack_team_identity, slack_id=slack_user_id)
63+
64+
make_organization(slack_team_identity=slack_team_identity)
65+
org2 = make_organization()
66+
event_payload_actions = [
67+
{
68+
"value": json.dumps({"organization_id": org2.id}),
69+
}
70+
]
71+
72+
event_payload = {
73+
"type": PAYLOAD_TYPE_BLOCK_ACTIONS,
74+
"trigger_id": EVENT_TRIGGER_ID,
75+
"user": {
76+
"id": slack_user_id,
77+
},
78+
"team": {
79+
"id": slack_team_id,
80+
},
81+
"actions": event_payload_actions,
82+
}
83+
84+
response = _make_request(event_payload)
85+
assert response.status_code == status.HTTP_200_OK
86+
87+
# SCENARIO 2
88+
# the org that was associated w/ the alert group, has since been deleted
89+
# and the slack message is now orphaned
90+
org2.hard_delete()
91+
92+
response = _make_request(event_payload)
93+
assert response.status_code == status.HTTP_200_OK
94+
95+
mock_call = call(event_payload, slack_team_identity, WARNING_TEXT)
96+
mock_open_warning_window_if_needed.assert_has_calls([mock_call, mock_call])

engine/apps/slack/views.py

+87-68
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from apps.slack.slack_client import SlackClientWithErrorHandling
5656
from apps.slack.slack_client.exceptions import SlackAPIException, SlackAPITokenException
5757
from apps.slack.tasks import clean_slack_integration_leftovers, unpopulate_slack_user_identities
58+
from apps.user_management.models import Organization
5859
from common.insight_log import ChatOpsEvent, ChatOpsTypePlug, write_chatops_insight_log
5960
from common.oncall_gateway import delete_slack_connector
6061

@@ -285,6 +286,19 @@ def post(self, request):
285286
# Open pop-up to inform user why OnCall bot doesn't work if any action was triggered
286287
self._open_warning_window_if_needed(payload, slack_team_identity, warning_text)
287288
return Response(status=200)
289+
elif organization is None:
290+
# see this GitHub issue for more context on how this situation can arise
291+
# https://github.com/grafana/oncall-private/issues/1836
292+
warning_text = (
293+
"OnCall is not able to process this action because one of the following scenarios: \n"
294+
"1. The Slack chatops integration was disconnected from the instance that the Alert Group belongs "
295+
"to, BUT the Slack workspace is still connected to another instance as well. In this case, simply log "
296+
"in to the OnCall web interface and re-install the Slack Integration with this workspace again.\n"
297+
"2. (Less likely) The Grafana instance belonging to this Alert Group was deleted. In this case the Alert Group is orphaned and cannot be acted upon."
298+
)
299+
# Open pop-up to inform user why OnCall bot doesn't work if any action was triggered
300+
self._open_warning_window_if_needed(payload, slack_team_identity, warning_text)
301+
return Response(status=200)
288302
elif not slack_user_identity.users.exists():
289303
# Means that slack_user_identity doesn't have any connected user
290304
# Open pop-up to inform user why OnCall bot doesn't work if any action was triggered
@@ -420,76 +434,81 @@ def _get_organization_from_payload(self, payload, slack_team_identity):
420434
channel_id = None
421435
organization = None
422436

423-
# view submission or actions in view
424-
if "view" in payload:
425-
organization_id = None
426-
private_metadata = payload["view"].get("private_metadata")
427-
# steps with private_metadata in which we know organization before open view
428-
if private_metadata and "organization_id" in private_metadata:
429-
organization_id = json.loads(private_metadata).get("organization_id")
430-
# steps with organization selection in view (e.g. slash commands)
431-
elif SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID in payload["view"].get("state", {}).get("values", {}):
432-
payload_values = payload["view"]["state"]["values"]
433-
selected_value = payload_values[SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID][
434-
SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID
435-
]["selected_option"]["value"]
436-
organization_id = int(selected_value.split("-")[0])
437-
if organization_id:
438-
organization = slack_team_identity.organizations.get(pk=organization_id)
439-
return organization
440-
# buttons and actions
441-
elif payload.get("type") in [
442-
PAYLOAD_TYPE_BLOCK_ACTIONS,
443-
PAYLOAD_TYPE_INTERACTIVE_MESSAGE,
444-
PAYLOAD_TYPE_MESSAGE_ACTION,
445-
]:
446-
# for cases when we put organization_id into action value (e.g. public suggestion)
447-
if (
448-
payload.get("actions")
449-
and payload["actions"][0].get("value", {})
450-
and "organization_id" in payload["actions"][0]["value"]
451-
):
452-
organization_id = int(json.loads(payload["actions"][0]["value"])["organization_id"])
453-
organization = slack_team_identity.organizations.get(pk=organization_id)
454-
return organization
455-
456-
channel_id = payload["channel"]["id"]
457-
if "message" in payload:
458-
message_ts = payload["message"].get("thread_ts") or payload["message"]["ts"]
459-
# for interactive message
460-
elif "message_ts" in payload:
461-
message_ts = payload["message_ts"]
462-
else:
463-
return
464-
# events
465-
elif payload.get("type") == PAYLOAD_TYPE_EVENT_CALLBACK:
466-
if "channel" in payload["event"]: # events without channel: user_change, events with subteam, etc.
467-
channel_id = payload["event"]["channel"]
468-
469-
if "message" in payload["event"]:
470-
message_ts = payload["event"]["message"].get("thread_ts") or payload["event"]["message"]["ts"]
471-
elif "thread_ts" in payload["event"]:
472-
message_ts = payload["event"]["thread_ts"]
473-
else:
474-
return
437+
try:
438+
# view submission or actions in view
439+
if "view" in payload:
440+
organization_id = None
441+
private_metadata = payload["view"].get("private_metadata")
442+
# steps with private_metadata in which we know organization before open view
443+
if private_metadata and "organization_id" in private_metadata:
444+
organization_id = json.loads(private_metadata).get("organization_id")
445+
# steps with organization selection in view (e.g. slash commands)
446+
elif SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID in payload["view"].get("state", {}).get("values", {}):
447+
payload_values = payload["view"]["state"]["values"]
448+
selected_value = payload_values[SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID][
449+
SELECT_ORGANIZATION_AND_ROUTE_BLOCK_ID
450+
]["selected_option"]["value"]
451+
organization_id = int(selected_value.split("-")[0])
452+
if organization_id:
453+
organization = slack_team_identity.organizations.get(pk=organization_id)
454+
return organization
455+
# buttons and actions
456+
elif payload.get("type") in [
457+
PAYLOAD_TYPE_BLOCK_ACTIONS,
458+
PAYLOAD_TYPE_INTERACTIVE_MESSAGE,
459+
PAYLOAD_TYPE_MESSAGE_ACTION,
460+
]:
461+
# for cases when we put organization_id into action value (e.g. public suggestion)
462+
if (
463+
payload.get("actions")
464+
and payload["actions"][0].get("value", {})
465+
and "organization_id" in payload["actions"][0]["value"]
466+
):
467+
organization_id = int(json.loads(payload["actions"][0]["value"])["organization_id"])
468+
organization = slack_team_identity.organizations.get(pk=organization_id)
469+
return organization
470+
471+
channel_id = payload["channel"]["id"]
472+
if "message" in payload:
473+
message_ts = payload["message"].get("thread_ts") or payload["message"]["ts"]
474+
# for interactive message
475+
elif "message_ts" in payload:
476+
message_ts = payload["message_ts"]
477+
else:
478+
return
479+
# events
480+
elif payload.get("type") == PAYLOAD_TYPE_EVENT_CALLBACK:
481+
if "channel" in payload["event"]: # events without channel: user_change, events with subteam, etc.
482+
channel_id = payload["event"]["channel"]
483+
484+
if "message" in payload["event"]:
485+
message_ts = payload["event"]["message"].get("thread_ts") or payload["event"]["message"]["ts"]
486+
elif "thread_ts" in payload["event"]:
487+
message_ts = payload["event"]["thread_ts"]
488+
else:
489+
return
475490

476-
if not (message_ts and channel_id):
477-
return
491+
if not (message_ts and channel_id):
492+
return
478493

479-
try:
480-
slack_message = SlackMessage.objects.get(
481-
slack_id=message_ts,
482-
_slack_team_identity=slack_team_identity,
483-
channel_id=channel_id,
484-
)
485-
except SlackMessage.DoesNotExist:
486-
pass
487-
else:
488-
alert_group = slack_message.get_alert_group()
489-
if alert_group:
490-
organization = alert_group.channel.organization
491-
return organization
492-
return organization
494+
try:
495+
slack_message = SlackMessage.objects.get(
496+
slack_id=message_ts,
497+
_slack_team_identity=slack_team_identity,
498+
channel_id=channel_id,
499+
)
500+
except SlackMessage.DoesNotExist:
501+
pass
502+
else:
503+
alert_group = slack_message.get_alert_group()
504+
if alert_group:
505+
organization = alert_group.channel.organization
506+
return organization
507+
return organization
508+
except Organization.DoesNotExist:
509+
# see this GitHub issue for more context on how this situation can arise
510+
# https://github.com/grafana/oncall-private/issues/1836
511+
return None
493512

494513
def _open_warning_window_if_needed(self, payload, slack_team_identity, warning_text) -> None:
495514
if payload.get("trigger_id") is not None:

0 commit comments

Comments
 (0)