44
55from slack_sdk .errors import SlackApiError
66from slack_sdk .oauth import InstallationStore
7- from slack_sdk .oauth .installation_store import Bot
7+ from slack_sdk .oauth .installation_store . models . bot import Bot
88from slack_sdk .oauth .installation_store .models .installation import Installation
99from slack_sdk .oauth .token_rotation .rotator import TokenRotator
10+ from slack_sdk .web import WebClient
1011
1112from slack_bolt .authorization .authorize_args import AuthorizeArgs
1213from slack_bolt .authorization .authorize_result import AuthorizeResult
@@ -33,8 +34,8 @@ def __call__(
3334
3435
3536class CallableAuthorize (Authorize ):
36- """When you pass the authorize argument in AsyncApp constructor,
37- This authorize implementation will be used.
37+ """When you pass the ` authorize` argument in AsyncApp constructor,
38+ This ` authorize` implementation will be used.
3839 """
3940
4041 def __init__ (
@@ -102,9 +103,9 @@ def __call__(
102103
103104
104105class InstallationStoreAuthorize (Authorize ):
105- """If you use the OAuth flow settings, this authorize implementation will be used.
106+ """If you use the OAuth flow settings, this ` authorize` implementation will be used.
106107 As long as your own InstallationStore (or the built-in ones) works as you expect,
107- you can expect that the authorize layer should work for you without any customization.
108+ you can expect that the ` authorize` layer should work for you without any customization.
108109 """
109110
110111 authorize_result_cache : Dict [str , AuthorizeResult ]
@@ -129,6 +130,7 @@ def __init__(
129130 # use only InstallationStore#find_bot(enterprise_id, team_id)
130131 bot_only : bool = False ,
131132 cache_enabled : bool = False ,
133+ client : Optional [WebClient ] = None ,
132134 ):
133135 self .logger = logger
134136 self .installation_store = installation_store
@@ -143,6 +145,7 @@ def __init__(
143145 self .token_rotator = TokenRotator (
144146 client_id = client_id ,
145147 client_secret = client_secret ,
148+ client = client ,
146149 )
147150 else :
148151 self .token_rotator = None
@@ -168,61 +171,78 @@ def __call__(
168171 try :
169172 # Note that this is the latest information for the org/workspace.
170173 # The installer may not be the user associated with this incoming request.
171- installation : Optional [
174+ latest_installation : Optional [
172175 Installation
173176 ] = self .installation_store .find_installation (
174177 enterprise_id = enterprise_id ,
175178 team_id = team_id ,
176179 is_enterprise_install = context .is_enterprise_install ,
177180 )
178- if installation is not None :
179- if installation .user_id != user_id :
181+ # If the user_token in the latest_installation is not for the user associated with this request,
182+ # we'll fetch a different installation for the user below.
183+ # The example use cases are:
184+ # - The app's installation requires both bot and user tokens
185+ # - The app has two installation paths 1) bot installation 2) individual user authorization
186+ this_user_installation : Optional [Installation ] = None
187+
188+ if latest_installation is not None :
189+ # Save the latest bot token
190+ bot_token = latest_installation .bot_token # this still can be None
191+ user_token = (
192+ latest_installation .user_token
193+ ) # this still can be None
194+
195+ if latest_installation .user_id != user_id :
180196 # First off, remove the user token as the installer is a different user
181- installation .user_token = None
182- installation .user_scopes = []
197+ latest_installation .user_token = None
198+ latest_installation .user_scopes = []
183199
184200 # try to fetch the request user's installation
185201 # to reflect the user's access token if exists
186- user_installation = self .installation_store .find_installation (
187- enterprise_id = enterprise_id ,
188- team_id = team_id ,
189- user_id = user_id ,
190- is_enterprise_install = context .is_enterprise_install ,
202+ this_user_installation = (
203+ self .installation_store .find_installation (
204+ enterprise_id = enterprise_id ,
205+ team_id = team_id ,
206+ user_id = user_id ,
207+ is_enterprise_install = context .is_enterprise_install ,
208+ )
191209 )
192- if user_installation is not None :
193- # Overwrite the installation with the one for this user
194- installation = user_installation
210+ if this_user_installation is not None :
211+ user_token = this_user_installation .user_token
212+ if latest_installation .bot_token is None :
213+ # If latest_installation has a bot token, we never overwrite the value
214+ bot_token = this_user_installation .bot_token
195215
196- bot_token , user_token = (
197- installation .bot_token ,
198- installation .user_token ,
199- )
200- if (
201- installation .user_refresh_token is not None
202- or installation .bot_refresh_token is not None
203- ):
204- if self .token_rotator is None :
205- raise BoltError (self ._config_error_message )
206- refreshed = self .token_rotator .perform_token_rotation (
207- installation = installation ,
208- minutes_before_expiration = self .token_rotation_expiration_minutes ,
209- )
210- if refreshed is not None :
211- self .installation_store .save (refreshed )
212- bot_token , user_token = (
213- refreshed .bot_token ,
214- refreshed .user_token ,
216+ # If token rotation is enabled, running rotation may be needed here
217+ refreshed = self ._rotate_and_save_tokens_if_necessary (
218+ this_user_installation
215219 )
220+ if refreshed is not None :
221+ user_token = refreshed .user_token
222+ if latest_installation .bot_token is None :
223+ # If latest_installation has a bot token, we never overwrite the value
224+ bot_token = refreshed .bot_token
225+
226+ # If token rotation is enabled, running rotation may be needed here
227+ refreshed = self ._rotate_and_save_tokens_if_necessary (
228+ latest_installation
229+ )
230+ if refreshed is not None :
231+ bot_token = refreshed .bot_token
232+ if this_user_installation is None :
233+ # Only when we don't have `this_user_installation` here,
234+ # the `user_token` is for the user associated with this request
235+ user_token = refreshed .user_token
216236
217237 except NotImplementedError as _ :
218238 self .find_installation_available = False
219239
220240 if (
221- # If you intentionally use only find_bot / delete_bot,
241+ # If you intentionally use only ` find_bot` / ` delete_bot` ,
222242 self .bot_only
223- # If find_installation method is not available,
243+ # If the ` find_installation` method is not available,
224244 or not self .find_installation_available
225- # If find_installation did not return data and find_bot method is available,
245+ # If the ` find_installation` method did not return data and find_bot method is available,
226246 or (
227247 self .find_bot_available is True
228248 and bot_token is None
@@ -290,3 +310,26 @@ def _debug_log_for_not_found(
290310 "No installation data found "
291311 f"for enterprise_id: { enterprise_id } team_id: { team_id } "
292312 )
313+
314+ def _rotate_and_save_tokens_if_necessary (
315+ self , installation : Optional [Installation ]
316+ ) -> Optional [Installation ]:
317+ if installation is None or (
318+ installation .user_refresh_token is None
319+ and installation .bot_refresh_token is None
320+ ):
321+ # No need to rotate tokens
322+ return None
323+
324+ if self .token_rotator is None :
325+ # Token rotation is required but this Bolt app is not properly configured
326+ raise BoltError (self ._config_error_message )
327+
328+ refreshed : Optional [Installation ] = self .token_rotator .perform_token_rotation (
329+ installation = installation ,
330+ minutes_before_expiration = self .token_rotation_expiration_minutes ,
331+ )
332+ if refreshed is not None :
333+ # Save the refreshed data in database for following requests
334+ self .installation_store .save (refreshed )
335+ return refreshed
0 commit comments