diff --git a/slack_bolt/app/async_app.py b/slack_bolt/app/async_app.py index 62adbc40e..86211f175 100644 --- a/slack_bolt/app/async_app.py +++ b/slack_bolt/app/async_app.py @@ -34,6 +34,8 @@ error_listener_function_must_be_coro_func, error_client_invalid_type_async, error_authorize_conflicts, + error_oauth_settings_invalid_type_async, + error_oauth_flow_invalid_type_async, ) from slack_bolt.lazy_listener.asyncio_runner import AsyncioLazyListenerRunner from slack_bolt.listener.async_listener import AsyncListener, AsyncCustomListener @@ -167,6 +169,9 @@ def __init__( oauth_settings = AsyncOAuthSettings() if oauth_flow: + if not isinstance(oauth_flow, AsyncOAuthFlow): + raise BoltError(error_oauth_flow_invalid_type_async()) + self._async_oauth_flow = oauth_flow installation_store = select_consistent_installation_store( client_id=self._async_oauth_flow.client_id, @@ -182,6 +187,9 @@ def __init__( if self._async_authorize is None: self._async_authorize = self._async_oauth_flow.settings.authorize elif oauth_settings is not None: + if not isinstance(oauth_settings, AsyncOAuthSettings): + raise BoltError(error_oauth_settings_invalid_type_async()) + installation_store = select_consistent_installation_store( client_id=oauth_settings.client_id, app_store=self._async_installation_store, diff --git a/slack_bolt/logger/messages.py b/slack_bolt/logger/messages.py index 29ee7bfe0..291ddf1d3 100644 --- a/slack_bolt/logger/messages.py +++ b/slack_bolt/logger/messages.py @@ -26,6 +26,14 @@ def error_client_invalid_type_async() -> str: return "`client` must be a slack_sdk.web.async_client.AsyncWebClient" +def error_oauth_flow_invalid_type_async() -> str: + return "`oauth_flow` must be a slack_bolt.oauth.async_oauth_flow.AsyncOAuthFlow" + + +def error_oauth_settings_invalid_type_async() -> str: + return "`oauth_settings` must be a slack_bolt.oauth.async_oauth_settings.AsyncOAuthSettings" + + def error_auth_test_failure(error_response: SlackResponse) -> str: return f"`token` is invalid (auth.test result: {error_response})" diff --git a/slack_bolt/oauth/async_oauth_flow.py b/slack_bolt/oauth/async_oauth_flow.py index 1157dd632..073a622e3 100644 --- a/slack_bolt/oauth/async_oauth_flow.py +++ b/slack_bolt/oauth/async_oauth_flow.py @@ -4,6 +4,7 @@ from typing import Optional, Dict, Callable, Awaitable, Sequence from slack_bolt.error import BoltError +from slack_bolt.logger.messages import error_oauth_settings_invalid_type_async from slack_bolt.oauth.async_callback_options import ( AsyncCallbackOptions, DefaultAsyncCallbackOptions, @@ -62,7 +63,11 @@ def __init__( """ self._async_client = client self._logger = logger + + if not isinstance(settings, AsyncOAuthSettings): + raise BoltError(error_oauth_settings_invalid_type_async()) self.settings = settings + self.settings.logger = self._logger self.client_id = self.settings.client_id diff --git a/slack_bolt/oauth/async_oauth_settings.py b/slack_bolt/oauth/async_oauth_settings.py index 6b2431ef7..f09df9a0d 100644 --- a/slack_bolt/oauth/async_oauth_settings.py +++ b/slack_bolt/oauth/async_oauth_settings.py @@ -1,7 +1,7 @@ import logging import os from logging import Logger -from typing import Optional, Sequence +from typing import Optional, Sequence, Union from slack_sdk.oauth import ( OAuthStateUtils, @@ -57,8 +57,8 @@ def __init__( # OAuth flow parameters/credentials client_id: Optional[str] = None, # required client_secret: Optional[str] = None, # required - scopes: Optional[Sequence[str]] = None, - user_scopes: Optional[Sequence[str]] = None, + scopes: Optional[Union[Sequence[str], str]] = None, + user_scopes: Optional[Union[Sequence[str], str]] = None, redirect_uri: Optional[str] = None, # Handler configuration install_path: str = "/slack/install", @@ -104,9 +104,13 @@ def __init__( raise BoltError("Both client_id and client_secret are required") self.scopes = scopes or os.environ.get("SLACK_SCOPES", "").split(",") + if isinstance(self.scopes, str): + self.scopes = self.scopes.split(",") self.user_scopes = user_scopes or os.environ.get("SLACK_USER_SCOPES", "").split( "," ) + if isinstance(self.user_scopes, str): + self.user_scopes = self.user_scopes.split(",") self.redirect_uri = redirect_uri or os.environ.get("SLACK_REDIRECT_URI") # Handler configuration self.install_path = install_path or os.environ.get( diff --git a/slack_bolt/oauth/oauth_settings.py b/slack_bolt/oauth/oauth_settings.py index 3dd49e1ef..1462925c2 100644 --- a/slack_bolt/oauth/oauth_settings.py +++ b/slack_bolt/oauth/oauth_settings.py @@ -1,7 +1,7 @@ import logging import os from logging import Logger -from typing import Optional, Sequence +from typing import Optional, Sequence, Union from slack_sdk.oauth import ( OAuthStateStore, @@ -52,8 +52,8 @@ def __init__( # OAuth flow parameters/credentials client_id: Optional[str] = None, # required client_secret: Optional[str] = None, # required - scopes: Optional[Sequence[str]] = None, - user_scopes: Optional[Sequence[str]] = None, + scopes: Optional[Union[Sequence[str], str]] = None, + user_scopes: Optional[Union[Sequence[str], str]] = None, redirect_uri: Optional[str] = None, # Handler configuration install_path: str = "/slack/install", @@ -98,9 +98,13 @@ def __init__( raise BoltError("Both client_id and client_secret are required") self.scopes = scopes or os.environ.get("SLACK_SCOPES", "").split(",") + if isinstance(self.scopes, str): + self.scopes = self.scopes.split(",") self.user_scopes = user_scopes or os.environ.get("SLACK_USER_SCOPES", "").split( "," ) + if isinstance(self.user_scopes, str): + self.user_scopes = self.user_scopes.split(",") self.redirect_uri = redirect_uri or os.environ.get("SLACK_REDIRECT_URI") # Handler configuration self.install_path = install_path or os.environ.get( diff --git a/tests/slack_bolt/oauth/test_oauth_flow.py b/tests/slack_bolt/oauth/test_oauth_flow.py index 5d390f7bf..a2797e5e0 100644 --- a/tests/slack_bolt/oauth/test_oauth_flow.py +++ b/tests/slack_bolt/oauth/test_oauth_flow.py @@ -46,6 +46,7 @@ def test_handle_installation(self): client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], + user_scopes=["search:read"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), ) @@ -54,9 +55,19 @@ def test_handle_installation(self): resp = oauth_flow.handle_installation(req) assert resp.status == 200 assert resp.headers.get("content-type") == ["text/html; charset=utf-8"] - assert resp.headers.get("content-length") == ["565"] + assert resp.headers.get("content-length") == ["576"] assert "https://slack.com/oauth/v2/authorize?state=" in resp.body + def test_scopes_as_str(self): + settings = OAuthSettings( + client_id="111.222", + client_secret="xxx", + scopes="chat:write,commands", + user_scopes="search:read", + ) + assert settings.scopes == ["chat:write", "commands"] + assert settings.user_scopes == ["search:read"] + def test_handle_callback(self): oauth_flow = OAuthFlow( client=WebClient(base_url=self.mock_api_server_base_url), diff --git a/tests/slack_bolt_async/oauth/test_async_oauth_flow.py b/tests/slack_bolt_async/oauth/test_async_oauth_flow.py index 85968ed0c..44f4e5ea7 100644 --- a/tests/slack_bolt_async/oauth/test_async_oauth_flow.py +++ b/tests/slack_bolt_async/oauth/test_async_oauth_flow.py @@ -11,6 +11,7 @@ from slack_bolt import BoltResponse from slack_bolt.app.async_app import AsyncApp +from slack_bolt.error import BoltError from slack_bolt.oauth.async_callback_options import ( AsyncFailureArgs, AsyncSuccessArgs, @@ -18,6 +19,7 @@ ) from slack_bolt.oauth.async_oauth_flow import AsyncOAuthFlow from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings +from slack_bolt.oauth.oauth_settings import OAuthSettings from slack_bolt.request.async_request import AsyncBoltRequest from tests.mock_web_api_server import ( cleanup_mock_web_api_server, @@ -46,6 +48,7 @@ async def test_instantiation(self): client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], + user_scopes=["search:read"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), ) @@ -54,6 +57,40 @@ async def test_instantiation(self): assert oauth_flow.logger is not None assert oauth_flow.client is not None + @pytest.mark.asyncio + async def test_scopes_as_str(self): + settings = AsyncOAuthSettings( + client_id="111.222", + client_secret="xxx", + scopes="chat:write,commands", + user_scopes="search:read", + ) + assert settings.scopes == ["chat:write", "commands"] + assert settings.user_scopes == ["search:read"] + + @pytest.mark.asyncio + async def test_instantiation_non_async_settings(self): + with pytest.raises(BoltError): + AsyncOAuthFlow( + settings=OAuthSettings( + client_id="111.222", + client_secret="xxx", + scopes="chat:write,commands", + ) + ) + + @pytest.mark.asyncio + async def test_instantiation_non_async_settings_to_app(self): + with pytest.raises(BoltError): + AsyncApp( + signing_secret="xxx", + oauth_settings=OAuthSettings( + client_id="111.222", + client_secret="xxx", + scopes="chat:write,commands", + ), + ) + @pytest.mark.asyncio async def test_handle_installation(self): oauth_flow = AsyncOAuthFlow(