Пример #1
0
    def post(self, request: Request) -> Response:
        try:
            slack_request = SlackActionRequest(request)
            slack_request.validate()
        except SlackRequestError as e:
            return self.respond(status=e.status)

        # Actions list may be empty when receiving a dialog response.
        action_list_raw = slack_request.data.get("actions", [])

        action_list = []
        action_option = None
        for action_data in action_list_raw:
            # Get the _first_ value in the action list.
            value = action_data.get("value")
            if value and not action_option:
                action_option = value
            if "name" in action_data:
                action_list.append(MessageAction(**action_data))

        # 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()

        # TODO(mgaeta): Stop short-circuiting here on VALUE alone.
        if action_option in ["link", "ignore"]:
            return self.handle_unfurl(slack_request, action_option)

        if action_option in ["approve_member", "reject_member"]:
            return self.handle_member_approval(slack_request, action_option)

        if action_list and action_list[0].name == "enable_notifications":
            return self.handle_enable_notifications(slack_request)

        return self._handle_group_actions(slack_request, request, action_list)
Пример #2
0
    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)
Пример #3
0
    def handle_enable_notifications(
            self, slack_request: SlackActionRequest) -> Response:
        try:
            identity = slack_request.get_identity()
        except IdentityProvider.DoesNotExist:
            identity = None
        if not identity:
            return self.respond_with_text(NO_IDENTITY_MESSAGE)

        NotificationSetting.objects.enable_settings_for_user(
            recipient=identity.user, provider=ExternalProviders.SLACK)
        return self.respond_with_text(ENABLE_SLACK_SUCCESS_MESSAGE)
Пример #4
0
 def slack_request(self):
     return SlackActionRequest(self.request)
Пример #5
0
    def handle_member_approval(self, slack_request: SlackActionRequest,
                               action: str) -> Response:
        try:
            # get_identity can return nobody
            identity = slack_request.get_identity()
        except IdentityProvider.DoesNotExist:
            identity = None

        if not identity:
            return self.respond_with_text(NO_IDENTITY_MESSAGE)

        member_id = slack_request.callback_data["member_id"]

        try:
            member = OrganizationMember.objects.get_member_invite_query(
                member_id).get()
        except OrganizationMember.DoesNotExist:
            # member request is gone, likely someone else rejected it
            member_email = slack_request.callback_data["member_email"]
            return self.respond_with_text(
                f"Member invitation for {member_email} no longer exists.")

        organization = member.organization

        if not organization.has_access(identity.user):
            return self.respond_with_text(NO_ACCESS_MESSAGE)

        # row should exist because we have access
        member_of_approver = OrganizationMember.objects.get(
            user=identity.user, organization=organization)
        access = from_member(member_of_approver)
        if not access.has_scope("member:admin"):
            return self.respond_with_text(NO_PERMISSION_MESSAGE)

        # validate the org options and check against allowed_roles
        allowed_roles = member_of_approver.get_allowed_roles_to_invite()
        try:
            member.validate_invitation(identity.user, allowed_roles)
        except UnableToAcceptMemberInvitationException as err:
            return self.respond_with_text(str(err))

        original_status = InviteStatus(member.invite_status)
        try:
            if action == "approve_member":
                member.approve_member_invitation(identity.user,
                                                 referrer="slack")
            else:
                member.reject_member_invitation(identity.user)
        except Exception as err:
            # shouldn't error but if it does, respond to the user
            logger.error(
                err,
                extra={
                    "organization_id": organization.id,
                    "member_id": member.id,
                },
            )
            return self.respond_ephemeral(DEFAULT_ERROR_MESSAGE)

        if action == "approve_member":
            event_name = "integrations.slack.approve_member_invitation"
            verb = "approved"
        else:
            event_name = "integrations.slack.reject_member_invitation"
            verb = "rejected"

        if original_status == InviteStatus.REQUESTED_TO_BE_INVITED:
            invite_type = "Invite"
        else:
            invite_type = "Join"

        analytics.record(
            event_name,
            actor_id=identity.user_id,
            organization_id=member.organization_id,
            invitation_type=invite_type.lower(),
            invited_member_id=member_id,
        )

        manage_url = absolute_uri(
            reverse("sentry-organization-members",
                    args=[member.organization.slug]))

        message = SUCCESS_MESSAGE.format(
            email=member.email,
            invite_type=invite_type,
            url=manage_url,
            verb=verb,
        )

        return self.respond({"text": message})
Пример #6
0
    def _handle_group_actions(
        self,
        slack_request: SlackActionRequest,
        request: Request,
        action_list: Sequence[MessageAction],
    ) -> Response:
        group = get_group(slack_request)
        if not group:
            return self.respond(status=403)

        # Determine the acting user by Slack identity.
        try:
            identity = slack_request.get_identity()
        except IdentityProvider.DoesNotExist:
            return self.respond(status=403)

        if not identity:
            associate_url = build_linking_url(
                integration=slack_request.integration,
                slack_id=slack_request.user_id,
                channel_id=slack_request.channel_id,
                response_url=slack_request.response_url,
            )
            return self.respond_ephemeral(
                LINK_IDENTITY_MESSAGE.format(associate_url=associate_url))

        # Handle status dialog submission
        if (slack_request.type == "dialog_submission"
                and "resolve_type" in slack_request.data["submission"]):
            # Masquerade a status action
            action = MessageAction(
                name="status",
                value=slack_request.data["submission"]["resolve_type"],
            )

            try:
                self.on_status(request, identity, group, action)
            except client.ApiError as error:
                return self.api_error(slack_request, group, identity, error,
                                      "status_dialog")

            attachment = SlackIssuesMessageBuilder(group,
                                                   identity=identity,
                                                   actions=[action]).build()
            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
        for action in action_list:
            try:
                if action.name == "status":
                    self.on_status(request, identity, group, action)
                elif action.name == "assign":
                    self.on_assign(request, identity, group, action)
                elif action.name == "resolve_dialog":
                    self.open_resolve_dialog(slack_request, group)
                    defer_attachment_update = True
            except client.ApiError as error:
                return self.api_error(slack_request, group, identity, error,
                                      action.name)

        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 = SlackIssuesMessageBuilder(group,
                                               identity=identity,
                                               actions=action_list).build()
        body = self.construct_reply(attachment,
                                    is_message=_is_message(slack_request.data))

        return self.respond(body)
Пример #7
0
    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)
Пример #8
0
    def handle_member_approval(self,
                               slack_request: SlackActionRequest) -> Response:
        try:
            # get_identity can return nobody
            identity = slack_request.get_identity()
        except IdentityProvider.DoesNotExist:
            identity = None

        if not identity:
            return self.respond_with_text("Identity not linked for user.")

        member_id = slack_request.callback_data["member_id"]

        try:
            member = OrganizationMember.objects.get_member_invite_query(
                member_id).get()
        except OrganizationMember.DoesNotExist:
            # member request is gone, likely someone else rejected it
            member_email = slack_request.callback_data["member_email"]
            return self.respond_with_text(
                f"Member invitation for {member_email} no longer exists.")

        organization = member.organization

        if not organization.has_access(identity.user):
            return self.respond_with_text(
                "You don't have access to the organization for the invitation."
            )

        # row should exist because we have access
        member_of_approver = OrganizationMember.objects.get(
            user=identity.user, organization=organization)
        access = from_member(member_of_approver)
        if not access.has_scope("member:admin"):
            return self.respond_with_text(
                "You don't have permission to approve member invitations.")

        # validate the org options and check against allowed_roles
        allowed_roles = member_of_approver.get_allowed_roles_to_invite()
        try:
            member.validate_invitation(identity.user, allowed_roles)
        except UnableToAcceptMemberInvitationException as err:
            return self.respond_with_text(str(err))

        original_status = member.invite_status
        member_email = member.email
        try:
            if slack_request.action_option == "approve_member":
                member.approve_member_invitation(identity.user,
                                                 referrer="slack")
            else:
                member.reject_member_invitation(identity.user)
        except Exception as err:
            # shouldn't error but if it does, respond to the user
            logger.error(
                err,
                extra={
                    "organization_id": organization.id,
                    "member_id": member.id,
                },
            )
            return self.respond_ephemeral(DEFAULT_ERROR_MESSAGE)

        # record analytics and respond with success
        approve_member = slack_request.action_option == "approve_member"
        event_name = ("integrations.slack.approve_member_invitation"
                      if approve_member else
                      "integrations.slack.reject_member_invitation")
        invite_type = ("Invite" if original_status
                       == InviteStatus.REQUESTED_TO_BE_INVITED.value else
                       "Join")
        analytics.record(
            event_name,
            actor_id=identity.user_id,
            organization_id=member.organization_id,
            invitation_type=invite_type.lower(),
            invited_member_id=member_id,
        )

        verb = "approved" if approve_member else "rejected"

        manage_url = absolute_uri(
            reverse("sentry-organization-members",
                    args=[member.organization.slug]))
        body = {
            "text":
            f"{invite_type} request for {member_email} has been {verb}. <{manage_url}|See Members and Requests>.",
        }
        return self.respond(body)
Пример #9
0
    def post(self, request: Request) -> Response:
        try:
            slack_request = SlackActionRequest(request)
            slack_request.validate()
        except SlackRequestError as e:
            return self.respond(status=e.status)

        action_option = slack_request.action_option

        if action_option in ["approve_member", "reject_member"]:
            return self.handle_member_approval(slack_request)

        # 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()

        # Actions list may be empty when receiving a dialog response
        data = slack_request.data
        action_list_raw = data.get("actions", [])
        action_list = [
            MessageAction(**action_data) for action_data in action_list_raw
        ]

        organizations = slack_request.integration.organizations.all()

        if action_option in ["link", "ignore"]:
            analytics.record(
                "integrations.slack.chart_unfurl_action",
                organization_id=organizations[0].id,
                action=action_option,
            )
            payload = {"delete_original": "true"}
            try:
                requests_.post(slack_request.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()

        # Determine the issue group action is being taken on
        group_id = slack_request.callback_data["issue"]
        logging_data = {**slack_request.logging_data, "group_id": group_id}

        try:
            group = Group.objects.select_related("project__organization").get(
                project__in=Project.objects.filter(
                    organization__in=organizations),
                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:
            identity = slack_request.get_identity()
        except IdentityProvider.DoesNotExist:
            return self.respond(status=403)

        if not identity:
            associate_url = build_linking_url(
                integration=slack_request.integration,
                slack_id=slack_request.user_id,
                channel_id=slack_request.channel_id,
                response_url=slack_request.response_url,
            )
            return self.respond_ephemeral(
                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 = MessageAction(
                name="status",
                value=data["submission"]["resolve_type"],
            )

            try:
                self.on_status(request, identity, group, action, data,
                               slack_request.integration)
            except client.ApiError as error:
                return self.api_error(slack_request, group, identity, error,
                                      "status_dialog")

            group = Group.objects.get(id=group.id)
            attachment = SlackIssuesMessageBuilder(group,
                                                   identity=identity,
                                                   actions=[action]).build()

            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
        for action in action_list:
            action_type = action.name
            try:
                if action_type == "status":
                    self.on_status(request, identity, group, action, data,
                                   slack_request.integration)
                elif action_type == "assign":
                    self.on_assign(request, identity, group, action)
                elif action_type == "resolve_dialog":
                    self.open_resolve_dialog(data, group,
                                             slack_request.integration)
                    defer_attachment_update = True
            except client.ApiError as error:
                return self.api_error(slack_request, group, identity, error,
                                      action_type)

        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 = SlackIssuesMessageBuilder(group,
                                               identity=identity,
                                               actions=action_list).build()
        body = self.construct_reply(attachment,
                                    is_message=self.is_message(data))

        return self.respond(body)