Skip to content

Commit ade7bc4

Browse files
authored
Enable developers to pass fully implemented authorize along with installation_store (#851)
* Enable developers to pass fully implemented authorize along with installation_store * Fix the CI build error with Python 3.6 due to Chalice 1.28
1 parent 57cb043 commit ade7bc4

File tree

5 files changed

+542
-8
lines changed

5 files changed

+542
-8
lines changed

setup.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,8 @@
7777
# used only under src/slack_bolt/adapter
7878
"boto3<=2",
7979
"bottle>=0.12,<1",
80-
"chalice>=1.27.3,<2",
80+
# TODO: chalice 1.28 dropped Python 3.6 support
81+
"chalice<=1.27.3",
8182
"CherryPy>=18,<19",
8283
"Django>=3,<5",
8384
"falcon>=3.1.1,<4" if sys.version_info.minor >= 11 else "falcon>=2,<4",

slack_bolt/app/app.py

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,9 +217,16 @@ def message_hello(message, say):
217217

218218
self._authorize: Optional[Authorize] = None
219219
if authorize is not None:
220-
if oauth_settings is not None or oauth_flow is not None:
221-
raise BoltError(error_authorize_conflicts())
222-
self._authorize = CallableAuthorize(logger=self._framework_logger, func=authorize)
220+
if isinstance(authorize, Authorize):
221+
# As long as an advanced developer understands what they're doing,
222+
# bolt-python should not prevent customizing authorize middleware
223+
self._authorize = authorize
224+
else:
225+
if oauth_settings is not None or oauth_flow is not None:
226+
# If the given authorize is a simple function,
227+
# it does not work along with installation_store.
228+
raise BoltError(error_authorize_conflicts())
229+
self._authorize = CallableAuthorize(logger=self._framework_logger, func=authorize)
223230

224231
self._installation_store: Optional[InstallationStore] = installation_store
225232
if self._installation_store is not None and self._authorize is None:

slack_bolt/app/async_app.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -222,10 +222,16 @@ async def message_hello(message, say): # async function
222222

223223
self._async_authorize: Optional[AsyncAuthorize] = None
224224
if authorize is not None:
225-
if oauth_settings is not None or oauth_flow is not None:
226-
raise BoltError(error_authorize_conflicts())
227-
228-
self._async_authorize = AsyncCallableAuthorize(logger=self._framework_logger, func=authorize)
225+
if isinstance(authorize, AsyncAuthorize):
226+
# As long as an advanced developer understands what they're doing,
227+
# bolt-python should not prevent customizing authorize middleware
228+
self._async_authorize = authorize
229+
else:
230+
if oauth_settings is not None or oauth_flow is not None:
231+
# If the given authorize is a simple function,
232+
# it does not work along with installation_store.
233+
raise BoltError(error_authorize_conflicts())
234+
self._async_authorize = AsyncCallableAuthorize(logger=self._framework_logger, func=authorize)
229235

230236
self._async_installation_store: Optional[AsyncInstallationStore] = installation_store
231237
if self._async_installation_store is not None and self._async_authorize is None:
Lines changed: 254 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,254 @@
1+
import datetime
2+
import json
3+
import logging
4+
from time import time, sleep
5+
from typing import Optional
6+
7+
import pytest
8+
from slack_sdk import WebClient
9+
from slack_sdk.errors import SlackApiError
10+
from slack_sdk.oauth import InstallationStore
11+
from slack_sdk.oauth.installation_store import Installation, Bot
12+
from slack_sdk.signature import SignatureVerifier
13+
14+
from slack_bolt import App, BoltRequest, Say, BoltContext
15+
from slack_bolt.authorization import AuthorizeResult
16+
from slack_bolt.authorization.authorize import Authorize
17+
from slack_bolt.error import BoltError
18+
from slack_bolt.oauth.oauth_settings import OAuthSettings
19+
from tests.mock_web_api_server import (
20+
setup_mock_web_api_server,
21+
cleanup_mock_web_api_server,
22+
assert_auth_test_count,
23+
)
24+
from tests.utils import remove_os_env_temporarily, restore_os_env
25+
26+
27+
class MemoryInstallationStore(InstallationStore):
28+
@property
29+
def logger(self) -> logging.Logger:
30+
return logging.getLogger(__name__)
31+
32+
def save(self, installation: Installation):
33+
pass
34+
35+
def find_bot(
36+
self,
37+
*,
38+
enterprise_id: Optional[str],
39+
team_id: Optional[str],
40+
is_enterprise_install: Optional[bool] = False,
41+
) -> Optional[Bot]:
42+
return Bot(
43+
app_id="A111",
44+
enterprise_id="E111",
45+
team_id="T0G9PQBBK",
46+
bot_token="xoxb-valid",
47+
bot_id="B",
48+
bot_user_id="W",
49+
bot_scopes=["commands", "chat:write"],
50+
installed_at=datetime.datetime.now().timestamp(),
51+
)
52+
53+
def find_installation(
54+
self,
55+
*,
56+
enterprise_id: Optional[str],
57+
team_id: Optional[str],
58+
user_id: Optional[str] = None,
59+
is_enterprise_install: Optional[bool] = False,
60+
) -> Optional[Installation]:
61+
return Installation(
62+
app_id="A111",
63+
enterprise_id="E111",
64+
team_id="T0G9PQBBK",
65+
bot_token="xoxb-valid-2",
66+
bot_id="B",
67+
bot_user_id="W",
68+
bot_scopes=["commands", "chat:write"],
69+
user_id="W11111",
70+
user_token="xoxp-valid",
71+
user_scopes=["search:read"],
72+
installed_at=datetime.datetime.now().timestamp(),
73+
)
74+
75+
76+
class CustomAuthorize(Authorize):
77+
def __init__(self, installation_store: InstallationStore):
78+
self.installation_store = installation_store
79+
80+
def __call__(
81+
self,
82+
*,
83+
context: BoltContext,
84+
enterprise_id: Optional[str],
85+
team_id: Optional[str],
86+
user_id: Optional[str],
87+
) -> Optional[AuthorizeResult]:
88+
bot_token: Optional[str] = None
89+
user_token: Optional[str] = None
90+
latest_installation: Optional[Installation] = self.installation_store.find_installation(
91+
enterprise_id=enterprise_id,
92+
team_id=team_id,
93+
is_enterprise_install=context.is_enterprise_install,
94+
)
95+
this_user_installation: Optional[Installation] = None
96+
if latest_installation is not None:
97+
bot_token = latest_installation.bot_token # this still can be None
98+
user_token = latest_installation.user_token # this still can be None
99+
if latest_installation.user_id != user_id:
100+
# First off, remove the user token as the installer is a different user
101+
user_token = None
102+
latest_installation.user_token = None
103+
latest_installation.user_refresh_token = None
104+
latest_installation.user_token_expires_at = None
105+
latest_installation.user_scopes = []
106+
107+
# try to fetch the request user's installation
108+
# to reflect the user's access token if exists
109+
this_user_installation = self.installation_store.find_installation(
110+
enterprise_id=enterprise_id,
111+
team_id=team_id,
112+
user_id=user_id,
113+
is_enterprise_install=context.is_enterprise_install,
114+
)
115+
if this_user_installation is not None:
116+
user_token = this_user_installation.user_token
117+
if latest_installation.bot_token is None:
118+
# If latest_installation has a bot token, we never overwrite the value
119+
bot_token = this_user_installation.bot_token
120+
token: Optional[str] = bot_token or user_token
121+
if token is None:
122+
return None
123+
try:
124+
auth_test_api_response = context.client.auth_test(token=token)
125+
authorize_result = AuthorizeResult.from_auth_test_response(
126+
auth_test_response=auth_test_api_response,
127+
bot_token=bot_token,
128+
user_token=user_token,
129+
)
130+
return authorize_result
131+
except SlackApiError:
132+
return None
133+
134+
135+
class TestApp:
136+
signing_secret = "secret"
137+
valid_token = "xoxb-valid"
138+
mock_api_server_base_url = "http://localhost:8888"
139+
signature_verifier = SignatureVerifier(signing_secret)
140+
web_client = WebClient(
141+
token=valid_token,
142+
base_url=mock_api_server_base_url,
143+
)
144+
145+
def setup_method(self):
146+
self.old_os_env = remove_os_env_temporarily()
147+
setup_mock_web_api_server(self)
148+
149+
def teardown_method(self):
150+
cleanup_mock_web_api_server(self)
151+
restore_os_env(self.old_os_env)
152+
153+
def generate_signature(self, body: str, timestamp: str):
154+
return self.signature_verifier.generate_signature(
155+
body=body,
156+
timestamp=timestamp,
157+
)
158+
159+
def build_headers(self, timestamp: str, body: str):
160+
return {
161+
"content-type": ["application/json"],
162+
"x-slack-signature": [self.generate_signature(body, timestamp)],
163+
"x-slack-request-timestamp": [timestamp],
164+
}
165+
166+
app_mention_request_body = {
167+
"token": "verification_token",
168+
"team_id": "T111",
169+
"enterprise_id": "E111",
170+
"api_app_id": "A111",
171+
"event": {
172+
"client_msg_id": "9cbd4c5b-7ddf-4ede-b479-ad21fca66d63",
173+
"type": "app_mention",
174+
"text": "<@W111> Hi there!",
175+
"user": "W222",
176+
"ts": "1595926230.009600",
177+
"team": "T111",
178+
"channel": "C111",
179+
"event_ts": "1595926230.009600",
180+
},
181+
"type": "event_callback",
182+
"event_id": "Ev111",
183+
"event_time": 1595926230,
184+
"authed_users": ["W111"],
185+
}
186+
187+
@staticmethod
188+
def handle_app_mention(body, say: Say, payload, event):
189+
assert body["event"] == payload
190+
assert payload == event
191+
say("What's up?")
192+
193+
def build_app_mention_request(self):
194+
timestamp, body = str(int(time())), json.dumps(self.app_mention_request_body)
195+
return BoltRequest(body=body, headers=self.build_headers(timestamp, body))
196+
197+
def test_installation_store_only(self):
198+
app = App(
199+
client=self.web_client,
200+
signing_secret=self.signing_secret,
201+
oauth_settings=OAuthSettings(
202+
client_id="111.222",
203+
client_secret="secret",
204+
scopes=["commands"],
205+
installation_store=MemoryInstallationStore(),
206+
),
207+
)
208+
209+
app.event("app_mention")(self.handle_app_mention)
210+
response = app.dispatch(self.build_app_mention_request())
211+
assert response.status == 200
212+
assert_auth_test_count(self, 1)
213+
sleep(1) # wait a bit after auto ack()
214+
assert self.mock_received_requests["/chat.postMessage"] == 1
215+
216+
def test_installation_store_and_authorize(self):
217+
installation_store = MemoryInstallationStore()
218+
app = App(
219+
client=self.web_client,
220+
signing_secret=self.signing_secret,
221+
oauth_settings=OAuthSettings(
222+
client_id="111.222",
223+
client_secret="secret",
224+
scopes=["commands"],
225+
installation_store=installation_store,
226+
),
227+
authorize=CustomAuthorize(installation_store),
228+
)
229+
230+
app.event("app_mention")(self.handle_app_mention)
231+
response = app.dispatch(self.build_app_mention_request())
232+
assert response.status == 200
233+
assert_auth_test_count(self, 1)
234+
sleep(1) # wait a bit after auto ack()
235+
assert self.mock_received_requests["/chat.postMessage"] == 1
236+
237+
def test_installation_store_and_func_authorize(self):
238+
installation_store = MemoryInstallationStore()
239+
240+
def authorize():
241+
pass
242+
243+
with pytest.raises(BoltError):
244+
App(
245+
client=self.web_client,
246+
signing_secret=self.signing_secret,
247+
oauth_settings=OAuthSettings(
248+
client_id="111.222",
249+
client_secret="secret",
250+
scopes=["commands"],
251+
installation_store=installation_store,
252+
),
253+
authorize=authorize,
254+
)

0 commit comments

Comments
 (0)