def test_permission_denied(self): user2 = self.create_user(is_superuser=False) user2_identity = Identity.objects.create( external_id="slack_id2", idp=self.idp, user=user2, status=IdentityStatus.VALID, scopes=[], ) status_action = {"name": "status", "value": "ignored", "type": "button"} resp = self.post_webhook( action_data=[status_action], slack_user={"id": user2_identity.external_id} ) self.group1 = Group.objects.get(id=self.group1.id) associate_url = build_unlinking_url( self.integration.id, self.org.id, "slack_id2", "C065W1189", self.response_url ) assert resp.status_code == 200, resp.content assert resp.data["response_type"] == "ephemeral" assert not resp.data["replace_original"] assert resp.data["text"] == UNLINK_IDENTITY_MESSAGE.format( associate_url=associate_url, user_email=user2.email, org_name=self.org.name )
def test_unlink_user_identity(self): self.link_user() assert self.find_identity() unlinking_url = build_unlinking_url( self.integration.id, "UXXXXXXX1", "CXXXXXXX9", "http://example.slack.com/response_url", ) response = self.client.get(unlinking_url) assert response.status_code == 200 self.assertTemplateUsed(response, "sentry/auth-unlink-identity.html") response = self.client.post(unlinking_url) assert response.status_code == 200 self.assertTemplateUsed(response, "sentry/integrations/slack/unlinked.html") # Assert that the identity was deleted. assert not self.find_identity() assert len(responses.calls) >= 1 data = json.loads(str(responses.calls[0].request.body.decode("utf-8"))) assert SUCCESS_UNLINKED_MESSAGE in data["text"]
def api_error( self, slack_request: SlackActionRequest, group: Group, identity: Identity, error: ApiClient.ApiError, action_type: str, ) -> Response: logger.info( "slack.action.api-error", extra={ **slack_request.get_logging_data(group), "response": str(error.body), "action_type": action_type, }, ) if error.status_code == 403: text = UNLINK_IDENTITY_MESSAGE.format( associate_url=build_unlinking_url( slack_request.integration.id, slack_request.user_id, slack_request.channel_id, slack_request.response_url, ), user_email=identity.user, org_name=group.organization.name, ) else: text = DEFAULT_ERROR_MESSAGE return self.respond_ephemeral(text)
def test_handle_submission_fail(self, client_put): status_action = {"name": "resolve_dialog", "value": "resolve_dialog"} # Expect request to open dialog on slack responses.add( method=responses.POST, url="https://slack.com/api/dialog.open", body='{"ok": true}', status=200, content_type="application/json", ) resp = self.post_webhook(action_data=[status_action]) assert resp.status_code == 200, resp.content # Opening dialog should *not* cause the current message to be updated assert resp.content == b"" data = parse_qs(responses.calls[0].request.body) assert data["token"][0] == self.integration.metadata["access_token"] assert data["trigger_id"][0] == self.trigger_id assert "dialog" in data dialog = json.loads(data["dialog"][0]) callback_data = json.loads(dialog["callback_id"]) assert int(callback_data["issue"]) == self.group.id assert callback_data["orig_response_url"] == self.response_url # Completing the dialog will update the message responses.add( method=responses.POST, url=self.response_url, body='{"ok": true}', status=200, content_type="application/json", ) # make the client raise an API error client_put.side_effect = client.ApiError( 403, '{"detail":"You do not have permission to perform this action."}' ) resp = self.post_webhook( type="dialog_submission", callback_id=dialog["callback_id"], data={"submission": {"resolve_type": "resolved"}}, ) # TODO(mgaeta): `assert_called` is deprecated. Find a replacement. # client_put.assert_called() associate_url = build_unlinking_url( self.integration.id, self.external_id, "C065W1189", self.response_url ) assert resp.status_code == 200, resp.content assert resp.data["text"] == UNLINK_IDENTITY_MESSAGE.format( associate_url=associate_url, user_email=self.user.email, org_name=self.organization.name )
def setUp(self): super().setUp() self.unlinking_url = build_unlinking_url( self.integration.id, self.external_id, self.channel_id, self.response_url, )
def unlink_user(self, slack_request: SlackRequest) -> Any: if not slack_request.has_identity: return self.reply(slack_request, NOT_LINKED_MESSAGE) integration = slack_request.integration associate_url = build_unlinking_url( integration_id=integration.id, slack_id=slack_request.user_id, channel_id=slack_request.channel_id, response_url=slack_request.response_url, ) return self.reply(slack_request, UNLINK_USER_MESSAGE.format(associate_url=associate_url))
def unlink_user(self, slack_request: SlackCommandRequest) -> Response: if not slack_request.has_identity: return self.send_ephemeral_notification(NOT_LINKED_MESSAGE) integration = slack_request.integration organization = integration.organizations.all()[0] associate_url = build_unlinking_url( integration_id=integration.id, organization_id=organization.id, slack_id=slack_request.user_id, channel_id=slack_request.channel_id, response_url=slack_request.response_url, ) return self.send_ephemeral_notification( UNLINK_USER_MESSAGE.format(associate_url=associate_url))
def test_unlink_user_identity(self): self.link_user() unlinking_url = build_unlinking_url( self.integration.id, self.slack_id, self.external_id, self.response_url, ) response = self.client.post(unlinking_url) assert response.status_code == 200 assert len(responses.calls) >= 1 data = json.loads(str(responses.calls[0].request.body.decode("utf-8"))) assert SUCCESS_UNLINKED_MESSAGE in get_response_text(data)
def unlink_user(self, slack_request: SlackDMRequest) -> Response: if not slack_request.has_identity: return self.reply(slack_request, NOT_LINKED_MESSAGE) if not (slack_request.integration and slack_request.user_id and slack_request.channel_id): raise SlackRequestError(status=status.HTTP_400_BAD_REQUEST) associate_url = build_unlinking_url( integration_id=slack_request.integration.id, slack_id=slack_request.user_id, channel_id=slack_request.channel_id, response_url=slack_request.response_url, ) return self.reply( slack_request, UNLINK_USER_MESSAGE.format(associate_url=associate_url))
def test_basic_flow(self, unsign): Identity.objects.create( user=self.user1, idp=self.idp, external_id="new-slack-id", status=IdentityStatus.VALID ) unsign.return_value = { "integration_id": self.integration.id, "organization_id": self.org.id, "slack_id": "new-slack-id", "channel_id": "my-channel", "response_url": "http://example.slack.com/response_url", } unlinking_url = build_unlinking_url( self.integration.id, self.org.id, "new-slack-id", "my-channel", "http://example.slack.com/response_url", ) resp = self.client.get(unlinking_url) assert resp.status_code == 200 self.assertTemplateUsed(resp, "sentry/auth-unlink-identity.html") responses.add( method=responses.POST, url="http://example.slack.com/response_url", body='{"ok": true}', status=200, content_type="application/json", ) # Unlink identity of user resp = self.client.post(unlinking_url) identity = Identity.objects.filter(external_id="new-slack-id", user=self.user1) assert len(identity) == 0 assert len(responses.calls) == 1
def post(self, request: Request) -> Response: logging_data: Dict[str, str] = {} try: slack_request = SlackActionRequest(request) slack_request.validate() except SlackRequestError as e: return self.respond(status=e.status) data = slack_request.data # Actions list may be empty when receiving a dialog response action_list = data.get("actions", []) action_option = action_list and action_list[0].get("value", "") # if a user is just clicking our auto response in the messages tab we just return a 200 if action_option == "sentry_docs_link_clicked": return self.respond() channel_id = slack_request.channel_id user_id = slack_request.user_id integration = slack_request.integration response_url = data.get("response_url") if action_option in ["link", "ignore"]: analytics.record( "integrations.slack.chart_unfurl_action", organization_id=integration.organizations.all()[0].id, action=action_option, ) payload = {"delete_original": "true"} try: post(response_url, json=payload) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond(status=403) return self.respond() logging_data["channel_id"] = channel_id logging_data["slack_user_id"] = user_id logging_data["response_url"] = response_url logging_data["integration_id"] = integration.id # Determine the issue group action is being taken on group_id = slack_request.callback_data["issue"] logging_data["group_id"] = group_id try: group = Group.objects.select_related("project__organization").get( project__in=Project.objects.filter( organization__in=integration.organizations.all()), id=group_id, ) except Group.DoesNotExist: logger.info("slack.action.invalid-issue", extra=logging_data) return self.respond(status=403) logging_data["organization_id"] = group.organization.id # Determine the acting user by slack identity try: idp = IdentityProvider.objects.get( type="slack", external_id=slack_request.team_id) except IdentityProvider.DoesNotExist: logger.error("slack.action.invalid-team-id", extra=logging_data) return self.respond(status=403) try: identity = Identity.objects.select_related("user").get( idp=idp, external_id=user_id) except Identity.DoesNotExist: associate_url = build_linking_url(integration, group.organization, user_id, channel_id, response_url) return self.respond({ "response_type": "ephemeral", "replace_original": False, "text": LINK_IDENTITY_MESSAGE.format(associate_url=associate_url), }) # Handle status dialog submission if slack_request.type == "dialog_submission" and "resolve_type" in data[ "submission"]: # Masquerade a status action action = { "name": "status", "value": data["submission"]["resolve_type"] } try: self.on_status(request, identity, group, action, data, integration) except client.ApiError as e: if e.status_code == 403: text = UNLINK_IDENTITY_MESSAGE.format( associate_url=build_unlinking_url( integration.id, user_id, channel_id, response_url), user_email=identity.user, org_name=group.organization.name, ) else: text = DEFAULT_ERROR_MESSAGE return self.api_error(e, "status_dialog", logging_data, text) group = Group.objects.get(id=group.id) attachment = build_group_attachment(group, identity=identity, actions=[action]) body = self.construct_reply( attachment, is_message=slack_request.callback_data["is_message"]) # use the original response_url to update the link attachment slack_client = SlackClient() try: slack_client.post( slack_request.callback_data["orig_response_url"], data=body, json=True) except ApiError as e: logger.error("slack.action.response-error", extra={"error": str(e)}) return self.respond() # Usually we'll want to respond with the updated attachment including # the list of actions taken. However, when opening a dialog we do not # have anything to update the message with and will use the # response_url later to update it. defer_attachment_update = False # Handle interaction actions action_type = None try: for action in action_list: action_type = action["name"] if action_type == "status": self.on_status(request, identity, group, action, data, integration) elif action_type == "assign": self.on_assign(request, identity, group, action) elif action_type == "resolve_dialog": self.open_resolve_dialog(data, group, integration) defer_attachment_update = True except client.ApiError as e: if e.status_code == 403: text = UNLINK_IDENTITY_MESSAGE.format( associate_url=build_unlinking_url(integration.id, user_id, channel_id, response_url), user_email=identity.user, org_name=group.organization.name, ) else: text = DEFAULT_ERROR_MESSAGE return self.api_error(e, action_type, logging_data, text) if defer_attachment_update: return self.respond() # Reload group as it may have been mutated by the action group = Group.objects.get(id=group.id) attachment = build_group_attachment(group, identity=identity, actions=action_list) body = self.construct_reply(attachment, is_message=self.is_message(data)) return self.respond(body)
def test_handle_submission_fail(self): status_action = {"name": "resolve_dialog", "value": "resolve_dialog"} # Expect request to open dialog on slack responses.add( method=responses.POST, url="https://slack.com/api/dialog.open", body='{"ok": true}', status=200, content_type="application/json", ) resp = self.post_webhook(action_data=[status_action]) assert resp.status_code == 200, resp.content # Opening dialog should *not* cause the current message to be updated assert resp.content == b"" data = parse_qs(responses.calls[0].request.body) assert data["token"][0] == self.integration.metadata["access_token"] assert data["trigger_id"][0] == self.trigger_id assert "dialog" in data dialog = json.loads(data["dialog"][0]) callback_data = json.loads(dialog["callback_id"]) assert int(callback_data["issue"]) == self.group.id assert callback_data["orig_response_url"] == self.response_url # Completing the dialog will update the message responses.add( method=responses.POST, url=self.response_url, body='{"ok": true}', status=200, content_type="application/json", ) # Remove the user from the organization. member = OrganizationMember.objects.get(user=self.user, organization=self.organization) member.remove_user() member.save() response = self.post_webhook( type="dialog_submission", callback_id=dialog["callback_id"], data={"submission": { "resolve_type": "resolved" }}, ) assert response.status_code == 200, response.content assert response.data["text"] == UNLINK_IDENTITY_MESSAGE.format( associate_url=build_unlinking_url( integration_id=self.integration.id, slack_id=self.external_id, channel_id="C065W1189", response_url=self.response_url, ), user_email=self.user.email, org_name=self.organization.name, )