diff --git a/slack_bolt/request/internals.py b/slack_bolt/request/internals.py index c4101699d..4e4342935 100644 --- a/slack_bolt/request/internals.py +++ b/slack_bolt/request/internals.py @@ -73,6 +73,12 @@ def extract_enterprise_id(payload: Dict[str, Any]) -> Optional[str]: def extract_team_id(payload: Dict[str, Any]) -> Optional[str]: + if payload.get("view", {}).get("app_installed_team_id") is not None: + # view_submission payloads can have `view.app_installed_team_id` when a modal view that was opened + # in a different workspace via some operations inside a Slack Connect channel. + # Note that the same for enterprise_id does not exist. When you need to know the enterprise_id as well, + # you have to run some query toward your InstallationStore to know the org where the team_id belongs to. + return payload.get("view")["app_installed_team_id"] if payload.get("team") is not None: # With org-wide installations, payload.team in interactivity payloads can be None # You need to extract either payload.user.team_id or payload.view.team_id as below diff --git a/tests/scenario_tests/test_view_submission.py b/tests/scenario_tests/test_view_submission.py index 9290cd27f..b0eb58212 100644 --- a/tests/scenario_tests/test_view_submission.py +++ b/tests/scenario_tests/test_view_submission.py @@ -5,7 +5,7 @@ from slack_sdk import WebClient from slack_sdk.signature import SignatureVerifier -from slack_bolt import BoltRequest +from slack_bolt import BoltRequest, BoltContext from slack_bolt.app import App from tests.mock_web_api_server import ( setup_mock_web_api_server, @@ -140,6 +140,58 @@ def simple_listener(ack, body, payload, view): raw_response_url_body = f"payload={quote(json.dumps(response_url_payload_body))}" +connect_channel_payload = { + "type": "view_submission", + "team": { + "id": "T-other-side", + "domain": "other-side", + "enterprise_id": "E-other-side", + "enterprise_name": "Kaz Sandbox Org", + }, + "user": {"id": "W111", "username": "kaz", "name": "kaz", "team_id": "T-other-side"}, + "api_app_id": "A1111", + "token": "legacy-fixed-token", + "trigger_id": "111.222.xxx", + "view": { + "id": "V11111", + "team_id": "T-other-side", + "type": "modal", + "blocks": [ + { + "type": "input", + "block_id": "zniAM", + "label": {"type": "plain_text", "text": "Label"}, + "element": { + "type": "plain_text_input", + "dispatch_action_config": {"trigger_actions_on": ["on_enter_pressed"]}, + "action_id": "qEJr", + }, + } + ], + "private_metadata": "", + "callback_id": "view-id", + "state": {"values": {"zniAM": {"qEJr": {"type": "plain_text_input", "value": "Hi there!"}}}}, + "hash": "1664950703.CmTS8F7U", + "title": {"type": "plain_text", "text": "My App"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "root_view_id": "V00000", + "app_id": "A1111", + "external_id": "", + "app_installed_team_id": "T-installed-workspace", + "bot_id": "B1111", + }, + "enterprise": {"id": "E-other-side", "name": "Kaz Sandbox Org"}, +} + +connect_channel_body = f"payload={quote(json.dumps(connect_channel_payload))}" + + +def verify_connected_channel(ack, context: BoltContext): + assert context.team_id == "T-installed-workspace" + ack() + + class TestViewSubmission: signing_secret = "secret" valid_token = "xoxb-valid" @@ -261,3 +313,15 @@ def check(ack, respond): response = app.dispatch(request) assert response.status == 200 assert_auth_test_count(self, 1) + + def test_connected_channels(self): + app = App( + client=self.web_client, + signing_secret=self.signing_secret, + ) + app.view("view-id")(verify_connected_channel) + + request = self.build_valid_request(body=connect_channel_body) + response = app.dispatch(request) + assert response.status == 200 + assert_auth_test_count(self, 1) diff --git a/tests/scenario_tests_async/test_view_submission.py b/tests/scenario_tests_async/test_view_submission.py index af75da750..21ddd3220 100644 --- a/tests/scenario_tests_async/test_view_submission.py +++ b/tests/scenario_tests_async/test_view_submission.py @@ -142,6 +142,53 @@ def simple_listener(ack, body, payload, view): raw_response_url_body = f"payload={quote(json.dumps(response_url_payload_body))}" +connect_channel_payload = { + "type": "view_submission", + "team": { + "id": "T-other-side", + "domain": "other-side", + "enterprise_id": "E-other-side", + "enterprise_name": "Kaz Sandbox Org", + }, + "user": {"id": "W111", "username": "kaz", "name": "kaz", "team_id": "T-other-side"}, + "api_app_id": "A1111", + "token": "legacy-fixed-token", + "trigger_id": "111.222.xxx", + "view": { + "id": "V11111", + "team_id": "T-other-side", + "type": "modal", + "blocks": [ + { + "type": "input", + "block_id": "zniAM", + "label": {"type": "plain_text", "text": "Label"}, + "element": { + "type": "plain_text_input", + "dispatch_action_config": {"trigger_actions_on": ["on_enter_pressed"]}, + "action_id": "qEJr", + }, + } + ], + "private_metadata": "", + "callback_id": "view-id", + "state": {"values": {"zniAM": {"qEJr": {"type": "plain_text_input", "value": "Hi there!"}}}}, + "hash": "1664950703.CmTS8F7U", + "title": {"type": "plain_text", "text": "My App"}, + "close": {"type": "plain_text", "text": "Cancel"}, + "submit": {"type": "plain_text", "text": "Submit"}, + "root_view_id": "V00000", + "app_id": "A1111", + "external_id": "", + "app_installed_team_id": "T-installed-workspace", + "bot_id": "B1111", + }, + "enterprise": {"id": "E-other-side", "name": "Kaz Sandbox Org"}, +} + +connect_channel_body = f"payload={quote(json.dumps(connect_channel_payload))}" + + class TestAsyncViewSubmission: signing_secret = "secret" valid_token = "xoxb-valid" @@ -275,6 +322,19 @@ async def check(ack, respond): assert response.status == 200 await assert_auth_test_count_async(self, 1) + @pytest.mark.asyncio + async def test_connected_channels(self): + app = AsyncApp( + client=self.web_client, + signing_secret=self.signing_secret, + ) + app.view("view-id")(verify_connected_channel) + + request = self.build_valid_request(body=connect_channel_body) + response = await app.async_dispatch(request) + assert response.status == 200 + await assert_auth_test_count_async(self, 1) + async def simple_listener(ack, body, payload, view): assert body["trigger_id"] == "111.222.valid" @@ -282,3 +342,8 @@ async def simple_listener(ack, body, payload, view): assert payload == view assert view["private_metadata"] == "This is for you!" await ack() + + +async def verify_connected_channel(ack, context): + assert context.team_id == "T-installed-workspace" + await ack() diff --git a/tests/slack_bolt/request/test_request.py b/tests/slack_bolt/request/test_request.py index ac1877d3e..e091f9a21 100644 --- a/tests/slack_bolt/request/test_request.py +++ b/tests/slack_bolt/request/test_request.py @@ -84,14 +84,14 @@ def test_org_wide_installations_view_submission(self): "id": "W111", "username": "primary-owner", "name": "primary-owner", - "team_id": "T_expected" + "team_id": "T_unexpected" }, "api_app_id": "A111", "token": "fixed-value", "trigger_id": "1111.222.xxx", "view": { "id": "V111", - "team_id": "T_expected", + "team_id": "T_unexpected", "type": "modal", "blocks": [ { @@ -147,7 +147,7 @@ def test_org_wide_installations_view_submission(self): "root_view_id": "V111", "app_id": "A111", "external_id": "", - "app_installed_team_id": "E111", + "app_installed_team_id": "T_expected", "bot_id": "B111" }, "response_urls": [], @@ -172,13 +172,13 @@ def test_org_wide_installations_view_closed(self): "id": "W111", "username": "primary-owner", "name": "primary-owner", - "team_id": "T_expected" + "team_id": "T_unexpected" }, "api_app_id": "A111", "token": "fixed-value", "view": { "id": "V111", - "team_id": "T_expected", + "team_id": "T_unexpected", "type": "modal", "blocks": [ { @@ -225,7 +225,7 @@ def test_org_wide_installations_view_closed(self): "root_view_id": "V111", "app_id": "A111", "external_id": "", - "app_installed_team_id": "E111", + "app_installed_team_id": "T_expected", "bot_id": "B0302M47727" }, "is_cleared": false,