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 __future__ import absolute_import
 
 
+from sentry import features
+
 from sentry.api.bases.organization import OrganizationEndpoint, OrganizationIntegrationsPermission
 from sentry.api.bases.organization import OrganizationEndpoint, OrganizationIntegrationsPermission
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.paginator import OffsetPaginator
 from sentry.api.serializers import serialize
 from sentry.api.serializers import serialize
 from sentry.models import ObjectStatus, OrganizationIntegration
 from sentry.models import ObjectStatus, OrganizationIntegration
+from sentry.integrations.slack.utils import get_integration_type
+from sentry.utils.compat import filter
 
 
 
 
 class OrganizationIntegrationsEndpoint(OrganizationEndpoint):
 class OrganizationIntegrationsEndpoint(OrganizationEndpoint):
@@ -17,6 +21,20 @@ class OrganizationIntegrationsEndpoint(OrganizationEndpoint):
         if "provider_key" in request.GET:
         if "provider_key" in request.GET:
             integrations = integrations.filter(integration__provider=request.GET["provider_key"])
             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 the configurations by default if no param
         include_config = True
         include_config = True
         if request.GET.get("includeConfig") == "0":
         if request.GET.get("includeConfig") == "0":

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

@@ -883,6 +883,9 @@ SENTRY_FEATURES = {
     "organizations:integrations-stacktrace-link": False,
     "organizations:integrations-stacktrace-link": False,
     # Enables aws lambda integration
     # Enables aws lambda integration
     "organizations:integrations-aws_lambda": False,
     "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.
     # Enable data forwarding functionality for organizations.
     "organizations:data-forwarding": True,
     "organizations:data-forwarding": True,
     # Enable custom dashboards (dashboards 2)
     # 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-vsts-limited-scopes", OrganizationFeature)  # NOQA
 default_manager.add("organizations:integrations-stacktrace-link", OrganizationFeature)  # NOQA
 default_manager.add("organizations:integrations-stacktrace-link", OrganizationFeature)  # NOQA
 default_manager.add("organizations:integrations-aws_lambda", 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:internal-catchall", OrganizationFeature)  # NOQA
 default_manager.add("organizations:invite-members", OrganizationFeature)  # NOQA
 default_manager.add("organizations:invite-members", OrganizationFeature)  # NOQA
 default_manager.add("organizations:images-loaded-v2", 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):
 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):
     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.compat import zip
 from sentry.utils.dates import to_timestamp
 from sentry.utils.dates import to_timestamp
 from sentry.utils.snuba import bulk_raw_query, is_measurement, SnubaQueryParams, SnubaTSResult
 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
 # 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
 # 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(
         _prefix, channel_id, timed_out = get_channel_id(
             organization, integration, name, use_async_lookup
             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:
     except DuplicateDisplayNameError as e:
         domain = integration.metadata["domain_name"]
         domain = integration.metadata["domain_name"]
 
 

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

@@ -238,12 +238,12 @@ class SlackIntegrationProvider(IntegrationProvider):
 
 
 class SlackReAuthIntro(PipelineView):
 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):
 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 import forms
 from django.utils.translation import ugettext_lazy as _
 from django.utils.translation import ugettext_lazy as _
 
 
+from sentry import features
+
 from sentry.models import Integration
 from sentry.models import Integration
 from sentry.rules.actions.base import IntegrationEventAction
 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 sentry.utils import metrics, json
 
 
 from .client import SlackClient
 from .client import SlackClient
@@ -83,6 +89,16 @@ class SlackNotifyServiceForm(forms.Form):
                 channel_prefix, channel_id, timed_out = self.channel_transformer(
                 channel_prefix, channel_id, timed_out = self.channel_transformer(
                     integration, channel
                     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:
             except DuplicateDisplayNameError:
                 domain = integration.metadata["domain_name"]
                 domain = integration.metadata["domain_name"]
 
 
@@ -151,6 +167,13 @@ class SlackNotifyServiceAction(IntegrationEventAction):
             # Integration removed, rule still active.
             # Integration removed, rule still active.
             return
             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):
         def send_notification(event, futures):
             with sentry_sdk.start_transaction(
             with sentry_sdk.start_transaction(
                 op=u"slack.send_notification", name=u"SlackSendNotification", sampled=1.0
                 op=u"slack.send_notification", name=u"SlackSendNotification", sampled=1.0
@@ -159,7 +182,8 @@ class SlackNotifyServiceAction(IntegrationEventAction):
                 attachments = [
                 attachments = [
                     build_group_attachment(event.group, event=event, tags=tags, rules=rules)
                     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)
                 integration_type = get_integration_type(integration)
                 if integration_type == "workspace_app":
                 if integration_type == "workspace_app":
                     # stick the upgrade attachment first
                     # 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.core.cache import cache
 from django.http import Http404
 from django.http import Http404
 
 
-from sentry import tagstore
+from sentry import tagstore, features
 from sentry.api.fields.actor import Actor
 from sentry.api.fields.actor import Actor
 from sentry.utils import json
 from sentry.utils import json
 from sentry.utils.assets import get_asset_url
 from sentry.utils.assets import get_asset_url
@@ -26,7 +26,11 @@ from sentry.models import (
     Team,
     Team,
     ReleaseProject,
     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 sentry.integrations.metric_alerts import incident_attachment_info
 
 
 from .client import SlackClient
 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,
     # 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
     # 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
         list_types = LEGACY_LIST_TYPES
     else:
     else:
         list_types = LIST_TYPES
         list_types = LIST_TYPES
@@ -483,6 +500,11 @@ def send_incident_alert_notification(action, incident, metric_value):
         "attachments": json.dumps([attachment]),
         "attachments": json.dumps([attachment]),
     }
     }
 
 
+    if get_integration_type(integration) == "workspace_app" and not features.has(
+        "organizations:slack-allow-workspace", incident.organization
+    ):
+        return
+
     client = SlackClient()
     client = SlackClient()
     try:
     try:
         client.post("/chat.postMessage", data=payload, timeout=5)
         client.post("/chat.postMessage", data=payload, timeout=5)

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

@@ -89,6 +89,10 @@ class DuplicateDisplayNameError(IntegrationError):
     pass
     pass
 
 
 
 
+class DeprecatedIntegrationError(IntegrationError):
+    pass
+
+
 class IntegrationFormError(IntegrationError):
 class IntegrationFormError(IntegrationError):
     def __init__(self, field_errors):
     def __init__(self, field_errors):
         super(IntegrationFormError, self).__init__("Invalid integration action")
         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",
             provider="slack",
             external_id="some_slack",
             external_id="some_slack",
             name="Test 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)
         model.add_organization(self.organization, self.user)

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