From 62bab0f73256c3f8ae39145b9fd54ae0a366b551 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aur=C3=A9lien=20Sibiril?= <81782+aureliensibiril@users.noreply.github.com> Date: Thu, 9 Apr 2026 01:02:18 +0200 Subject: [PATCH] fix(connector): relax Slack incoming webhook requirement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ParseSlackTokenResponse treated the incoming_webhook field as mandatory, which blocked any Slack OAuth2 flow that did not request the incoming-webhook scope. Access review Slack connects only ask for users:read and users:read.email and would fail at token parsing. Treat incoming_webhook as optional: populate SlackSettings when it is present, leave them empty otherwise. The existing compliance-page webhook URL is preserved through Reconnect in a later commit. Signed-off-by: Aurélien Sibiril <81782+aureliensibiril@users.noreply.github.com> --- pkg/connector/slack.go | 17 ++++------ pkg/connector/slack_test.go | 67 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 11 deletions(-) create mode 100644 pkg/connector/slack_test.go diff --git a/pkg/connector/slack.go b/pkg/connector/slack.go index 8c71d2658..52b44f96d 100644 --- a/pkg/connector/slack.go +++ b/pkg/connector/slack.go @@ -111,20 +111,15 @@ func (c *SlackConnection) UnmarshalJSON(data []byte) error { func ParseSlackTokenResponse(body []byte, oauth2Conn OAuth2Connection, organizationID gid.GID) (*SlackConnection, *gid.GID, error) { var slackResponse SlackTokenResponse - var buf bytes.Buffer - buf.Write(body) - if err := json.NewDecoder(&buf).Decode(&slackResponse); err != nil { + if err := json.NewDecoder(bytes.NewReader(body)).Decode(&slackResponse); err != nil { return nil, nil, fmt.Errorf("cannot decode Slack token response: %w", err) } - if slackResponse.IncomingWebhook == nil { - return nil, nil, fmt.Errorf("incoming webhook is required for Slack") - } - - settings := SlackSettings{ - WebhookURL: slackResponse.IncomingWebhook.URL, - Channel: slackResponse.IncomingWebhook.Channel, - ChannelID: slackResponse.IncomingWebhook.ChannelID, + settings := SlackSettings{} + if slackResponse.IncomingWebhook != nil { + settings.WebhookURL = slackResponse.IncomingWebhook.URL + settings.Channel = slackResponse.IncomingWebhook.Channel + settings.ChannelID = slackResponse.IncomingWebhook.ChannelID } return &SlackConnection{ diff --git a/pkg/connector/slack_test.go b/pkg/connector/slack_test.go new file mode 100644 index 000000000..681819742 --- /dev/null +++ b/pkg/connector/slack_test.go @@ -0,0 +1,67 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package connector + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.probo.inc/probo/pkg/gid" +) + +func TestParseSlackTokenResponse(t *testing.T) { + t.Parallel() + + orgID := gid.New(gid.NewTenantID(), 0) + base := OAuth2Connection{ + AccessToken: "xoxb-test", + TokenType: "bot", + ExpiresAt: time.Now().Add(time.Hour), + Scope: "chat:write channels:join incoming-webhook", + } + + t.Run("with incoming webhook", func(t *testing.T) { + t.Parallel() + body := []byte(`{"ok":true,"incoming_webhook":{"url":"https://hooks.slack.com/services/T/B/X","channel":"#general","channel_id":"C123"}}`) + + conn, returnedOrgID, err := ParseSlackTokenResponse(body, base, orgID) + require.NoError(t, err) + require.NotNil(t, conn) + require.NotNil(t, returnedOrgID) + + assert.Equal(t, orgID, *returnedOrgID) + assert.Equal(t, "https://hooks.slack.com/services/T/B/X", conn.Settings.WebhookURL) + assert.Equal(t, "#general", conn.Settings.Channel) + assert.Equal(t, "C123", conn.Settings.ChannelID) + assert.Equal(t, "xoxb-test", conn.AccessToken) + }) + + t.Run("without incoming webhook", func(t *testing.T) { + t.Parallel() + body := []byte(`{"ok":true}`) + + conn, returnedOrgID, err := ParseSlackTokenResponse(body, base, orgID) + require.NoError(t, err) + require.NotNil(t, conn) + require.NotNil(t, returnedOrgID) + + assert.Empty(t, conn.Settings.WebhookURL) + assert.Empty(t, conn.Settings.Channel) + assert.Empty(t, conn.Settings.ChannelID) + assert.Equal(t, "xoxb-test", conn.AccessToken) + }) +}