From 5c07faf5464f8d0a3f7e451fa352ab2c79cda7ee Mon Sep 17 00:00:00 2001 From: Kazuhiro Sera Date: Wed, 28 Apr 2021 09:37:21 +0900 Subject: [PATCH] Fix #307 Add options to disable the built-in middleware --- slack_bolt/app/app.py | 53 ++++++-- slack_bolt/app/async_app.py | 58 ++++++-- tests/scenario_tests/test_events.py | 41 ------ .../scenario_tests/test_events_ignore_self.py | 85 ++++++++++++ .../test_events_request_verification.py | 110 ++++++++++++++++ .../test_events_url_verification.py | 84 ++++++++++++ tests/scenario_tests/test_ssl_check.py | 21 +++ tests/scenario_tests_async/test_events.py | 41 ------ .../test_events_ignore_self.py | 88 +++++++++++++ .../test_events_request_verification.py | 124 ++++++++++++++++++ .../test_events_url_verification.py | 88 +++++++++++++ 11 files changed, 692 insertions(+), 101 deletions(-) create mode 100644 tests/scenario_tests/test_events_ignore_self.py create mode 100644 tests/scenario_tests/test_events_request_verification.py create mode 100644 tests/scenario_tests/test_events_url_verification.py create mode 100644 tests/scenario_tests_async/test_events_ignore_self.py create mode 100644 tests/scenario_tests_async/test_events_request_verification.py create mode 100644 tests/scenario_tests_async/test_events_url_verification.py diff --git a/slack_bolt/app/app.py b/slack_bolt/app/app.py index 86f59b296..2bf8213cb 100644 --- a/slack_bolt/app/app.py +++ b/slack_bolt/app/app.py @@ -99,6 +99,11 @@ def __init__( installation_store: Optional[InstallationStore] = None, # for either only bot scope usage or v1.0.x compatibility installation_store_bot_only: Optional[bool] = None, + # for customizing the built-in middleware + request_verification_enabled: bool = True, + ignoring_self_events_enabled: bool = True, + ssl_check_enabled: bool = True, + url_verification_enabled: bool = True, # for the OAuth flow oauth_settings: Optional[OAuthSettings] = None, oauth_flow: Optional[OAuthFlow] = None, @@ -146,6 +151,21 @@ def message_hello(message, say): by checking if there is a team/user in the installation data. installation_store: The module offering save/find operations of installation data installation_store_bot_only: Use `InstallationStore#find_bot()` if True (Default: False) + request_verification_enabled: False if you would like to disable the built-in middleware (Default: True). + `RequestVerification` is a built-in middleware that verifies the signature in HTTP Mode requests. + Make sure if it's safe enough when you turn a built-in middleware off. + We strongly recommend using RequestVerification for better security. + If you have a proxy that verifies request signature in front of the Bolt app, + it's totally fine to disable RequestVerification to avoid duplication of work. + Don't turn it off just for easiness of development. + ignoring_self_events_enabled: False if you would like to disable the built-in middleware (Default: True). + `IgnoringSelfEvents` is a built-in middleware that enables Bolt apps to easily skip the events + generated by this app's bot user (this is useful for avoiding code error causing an infinite loop). + url_verification_enabled: False if you would like to disable the built-in middleware (Default: True). + `UrlVerification` is a built-in middleware that handles url_verification requests + that verify the endpoint for Events API in HTTP Mode requests. + ssl_check_enabled: bool = False if you would like to disable the built-in middleware (Default: True). + `SslCheck` is a built-in middleware that handles ssl_check requests from Slack. oauth_settings: The settings related to Slack app installation flow (OAuth flow) oauth_flow: Instantiated `slack_bolt.oauth.OAuthFlow`. This is always prioritized over oauth_settings. verification_token: Deprecated verification mechanism. This can used only for ssl_check requests. @@ -292,22 +312,37 @@ def message_hello(message, say): self._init_middleware_list_done = False self._init_middleware_list( - token_verification_enabled=token_verification_enabled + token_verification_enabled=token_verification_enabled, + request_verification_enabled=request_verification_enabled, + ignoring_self_events_enabled=ignoring_self_events_enabled, + ssl_check_enabled=ssl_check_enabled, + url_verification_enabled=url_verification_enabled, ) - def _init_middleware_list(self, token_verification_enabled: bool): + def _init_middleware_list( + self, + token_verification_enabled: bool = True, + request_verification_enabled: bool = True, + ignoring_self_events_enabled: bool = True, + ssl_check_enabled: bool = True, + url_verification_enabled: bool = True, + ): if self._init_middleware_list_done: return - self._middleware_list.append( - SslCheck(verification_token=self._verification_token) - ) - self._middleware_list.append(RequestVerification(self._signing_secret)) + if ssl_check_enabled is True: + self._middleware_list.append( + SslCheck(verification_token=self._verification_token) + ) + if request_verification_enabled is True: + self._middleware_list.append(RequestVerification(self._signing_secret)) + # As authorize is required for making a Bolt app function, we don't offer the flag to disable this if self._oauth_flow is None: if self._token is not None: try: auth_test_result = None if token_verification_enabled: + # This API call is for eagerly validating the token auth_test_result = self._client.auth_test(token=self._token) self._middleware_list.append( SingleTeamAuthorization(auth_test_result=auth_test_result) @@ -324,8 +359,10 @@ def _init_middleware_list(self, token_verification_enabled: bool): self._middleware_list.append( MultiTeamsAuthorization(authorize=self._authorize) ) - self._middleware_list.append(IgnoringSelfEvents()) - self._middleware_list.append(UrlVerification()) + if ignoring_self_events_enabled is True: + self._middleware_list.append(IgnoringSelfEvents()) + if url_verification_enabled is True: + self._middleware_list.append(UrlVerification()) self._init_middleware_list_done = True # ------------------------- diff --git a/slack_bolt/app/async_app.py b/slack_bolt/app/async_app.py index ce1b968bf..131650262 100644 --- a/slack_bolt/app/async_app.py +++ b/slack_bolt/app/async_app.py @@ -105,10 +105,15 @@ def __init__( token: Optional[str] = None, client: Optional[AsyncWebClient] = None, # for multi-workspace apps + authorize: Optional[Callable[..., Awaitable[AuthorizeResult]]] = None, installation_store: Optional[AsyncInstallationStore] = None, # for either only bot scope usage or v1.0.x compatibility installation_store_bot_only: Optional[bool] = None, - authorize: Optional[Callable[..., Awaitable[AuthorizeResult]]] = None, + # for customizing the built-in middleware + request_verification_enabled: bool = True, + ignoring_self_events_enabled: bool = True, + ssl_check_enabled: bool = True, + url_verification_enabled: bool = True, # for the OAuth flow oauth_settings: Optional[AsyncOAuthSettings] = None, oauth_flow: Optional[AsyncOAuthFlow] = None, @@ -155,6 +160,21 @@ async def message_hello(message, say): # async function by checking if there is a team/user in the installation data. installation_store: The module offering save/find operations of installation data installation_store_bot_only: Use `AsyncInstallationStore#async_find_bot()` if True (Default: False) + request_verification_enabled: False if you would like to disable the built-in middleware (Default: True). + `AsyncRequestVerification` is a built-in middleware that verifies the signature in HTTP Mode requests. + Make sure if it's safe enough when you turn a built-in middleware off. + We strongly recommend using RequestVerification for better security. + If you have a proxy that verifies request signature in front of the Bolt app, + it's totally fine to disable RequestVerification to avoid duplication of work. + Don't turn it off just for easiness of development. + ignoring_self_events_enabled: False if you would like to disable the built-in middleware (Default: True). + `AsyncIgnoringSelfEvents` is a built-in middleware that enables Bolt apps to easily skip the events + generated by this app's bot user (this is useful for avoiding code error causing an infinite loop). + url_verification_enabled: False if you would like to disable the built-in middleware (Default: True). + `AsyncUrlVerification` is a built-in middleware that handles url_verification requests + that verify the endpoint for Events API in HTTP Mode requests. + ssl_check_enabled: bool = False if you would like to disable the built-in middleware (Default: True). + `AsyncSslCheck` is a built-in middleware that handles ssl_check requests from Slack. oauth_settings: The settings related to Slack app installation flow (OAuth flow) oauth_flow: Instantiated `slack_bolt.oauth.AsyncOAuthFlow`. This is always prioritized over oauth_settings. verification_token: Deprecated verification mechanism. This can used only for ssl_check requests. @@ -316,19 +336,33 @@ async def message_hello(message, say): # async function ) self._init_middleware_list_done = False - self._init_async_middleware_list() + self._init_async_middleware_list( + request_verification_enabled=request_verification_enabled, + ignoring_self_events_enabled=ignoring_self_events_enabled, + ssl_check_enabled=ssl_check_enabled, + url_verification_enabled=url_verification_enabled, + ) self._server: Optional[AsyncSlackAppServer] = None - def _init_async_middleware_list(self): + def _init_async_middleware_list( + self, + request_verification_enabled: bool = True, + ignoring_self_events_enabled: bool = True, + ssl_check_enabled: bool = True, + url_verification_enabled: bool = True, + ): if self._init_middleware_list_done: return - self._async_middleware_list.append( - AsyncSslCheck(verification_token=self._verification_token) - ) - self._async_middleware_list.append( - AsyncRequestVerification(self._signing_secret) - ) + if ssl_check_enabled is True: + self._async_middleware_list.append( + AsyncSslCheck(verification_token=self._verification_token) + ) + if request_verification_enabled is True: + self._async_middleware_list.append( + AsyncRequestVerification(self._signing_secret) + ) + # As authorize is required for making a Bolt app function, we don't offer the flag to disable this if self._async_oauth_flow is None: if self._token: self._async_middleware_list.append(AsyncSingleTeamAuthorization()) @@ -343,8 +377,10 @@ def _init_async_middleware_list(self): AsyncMultiTeamsAuthorization(authorize=self._async_authorize) ) - self._async_middleware_list.append(AsyncIgnoringSelfEvents()) - self._async_middleware_list.append(AsyncUrlVerification()) + if ignoring_self_events_enabled is True: + self._async_middleware_list.append(AsyncIgnoringSelfEvents()) + if url_verification_enabled is True: + self._async_middleware_list.append(AsyncUrlVerification()) self._init_middleware_list_done = True # ------------------------- diff --git a/tests/scenario_tests/test_events.py b/tests/scenario_tests/test_events.py index e5a1e9f49..49c1f409c 100644 --- a/tests/scenario_tests/test_events.py +++ b/tests/scenario_tests/test_events.py @@ -169,47 +169,6 @@ def handle_app_mention(): response = app.dispatch(request) assert response.status == 200 - def test_self_events(self): - app = App(client=self.web_client, signing_secret=self.signing_secret) - - event_body = { - "token": "verification_token", - "team_id": "T111", - "enterprise_id": "E111", - "api_app_id": "A111", - "event": { - "type": "reaction_added", - "user": "W23456789", # bot_user_id - "item": { - "type": "message", - "channel": "C111", - "ts": "1599529504.000400", - }, - "reaction": "heart_eyes", - "item_user": "W111", - "event_ts": "1599616881.000800", - }, - "type": "event_callback", - "event_id": "Ev111", - "event_time": 1599616881, - "authed_users": ["W111"], - } - - @app.event("reaction_added") - def handle_app_mention(say): - say("What's up?") - - timestamp, body = str(int(time())), json.dumps(event_body) - request: BoltRequest = BoltRequest( - body=body, headers=self.build_headers(timestamp, body) - ) - response = app.dispatch(request) - assert response.status == 200 - assert_auth_test_count(self, 1) - sleep(1) # wait a bit after auto ack() - # The listener should not be executed - assert self.mock_received_requests.get("/chat.postMessage") is None - def test_self_member_join_left_events(self): app = App(client=self.web_client, signing_secret=self.signing_secret) diff --git a/tests/scenario_tests/test_events_ignore_self.py b/tests/scenario_tests/test_events_ignore_self.py new file mode 100644 index 000000000..7fb3f2e7d --- /dev/null +++ b/tests/scenario_tests/test_events_ignore_self.py @@ -0,0 +1,85 @@ +from time import sleep + +from slack_sdk.web import WebClient + +from slack_bolt import App, BoltRequest +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, + assert_auth_test_count, +) +from tests.utils import remove_os_env_temporarily, restore_os_env + + +class TestEventsIgnoreSelf: + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + web_client = WebClient( + token=valid_token, + base_url=mock_api_server_base_url, + ) + + def setup_method(self): + self.old_os_env = remove_os_env_temporarily() + setup_mock_web_api_server(self) + + def teardown_method(self): + cleanup_mock_web_api_server(self) + restore_os_env(self.old_os_env) + + def test_self_events(self): + app = App(client=self.web_client) + + @app.event("reaction_added") + def handle_app_mention(say): + say("What's up?") + + request: BoltRequest = BoltRequest(body=event_body, mode="socket_mode") + response = app.dispatch(request) + assert response.status == 200 + assert_auth_test_count(self, 1) + sleep(1) # wait a bit after auto ack() + # The listener should not be executed + assert self.mock_received_requests.get("/chat.postMessage") is None + + def test_self_events_disabled(self): + app = App( + client=self.web_client, + ignoring_self_events_enabled=False, + ) + + @app.event("reaction_added") + def handle_app_mention(say): + say("What's up?") + + request: BoltRequest = BoltRequest(body=event_body, mode="socket_mode") + response = app.dispatch(request) + assert response.status == 200 + assert_auth_test_count(self, 1) + sleep(1) # wait a bit after auto ack() + # The listener should be executed as the ignoring logic is disabled + assert self.mock_received_requests.get("/chat.postMessage") == 1 + + +event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "reaction_added", + "user": "W23456789", # bot_user_id + "item": { + "type": "message", + "channel": "C111", + "ts": "1599529504.000400", + }, + "reaction": "heart_eyes", + "item_user": "W111", + "event_ts": "1599616881.000800", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], +} diff --git a/tests/scenario_tests/test_events_request_verification.py b/tests/scenario_tests/test_events_request_verification.py new file mode 100644 index 000000000..c8a292a3b --- /dev/null +++ b/tests/scenario_tests/test_events_request_verification.py @@ -0,0 +1,110 @@ +import json +from time import sleep, time + +from slack_sdk.web import WebClient +from slack_sdk.signature import SignatureVerifier + +from slack_bolt import App, BoltRequest +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, + assert_auth_test_count, +) +from tests.utils import remove_os_env_temporarily, restore_os_env + + +class TestEventsRequestVerification: + valid_token = "xoxb-valid" + signing_secret = "secret" + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + signature_verifier = SignatureVerifier(signing_secret) + web_client = WebClient( + token=valid_token, + base_url=mock_api_server_base_url, + ) + + def setup_method(self): + self.old_os_env = remove_os_env_temporarily() + setup_mock_web_api_server(self) + + def teardown_method(self): + cleanup_mock_web_api_server(self) + restore_os_env(self.old_os_env) + + def generate_signature(self, body: str, timestamp: str): + return self.signature_verifier.generate_signature( + body=body, + timestamp=timestamp, + ) + + def build_headers(self, timestamp: str, body: str): + return { + "content-type": ["application/json"], + "x-slack-signature": [self.generate_signature(body, timestamp)], + "x-slack-request-timestamp": [timestamp], + } + + def test_default(self): + app = App(client=self.web_client, signing_secret=self.signing_secret) + + @app.event("reaction_added") + def handle_app_mention(say): + say("What's up?") + + timestamp, body = str(int(time())), json.dumps(event_body) + request: BoltRequest = BoltRequest( + body=body, headers=self.build_headers(timestamp, body) + ) + response = app.dispatch(request) + assert response.status == 200 + assert_auth_test_count(self, 1) + sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests.get("/chat.postMessage") == 1 + + def test_disabled(self): + app = App( + client=self.web_client, + signing_secret=self.signing_secret, + request_verification_enabled=False, + ) + + @app.event("reaction_added") + def handle_app_mention(say): + say("What's up?") + + # request including invalid headers + expired = int(time()) - 3600 + timestamp, body = str(expired), json.dumps(event_body) + request: BoltRequest = BoltRequest( + body=body, headers=self.build_headers(timestamp, body) + ) + response = app.dispatch(request) + assert response.status == 200 + assert_auth_test_count(self, 1) + sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests.get("/chat.postMessage") == 1 + + +event_body = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "reaction_added", + "user": "W111", + "item": { + "type": "message", + "channel": "C111", + "ts": "1599529504.000400", + }, + "reaction": "heart_eyes", + "item_user": "W111", + "event_ts": "1599616881.000800", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], +} diff --git a/tests/scenario_tests/test_events_url_verification.py b/tests/scenario_tests/test_events_url_verification.py new file mode 100644 index 000000000..4f92bca13 --- /dev/null +++ b/tests/scenario_tests/test_events_url_verification.py @@ -0,0 +1,84 @@ +import json +from time import time + +from slack_sdk.web import WebClient +from slack_sdk.signature import SignatureVerifier + +from slack_bolt import App, BoltRequest +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, + assert_auth_test_count, +) +from tests.utils import remove_os_env_temporarily, restore_os_env + + +class TestEventsUrlVerification: + valid_token = "xoxb-valid" + signing_secret = "secret" + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + signature_verifier = SignatureVerifier(signing_secret) + web_client = WebClient( + token=valid_token, + base_url=mock_api_server_base_url, + ) + + def setup_method(self): + self.old_os_env = remove_os_env_temporarily() + setup_mock_web_api_server(self) + + def teardown_method(self): + cleanup_mock_web_api_server(self) + restore_os_env(self.old_os_env) + + def generate_signature(self, body: str, timestamp: str): + return self.signature_verifier.generate_signature( + body=body, + timestamp=timestamp, + ) + + def build_headers(self, timestamp: str, body: str): + return { + "content-type": ["application/json"], + "x-slack-signature": [self.generate_signature(body, timestamp)], + "x-slack-request-timestamp": [timestamp], + } + + def test_default(self): + app = App(client=self.web_client, signing_secret=self.signing_secret) + + timestamp, body = str(int(time())), json.dumps(event_body) + request: BoltRequest = BoltRequest( + body=body, headers=self.build_headers(timestamp, body) + ) + response = app.dispatch(request) + assert response.status == 200 + assert ( + response.body + == """{"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P"}""" + ) + assert_auth_test_count(self, 0) + + def test_disabled(self): + app = App( + client=self.web_client, + signing_secret=self.signing_secret, + url_verification_enabled=False, + ) + + timestamp, body = str(int(time())), json.dumps(event_body) + request: BoltRequest = BoltRequest( + body=body, headers=self.build_headers(timestamp, body) + ) + response = app.dispatch(request) + assert response.status == 404 + assert response.body == """{"error": "unhandled request"}""" + assert_auth_test_count(self, 0) + + +event_body = { + "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl", + "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P", + "type": "url_verification", +} diff --git a/tests/scenario_tests/test_ssl_check.py b/tests/scenario_tests/test_ssl_check.py index 3470321de..937f9d7e4 100644 --- a/tests/scenario_tests/test_ssl_check.py +++ b/tests/scenario_tests/test_ssl_check.py @@ -55,3 +55,24 @@ def test_ssl_check(self): response = app.dispatch(request) assert response.status == 200 assert response.body == "" + + def test_ssl_check_disabled(self): + app = App( + client=self.web_client, + signing_secret=self.signing_secret, + ssl_check_enabled=False, + ) + + timestamp, body = str(int(time())), "token=random&ssl_check=1" + request: BoltRequest = BoltRequest( + body=body, + query={}, + headers={ + "content-type": ["application/x-www-form-urlencoded"], + "x-slack-signature": [self.generate_signature(body, timestamp)], + "x-slack-request-timestamp": [timestamp], + }, + ) + response = app.dispatch(request) + assert response.status == 404 + assert response.body == """{"error": "unhandled request"}""" diff --git a/tests/scenario_tests_async/test_events.py b/tests/scenario_tests_async/test_events.py index 7e6f25a9c..5e5af42e2 100644 --- a/tests/scenario_tests_async/test_events.py +++ b/tests/scenario_tests_async/test_events.py @@ -158,47 +158,6 @@ async def test_stable_auto_ack(self): response = await app.async_dispatch(request) assert response.status == 200 - @pytest.mark.asyncio - async def test_self_events(self): - app = AsyncApp( - client=self.web_client, - signing_secret=self.signing_secret, - ) - app.event("reaction_added")(whats_up) - - self_event = { - "token": "verification_token", - "team_id": "T111", - "enterprise_id": "E111", - "api_app_id": "A111", - "event": { - "type": "reaction_added", - "user": "W23456789", # bot_user_id - "item": { - "type": "message", - "channel": "C111", - "ts": "1599529504.000400", - }, - "reaction": "heart_eyes", - "item_user": "W111", - "event_ts": "1599616881.000800", - }, - "type": "event_callback", - "event_id": "Ev111", - "event_time": 1599616881, - "authed_users": ["W111"], - } - timestamp, body = str(int(time())), json.dumps(self_event) - request = AsyncBoltRequest( - body=body, headers=self.build_headers(timestamp, body) - ) - response = await app.async_dispatch(request) - assert response.status == 200 - await assert_auth_test_count_async(self, 1) - await asyncio.sleep(1) # wait a bit after auto ack() - # The listener should not be executed - assert self.mock_received_requests.get("/chat.postMessage") is None - @pytest.mark.asyncio async def test_self_joined_left_events(self): app = AsyncApp( diff --git a/tests/scenario_tests_async/test_events_ignore_self.py b/tests/scenario_tests_async/test_events_ignore_self.py new file mode 100644 index 000000000..5749b980b --- /dev/null +++ b/tests/scenario_tests_async/test_events_ignore_self.py @@ -0,0 +1,88 @@ +import asyncio + +import pytest +from slack_sdk.web.async_client import AsyncWebClient + +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.request.async_request import AsyncBoltRequest +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, + assert_auth_test_count_async, +) +from tests.utils import remove_os_env_temporarily, restore_os_env + + +class TestAsyncEventsIgnoreSelf: + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + web_client = AsyncWebClient( + token=valid_token, + base_url=mock_api_server_base_url, + ) + + @pytest.fixture + def event_loop(self): + old_os_env = remove_os_env_temporarily() + try: + setup_mock_web_api_server(self) + loop = asyncio.get_event_loop() + yield loop + loop.close() + cleanup_mock_web_api_server(self) + finally: + restore_os_env(old_os_env) + + @pytest.mark.asyncio + async def test_self_events(self): + app = AsyncApp(client=self.web_client) + app.event("reaction_added")(whats_up) + request = AsyncBoltRequest(body=self_event, mode="socket_mode") + response = await app.async_dispatch(request) + assert response.status == 200 + await assert_auth_test_count_async(self, 1) + await asyncio.sleep(1) # wait a bit after auto ack() + # The listener should not be executed + assert self.mock_received_requests.get("/chat.postMessage") is None + + @pytest.mark.asyncio + async def test_self_events_disabled(self): + app = AsyncApp(client=self.web_client, ignoring_self_events_enabled=False) + app.event("reaction_added")(whats_up) + request = AsyncBoltRequest(body=self_event, mode="socket_mode") + response = await app.async_dispatch(request) + assert response.status == 200 + await assert_auth_test_count_async(self, 0) + await asyncio.sleep(1) # wait a bit after auto ack() + # The listener should be executed + assert self.mock_received_requests.get("/chat.postMessage") == 1 + + +self_event = { + "token": "verification_token", + "team_id": "T111", + "enterprise_id": "E111", + "api_app_id": "A111", + "event": { + "type": "reaction_added", + "user": "W23456789", # bot_user_id + "item": { + "type": "message", + "channel": "C111", + "ts": "1599529504.000400", + }, + "reaction": "heart_eyes", + "item_user": "W111", + "event_ts": "1599616881.000800", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1599616881, + "authed_users": ["W111"], +} + + +async def whats_up(body, say, payload, event): + assert body["event"] == payload + assert payload == event + await say("What's up?") diff --git a/tests/scenario_tests_async/test_events_request_verification.py b/tests/scenario_tests_async/test_events_request_verification.py new file mode 100644 index 000000000..3c59c2a00 --- /dev/null +++ b/tests/scenario_tests_async/test_events_request_verification.py @@ -0,0 +1,124 @@ +import asyncio +import json +from time import time + +import pytest +from slack_sdk.signature import SignatureVerifier +from slack_sdk.web.async_client import AsyncWebClient + +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.request.async_request import AsyncBoltRequest +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, + assert_auth_test_count_async, +) +from tests.utils import remove_os_env_temporarily, restore_os_env + + +class TestAsyncEventsRequestVerification: + signing_secret = "secret" + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + signature_verifier = SignatureVerifier(signing_secret) + web_client = AsyncWebClient(token=valid_token, base_url=mock_api_server_base_url) + + @pytest.fixture + def event_loop(self): + old_os_env = remove_os_env_temporarily() + try: + setup_mock_web_api_server(self) + loop = asyncio.get_event_loop() + yield loop + loop.close() + cleanup_mock_web_api_server(self) + finally: + restore_os_env(old_os_env) + + def generate_signature(self, body: str, timestamp: str): + return self.signature_verifier.generate_signature( + body=body, + timestamp=timestamp, + ) + + def build_headers(self, timestamp: str, body: str): + return { + "content-type": ["application/json"], + "x-slack-signature": [self.generate_signature(body, timestamp)], + "x-slack-request-timestamp": [timestamp], + } + + @pytest.mark.asyncio + async def test_default(self): + app = AsyncApp( + client=self.web_client, + signing_secret=self.signing_secret, + ) + app.event("app_mention")(whats_up) + + timestamp, body = str(int(time())), json.dumps(app_mention_body) + request = AsyncBoltRequest( + body=body, headers=self.build_headers(timestamp, body) + ) + response = await app.async_dispatch(request) + assert response.status == 200 + await assert_auth_test_count_async(self, 1) + await asyncio.sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 1 + + @pytest.mark.asyncio + async def test_disabled(self): + app = AsyncApp( + client=self.web_client, + signing_secret=self.signing_secret, + request_verification_enabled=False, + ) + app.event("app_mention")(whats_up) + + # request including invalid headers + expired = int(time()) - 3600 + timestamp, body = str(expired), json.dumps(app_mention_body) + request = AsyncBoltRequest( + body=body, headers=self.build_headers(timestamp, body) + ) + response = await app.async_dispatch(request) + assert response.status == 200 + await assert_auth_test_count_async(self, 1) + await asyncio.sleep(1) # wait a bit after auto ack() + assert self.mock_received_requests["/chat.postMessage"] == 1 + + +app_mention_body = { + "token": "verification_token", + "team_id": "T_INSTALLED", + "enterprise_id": "E_SOURCE", + "api_app_id": "A111", + "event": { + "client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63", + "type": "app_mention", + "text": "<@W111> Hi there!", + "user": "W222", + "ts": "1595926230.009600", + "team": "T_INSTALLED", + "channel": "C111", + "event_ts": "1595926230.009600", + }, + "type": "event_callback", + "event_id": "Ev111", + "event_time": 1595926230, + "authorizations": [ + { + "enterprise_id": "E_INSTALLED", + "team_id": "T_INSTALLED", + "user_id": "W111", + "is_bot": True, + "is_enterprise_install": False, + } + ], +} + + +async def whats_up(body, say, payload, event): + assert body["event"] == payload + assert payload == event + await say("What's up?") diff --git a/tests/scenario_tests_async/test_events_url_verification.py b/tests/scenario_tests_async/test_events_url_verification.py new file mode 100644 index 000000000..e47efcb65 --- /dev/null +++ b/tests/scenario_tests_async/test_events_url_verification.py @@ -0,0 +1,88 @@ +import asyncio +import json +from time import time + +import pytest +from slack_sdk.signature import SignatureVerifier +from slack_sdk.web.async_client import AsyncWebClient + +from slack_bolt.app.async_app import AsyncApp +from slack_bolt.request.async_request import AsyncBoltRequest +from tests.mock_web_api_server import ( + setup_mock_web_api_server, + cleanup_mock_web_api_server, + assert_auth_test_count_async, +) +from tests.utils import remove_os_env_temporarily, restore_os_env + + +class TestAsyncEventsUrlVerification: + signing_secret = "secret" + valid_token = "xoxb-valid" + mock_api_server_base_url = "http://localhost:8888" + signature_verifier = SignatureVerifier(signing_secret) + web_client = AsyncWebClient(token=valid_token, base_url=mock_api_server_base_url) + + @pytest.fixture + def event_loop(self): + old_os_env = remove_os_env_temporarily() + try: + setup_mock_web_api_server(self) + loop = asyncio.get_event_loop() + yield loop + loop.close() + cleanup_mock_web_api_server(self) + finally: + restore_os_env(old_os_env) + + def generate_signature(self, body: str, timestamp: str): + return self.signature_verifier.generate_signature( + body=body, + timestamp=timestamp, + ) + + def build_headers(self, timestamp: str, body: str): + return { + "content-type": ["application/json"], + "x-slack-signature": [self.generate_signature(body, timestamp)], + "x-slack-request-timestamp": [timestamp], + } + + def build_valid_request(self) -> AsyncBoltRequest: + timestamp, body = str(int(time())), json.dumps(event_body) + return AsyncBoltRequest(body=body, headers=self.build_headers(timestamp, body)) + + @pytest.mark.asyncio + async def test_default(self): + app = AsyncApp( + client=self.web_client, + signing_secret=self.signing_secret, + ) + request = self.build_valid_request() + response = await app.async_dispatch(request) + assert response.status == 200 + assert ( + response.body + == """{"challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P"}""" + ) + await assert_auth_test_count_async(self, 0) + + @pytest.mark.asyncio + async def test_disabled(self): + app = AsyncApp( + client=self.web_client, + signing_secret=self.signing_secret, + url_verification_enabled=False, + ) + request = self.build_valid_request() + response = await app.async_dispatch(request) + assert response.status == 404 + assert response.body == """{"error": "unhandled request"}""" + await assert_auth_test_count_async(self, 0) + + +event_body = { + "token": "Jhj5dZrVaK7ZwHHjRyZWjbDl", + "challenge": "3eZbrw1aBm2rZgRNFdxV2595E9CY3gmdALWMmHkvFXO7tYXAYM8P", + "type": "url_verification", +}