From 87d79822502b5da8ad982da92050a38c74bbc077 Mon Sep 17 00:00:00 2001 From: Vadim Stepanov Date: Fri, 19 Jul 2024 12:53:06 +0100 Subject: [PATCH] Unified Slack app reinstall (#4682) # What this PR does Adds a button to reinstall the Slack app to migrate to Unified Slack App. Also adds backend support for this. Screenshot 2024-07-18 at 18 33 08 ## Which issue(s) this PR closes Related to https://github.com/grafana/oncall-gateway/issues/252 ## Checklist - [x] Unit, integration, and e2e (if applicable) tests updated - [x] Documentation added (or `pr:no public docs` PR label added if not required) - [x] Added the relevant release notes label (see labels prefixed w/ `release:`). These labels dictate how your PR will show up in the autogenerated release notes. --- engine/apps/api/serializers/organization.py | 2 +- engine/apps/slack/installation.py | 6 ++---- ...amidentity__unified_slack_app_installed.py | 18 +++++++++++++++++ .../apps/slack/models/slack_team_identity.py | 12 +++++++++++ engine/apps/slack/tests/test_installation.py | 20 +++++++++++++++++++ .../models/organization/organization.types.ts | 3 +-- .../tabs/SlackSettings/SlackSettings.tsx | 14 +++++++++++++ 7 files changed, 68 insertions(+), 7 deletions(-) create mode 100644 engine/apps/slack/migrations/0005_slackteamidentity__unified_slack_app_installed.py diff --git a/engine/apps/api/serializers/organization.py b/engine/apps/api/serializers/organization.py index 6279c0a1..cac0edd1 100644 --- a/engine/apps/api/serializers/organization.py +++ b/engine/apps/api/serializers/organization.py @@ -13,7 +13,7 @@ from common.api_helpers.mixins import EagerLoadingMixin class FastSlackTeamIdentitySerializer(serializers.ModelSerializer): class Meta: model = SlackTeamIdentity - fields = ["cached_name"] + fields = ["cached_name", "needs_reinstall"] class OrganizationSerializer(EagerLoadingMixin, serializers.ModelSerializer): diff --git a/engine/apps/slack/installation.py b/engine/apps/slack/installation.py index a48784bc..393a2a9a 100644 --- a/engine/apps/slack/installation.py +++ b/engine/apps/slack/installation.py @@ -32,13 +32,11 @@ def install_slack_integration(organization, user, oauth_response): """ from apps.slack.models import SlackTeamIdentity - if organization.slack_team_identity is not None: + if organization.slack_team_identity and not organization.slack_team_identity.needs_reinstall: raise SlackInstallationExc("Organization already has Slack integration") slack_team_id = oauth_response["team"]["id"] - slack_team_identity, is_slack_team_identity_created = SlackTeamIdentity.objects.get_or_create( - slack_id=slack_team_id, - ) + slack_team_identity, _ = SlackTeamIdentity.objects.get_or_create(slack_id=slack_team_id) # update slack oauth fields by data from response slack_team_identity.update_oauth_fields(user, organization, oauth_response) write_chatops_insight_log( diff --git a/engine/apps/slack/migrations/0005_slackteamidentity__unified_slack_app_installed.py b/engine/apps/slack/migrations/0005_slackteamidentity__unified_slack_app_installed.py new file mode 100644 index 00000000..f4798876 --- /dev/null +++ b/engine/apps/slack/migrations/0005_slackteamidentity__unified_slack_app_installed.py @@ -0,0 +1,18 @@ +# Generated by Django 4.2.11 on 2024-07-16 16:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('slack', '0004_auto_20230913_1020'), + ] + + operations = [ + migrations.AddField( + model_name='slackteamidentity', + name='_unified_slack_app_installed', + field=models.BooleanField(default=False, null=True), + ), + ] diff --git a/engine/apps/slack/models/slack_team_identity.py b/engine/apps/slack/models/slack_team_identity.py index a5e24c12..79e603a7 100644 --- a/engine/apps/slack/models/slack_team_identity.py +++ b/engine/apps/slack/models/slack_team_identity.py @@ -1,6 +1,7 @@ import logging import typing +from django.conf import settings from django.db import models from django.db.models import JSONField @@ -48,6 +49,9 @@ class SlackTeamIdentity(models.Model): # response after oauth.access. This field is used to reinstall app to another OnCall workspace cached_reinstall_data = JSONField(null=True, default=None) + # Do not use directly, use the "needs_reinstall" property instead + _unified_slack_app_installed = models.BooleanField(null=True, default=False) + class Meta: ordering = ("datetime",) @@ -74,6 +78,10 @@ class SlackTeamIdentity(models.Model): self.installed_by = slack_user_identity self.cached_reinstall_data = None self.installed_via_granular_permissions = True + + if settings.UNIFIED_SLACK_APP_ENABLED: + self._unified_slack_app_installed = True + self.save() def get_cached_channels(self, search_term=None, slack_id=None): @@ -129,6 +137,10 @@ class SlackTeamIdentity(models.Model): self.save(update_fields=["cached_app_id"]) return self.cached_app_id + @property + def needs_reinstall(self): + return settings.UNIFIED_SLACK_APP_ENABLED and not self._unified_slack_app_installed + def get_users_from_slack_conversation_for_organization(self, channel_id, organization): sc = SlackClient(self) members = self.get_conversation_members(sc, channel_id) diff --git a/engine/apps/slack/tests/test_installation.py b/engine/apps/slack/tests/test_installation.py index 7075c276..23f313e1 100644 --- a/engine/apps/slack/tests/test_installation.py +++ b/engine/apps/slack/tests/test_installation.py @@ -95,6 +95,26 @@ def test_install_slack_integration_raises_exception_for_existing_integration( install_slack_integration(organization, user, SLACK_OAUTH_ACCESS_RESPONSE) +@pytest.mark.django_db +def test_install_slack_integration_legacy(settings, make_organization_and_user, make_slack_team_identity): + settings.UNIFIED_SLACK_APP_ENABLED = True + + slack_team_identity = make_slack_team_identity( + slack_id=SLACK_OAUTH_ACCESS_RESPONSE["team"]["id"], _unified_slack_app_installed=False + ) + organization, user = make_organization_and_user() + organization.slack_team_identity = slack_team_identity + organization.save() + + install_slack_integration(organization, user, SLACK_OAUTH_ACCESS_RESPONSE) + slack_team_identity.refresh_from_db() + assert slack_team_identity.needs_reinstall is False + + # raises exception if organization already re-installed the app + with pytest.raises(SlackInstallationExc): + install_slack_integration(organization, user, SLACK_OAUTH_ACCESS_RESPONSE) + + @patch("apps.slack.tasks.clean_slack_integration_leftovers.apply_async", return_value=None) @pytest.mark.django_db def test_uninstall_slack_integration( diff --git a/grafana-plugin/src/models/organization/organization.types.ts b/grafana-plugin/src/models/organization/organization.types.ts index 889c461f..2c01dc4b 100644 --- a/grafana-plugin/src/models/organization/organization.types.ts +++ b/grafana-plugin/src/models/organization/organization.types.ts @@ -15,9 +15,8 @@ export interface Organization { name: string; stack_slug: string; slack_team_identity: { - general_log_channel_id: string; - general_log_channel_pk: string; cached_name: string; + needs_reinstall: boolean; }; slack_channel: SlackChannel | null; is_resolution_note_required: boolean; diff --git a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx index 1933359f..278a1eab 100644 --- a/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx +++ b/grafana-plugin/src/pages/settings/tabs/ChatOps/tabs/SlackSettings/SlackSettings.tsx @@ -201,6 +201,20 @@ class _SlackSettings extends Component { + {currentOrganization.slack_team_identity.needs_reinstall && ( + <> + Unified Slack App + + + + + + + )} ); };