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.

<img width="1204" alt="Screenshot 2024-07-18 at 18 33 08"
src="https://github.com/user-attachments/assets/a326b4a2-fc65-4b88-98c0-2955e3717e3a">


## Which issue(s) this PR closes

Related to https://github.com/grafana/oncall-gateway/issues/252

<!--
*Note*: if you have more than one GitHub issue that this PR closes, be
sure to preface
each issue link with a [closing
keyword](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/using-keywords-in-issues-and-pull-requests#linking-a-pull-request-to-an-issue).
This ensures that the issue(s) are auto-closed once the PR has been
merged.
-->

## 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.
This commit is contained in:
Vadim Stepanov 2024-07-19 12:53:06 +01:00 committed by GitHub
parent 1465db36e5
commit 87d7982250
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 68 additions and 7 deletions

View file

@ -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):

View file

@ -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(

View file

@ -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),
),
]

View file

@ -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)

View file

@ -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(

View file

@ -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;

View file

@ -201,6 +201,20 @@ class _SlackSettings extends Component<SlackProps, SlackState> {
</WithPermissionControlTooltip>
</HorizontalGroup>
</InlineField>
{currentOrganization.slack_team_identity.needs_reinstall && (
<>
<Legend>Unified Slack App</Legend>
<InlineField>
<WithPermissionControlTooltip userAction={UserActions.ChatOpsUpdateSettings}>
<Button onClick={this.handleOpenSlackInstructions}>
<HorizontalGroup spacing="xs" align="center">
<Icon name="external-link-alt" className={cx('external-link-style')} /> Reinstall Slack App
</HorizontalGroup>
</Button>
</WithPermissionControlTooltip>
</InlineField>
</>
)}
</div>
);
};