Skip to content

Commit 2837e11

Browse files
authored
Enhance AuthorizeResult to have bot/user_scopes & resolve user_id for user token (#855)
Enhance AuthorizeResult to have bot/user_scopes when they're available (optional) Also, this commit fixes the bug where authorize_result.user_id is not resolved when both bot and user tokens exist.
1 parent 20d250f commit 2837e11

File tree

5 files changed

+410
-31
lines changed

5 files changed

+410
-31
lines changed

slack_bolt/authorization/async_authorize.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from logging import Logger
2-
from typing import Optional, Callable, Awaitable, Dict, Any
2+
from typing import Optional, Callable, Awaitable, Dict, Any, List
33

44
from slack_sdk.errors import SlackApiError
55
from slack_sdk.oauth.installation_store import Bot, Installation
@@ -156,14 +156,18 @@ async def __call__(
156156

157157
bot_token: Optional[str] = None
158158
user_token: Optional[str] = None
159+
bot_scopes: Optional[List[str]] = None
160+
user_scopes: Optional[List[str]] = None
161+
latest_bot_installation: Optional[Installation] = None
162+
this_user_installation: Optional[Installation] = None
159163

160164
if not self.bot_only and self.find_installation_available:
161165
# Since v1.1, this is the default way.
162166
# If you want to use find_bot / delete_bot only, you can set bot_only as True.
163167
try:
164168
# Note that this is the latest information for the org/workspace.
165169
# The installer may not be the user associated with this incoming request.
166-
latest_installation: Optional[Installation] = await self.installation_store.async_find_installation(
170+
latest_bot_installation = await self.installation_store.async_find_installation(
167171
enterprise_id=enterprise_id,
168172
team_id=team_id,
169173
is_enterprise_install=context.is_enterprise_install,
@@ -173,20 +177,20 @@ async def __call__(
173177
# The example use cases are:
174178
# - The app's installation requires both bot and user tokens
175179
# - The app has two installation paths 1) bot installation 2) individual user authorization
176-
this_user_installation: Optional[Installation] = None
177-
178-
if latest_installation is not None:
180+
if latest_bot_installation is not None:
179181
# Save the latest bot token
180-
bot_token = latest_installation.bot_token # this still can be None
181-
user_token = latest_installation.user_token # this still can be None
182+
bot_token = latest_bot_installation.bot_token # this still can be None
183+
user_token = latest_bot_installation.user_token # this still can be None
184+
bot_scopes = latest_bot_installation.bot_scopes # this still can be None
185+
user_scopes = latest_bot_installation.user_scopes # this still can be None
182186

183-
if latest_installation.user_id != user_id:
187+
if latest_bot_installation.user_id != user_id:
184188
# First off, remove the user token as the installer is a different user
185189
user_token = None
186-
latest_installation.user_token = None
187-
latest_installation.user_refresh_token = None
188-
latest_installation.user_token_expires_at = None
189-
latest_installation.user_scopes = []
190+
latest_bot_installation.user_token = None
191+
latest_bot_installation.user_refresh_token = None
192+
latest_bot_installation.user_token_expires_at = None
193+
latest_bot_installation.user_scopes = []
190194

191195
# try to fetch the request user's installation
192196
# to reflect the user's access token if exists
@@ -198,26 +202,32 @@ async def __call__(
198202
)
199203
if this_user_installation is not None:
200204
user_token = this_user_installation.user_token
201-
if latest_installation.bot_token is None:
205+
user_scopes = this_user_installation.user_scopes
206+
if latest_bot_installation.bot_token is None:
202207
# If latest_installation has a bot token, we never overwrite the value
203208
bot_token = this_user_installation.bot_token
209+
bot_scopes = this_user_installation.bot_scopes
204210

205211
# If token rotation is enabled, running rotation may be needed here
206212
refreshed = await self._rotate_and_save_tokens_if_necessary(this_user_installation)
207213
if refreshed is not None:
208214
user_token = refreshed.user_token
209-
if latest_installation.bot_token is None:
215+
user_scopes = refreshed.user_scopes
216+
if latest_bot_installation.bot_token is None:
210217
# If latest_installation has a bot token, we never overwrite the value
211218
bot_token = refreshed.bot_token
219+
bot_scopes = refreshed.bot_scopes
212220

213221
# If token rotation is enabled, running rotation may be needed here
214-
refreshed = await self._rotate_and_save_tokens_if_necessary(latest_installation)
222+
refreshed = await self._rotate_and_save_tokens_if_necessary(latest_bot_installation)
215223
if refreshed is not None:
216224
bot_token = refreshed.bot_token
225+
bot_scopes = refreshed.bot_scopes
217226
if this_user_installation is None:
218227
# Only when we don't have `this_user_installation` here,
219228
# the `user_token` is for the user associated with this request
220229
user_token = refreshed.user_token
230+
user_scopes = refreshed.user_scopes
221231

222232
except NotImplementedError as _:
223233
self.find_installation_available = False
@@ -238,6 +248,7 @@ async def __call__(
238248
)
239249
if bot is not None:
240250
bot_token = bot.bot_token
251+
bot_scopes = bot.bot_scopes
241252
if bot.bot_refresh_token is not None:
242253
# Token rotation
243254
if self.token_rotator is None:
@@ -249,6 +260,7 @@ async def __call__(
249260
if refreshed is not None:
250261
await self.installation_store.async_save_bot(refreshed)
251262
bot_token = refreshed.bot_token
263+
bot_scopes = refreshed.bot_scopes
252264

253265
except NotImplementedError as _:
254266
self.find_bot_available = False
@@ -267,10 +279,16 @@ async def __call__(
267279

268280
try:
269281
auth_test_api_response = await context.client.auth_test(token=token)
282+
user_auth_test_response = None
283+
if user_token is not None and token != user_token:
284+
user_auth_test_response = await context.client.auth_test(token=user_token)
270285
authorize_result = AuthorizeResult.from_auth_test_response(
271286
auth_test_response=auth_test_api_response,
287+
user_auth_test_response=user_auth_test_response,
272288
bot_token=bot_token,
273289
user_token=user_token,
290+
bot_scopes=bot_scopes,
291+
user_scopes=user_scopes,
274292
)
275293
if self.cache_enabled:
276294
self.authorize_result_cache[token] = authorize_result

slack_bolt/authorization/authorize.py

Lines changed: 33 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from logging import Logger
2-
from typing import Optional, Callable, Dict, Any
2+
from typing import Optional, Callable, Dict, Any, List
33

44
from slack_sdk.errors import SlackApiError
55
from slack_sdk.oauth import InstallationStore
@@ -156,14 +156,18 @@ def __call__(
156156

157157
bot_token: Optional[str] = None
158158
user_token: Optional[str] = None
159+
bot_scopes: Optional[List[str]] = None
160+
user_scopes: Optional[List[str]] = None
161+
latest_bot_installation: Optional[Installation] = None
162+
this_user_installation: Optional[Installation] = None
159163

160164
if not self.bot_only and self.find_installation_available:
161165
# Since v1.1, this is the default way.
162166
# If you want to use find_bot / delete_bot only, you can set bot_only as True.
163167
try:
164168
# Note that this is the latest information for the org/workspace.
165169
# The installer may not be the user associated with this incoming request.
166-
latest_installation: Optional[Installation] = self.installation_store.find_installation(
170+
latest_bot_installation = self.installation_store.find_installation(
167171
enterprise_id=enterprise_id,
168172
team_id=team_id,
169173
is_enterprise_install=context.is_enterprise_install,
@@ -173,20 +177,20 @@ def __call__(
173177
# The example use cases are:
174178
# - The app's installation requires both bot and user tokens
175179
# - The app has two installation paths 1) bot installation 2) individual user authorization
176-
this_user_installation: Optional[Installation] = None
177-
178-
if latest_installation is not None:
180+
if latest_bot_installation is not None:
179181
# Save the latest bot token
180-
bot_token = latest_installation.bot_token # this still can be None
181-
user_token = latest_installation.user_token # this still can be None
182+
bot_token = latest_bot_installation.bot_token # this still can be None
183+
user_token = latest_bot_installation.user_token # this still can be None
184+
bot_scopes = latest_bot_installation.bot_scopes # this still can be None
185+
user_scopes = latest_bot_installation.user_scopes # this still can be None
182186

183-
if latest_installation.user_id != user_id:
187+
if latest_bot_installation.user_id != user_id:
184188
# First off, remove the user token as the installer is a different user
185189
user_token = None
186-
latest_installation.user_token = None
187-
latest_installation.user_refresh_token = None
188-
latest_installation.user_token_expires_at = None
189-
latest_installation.user_scopes = []
190+
latest_bot_installation.user_token = None
191+
latest_bot_installation.user_refresh_token = None
192+
latest_bot_installation.user_token_expires_at = None
193+
latest_bot_installation.user_scopes = []
190194

191195
# try to fetch the request user's installation
192196
# to reflect the user's access token if exists
@@ -198,26 +202,32 @@ def __call__(
198202
)
199203
if this_user_installation is not None:
200204
user_token = this_user_installation.user_token
201-
if latest_installation.bot_token is None:
205+
user_scopes = this_user_installation.user_scopes
206+
if latest_bot_installation.bot_token is None:
202207
# If latest_installation has a bot token, we never overwrite the value
203208
bot_token = this_user_installation.bot_token
209+
bot_scopes = this_user_installation.bot_scopes
204210

205211
# If token rotation is enabled, running rotation may be needed here
206212
refreshed = self._rotate_and_save_tokens_if_necessary(this_user_installation)
207213
if refreshed is not None:
208214
user_token = refreshed.user_token
209-
if latest_installation.bot_token is None:
215+
user_scopes = refreshed.user_scopes
216+
if latest_bot_installation.bot_token is None:
210217
# If latest_installation has a bot token, we never overwrite the value
211218
bot_token = refreshed.bot_token
219+
bot_scopes = refreshed.bot_scopes
212220

213221
# If token rotation is enabled, running rotation may be needed here
214-
refreshed = self._rotate_and_save_tokens_if_necessary(latest_installation)
222+
refreshed = self._rotate_and_save_tokens_if_necessary(latest_bot_installation)
215223
if refreshed is not None:
216224
bot_token = refreshed.bot_token
225+
bot_scopes = refreshed.bot_scopes
217226
if this_user_installation is None:
218227
# Only when we don't have `this_user_installation` here,
219228
# the `user_token` is for the user associated with this request
220229
user_token = refreshed.user_token
230+
user_scopes = refreshed.user_scopes
221231

222232
except NotImplementedError as _:
223233
self.find_installation_available = False
@@ -238,6 +248,7 @@ def __call__(
238248
)
239249
if bot is not None:
240250
bot_token = bot.bot_token
251+
bot_scopes = bot.bot_scopes
241252
if bot.bot_refresh_token is not None:
242253
# Token rotation
243254
if self.token_rotator is None:
@@ -249,6 +260,7 @@ def __call__(
249260
if refreshed is not None:
250261
self.installation_store.save_bot(refreshed)
251262
bot_token = refreshed.bot_token
263+
bot_scopes = refreshed.bot_scopes
252264

253265
except NotImplementedError as _:
254266
self.find_bot_available = False
@@ -267,10 +279,16 @@ def __call__(
267279

268280
try:
269281
auth_test_api_response = context.client.auth_test(token=token)
282+
user_auth_test_response = None
283+
if user_token is not None and token != user_token:
284+
user_auth_test_response = context.client.auth_test(token=user_token)
270285
authorize_result = AuthorizeResult.from_auth_test_response(
271286
auth_test_response=auth_test_api_response,
287+
user_auth_test_response=user_auth_test_response,
272288
bot_token=bot_token,
273289
user_token=user_token,
290+
bot_scopes=bot_scopes,
291+
user_scopes=user_scopes,
274292
)
275293
if self.cache_enabled:
276294
self.authorize_result_cache[token] = authorize_result

slack_bolt/authorization/authorize_result.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Optional
1+
from typing import Optional, List, Union
22

33
from slack_sdk.web import SlackResponse
44

@@ -11,8 +11,10 @@ class AuthorizeResult(dict):
1111
bot_id: Optional[str]
1212
bot_user_id: Optional[str]
1313
bot_token: Optional[str]
14+
bot_scopes: Optional[List[str]] # since v1.17
1415
user_id: Optional[str]
1516
user_token: Optional[str]
17+
user_scopes: Optional[List[str]] # since v1.17
1618

1719
def __init__(
1820
self,
@@ -23,9 +25,11 @@ def __init__(
2325
bot_user_id: Optional[str] = None,
2426
bot_id: Optional[str] = None,
2527
bot_token: Optional[str] = None,
28+
bot_scopes: Optional[Union[List[str], str]] = None,
2629
# user
2730
user_id: Optional[str] = None,
2831
user_token: Optional[str] = None,
32+
user_scopes: Optional[Union[List[str], str]] = None,
2933
):
3034
"""
3135
Args:
@@ -34,39 +38,56 @@ def __init__(
3438
bot_user_id: Bot user's User ID starting with either `U` or `W`
3539
bot_id: Bot ID starting with `B`
3640
bot_token: Bot user access token starting with `xoxb-`
41+
bot_scopes: The scopes associated with the bot token
3742
user_id: The request user ID
3843
user_token: User access token starting with `xoxp-`
44+
user_scopes: The scopes associated wth the user token
3945
"""
4046
self["enterprise_id"] = self.enterprise_id = enterprise_id
4147
self["team_id"] = self.team_id = team_id
4248
# bot
4349
self["bot_user_id"] = self.bot_user_id = bot_user_id
4450
self["bot_id"] = self.bot_id = bot_id
4551
self["bot_token"] = self.bot_token = bot_token
52+
if bot_scopes is not None and isinstance(bot_scopes, str):
53+
bot_scopes = [scope.strip() for scope in bot_scopes.split(",")]
54+
self["bot_scopes"] = self.bot_scopes = bot_scopes # type: ignore
4655
# user
4756
self["user_id"] = self.user_id = user_id
4857
self["user_token"] = self.user_token = user_token
58+
if user_scopes is not None and isinstance(user_scopes, str):
59+
user_scopes = [scope.strip() for scope in user_scopes.split(",")]
60+
self["user_scopes"] = self.user_scopes = user_scopes # type: ignore
4961

5062
@classmethod
5163
def from_auth_test_response(
5264
cls,
5365
*,
5466
bot_token: Optional[str] = None,
5567
user_token: Optional[str] = None,
68+
bot_scopes: Optional[Union[List[str], str]] = None,
69+
user_scopes: Optional[Union[List[str], str]] = None,
5670
auth_test_response: SlackResponse,
71+
user_auth_test_response: Optional[SlackResponse] = None,
5772
) -> "AuthorizeResult":
5873
bot_user_id: Optional[str] = ( # type:ignore
5974
auth_test_response.get("user_id") if auth_test_response.get("bot_id") is not None else None
6075
)
6176
user_id: Optional[str] = ( # type:ignore
6277
auth_test_response.get("user_id") if auth_test_response.get("bot_id") is None else None
6378
)
79+
# Since v1.28, user_id can be set when user_token w/ its auth.test response exists
80+
if user_id is None and user_auth_test_response is not None:
81+
user_id: Optional[str] = user_auth_test_response.get("user_id") # type:ignore
82+
6483
return AuthorizeResult(
6584
enterprise_id=auth_test_response.get("enterprise_id"),
6685
team_id=auth_test_response.get("team_id"),
6786
bot_id=auth_test_response.get("bot_id"),
6887
bot_user_id=bot_user_id,
88+
bot_scopes=bot_scopes,
6989
user_id=user_id,
7090
bot_token=bot_token,
7191
user_token=user_token,
92+
user_scopes=user_scopes,
7293
)

0 commit comments

Comments
 (0)