Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions src/sentry/identity/slack/provider.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,13 @@ def get_oauth_access_token_url(self) -> str:
return "https://slack.com/api/oauth.v2.access"

def get_oauth_client_id(self):
if self.config.get("use_staging"):
return options.get("slack-staging.client-id")
return options.get("slack.client-id")

def get_oauth_client_secret(self):
if self.config.get("use_staging"):
return options.get("slack-staging.client-secret")
return options.get("slack.client-secret")

def get_user_scopes(self):
Expand Down
16 changes: 10 additions & 6 deletions src/sentry/integrations/slack/integration.py
Original file line number Diff line number Diff line change
Expand Up @@ -328,15 +328,19 @@ def _get_oauth_scopes(self) -> frozenset[str]:
setup_dialog_config = {"width": 600, "height": 900}

def _identity_pipeline_view(self) -> PipelineView[IntegrationPipeline]:
nested_config: dict[str, Any] = {
"oauth_scopes": self._get_oauth_scopes(),
"user_scopes": self.user_scopes,
"redirect_url": absolute_uri("/extensions/slack/setup/"),
}
if self.config.get("use_staging"):
nested_config["use_staging"] = True

return NestedPipelineView(
bind_key="identity",
provider_key=IntegrationProviderSlug.SLACK.value,
pipeline_cls=IdentityPipeline,
config={
"oauth_scopes": self._get_oauth_scopes(),
"user_scopes": self.user_scopes,
"redirect_url": absolute_uri("/extensions/slack/setup/"),
},
config=nested_config,
)

def get_pipeline_views(self) -> Sequence[PipelineView[IntegrationPipeline]]:
Expand Down Expand Up @@ -377,7 +381,7 @@ def build_integration(self, state: Mapping[str, Any]) -> IntegrationData:
"scopes": sorted(scopes),
"icon": team_data["icon"]["image_132"],
"domain_name": team_data["domain"] + ".slack.com",
"installation_type": "born_as_bot",
"installation_type": "staging" if self.config.get("use_staging") else "born_as_bot",
}

return {
Expand Down
5 changes: 5 additions & 0 deletions src/sentry/integrations/slack/requests/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,11 @@ def authorize(self) -> None:
elif verification_token and self._check_verification_token(verification_token):
return

# If production credentials failed, try the staging app's signing secret.
staging_signing_secret = options.get("slack-staging.signing-secret")
if staging_signing_secret and self._check_signing_secret(staging_signing_secret):
return

# unfortunately, we can't know which auth was supposed to succeed
self._error("slack.action.auth")
raise SlackRequestError(status=status_.HTTP_401_UNAUTHORIZED)
Expand Down
14 changes: 13 additions & 1 deletion src/sentry/integrations/web/organization_integration_setup.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import logging
from typing import Any

import sentry_sdk
from django.http import Http404, HttpRequest
Expand All @@ -24,8 +25,19 @@ def handle(self, request: HttpRequest, organization, provider_id) -> HttpRespons
scope = sentry_sdk.get_current_scope()
scope.set_transaction_name(f"integration.{provider_id}", source=TransactionSource.VIEW)

config: dict[str, Any] = {}
use_staging = request.GET.get("use_staging") == "1" and provider_id == "slack"

if use_staging:
if not features.has("organizations:slack-staging-app", organization):
raise Http404
config["use_staging"] = True

pipeline = IntegrationPipeline(
request=request, organization=organization, provider_key=provider_id
request=request,
organization=organization,
provider_key=provider_id,
config=config,
)

is_feature_enabled = {}
Expand Down
109 changes: 109 additions & 0 deletions tests/sentry/integrations/slack/test_helpers.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
from __future__ import annotations

from collections.abc import Mapping
from typing import Any
from unittest.mock import patch
from urllib.parse import parse_qs, urlencode, urlparse

import responses
from slack_sdk.web import SlackResponse

from sentry.testutils.cases import IntegrationTestCase


def assert_slack_setup_flow(
test_case: IntegrationTestCase,
team_id: str = "TXXXXXXX1",
authorizing_user_id: str = "UXXXXXXX1",
expected_client_id: str = "slack-client-id",
expected_client_secret: str = "slack-client-secret",
customer_domain: str | None = None,
init_params: Mapping[str, str] | None = None,
) -> None:
responses.reset()

extra_kwargs: dict[str, Any] = {}
if customer_domain:
extra_kwargs["HTTP_HOST"] = customer_domain

init_path = test_case.init_path
if init_params:
init_path = f"{init_path}?{urlencode(init_params)}"

resp = test_case.client.get(init_path, **extra_kwargs)
assert resp.status_code == 302
redirect = urlparse(resp["Location"])
assert redirect.scheme == "https"
assert redirect.netloc == "slack.com"
assert redirect.path == "/oauth/v2/authorize"
params = parse_qs(redirect.query)
scopes = test_case.provider.identity_oauth_scopes
assert params["scope"] == [" ".join(scopes)]
assert params["state"]
assert params["redirect_uri"] == ["http://testserver/extensions/slack/setup/"]
assert params["response_type"] == ["code"]
assert params["client_id"] == [expected_client_id]

assert params.get("user_scope") == [" ".join(test_case.provider.user_scopes)]
# once we've asserted on it, switch to singular values to make life easier
authorize_params = {k: v[0] for k, v in params.items()}

access_json = {
"ok": True,
"access_token": "xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx",
"scope": ",".join(sorted(test_case.provider.identity_oauth_scopes)),
"team": {"id": team_id, "name": "Example"},
"authed_user": {"id": authorizing_user_id},
}
responses.add(responses.POST, "https://slack.com/api/oauth.v2.access", json=access_json)

response_json = {
"ok": True,
"members": [
{
"id": authorizing_user_id,
"team_id": team_id,
"deleted": False,
"profile": {
"email": test_case.user.email,
"team": team_id,
},
},
],
"response_metadata": {"next_cursor": ""},
}
with patch(
"slack_sdk.web.client.WebClient.users_list",
return_value=SlackResponse(
client=None,
http_verb="GET",
api_url="https://slack.com/api/users.list",
req_args={},
data=response_json,
headers={},
status_code=200,
),
) as mock_post:
test_case.mock_post = mock_post # type: ignore[attr-defined]
resp = test_case.client.get(
"{}?{}".format(
test_case.setup_path,
urlencode({"code": "oauth-code", "state": authorize_params["state"]}),
)
)

if customer_domain:
assert resp.status_code == 302
assert resp["Location"].startswith(f"http://{customer_domain}/extensions/slack/setup/")
resp = test_case.client.get(resp["Location"], **extra_kwargs)

mock_request = responses.calls[0].request
req_params = parse_qs(mock_request.body)
assert req_params["grant_type"] == ["authorization_code"]
assert req_params["code"] == ["oauth-code"]
assert req_params["redirect_uri"] == ["http://testserver/extensions/slack/setup/"]
assert req_params["client_id"] == [expected_client_id]
assert req_params["client_secret"] == [expected_client_secret]

assert resp.status_code == 200
test_case.assertDialogSuccess(resp)
98 changes: 3 additions & 95 deletions tests/sentry/integrations/slack/test_integration.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
from unittest.mock import MagicMock, patch
from urllib.parse import parse_qs, urlencode, urlparse

import orjson
import pytest
import responses
from responses.matchers import query_string_matcher
from slack_sdk.errors import SlackApiError
from slack_sdk.web import SlackResponse

from sentry import audit_log
from sentry.integrations.models.integration import Integration
Expand All @@ -27,6 +25,7 @@
from sentry.testutils.notifications.platform import MockNotification, MockNotificationTemplate
from sentry.testutils.silo import control_silo_test
from sentry.users.models.identity import Identity, IdentityProvider, IdentityStatus
from tests.sentry.integrations.slack.test_helpers import assert_slack_setup_flow


@control_silo_test
Expand All @@ -52,99 +51,8 @@ class SlackIntegrationTest(IntegrationTestCase):
def setUp(self) -> None:
super().setUp()

def assert_setup_flow(
self,
team_id="TXXXXXXX1",
authorizing_user_id="UXXXXXXX1",
expected_client_id="slack-client-id",
expected_client_secret="slack-client-secret",
customer_domain=None,
):
responses.reset()

kwargs = {}
if customer_domain:
kwargs["HTTP_HOST"] = customer_domain

resp = self.client.get(self.init_path, **kwargs)
assert resp.status_code == 302
redirect = urlparse(resp["Location"])
assert redirect.scheme == "https"
assert redirect.netloc == "slack.com"
assert redirect.path == "/oauth/v2/authorize"
params = parse_qs(redirect.query)
scopes = self.provider.identity_oauth_scopes
assert params["scope"] == [" ".join(scopes)]
assert params["state"]
assert params["redirect_uri"] == ["http://testserver/extensions/slack/setup/"]
assert params["response_type"] == ["code"]
assert params["client_id"] == [expected_client_id]

assert params.get("user_scope") == [" ".join(self.provider.user_scopes)]
# once we've asserted on it, switch to a singular values to make life
# easier
authorize_params = {k: v[0] for k, v in params.items()}

access_json = {
"ok": True,
"access_token": "xoxb-xxxxxxxxx-xxxxxxxxxx-xxxxxxxxxxxx",
"scope": ",".join(sorted(self.provider.identity_oauth_scopes)),
"team": {"id": team_id, "name": "Example"},
"authed_user": {"id": authorizing_user_id},
}
responses.add(responses.POST, "https://slack.com/api/oauth.v2.access", json=access_json)

response_json = {
"ok": True,
"members": [
{
"id": authorizing_user_id,
"team_id": team_id,
"deleted": False,
"profile": {
"email": self.user.email,
"team": team_id,
},
},
],
"response_metadata": {"next_cursor": ""},
}
with patch(
"slack_sdk.web.client.WebClient.users_list",
return_value=SlackResponse(
client=None,
http_verb="GET",
api_url="https://slack.com/api/users.list",
req_args={},
data=response_json,
headers={},
status_code=200,
),
) as self.mock_post:
resp = self.client.get(
"{}?{}".format(
self.setup_path,
urlencode({"code": "oauth-code", "state": authorize_params["state"]}),
)
)

if customer_domain:
assert resp.status_code == 302
assert resp["Location"].startswith(
f"http://{customer_domain}/extensions/slack/setup/"
)
resp = self.client.get(resp["Location"], **kwargs)

mock_request = responses.calls[0].request
req_params = parse_qs(mock_request.body)
assert req_params["grant_type"] == ["authorization_code"]
assert req_params["code"] == ["oauth-code"]
assert req_params["redirect_uri"] == ["http://testserver/extensions/slack/setup/"]
assert req_params["client_id"] == [expected_client_id]
assert req_params["client_secret"] == [expected_client_secret]

assert resp.status_code == 200
self.assertDialogSuccess(resp)
def assert_setup_flow(self, **kwargs):
assert_slack_setup_flow(self, **kwargs)

@responses.activate
def test_bot_flow(self, mock_api_call: MagicMock) -> None:
Expand Down
Loading
Loading