Skip to content

Commit c279590

Browse files
committed
Improve the app.installation_store field consistency
1 parent 68e9c25 commit c279590

File tree

9 files changed

+208
-24
lines changed

9 files changed

+208
-24
lines changed

slack_bolt/app/app.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@
5555
from slack_bolt.middleware.message_listener_matches import MessageListenerMatches
5656
from slack_bolt.middleware.url_verification import UrlVerification
5757
from slack_bolt.oauth import OAuthFlow
58+
from slack_bolt.oauth.internals import select_consistent_installation_store
5859
from slack_bolt.oauth.oauth_settings import OAuthSettings
5960
from slack_bolt.request import BoltRequest
6061
from slack_bolt.response import BoltResponse
@@ -157,17 +158,28 @@ def __init__(
157158

158159
if oauth_flow:
159160
self._oauth_flow = oauth_flow
160-
if self._installation_store is None:
161-
self._installation_store = self._oauth_flow.settings.installation_store
161+
installation_store = select_consistent_installation_store(
162+
client_id=self._oauth_flow.client_id,
163+
app_store=self._installation_store,
164+
oauth_flow_store=self._oauth_flow.settings.installation_store,
165+
logger=self._framework_logger,
166+
)
167+
self._installation_store = installation_store
168+
self._oauth_flow.settings.installation_store = installation_store
169+
162170
if self._oauth_flow._client is None:
163171
self._oauth_flow._client = self._client
164172
if self._authorize is None:
165173
self._authorize = self._oauth_flow.settings.authorize
166174
elif oauth_settings is not None:
167-
if self._installation_store:
168-
# Consistently use a single installation_store
169-
oauth_settings.installation_store = self._installation_store
170-
175+
installation_store = select_consistent_installation_store(
176+
client_id=oauth_settings.client_id,
177+
app_store=self._installation_store,
178+
oauth_flow_store=oauth_settings.installation_store,
179+
logger=self._framework_logger,
180+
)
181+
self._installation_store = installation_store
182+
oauth_settings.installation_store = installation_store
171183
self._oauth_flow = OAuthFlow(
172184
client=self.client, logger=self.logger, settings=oauth_settings
173185
)

slack_bolt/app/async_app.py

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from slack_bolt.middleware.message_listener_matches.async_message_listener_matches import (
88
AsyncMessageListenerMatches,
99
)
10+
from slack_bolt.oauth.async_internals import select_consistent_installation_store
1011
from slack_bolt.workflows.step.async_step import AsyncWorkflowStep
1112
from slack_bolt.workflows.step.async_step_middleware import AsyncWorkflowStepMiddleware
1213
from slack_sdk.oauth.installation_store.async_installation_store import (
@@ -167,18 +168,28 @@ def __init__(
167168

168169
if oauth_flow:
169170
self._async_oauth_flow = oauth_flow
170-
if self._async_installation_store is None:
171-
self._async_installation_store = (
172-
self._async_oauth_flow.settings.installation_store
173-
)
171+
installation_store = select_consistent_installation_store(
172+
client_id=self._async_oauth_flow.client_id,
173+
app_store=self._async_installation_store,
174+
oauth_flow_store=self._async_oauth_flow.settings.installation_store,
175+
logger=self._framework_logger,
176+
)
177+
self._async_installation_store = installation_store
178+
self._async_oauth_flow.settings.installation_store = installation_store
179+
174180
if self._async_oauth_flow._async_client is None:
175181
self._async_oauth_flow._async_client = self._async_client
176182
if self._async_authorize is None:
177183
self._async_authorize = self._async_oauth_flow.settings.authorize
178184
elif oauth_settings is not None:
179-
if self._async_installation_store:
180-
# Consistently use a single installation_store
181-
oauth_settings.installation_store = self._async_installation_store
185+
installation_store = select_consistent_installation_store(
186+
client_id=oauth_settings.client_id,
187+
app_store=self._async_installation_store,
188+
oauth_flow_store=oauth_settings.installation_store,
189+
logger=self._framework_logger,
190+
)
191+
self._async_installation_store = installation_store
192+
oauth_settings.installation_store = installation_store
182193

183194
self._async_oauth_flow = AsyncOAuthFlow(
184195
client=self._async_client, logger=self.logger, settings=oauth_settings

slack_bolt/logger/messages.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ def warning_token_skipped() -> str:
6464
)
6565

6666

67+
def warning_installation_store_conflicts() -> str:
68+
return "As you gave both `installation_store` and `oauth_settings`/`auth_flow`, the top level one is unused."
69+
70+
6771
def warning_unhandled_request(req: Union[BoltRequest, "AsyncBoltRequest"]) -> str: # type: ignore
6872
return f"Unhandled request ({req.body})"
6973

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
from logging import Logger
2+
from typing import Optional
3+
4+
from slack_sdk.oauth.installation_store import FileInstallationStore
5+
from slack_sdk.oauth.installation_store.async_installation_store import (
6+
AsyncInstallationStore,
7+
)
8+
9+
from ..logger.messages import warning_installation_store_conflicts
10+
11+
# key: client_id, value: AsyncInstallationStore
12+
default_installation_stores = {}
13+
14+
15+
def get_or_create_default_installation_store(client_id: str) -> AsyncInstallationStore:
16+
store = default_installation_stores.get(client_id)
17+
if store is None:
18+
store = FileInstallationStore(client_id=client_id)
19+
default_installation_stores[client_id] = store
20+
return store
21+
22+
23+
def select_consistent_installation_store(
24+
client_id: str,
25+
app_store: Optional[AsyncInstallationStore],
26+
oauth_flow_store: Optional[AsyncInstallationStore],
27+
logger: Logger,
28+
) -> Optional[AsyncInstallationStore]:
29+
default = get_or_create_default_installation_store(client_id)
30+
if app_store is not None:
31+
if oauth_flow_store is not None:
32+
if oauth_flow_store is default:
33+
# only app_store is intentionally set in this case
34+
return app_store
35+
36+
# if both are intentionally set, prioritize app_store
37+
if oauth_flow_store is not app_store:
38+
logger.warning(warning_installation_store_conflicts())
39+
return oauth_flow_store
40+
else:
41+
# only app_store is available
42+
return app_store
43+
else:
44+
# only oauth_flow_store is available
45+
return oauth_flow_store

slack_bolt/oauth/async_oauth_settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
AuthorizeUrlGenerator,
99
RedirectUriPageRenderer,
1010
)
11-
from slack_sdk.oauth.installation_store import FileInstallationStore
1211
from slack_sdk.oauth.installation_store.async_installation_store import (
1312
AsyncInstallationStore,
1413
)
@@ -20,6 +19,7 @@
2019
AsyncAuthorize,
2120
)
2221
from slack_bolt.error import BoltError
22+
from slack_bolt.oauth.async_internals import get_or_create_default_installation_store
2323
from slack_bolt.oauth.callback_options import CallbackOptions
2424

2525

@@ -122,8 +122,8 @@ def __init__(
122122
authorization_url or "https://slack.com/oauth/v2/authorize"
123123
)
124124
# Installation Management
125-
self.installation_store = installation_store or FileInstallationStore(
126-
client_id=client_id
125+
self.installation_store = (
126+
installation_store or get_or_create_default_installation_store(client_id)
127127
)
128128
self.authorize = AsyncInstallationStoreAuthorize(
129129
logger=logger, installation_store=self.installation_store,

slack_bolt/oauth/internals.py

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
from logging import Logger
2-
from typing import Optional, Union
2+
from typing import Optional
3+
from typing import Union
34

5+
from slack_sdk.oauth import InstallationStore
46
from slack_sdk.oauth import OAuthStateUtils, RedirectUriPageRenderer
7+
from slack_sdk.oauth.installation_store import FileInstallationStore
58
from slack_sdk.oauth.installation_store import Installation
69

710
from slack_bolt.request import BoltRequest
811
from slack_bolt.response import BoltResponse
12+
from ..logger.messages import warning_installation_store_conflicts
913

1014

1115
class CallbackResponseBuilder:
@@ -83,3 +87,40 @@ def _build_default_install_page_html(url: str) -> str:
8387
</body>
8488
</html>
8589
"""
90+
91+
92+
# key: client_id, value: InstallationStore
93+
default_installation_stores = {}
94+
95+
96+
def get_or_create_default_installation_store(client_id: str) -> InstallationStore:
97+
store = default_installation_stores.get(client_id)
98+
if store is None:
99+
store = FileInstallationStore(client_id=client_id)
100+
default_installation_stores[client_id] = store
101+
return store
102+
103+
104+
def select_consistent_installation_store(
105+
client_id: str,
106+
app_store: Optional[InstallationStore],
107+
oauth_flow_store: Optional[InstallationStore],
108+
logger: Logger,
109+
) -> Optional[InstallationStore]:
110+
default = get_or_create_default_installation_store(client_id)
111+
if app_store is not None:
112+
if oauth_flow_store is not None:
113+
if oauth_flow_store is default:
114+
# only app_store is intentionally set in this case
115+
return app_store
116+
117+
# if both are intentionally set, prioritize app_store
118+
if oauth_flow_store is not app_store:
119+
logger.warning(warning_installation_store_conflicts())
120+
return oauth_flow_store
121+
else:
122+
# only app_store is available
123+
return app_store
124+
else:
125+
# only oauth_flow_store is available
126+
return oauth_flow_store

slack_bolt/oauth/oauth_settings.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,11 @@
1010
AuthorizeUrlGenerator,
1111
RedirectUriPageRenderer,
1212
)
13-
from slack_sdk.oauth.installation_store import FileInstallationStore
1413
from slack_sdk.oauth.state_store import FileOAuthStateStore
1514

1615
from slack_bolt.authorization.authorize import Authorize, InstallationStoreAuthorize
1716
from slack_bolt.error import BoltError
17+
from slack_bolt.oauth.internals import get_or_create_default_installation_store
1818
from slack_bolt.oauth.callback_options import CallbackOptions
1919

2020

@@ -116,8 +116,8 @@ def __init__(
116116
authorization_url or "https://slack.com/oauth/v2/authorize"
117117
)
118118
# Installation Management
119-
self.installation_store = installation_store or FileInstallationStore(
120-
client_id=client_id
119+
self.installation_store = (
120+
installation_store or get_or_create_default_installation_store(client_id)
121121
)
122122
self.authorize = InstallationStoreAuthorize(
123123
logger=logger, installation_store=self.installation_store,

tests/scenario_tests/test_app.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,3 +121,37 @@ def authorize() -> AuthorizeResult:
121121

122122
with pytest.raises(BoltError):
123123
App(signing_secret="valid", authorize=authorize, oauth_flow=oauth_flow)
124+
125+
def test_installation_store_conflicts(self):
126+
store1 = FileInstallationStore()
127+
store2 = FileInstallationStore()
128+
app = App(
129+
signing_secret="valid",
130+
oauth_settings=OAuthSettings(
131+
client_id="111.222", client_secret="valid", installation_store=store1,
132+
),
133+
installation_store=store2,
134+
)
135+
assert app.installation_store is store1
136+
137+
app = App(
138+
signing_secret="valid",
139+
oauth_flow=OAuthFlow(
140+
settings=OAuthSettings(
141+
client_id="111.222",
142+
client_secret="valid",
143+
installation_store=store1,
144+
)
145+
),
146+
installation_store=store2,
147+
)
148+
assert app.installation_store is store1
149+
150+
app = App(
151+
signing_secret="valid",
152+
oauth_flow=OAuthFlow(
153+
settings=OAuthSettings(client_id="111.222", client_secret="valid",)
154+
),
155+
installation_store=store1,
156+
)
157+
assert app.installation_store is store1

tests/scenario_tests_async/test_app.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@
88
from slack_bolt.error import BoltError
99
from slack_bolt.oauth.async_oauth_flow import AsyncOAuthFlow
1010
from slack_bolt.oauth.async_oauth_settings import AsyncOAuthSettings
11-
from slack_bolt.oauth.oauth_settings import OAuthSettings
1211
from tests.utils import remove_os_env_temporarily, restore_os_env
1312

1413

@@ -90,18 +89,22 @@ def test_valid_multi_auth_client_id_absence(self):
9089
with pytest.raises(BoltError):
9190
AsyncApp(
9291
signing_secret="valid",
93-
oauth_settings=OAuthSettings(client_id=None, client_secret="valid"),
92+
oauth_settings=AsyncOAuthSettings(
93+
client_id=None, client_secret="valid"
94+
),
9495
)
9596

9697
def test_valid_multi_auth_secret_absence(self):
9798
with pytest.raises(BoltError):
9899
AsyncApp(
99100
signing_secret="valid",
100-
oauth_settings=OAuthSettings(client_id="111.222", client_secret=None),
101+
oauth_settings=AsyncOAuthSettings(
102+
client_id="111.222", client_secret=None
103+
),
101104
)
102105

103106
def test_authorize_conflicts(self):
104-
oauth_settings = OAuthSettings(
107+
oauth_settings = AsyncOAuthSettings(
105108
client_id="111.222",
106109
client_secret="valid",
107110
installation_store=FileInstallationStore(),
@@ -127,3 +130,37 @@ def authorize() -> AuthorizeResult:
127130

128131
with pytest.raises(BoltError):
129132
AsyncApp(signing_secret="valid", authorize=authorize, oauth_flow=oauth_flow)
133+
134+
def test_installation_store_conflicts(self):
135+
store1 = FileInstallationStore()
136+
store2 = FileInstallationStore()
137+
app = AsyncApp(
138+
signing_secret="valid",
139+
oauth_settings=AsyncOAuthSettings(
140+
client_id="111.222", client_secret="valid", installation_store=store1,
141+
),
142+
installation_store=store2,
143+
)
144+
assert app.installation_store is store1
145+
146+
app = AsyncApp(
147+
signing_secret="valid",
148+
oauth_flow=AsyncOAuthFlow(
149+
settings=AsyncOAuthSettings(
150+
client_id="111.222",
151+
client_secret="valid",
152+
installation_store=store1,
153+
)
154+
),
155+
installation_store=store2,
156+
)
157+
assert app.installation_store is store1
158+
159+
app = AsyncApp(
160+
signing_secret="valid",
161+
oauth_flow=AsyncOAuthFlow(
162+
settings=AsyncOAuthSettings(client_id="111.222", client_secret="valid",)
163+
),
164+
installation_store=store1,
165+
)
166+
assert app.installation_store is store1

0 commit comments

Comments
 (0)