Skip to content

Commit 65d1435

Browse files
authored
Introduce actor enterprise/team/user_id for Slack Connect events (#854)
1 parent b48c9fa commit 65d1435

File tree

15 files changed

+1526
-31
lines changed

15 files changed

+1526
-31
lines changed

slack_bolt/adapter/aws_lambda/lambda_s3_oauth_flow.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ def __init__(
5959
client_secret=settings.client_secret,
6060
installation_store=settings.installation_store,
6161
bot_only=settings.installation_store_bot_only,
62+
user_token_resolution=(settings.user_token_resolution if settings is not None else "authed_user"),
6263
)
6364

6465
OAuthFlow.__init__(self, client=client, logger=logger, settings=settings)

slack_bolt/app/app.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,7 @@ def message_hello(message, say):
238238
logger=self._framework_logger,
239239
bot_only=installation_store_bot_only,
240240
client=self._client, # for proxy use cases etc.
241+
user_token_resolution=(settings.user_token_resolution if settings is not None else "authed_user"),
241242
)
242243

243244
self._oauth_flow: Optional[OAuthFlow] = None
@@ -379,7 +380,13 @@ def _init_middleware_list(
379380
else:
380381
raise BoltError(error_token_required())
381382
else:
382-
self._middleware_list.append(MultiTeamsAuthorization(authorize=self._authorize, base_logger=self._base_logger))
383+
self._middleware_list.append(
384+
MultiTeamsAuthorization(
385+
authorize=self._authorize,
386+
base_logger=self._base_logger,
387+
user_token_resolution=self._oauth_flow.settings.user_token_resolution,
388+
)
389+
)
383390
if ignoring_self_events_enabled is True:
384391
self._middleware_list.append(IgnoringSelfEvents(base_logger=self._base_logger))
385392
if url_verification_enabled is True:

slack_bolt/app/async_app.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,7 @@ async def message_hello(message, say): # async function
243243
logger=self._framework_logger,
244244
bot_only=installation_store_bot_only,
245245
client=self._async_client, # for proxy use cases etc.
246+
user_token_resolution=(settings.user_token_resolution if settings is not None else "authed_user"),
246247
)
247248

248249
self._async_oauth_flow: Optional[AsyncOAuthFlow] = None
@@ -374,7 +375,11 @@ def _init_async_middleware_list(
374375
raise BoltError(error_token_required())
375376
else:
376377
self._async_middleware_list.append(
377-
AsyncMultiTeamsAuthorization(authorize=self._async_authorize, base_logger=self._base_logger)
378+
AsyncMultiTeamsAuthorization(
379+
authorize=self._async_authorize,
380+
base_logger=self._base_logger,
381+
user_token_resolution=self._async_oauth_flow.settings.user_token_resolution,
382+
)
378383
)
379384

380385
if ignoring_self_events_enabled is True:

slack_bolt/authorization/async_authorize.py

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,10 @@ async def __call__(
3030
enterprise_id: Optional[str],
3131
team_id: Optional[str], # can be None for org-wide installed apps
3232
user_id: Optional[str],
33+
# actor_* can be used only when user_token_resolution: "actor" is set
34+
actor_enterprise_id: Optional[str] = None,
35+
actor_team_id: Optional[str] = None,
36+
actor_user_id: Optional[str] = None,
3337
) -> Optional[AuthorizeResult]:
3438
raise NotImplementedError()
3539

@@ -51,6 +55,10 @@ async def __call__(
5155
enterprise_id: Optional[str],
5256
team_id: Optional[str], # can be None for org-wide installed apps
5357
user_id: Optional[str],
58+
# actor_* can be used only when user_token_resolution: "actor" is set
59+
actor_enterprise_id: Optional[str] = None,
60+
actor_team_id: Optional[str] = None,
61+
actor_user_id: Optional[str] = None,
5462
) -> Optional[AuthorizeResult]:
5563
try:
5664
all_available_args = {
@@ -66,6 +74,9 @@ async def __call__(
6674
"enterprise_id": enterprise_id,
6775
"team_id": team_id,
6876
"user_id": user_id,
77+
"actor_enterprise_id": actor_enterprise_id,
78+
"actor_team_id": actor_team_id,
79+
"actor_user_id": actor_user_id,
6980
}
7081
for k, v in context.items():
7182
if k not in all_available_args:
@@ -103,6 +114,8 @@ class AsyncInstallationStoreAuthorize(AsyncAuthorize):
103114
"""
104115

105116
authorize_result_cache: Dict[str, AuthorizeResult]
117+
bot_only: bool
118+
user_token_resolution: str
106119
find_installation_available: Optional[bool]
107120
find_bot_available: Optional[bool]
108121
token_rotator: Optional[AsyncTokenRotator]
@@ -122,10 +135,13 @@ def __init__(
122135
bot_only: bool = False,
123136
cache_enabled: bool = False,
124137
client: Optional[AsyncWebClient] = None,
138+
# Since v1.27, user token resolution can be actor ID based when the mode is enabled
139+
user_token_resolution: str = "authed_user",
125140
):
126141
self.logger = logger
127142
self.installation_store = installation_store
128143
self.bot_only = bot_only
144+
self.user_token_resolution = user_token_resolution
129145
self.cache_enabled = cache_enabled
130146
self.authorize_result_cache = {}
131147
self.find_installation_available = None
@@ -147,6 +163,10 @@ async def __call__(
147163
enterprise_id: Optional[str],
148164
team_id: Optional[str], # can be None for org-wide installed apps
149165
user_id: Optional[str],
166+
# actor_* can be used only when user_token_resolution: "actor" is set
167+
actor_enterprise_id: Optional[str] = None,
168+
actor_team_id: Optional[str] = None,
169+
actor_user_id: Optional[str] = None,
150170
) -> Optional[AuthorizeResult]:
151171

152172
if self.find_installation_available is None:
@@ -194,16 +214,34 @@ async def __call__(
194214

195215
# try to fetch the request user's installation
196216
# to reflect the user's access token if exists
197-
this_user_installation = await self.installation_store.async_find_installation(
198-
enterprise_id=enterprise_id,
199-
team_id=team_id,
200-
user_id=user_id,
201-
is_enterprise_install=context.is_enterprise_install,
202-
)
217+
# try to fetch the request user's installation
218+
# to reflect the user's access token if exists
219+
if self.user_token_resolution == "actor":
220+
if actor_enterprise_id is not None or actor_team_id is not None:
221+
# Note that actor_team_id can be absent for app_mention events
222+
this_user_installation = await self.installation_store.async_find_installation(
223+
enterprise_id=actor_enterprise_id,
224+
team_id=actor_team_id,
225+
user_id=actor_user_id,
226+
is_enterprise_install=None,
227+
)
228+
else:
229+
this_user_installation = await self.installation_store.async_find_installation(
230+
enterprise_id=enterprise_id,
231+
team_id=team_id,
232+
user_id=user_id,
233+
is_enterprise_install=context.is_enterprise_install,
234+
)
203235
if this_user_installation is not None:
204236
user_token = this_user_installation.user_token
205237
user_scopes = this_user_installation.user_scopes
206-
if latest_bot_installation.bot_token is None:
238+
if (
239+
latest_bot_installation.bot_token is None
240+
# enterprise_id/team_id can be different for Slack Connect channel events
241+
# when enabling user_token_resolution: "actor"
242+
and latest_bot_installation.enterprise_id == this_user_installation.enterprise_id
243+
and latest_bot_installation.team_id == this_user_installation.team_id
244+
):
207245
# If latest_installation has a bot token, we never overwrite the value
208246
bot_token = this_user_installation.bot_token
209247
bot_scopes = this_user_installation.bot_scopes
@@ -213,7 +251,13 @@ async def __call__(
213251
if refreshed is not None:
214252
user_token = refreshed.user_token
215253
user_scopes = refreshed.user_scopes
216-
if latest_bot_installation.bot_token is None:
254+
if (
255+
latest_bot_installation.bot_token is None
256+
# enterprise_id/team_id can be different for Slack Connect channel events
257+
# when enabling user_token_resolution: "actor"
258+
and latest_bot_installation.enterprise_id == this_user_installation.enterprise_id
259+
and latest_bot_installation.team_id == this_user_installation.team_id
260+
):
217261
# If latest_installation has a bot token, we never overwrite the value
218262
bot_token = refreshed.bot_token
219263
bot_scopes = refreshed.bot_scopes

slack_bolt/authorization/authorize.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@ def __call__(
2929
enterprise_id: Optional[str],
3030
team_id: Optional[str], # can be None for org-wide installed apps
3131
user_id: Optional[str],
32+
# actor_* can be used only when user_token_resolution: "actor" is set
33+
actor_enterprise_id: Optional[str] = None,
34+
actor_team_id: Optional[str] = None,
35+
actor_user_id: Optional[str] = None,
3236
) -> Optional[AuthorizeResult]:
3337
raise NotImplementedError()
3438

@@ -55,6 +59,10 @@ def __call__(
5559
enterprise_id: Optional[str],
5660
team_id: Optional[str], # can be None for org-wide installed apps
5761
user_id: Optional[str],
62+
# actor_* can be used only when user_token_resolution: "actor" is set
63+
actor_enterprise_id: Optional[str] = None,
64+
actor_team_id: Optional[str] = None,
65+
actor_user_id: Optional[str] = None,
5866
) -> Optional[AuthorizeResult]:
5967
try:
6068
all_available_args = {
@@ -70,6 +78,9 @@ def __call__(
7078
"enterprise_id": enterprise_id,
7179
"team_id": team_id,
7280
"user_id": user_id,
81+
"actor_enterprise_id": actor_enterprise_id,
82+
"actor_team_id": actor_team_id,
83+
"actor_user_id": actor_user_id,
7384
}
7485
for k, v in context.items():
7586
if k not in all_available_args:
@@ -108,6 +119,7 @@ class InstallationStoreAuthorize(Authorize):
108119

109120
authorize_result_cache: Dict[str, AuthorizeResult]
110121
bot_only: bool
122+
user_token_resolution: str
111123
find_installation_available: bool
112124
find_bot_available: bool
113125
token_rotator: Optional[TokenRotator]
@@ -127,10 +139,13 @@ def __init__(
127139
bot_only: bool = False,
128140
cache_enabled: bool = False,
129141
client: Optional[WebClient] = None,
142+
# Since v1.27, user token resolution can be actor ID based when the mode is enabled
143+
user_token_resolution: str = "authed_user",
130144
):
131145
self.logger = logger
132146
self.installation_store = installation_store
133147
self.bot_only = bot_only
148+
self.user_token_resolution = user_token_resolution
134149
self.cache_enabled = cache_enabled
135150
self.authorize_result_cache = {}
136151
self.find_installation_available = hasattr(installation_store, "find_installation")
@@ -152,6 +167,10 @@ def __call__(
152167
enterprise_id: Optional[str],
153168
team_id: Optional[str], # can be None for org-wide installed apps
154169
user_id: Optional[str],
170+
# actor_* can be used only when user_token_resolution: "actor" is set
171+
actor_enterprise_id: Optional[str] = None,
172+
actor_team_id: Optional[str] = None,
173+
actor_user_id: Optional[str] = None,
155174
) -> Optional[AuthorizeResult]:
156175

157176
bot_token: Optional[str] = None
@@ -194,16 +213,32 @@ def __call__(
194213

195214
# try to fetch the request user's installation
196215
# to reflect the user's access token if exists
197-
this_user_installation = self.installation_store.find_installation(
198-
enterprise_id=enterprise_id,
199-
team_id=team_id,
200-
user_id=user_id,
201-
is_enterprise_install=context.is_enterprise_install,
202-
)
216+
if self.user_token_resolution == "actor":
217+
if actor_enterprise_id is not None or actor_team_id is not None:
218+
# Note that actor_team_id can be absent for app_mention events
219+
this_user_installation = self.installation_store.find_installation(
220+
enterprise_id=actor_enterprise_id,
221+
team_id=actor_team_id,
222+
user_id=actor_user_id,
223+
is_enterprise_install=None,
224+
)
225+
else:
226+
this_user_installation = self.installation_store.find_installation(
227+
enterprise_id=enterprise_id,
228+
team_id=team_id,
229+
user_id=user_id,
230+
is_enterprise_install=context.is_enterprise_install,
231+
)
203232
if this_user_installation is not None:
204233
user_token = this_user_installation.user_token
205234
user_scopes = this_user_installation.user_scopes
206-
if latest_bot_installation.bot_token is None:
235+
if (
236+
latest_bot_installation.bot_token is None
237+
# enterprise_id/team_id can be different for Slack Connect channel events
238+
# when enabling user_token_resolution: "actor"
239+
and latest_bot_installation.enterprise_id == this_user_installation.enterprise_id
240+
and latest_bot_installation.team_id == this_user_installation.team_id
241+
):
207242
# If latest_installation has a bot token, we never overwrite the value
208243
bot_token = this_user_installation.bot_token
209244
bot_scopes = this_user_installation.bot_scopes
@@ -213,7 +248,13 @@ def __call__(
213248
if refreshed is not None:
214249
user_token = refreshed.user_token
215250
user_scopes = refreshed.user_scopes
216-
if latest_bot_installation.bot_token is None:
251+
if (
252+
latest_bot_installation.bot_token is None
253+
# enterprise_id/team_id can be different for Slack Connect channel events
254+
# when enabling user_token_resolution: "actor"
255+
and latest_bot_installation.enterprise_id == this_user_installation.enterprise_id
256+
and latest_bot_installation.team_id == this_user_installation.team_id
257+
):
217258
# If latest_installation has a bot token, we never overwrite the value
218259
bot_token = refreshed.bot_token
219260
bot_scopes = refreshed.bot_scopes

slack_bolt/context/base_context.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ class BaseContext(dict):
1717
"is_enterprise_install",
1818
"team_id",
1919
"user_id",
20+
"actor_enterprise_id",
21+
"actor_team_id",
22+
"actor_user_id",
2023
"channel_id",
2124
"response_url",
2225
"matches",
@@ -61,6 +64,30 @@ def user_id(self) -> Optional[str]:
6164
"""The user ID associated ith this request."""
6265
return self.get("user_id")
6366

67+
@property
68+
def actor_enterprise_id(self) -> Optional[str]:
69+
"""The action's actor's Enterprise Grid organization ID.
70+
Note that this property is especially useful for handling events in Slack Connect channels.
71+
That being said, it's not guaranteed to have a valid ID for all events due to server-side inconsistency.
72+
"""
73+
return self.get("actor_enterprise_id")
74+
75+
@property
76+
def actor_team_id(self) -> Optional[str]:
77+
"""The action's actor's workspace ID.
78+
Note that this property is especially useful for handling events in Slack Connect channels.
79+
That being said, it's not guaranteed to have a valid ID for all events due to server-side inconsistency.
80+
"""
81+
return self.get("actor_team_id")
82+
83+
@property
84+
def actor_user_id(self) -> Optional[str]:
85+
"""The action's actor's user ID.
86+
Note that this property is especially useful for handling events in Slack Connect channels.
87+
That being said, it's not guaranteed to have a valid ID for all events due to server-side inconsistency.
88+
"""
89+
return self.get("actor_user_id")
90+
6491
@property
6592
def channel_id(self) -> Optional[str]:
6693
"""The conversation ID associated with this request."""

slack_bolt/middleware/authorization/async_multi_teams_authorization.py

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,16 +14,24 @@
1414

1515
class AsyncMultiTeamsAuthorization(AsyncAuthorization):
1616
authorize: AsyncAuthorize
17+
user_token_resolution: str
1718

18-
def __init__(self, authorize: AsyncAuthorize, base_logger: Optional[Logger] = None):
19+
def __init__(
20+
self,
21+
authorize: AsyncAuthorize,
22+
base_logger: Optional[Logger] = None,
23+
user_token_resolution: str = "authed_user",
24+
):
1925
"""Multi-workspace authorization.
2026
2127
Args:
2228
authorize: The function to authorize incoming requests from Slack.
2329
base_logger: The base logger
30+
user_token_resolution: "authed_user" or "actor"
2431
"""
2532
self.authorize = authorize
2633
self.logger = get_bolt_logger(AsyncMultiTeamsAuthorization, base_logger=base_logger)
34+
self.user_token_resolution = user_token_resolution
2735

2836
async def async_process(
2937
self,
@@ -49,12 +57,24 @@ async def async_process(
4957
return await next()
5058

5159
try:
52-
auth_result: Optional[AuthorizeResult] = await self.authorize(
53-
context=req.context,
54-
enterprise_id=req.context.enterprise_id,
55-
team_id=req.context.team_id,
56-
user_id=req.context.user_id,
57-
)
60+
auth_result: Optional[AuthorizeResult] = None
61+
if self.user_token_resolution == "actor":
62+
auth_result = await self.authorize(
63+
context=req.context,
64+
enterprise_id=req.context.enterprise_id,
65+
team_id=req.context.team_id,
66+
user_id=req.context.user_id,
67+
actor_enterprise_id=req.context.actor_enterprise_id,
68+
actor_team_id=req.context.actor_team_id,
69+
actor_user_id=req.context.actor_user_id,
70+
)
71+
else:
72+
auth_result = await self.authorize(
73+
context=req.context,
74+
enterprise_id=req.context.enterprise_id,
75+
team_id=req.context.team_id,
76+
user_id=req.context.user_id,
77+
)
5878
if auth_result:
5979
req.context.set_authorize_result(auth_result)
6080
token = auth_result.bot_token or auth_result.user_token

0 commit comments

Comments
 (0)