Browse Source

ref(slack): Feature gate workspace app functionality (#22926)

* ref(slack): Feature gate  workspace app functionality
MeredithAnya 4 years ago
parent
commit
037e9f1ab4

+ 18 - 0
src/sentry/api/endpoints/organization_integrations.py

@@ -1,9 +1,13 @@
 from __future__ import absolute_import
 
+from sentry import features
+
 from sentry.api.bases.organization import OrganizationEndpoint, OrganizationIntegrationsPermission
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.serializers import serialize
 from sentry.models import ObjectStatus, OrganizationIntegration
+from sentry.integrations.slack.utils import get_integration_type
+from sentry.utils.compat import filter
 
 
 class OrganizationIntegrationsEndpoint(OrganizationEndpoint):
@@ -17,6 +21,20 @@ class OrganizationIntegrationsEndpoint(OrganizationEndpoint):
         if "provider_key" in request.GET:
             integrations = integrations.filter(integration__provider=request.GET["provider_key"])
 
+        # XXX(meredith): Filter out workspace apps if there are any.
+        if not features.has(
+            "organizations:slack-allow-workspace", organization=organization, actor=request.user
+        ):
+            slack_integrations = integrations.filter(integration__provider="slack")
+            workspace_ids = [
+                workspace_app.id
+                for workspace_app in filter(
+                    lambda i: get_integration_type(i.integration) == "workspace_app",
+                    slack_integrations,
+                )
+            ]
+            integrations = integrations.exclude(id__in=workspace_ids)
+
         # include the configurations by default if no param
         include_config = True
         if request.GET.get("includeConfig") == "0":

+ 3 - 0
src/sentry/conf/server.py

@@ -883,6 +883,9 @@ SENTRY_FEATURES = {
     "organizations:integrations-stacktrace-link": False,
     # Enables aws lambda integration
     "organizations:integrations-aws_lambda": False,
+    # Temporary safety measure, turned on for specific orgs only if
+    # absolutely necessary, to be removed shortly
+    "organizations:slack-allow-workspace": False,
     # Enable data forwarding functionality for organizations.
     "organizations:data-forwarding": True,
     # Enable custom dashboards (dashboards 2)

+ 1 - 0
src/sentry/features/__init__.py

@@ -83,6 +83,7 @@ default_manager.add("organizations:integrations-ticket-rules", OrganizationFeatu
 default_manager.add("organizations:integrations-vsts-limited-scopes", OrganizationFeature)  # NOQA
 default_manager.add("organizations:integrations-stacktrace-link", OrganizationFeature)  # NOQA
 default_manager.add("organizations:integrations-aws_lambda", OrganizationFeature)  # NOQA
+default_manager.add("organizations:slack-allow-workspace", OrganizationFeature)  # NOQA
 default_manager.add("organizations:internal-catchall", OrganizationFeature)  # NOQA
 default_manager.add("organizations:invite-members", OrganizationFeature)  # NOQA
 default_manager.add("organizations:images-loaded-v2", OrganizationFeature)  # NOQA

+ 4 - 4
src/sentry/features/manager.py

@@ -13,11 +13,11 @@ from .exceptions import FeatureNotRegistered
 
 class RegisteredFeatureManager(object):
     """
-        Feature functions that are built around the need to register feature
-        handlers
+    Feature functions that are built around the need to register feature
+    handlers
 
-        TODO: Once features have been audited and migrated to the entity
-        handler, remove this class entirely
+    TODO: Once features have been audited and migrated to the entity
+    handler, remove this class entirely
     """
 
     def __init__(self):

+ 11 - 1
src/sentry/incidents/logic.py

@@ -51,7 +51,10 @@ from sentry.snuba.tasks import build_snuba_filter
 from sentry.utils.compat import zip
 from sentry.utils.dates import to_timestamp
 from sentry.utils.snuba import bulk_raw_query, is_measurement, SnubaQueryParams, SnubaTSResult
-from sentry.shared_integrations.exceptions import DuplicateDisplayNameError
+from sentry.shared_integrations.exceptions import (
+    DuplicateDisplayNameError,
+    DeprecatedIntegrationError,
+)
 
 # We can return an incident as "windowed" which returns a range of points around the start of the incident
 # It attempts to center the start of the incident, only showing earlier data if there isn't enough time
@@ -1262,6 +1265,13 @@ def get_alert_rule_trigger_action_slack_channel_id(
         _prefix, channel_id, timed_out = get_channel_id(
             organization, integration, name, use_async_lookup
         )
+
+    # XXX(meredith): Will be removed when we rip out workspace app support completely.
+    except DeprecatedIntegrationError:
+        raise InvalidTriggerActionError(
+            "This workspace is using the deprecated Slack integration. Please re-install your integration to enable Slack alerting again."
+        )
+
     except DuplicateDisplayNameError as e:
         domain = integration.metadata["domain_name"]
 

+ 12 - 12
src/sentry/integrations/slack/integration.py

@@ -238,12 +238,12 @@ class SlackIntegrationProvider(IntegrationProvider):
 
 class SlackReAuthIntro(PipelineView):
     """
-        This pipeline step handles rendering the migration
-        intro with context about the migration.
+    This pipeline step handles rendering the migration
+    intro with context about the migration.
 
-        If the `integration_id` is not present in the request
-        then we can fast forward through the pipeline to move
-        on to installing the integration as normal.
+    If the `integration_id` is not present in the request
+    then we can fast forward through the pipeline to move
+    on to installing the integration as normal.
 
     """
 
@@ -296,15 +296,15 @@ class SlackReAuthIntro(PipelineView):
 
 class SlackReAuthChannels(PipelineView):
     """
-        This pipeline step handles making requests to Slack and
-        displaying the channels (if any) that are problematic:
+    This pipeline step handles making requests to Slack and
+    displaying the channels (if any) that are problematic:
 
-        1. private
-        2. removed
-        3. unauthorized
+    1. private
+    2. removed
+    3. unauthorized
 
-        Any private channels in alert rules will also be binded
-        to the pipeline state to be used later.
+    Any private channels in alert rules will also be binded
+    to the pipeline state to be used later.
 
     """
 

+ 26 - 2
src/sentry/integrations/slack/notify_action.py

@@ -8,9 +8,15 @@ import sentry_sdk
 from django import forms
 from django.utils.translation import ugettext_lazy as _
 
+from sentry import features
+
 from sentry.models import Integration
 from sentry.rules.actions.base import IntegrationEventAction
-from sentry.shared_integrations.exceptions import ApiError, DuplicateDisplayNameError
+from sentry.shared_integrations.exceptions import (
+    ApiError,
+    DeprecatedIntegrationError,
+    DuplicateDisplayNameError,
+)
 from sentry.utils import metrics, json
 
 from .client import SlackClient
@@ -83,6 +89,16 @@ class SlackNotifyServiceForm(forms.Form):
                 channel_prefix, channel_id, timed_out = self.channel_transformer(
                     integration, channel
                 )
+            except DeprecatedIntegrationError:
+                raise forms.ValidationError(
+                    _(
+                        'Workspace "%(workspace)s" is using the deprecated Slack integration. Please re-install your integration to enable Slack alerting again.',
+                    ),
+                    code="invalid",
+                    params={
+                        "workspace": dict(self.fields["workspace"].choices).get(int(workspace)),
+                    },
+                )
             except DuplicateDisplayNameError:
                 domain = integration.metadata["domain_name"]
 
@@ -151,6 +167,13 @@ class SlackNotifyServiceAction(IntegrationEventAction):
             # Integration removed, rule still active.
             return
 
+        # XXX:(meredith) No longer support sending workspace app notifications unless explicitly
+        # flagged in. Flag is temporary and will be taken out shortly
+        if get_integration_type(integration) == "workspace_app" and not features.has(
+            "organizations:slack-allow-workspace", event.group.project.organization
+        ):
+            return
+
         def send_notification(event, futures):
             with sentry_sdk.start_transaction(
                 op=u"slack.send_notification", name=u"SlackSendNotification", sampled=1.0
@@ -159,7 +182,8 @@ class SlackNotifyServiceAction(IntegrationEventAction):
                 attachments = [
                     build_group_attachment(event.group, event=event, tags=tags, rules=rules)
                 ]
-                # check if we should have the upgrade notice attachment
+                # XXX(meredith): Needs to be ripped out along with above feature flag. This will
+                # not be used unless someone was explicitly flagged in to continue workspace alerts
                 integration_type = get_integration_type(integration)
                 if integration_type == "workspace_app":
                     # stick the upgrade attachment first

+ 25 - 3
src/sentry/integrations/slack/utils.py

@@ -7,7 +7,7 @@ import six
 from django.core.cache import cache
 from django.http import Http404
 
-from sentry import tagstore
+from sentry import tagstore, features
 from sentry.api.fields.actor import Actor
 from sentry.utils import json
 from sentry.utils.assets import get_asset_url
@@ -26,7 +26,11 @@ from sentry.models import (
     Team,
     ReleaseProject,
 )
-from sentry.shared_integrations.exceptions import ApiError, DuplicateDisplayNameError
+from sentry.shared_integrations.exceptions import (
+    ApiError,
+    DuplicateDisplayNameError,
+    DeprecatedIntegrationError,
+)
 from sentry.integrations.metric_alerts import incident_attachment_info
 
 from .client import SlackClient
@@ -419,7 +423,20 @@ def get_channel_id_with_timeout(integration, name, timeout):
 
     # workspace tokens are the only tokens that don't works with the conversations.list endpoint,
     # once eveyone is migrated we can remove this check and usages of channels.list
-    if get_integration_type(integration) == "workspace_app":
+
+    # XXX(meredith): Prevent anyone from creating new rules or editing existing rules that
+    # have workspace app integrations. Force them to either remove slack action or re-install
+    # their integration.
+    integration_type = get_integration_type(integration)
+    if integration_type == "workspace_app" and not any(
+        [
+            features.has("organizations:slack-allow-workspace", org)
+            for org in integration.organizations.all()
+        ]
+    ):
+        raise DeprecatedIntegrationError
+
+    if integration_type == "workspace_app":
         list_types = LEGACY_LIST_TYPES
     else:
         list_types = LIST_TYPES
@@ -483,6 +500,11 @@ def send_incident_alert_notification(action, incident, metric_value):
         "attachments": json.dumps([attachment]),
     }
 
+    if get_integration_type(integration) == "workspace_app" and not features.has(
+        "organizations:slack-allow-workspace", incident.organization
+    ):
+        return
+
     client = SlackClient()
     try:
         client.post("/chat.postMessage", data=payload, timeout=5)

+ 4 - 0
src/sentry/shared_integrations/exceptions.py

@@ -89,6 +89,10 @@ class DuplicateDisplayNameError(IntegrationError):
     pass
 
 
+class DeprecatedIntegrationError(IntegrationError):
+    pass
+
+
 class IntegrationFormError(IntegrationError):
     def __init__(self, field_errors):
         super(IntegrationFormError, self).__init__("Invalid integration action")

+ 1 - 1
tests/acceptance/test_organization_integration_detail_view.py

@@ -60,7 +60,7 @@ class OrganizationIntegrationDetailView(AcceptanceTestCase):
             provider="slack",
             external_id="some_slack",
             name="Test Slack",
-            metadata={"domain_name": "slack-test.slack.com"},
+            metadata={"domain_name": "slack-test.slack.com", "installation_type": "born_as_bot"},
         )
 
         model.add_organization(self.organization, self.user)

Some files were not shown because too many files changed in this diff