Skip to content

Commit

Permalink
Add v1.0 compatible mode to slackapi#148 Org-wide App support
Browse files Browse the repository at this point in the history
  • Loading branch information
seratch committed Dec 8, 2020
1 parent 0603e0f commit f3f1455
Show file tree
Hide file tree
Showing 12 changed files with 162 additions and 10 deletions.
2 changes: 1 addition & 1 deletion scripts/run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ else
black slack_bolt/ tests/ \
&& pytest \
&& pip install -e ".[adapter]" \
&& pip install -U pytype \
&& pip install -U pip setuptools wheel pytype \
&& pytype slack_bolt/
else
black slack_bolt/ tests/ && pytest
Expand Down
4 changes: 3 additions & 1 deletion slack_bolt/adapter/aws_lambda/lambda_s3_oauth_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,9 @@ def __init__(
# the settings may already have pre-defined authorize.
# In this case, the /slack/events endpoint doesn't work along with the OAuth flow.
settings.authorize = InstallationStoreAuthorize(
logger=logger, installation_store=settings.installation_store
logger=logger,
installation_store=settings.installation_store,
bot_only=settings.installation_store_bot_only,
)

OAuthFlow.__init__(self, client=client, logger=logger, settings=settings)
Expand Down
4 changes: 4 additions & 0 deletions slack_bolt/app/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ def __init__(
# for multi-workspace apps
authorize: Optional[Callable[..., AuthorizeResult]] = None,
installation_store: Optional[InstallationStore] = None,
# for v1.0.x compatibility
installation_store_bot_only: bool = False,
# for the OAuth flow
oauth_settings: Optional[OAuthSettings] = None,
oauth_flow: Optional[OAuthFlow] = None,
Expand All @@ -96,6 +98,7 @@ def __init__(
:param authorize: The function to authorize an incoming request from Slack
by checking if there is a team/user in the installation data.
:param installation_store: The module offering save/find operations of installation data
:param installation_store_bot_only: Use InstallationStore#find_bot if True (Default: False)
:param oauth_settings: The settings related to Slack app installation flow (OAuth flow)
:param oauth_flow: Manually instantiated slack_bolt.oauth.OAuthFlow.
This is always prioritized over oauth_settings.
Expand Down Expand Up @@ -140,6 +143,7 @@ def __init__(
self._authorize = InstallationStoreAuthorize(
installation_store=self._installation_store,
logger=self._framework_logger,
bot_only=installation_store_bot_only,
)

self._oauth_flow: Optional[OAuthFlow] = None
Expand Down
3 changes: 3 additions & 0 deletions slack_bolt/app/async_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ def __init__(
client: Optional[AsyncWebClient] = None,
# for multi-workspace apps
installation_store: Optional[AsyncInstallationStore] = None,
installation_store_bot_only: bool = False,
authorize: Optional[Callable[..., Awaitable[AuthorizeResult]]] = None,
# for the OAuth flow
oauth_settings: Optional[AsyncOAuthSettings] = None,
Expand All @@ -106,6 +107,7 @@ def __init__(
:param token: The bot access token required only for single-workspace app.
:param client: The singleton slack_sdk.web.async_client.AsyncWebClient instance for this app.
:param installation_store: The module offering save/find operations of installation data
:param installation_store_bot_only: Use InstallationStore#find_bot if True (Default: False)
:param authorize: The function to authorize an incoming request from Slack
by checking if there is a team/user in the installation data.
:param oauth_settings: The settings related to Slack app installation flow (OAuth flow)
Expand Down Expand Up @@ -155,6 +157,7 @@ def __init__(
self._async_authorize = AsyncInstallationStoreAuthorize(
installation_store=self._async_installation_store,
logger=self._framework_logger,
bot_only=installation_store_bot_only,
)

self._async_oauth_flow: Optional[AsyncOAuthFlow] = None
Expand Down
11 changes: 8 additions & 3 deletions slack_bolt/authorization/async_authorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,19 +91,24 @@ async def __call__(


class AsyncInstallationStoreAuthorize(AsyncAuthorize):
authorize_result_cache: Dict[str, AuthorizeResult] = {}
authorize_result_cache: Dict[str, AuthorizeResult]
find_installation_available: Optional[bool]

def __init__(
self,
*,
logger: Logger,
installation_store: AsyncInstallationStore,
# For v1.0.x compatibility and people who still want its simplicity
# use only InstallationStore#find_bot(enterprise_id, team_id)
bot_only: bool = False,
cache_enabled: bool = False,
):
self.logger = logger
self.installation_store = installation_store
self.bot_only = bot_only
self.cache_enabled = cache_enabled
self.authorize_result_cache = {}
self.find_installation_available = None

async def __call__(
Expand All @@ -123,7 +128,7 @@ async def __call__(
bot_token: Optional[str] = None
user_token: Optional[str] = None

if self.find_installation_available:
if not self.bot_only and self.find_installation_available:
# since v1.1, this is the default way
try:
installation: Optional[
Expand Down Expand Up @@ -153,7 +158,7 @@ async def __call__(
except NotImplementedError as _:
self.find_installation_available = False

if not self.find_installation_available:
if self.bot_only or not self.find_installation_available:
# Use find_bot to get bot value (legacy)
bot: Optional[Bot] = await self.installation_store.async_find_bot(
enterprise_id=enterprise_id,
Expand Down
12 changes: 9 additions & 3 deletions slack_bolt/authorization/authorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,19 +90,25 @@ def __call__(


class InstallationStoreAuthorize(Authorize):
authorize_result_cache: Dict[str, AuthorizeResult] = {}
authorize_result_cache: Dict[str, AuthorizeResult]
bot_only: bool
find_installation_available: bool

def __init__(
self,
*,
logger: Logger,
installation_store: InstallationStore,
# For v1.0.x compatibility and people who still want its simplicity
# use only InstallationStore#find_bot(enterprise_id, team_id)
bot_only: bool = False,
cache_enabled: bool = False,
):
self.logger = logger
self.installation_store = installation_store
self.bot_only = bot_only
self.cache_enabled = cache_enabled
self.authorize_result_cache = {}
self.find_installation_available = hasattr(
installation_store, "find_installation"
)
Expand All @@ -119,7 +125,7 @@ def __call__(
bot_token: Optional[str] = None
user_token: Optional[str] = None

if self.find_installation_available:
if not self.bot_only and self.find_installation_available:
# since v1.1, this is the default way
try:
installation: Optional[
Expand Down Expand Up @@ -149,7 +155,7 @@ def __call__(
except NotImplementedError as _:
self.find_installation_available = False

if not self.find_installation_available:
if self.bot_only or not self.find_installation_available:
# Use find_bot to get bot value (legacy)
bot: Optional[Bot] = self.installation_store.find_bot(
enterprise_id=enterprise_id,
Expand Down
2 changes: 2 additions & 0 deletions slack_bolt/oauth/async_oauth_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ def sqlite3(
# state parameter related configurations
state_cookie_name: str = OAuthStateUtils.default_cookie_name,
state_expiration_seconds: int = OAuthStateUtils.default_expiration_seconds,
installation_store_bot_only: bool = False,
client: Optional[AsyncWebClient] = None,
logger: Optional[Logger] = None,
) -> "AsyncOAuthFlow":
Expand Down Expand Up @@ -140,6 +141,7 @@ def sqlite3(
installation_store=SQLite3InstallationStore(
database=database, client_id=client_id, logger=logger,
),
installation_store_bot_only=installation_store_bot_only,
# state parameter related configurations
state_store=SQLite3OAuthStateStore(
database=database,
Expand Down
8 changes: 7 additions & 1 deletion slack_bolt/oauth/async_oauth_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ class AsyncOAuthSettings:
authorization_url: str # default: https://slack.com/oauth/v2/authorize
# Installation Management
installation_store: AsyncInstallationStore
installation_store_bot_only: bool
authorize: AsyncAuthorize
# state parameter related configurations
state_store: AsyncOAuthStateStore
Expand Down Expand Up @@ -69,6 +70,7 @@ def __init__(
authorization_url: Optional[str] = None,
# Installation Management
installation_store: Optional[AsyncInstallationStore] = None,
installation_store_bot_only: bool = False,
# state parameter related configurations
state_store: Optional[AsyncOAuthStateStore] = None,
state_cookie_name: str = OAuthStateUtils.default_cookie_name,
Expand All @@ -90,6 +92,7 @@ def __init__(
:param failure_url: Set a complete URL if you want to redirect end-users when an installation fails.
:param authorization_url: Set a URL if you want to customize the URL https://slack.com/oauth/v2/authorize
:param installation_store: Specify the instance of InstallationStore (Default: FileInstallationStore)
:param installation_store_bot_only: Use AsyncInstallationStore#find_bot if True (Default: False)
:param state_store: Specify the instance of InstallationStore (Default: FileOAuthStateStore)
:param state_cookie_name: The cookie name that is set for installers' browser. (Default: slack-app-oauth-state)
:param state_expiration_seconds: The seconds that the state value is alive (Default: 600 seconds)
Expand Down Expand Up @@ -129,8 +132,11 @@ def __init__(
self.installation_store = (
installation_store or get_or_create_default_installation_store(client_id)
)
self.installation_store_bot_only = installation_store_bot_only
self.authorize = AsyncInstallationStoreAuthorize(
logger=logger, installation_store=self.installation_store,
logger=logger,
installation_store=self.installation_store,
bot_only=self.installation_store_bot_only,
)
# state parameter related configurations
self.state_store = state_store or FileOAuthStateStore(
Expand Down
2 changes: 2 additions & 0 deletions slack_bolt/oauth/oauth_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,7 @@ def sqlite3(
# state parameter related configurations
state_cookie_name: str = OAuthStateUtils.default_cookie_name,
state_expiration_seconds: int = OAuthStateUtils.default_expiration_seconds,
installation_store_bot_only: bool = False,
client: Optional[WebClient] = None,
logger: Optional[Logger] = None,
) -> "OAuthFlow":
Expand Down Expand Up @@ -135,6 +136,7 @@ def sqlite3(
installation_store=SQLite3InstallationStore(
database=database, client_id=client_id, logger=logger,
),
installation_store_bot_only=installation_store_bot_only,
# state parameter related configurations
state_store=SQLite3OAuthStateStore(
database=database,
Expand Down
8 changes: 7 additions & 1 deletion slack_bolt/oauth/oauth_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ class OAuthSettings:
authorization_url: str # default: https://slack.com/oauth/v2/authorize
# Installation Management
installation_store: InstallationStore
installation_store_bot_only: bool
authorize: Authorize
# state parameter related configurations
state_store: OAuthStateStore
Expand Down Expand Up @@ -64,6 +65,7 @@ def __init__(
authorization_url: Optional[str] = None,
# Installation Management
installation_store: Optional[InstallationStore] = None,
installation_store_bot_only: bool = False,
# state parameter related configurations
state_store: Optional[OAuthStateStore] = None,
state_cookie_name: str = OAuthStateUtils.default_cookie_name,
Expand All @@ -85,6 +87,7 @@ def __init__(
:param failure_url: Set a complete URL if you want to redirect end-users when an installation fails.
:param authorization_url: Set a URL if you want to customize the URL https://slack.com/oauth/v2/authorize
:param installation_store: Specify the instance of InstallationStore (Default: FileInstallationStore)
:param installation_store_bot_only: Use InstallationStore#find_bot if True (Default: False)
:param state_store: Specify the instance of InstallationStore (Default: FileOAuthStateStore)
:param state_cookie_name: The cookie name that is set for installers' browser. (Default: slack-app-oauth-state)
:param state_expiration_seconds: The seconds that the state value is alive (Default: 600 seconds)
Expand Down Expand Up @@ -123,8 +126,11 @@ def __init__(
self.installation_store = (
installation_store or get_or_create_default_installation_store(client_id)
)
self.installation_store_bot_only = installation_store_bot_only
self.authorize = InstallationStoreAuthorize(
logger=logger, installation_store=self.installation_store,
logger=logger,
installation_store=self.installation_store,
bot_only=self.installation_store_bot_only,
)
# state parameter related configurations
self.state_store = state_store or FileOAuthStateStore(
Expand Down
57 changes: 57 additions & 0 deletions tests/slack_bolt/authorization/test_authorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,63 @@ def test_installation_store_cached_legacy(self):
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 1 # cached

def test_installation_store_bot_only(self):
installation_store = MemoryInstallationStore()
authorize = InstallationStoreAuthorize(
logger=installation_store.logger,
installation_store=installation_store,
bot_only=True,
)
assert authorize.find_installation_available is True
assert authorize.bot_only is True
context = BoltContext()
context["client"] = WebClient(base_url=self.mock_api_server_base_url)
result = authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W11111"
)
assert authorize.find_installation_available is True
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 1

result = authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W11111"
)
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 2

def test_installation_store_cached_bot_only(self):
installation_store = MemoryInstallationStore()
authorize = InstallationStoreAuthorize(
logger=installation_store.logger,
installation_store=installation_store,
cache_enabled=True,
bot_only=True,
)
assert authorize.find_installation_available is True
assert authorize.bot_only is True
context = BoltContext()
context["client"] = WebClient(base_url=self.mock_api_server_base_url)
result = authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W11111"
)
assert authorize.find_installation_available is True
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 1

result = authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W11111"
)
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 1 # cached

def test_installation_store(self):
installation_store = MemoryInstallationStore()
authorize = InstallationStoreAuthorize(
Expand Down
59 changes: 59 additions & 0 deletions tests/slack_bolt_async/authorization/test_async_authorize.py
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,65 @@ async def test_installation_store_cached_legacy(self):
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 1 # cached

@pytest.mark.asyncio
async def test_installation_store_bot_only(self):
installation_store = MemoryInstallationStore()
authorize = AsyncInstallationStoreAuthorize(
logger=installation_store.logger,
installation_store=installation_store,
bot_only=True,
)
assert authorize.find_installation_available is None
assert authorize.bot_only is True
context = AsyncBoltContext()
context["client"] = self.client
result = await authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W11111"
)
assert authorize.find_installation_available is True
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 1

result = await authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W11111"
)
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 2

@pytest.mark.asyncio
async def test_installation_store_cached_bot_only(self):
installation_store = MemoryInstallationStore()
authorize = AsyncInstallationStoreAuthorize(
logger=installation_store.logger,
installation_store=installation_store,
cache_enabled=True,
bot_only=True,
)
assert authorize.find_installation_available is None
assert authorize.bot_only is True
context = AsyncBoltContext()
context["client"] = self.client
result = await authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W11111"
)
assert authorize.find_installation_available is True
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 1

result = await authorize(
context=context, enterprise_id="E111", team_id="T0G9PQBBK", user_id="W11111"
)
assert result.bot_id == "BZYBOTHED"
assert result.bot_user_id == "W23456789"
assert result.user_token is None
assert self.mock_received_requests["/auth.test"] == 1 # cached

@pytest.mark.asyncio
async def test_installation_store(self):
installation_store = MemoryInstallationStore()
Expand Down

0 comments on commit f3f1455

Please sign in to comment.