def test_installation_store_conflicts(self): store1 = FileInstallationStore() store2 = FileInstallationStore() app = App( signing_secret="valid", oauth_settings=OAuthSettings( client_id="111.222", client_secret="valid", installation_store=store1, ), installation_store=store2, ) assert app.installation_store is store1 app = App( signing_secret="valid", oauth_flow=OAuthFlow(settings=OAuthSettings( client_id="111.222", client_secret="valid", installation_store=store1, )), installation_store=store2, ) assert app.installation_store is store1 app = App( signing_secret="valid", oauth_flow=OAuthFlow(settings=OAuthSettings( client_id="111.222", client_secret="valid", )), installation_store=store1, ) assert app.installation_store is store1
def test_authorize_conflicts(self): oauth_settings = OAuthSettings( client_id="111.222", client_secret="valid", installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), ) # no error with this App(signing_secret="valid", oauth_settings=oauth_settings) def authorize() -> AuthorizeResult: return AuthorizeResult(enterprise_id="E111", team_id="T111") with pytest.raises(BoltError): App( signing_secret="valid", authorize=authorize, oauth_settings=oauth_settings, ) oauth_flow = OAuthFlow(settings=oauth_settings) # no error with this App(signing_secret="valid", oauth_flow=oauth_flow) with pytest.raises(BoltError): App(signing_secret="valid", authorize=authorize, oauth_flow=oauth_flow)
def test_handle_callback(self): oauth_flow = OAuthFlow( client=WebClient(base_url=self.mock_api_server_base_url), settings=OAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), success_url="https://www.example.com/completion", failure_url="https://www.example.com/failure", ), ) state = oauth_flow.issue_new_state(None) req = BoltRequest( body="", query=f"code=foo&state={state}", headers={ "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"] }, ) resp = oauth_flow.handle_callback(req) assert resp.status == 200 assert "https://www.example.com/completion" in resp.body app = App(signing_secret="signing_secret", oauth_flow=oauth_flow) global_shortcut_body = { "type": "shortcut", "token": "verification_token", "action_ts": "111.111", "team": { "id": "T111", "domain": "workspace-domain", "enterprise_id": "E111", "enterprise_name": "Org Name", }, "user": { "id": "W111", "username": "******", "team_id": "T111" }, "callback_id": "test-shortcut", "trigger_id": "111.111.xxxxxx", } body = f"payload={quote(json.dumps(global_shortcut_body))}" timestamp = str(int(time())) signature_verifier = SignatureVerifier("signing_secret") headers = { "content-type": ["application/x-www-form-urlencoded"], "x-slack-signature": [ signature_verifier.generate_signature(body=body, timestamp=timestamp) ], "x-slack-request-timestamp": [timestamp], } request = BoltRequest(body=body, headers=headers) response = app.dispatch(request) assert response.status == 200 assert self.mock_received_requests["/auth.test"] == 1
def test_valid_multi_auth_oauth_flow(self): oauth_flow = OAuthFlow(settings=OAuthSettings( client_id="111.222", client_secret="valid", installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), )) app = App(signing_secret="valid", oauth_flow=oauth_flow) assert app != None
def test_instantiation(self): oauth_flow = OAuthFlow(settings=OAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), )) assert oauth_flow is not None assert oauth_flow.logger is not None assert oauth_flow.client is not None
def test_installation_store_bot_only_oauth_flow(self): app = App( client=self.web_client, signing_secret=self.signing_secret, oauth_flow=OAuthFlow(settings=self.oauth_settings_bot_only), ) app.event("app_mention")(self.handle_app_mention) response = app.dispatch(self.build_app_mention_request()) assert response.status == 200 assert_auth_test_count(self, 1) sleep(1) # wait a bit after auto ack() assert self.mock_received_requests["/chat.postMessage"] == 1
def test_handle_installation(self): oauth_flow = OAuthFlow(settings=OAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), )) req = BoltRequest(body="") resp = oauth_flow.handle_installation(req) assert resp.status == 200 assert resp.headers.get("content-type") == ["text/html; charset=utf-8"] assert resp.headers.get("content-length") == ["565"] assert "https://slack.com/oauth/v2/authorize?state=" in resp.body
def test_handle_installation_no_rendering(self): oauth_flow = OAuthFlow(settings=OAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], user_scopes=["search:read"], installation_store=FileInstallationStore(), install_page_rendering_enabled=False, # disabled state_store=FileOAuthStateStore(expiration_seconds=120), )) req = BoltRequest(body="") resp = oauth_flow.handle_installation(req) assert resp.status == 302 location_header = resp.headers.get("location")[0] assert "https://slack.com/oauth/v2/authorize?state=" in location_header
def test_handle_callback_using_options(self): def success(args: SuccessArgs) -> BoltResponse: assert args.request is not None return BoltResponse(status=200, body="customized") def failure(args: FailureArgs) -> BoltResponse: assert args.request is not None assert args.reason is not None return BoltResponse(status=502, body="customized") oauth_flow = OAuthFlow( client=WebClient(base_url=self.mock_api_server_base_url), settings=OAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), callback_options=CallbackOptions(success=success, failure=failure), ), ) state = oauth_flow.issue_new_state(None) req = BoltRequest( body="", query=f"code=foo&state={state}", headers={ "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"] }, ) resp = oauth_flow.handle_callback(req) assert resp.status == 200 assert resp.body == "customized" state = oauth_flow.issue_new_state(None) req = BoltRequest( body="", query=f"code=foo&state=invalid", headers={ "cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"] }, ) resp = oauth_flow.handle_callback(req) assert resp.status == 502 assert resp.body == "customized"
def test_handle_callback_invalid_state(self): oauth_flow = OAuthFlow( settings=OAuthSettings( client_id="111.222", client_secret="xxx", scopes=["chat:write", "commands"], installation_store=FileInstallationStore(), state_store=FileOAuthStateStore(expiration_seconds=120), ) ) state = oauth_flow.issue_new_state(None) req = BoltRequest( body="", query=f"code=foo&state=invalid", headers={"cookie": [f"{oauth_flow.settings.state_cookie_name}={state}"]}, ) resp = oauth_flow.handle_callback(req) assert resp.status == 400
def __init__( self, *, logger: Optional[logging.Logger] = None, # Used in logger name: Optional[str] = None, # Set True when you run this app on a FaaS platform process_before_response: bool = False, # Basic Information > Credentials > Signing Secret signing_secret: Optional[str] = None, # for single-workspace apps token: Optional[str] = None, client: Optional[WebClient] = None, # for multi-workspace apps authorize: Optional[Callable[..., AuthorizeResult]] = None, installation_store: Optional[InstallationStore] = None, # for the OAuth flow oauth_settings: Optional[OAuthSettings] = None, oauth_flow: Optional[OAuthFlow] = None, # No need to set (the value is used only in response to ssl_check requests) verification_token: Optional[str] = None, ): """Bolt App that provides functionalities to register middleware/listeners :param name: The application name that will be used in logging. If absent, the source file name will be used instead. :param process_before_response: True if this app runs on Function as a Service. (Default: False) :param signing_secret: The Signing Secret value used for verifying requests from Slack. :param token: The bot access token required only for single-workspace app. :param client: The singleton slack_sdk.WebClient instance for this app. :param authorize: The function to authorize an incoming request from Slack by checking if there is a team/user in the installation data. :param installation_store: The module offering save/find operations of installation data :param oauth_settings: The settings related to Slack app installation flow (OAuth flow) :param oauth_flow: Manually instantiated slack_bolt.oauth.OAuthFlow. This is always prioritized over oauth_settings. :param verification_token: Deprecated verification mechanism. This can used only for ssl_check requests. """ signing_secret = signing_secret or os.environ.get("SLACK_SIGNING_SECRET") token = token or os.environ.get("SLACK_BOT_TOKEN") self._name: str = name or inspect.stack()[1].filename.split(os.path.sep)[-1] self._signing_secret: str = signing_secret self._verification_token: Optional[str] = verification_token or os.environ.get( "SLACK_VERIFICATION_TOKEN", None ) self._framework_logger = logger or get_bolt_logger(App) self._token: Optional[str] = token if client is not None: if not isinstance(client, WebClient): raise BoltError(error_client_invalid_type()) self._client = client self._token = client.token if token is not None: self._framework_logger.warning( warning_client_prioritized_and_token_skipped() ) else: self._client = create_web_client(token) # NOTE: the token here can be None self._authorize: Optional[Authorize] = None if authorize is not None: if oauth_settings is not None or oauth_flow is not None: raise BoltError(error_authorize_conflicts()) self._authorize = CallableAuthorize( logger=self._framework_logger, func=authorize ) self._installation_store: Optional[InstallationStore] = installation_store if self._installation_store is not None and self._authorize is None: self._authorize = InstallationStoreAuthorize( installation_store=self._installation_store, logger=self._framework_logger, ) self._oauth_flow: Optional[OAuthFlow] = None if ( oauth_settings is None and os.environ.get("SLACK_CLIENT_ID") is not None and os.environ.get("SLACK_CLIENT_SECRET") is not None ): # initialize with the default settings oauth_settings = OAuthSettings() if oauth_flow: self._oauth_flow = oauth_flow installation_store = select_consistent_installation_store( client_id=self._oauth_flow.client_id, app_store=self._installation_store, oauth_flow_store=self._oauth_flow.settings.installation_store, logger=self._framework_logger, ) self._installation_store = installation_store self._oauth_flow.settings.installation_store = installation_store if self._oauth_flow._client is None: self._oauth_flow._client = self._client if self._authorize is None: self._authorize = self._oauth_flow.settings.authorize elif oauth_settings is not None: installation_store = select_consistent_installation_store( client_id=oauth_settings.client_id, app_store=self._installation_store, oauth_flow_store=oauth_settings.installation_store, logger=self._framework_logger, ) self._installation_store = installation_store oauth_settings.installation_store = installation_store self._oauth_flow = OAuthFlow( client=self.client, logger=self.logger, settings=oauth_settings ) if self._authorize is None: self._authorize = self._oauth_flow.settings.authorize if ( self._installation_store is not None or self._authorize is not None ) and self._token is not None: self._token = None self._framework_logger.warning(warning_token_skipped()) self._middleware_list: List[Union[Callable, Middleware]] = [] self._listeners: List[Listener] = [] listener_executor = ThreadPoolExecutor(max_workers=5) self._listener_runner = ThreadListenerRunner( logger=self._framework_logger, process_before_response=process_before_response, listener_error_handler=DefaultListenerErrorHandler( logger=self._framework_logger ), listener_executor=listener_executor, lazy_listener_runner=ThreadLazyListenerRunner( logger=self._framework_logger, executor=listener_executor, ), ) self._init_middleware_list_done = False self._init_middleware_list()
def __init__( self, *, # Used in logger name: Optional[str] = None, # Set True when you run this app on a FaaS platform process_before_response: bool = False, # Basic Information > Credentials > Signing Secret signing_secret: Optional[str] = None, # for single-workspace apps token: Optional[str] = None, client: Optional[WebClient] = None, # for multi-workspace apps installation_store: Optional[InstallationStore] = None, oauth_state_store: Optional[OAuthStateStore] = None, oauth_state_cookie_name: str = OAuthStateUtils.default_cookie_name, oauth_state_expiration_seconds: int = OAuthStateUtils. default_expiration_seconds, # for the OAuth flow oauth_flow: Optional[OAuthFlow] = None, authorization_test_enabled: bool = True, client_id: Optional[str] = None, client_secret: Optional[str] = None, scopes: Optional[List[str]] = None, user_scopes: Optional[List[str]] = None, redirect_uri: Optional[str] = None, oauth_install_path: Optional[str] = None, oauth_redirect_uri_path: Optional[str] = None, oauth_success_url: Optional[str] = None, oauth_failure_url: Optional[str] = None, # No need to set (the value is used only in response to ssl_check requests) verification_token: Optional[str] = None, ): signing_secret = signing_secret or os.environ.get( "SLACK_SIGNING_SECRET", None) token = token or os.environ.get("SLACK_BOT_TOKEN", None) if signing_secret is None or signing_secret == "": raise BoltError( "Signing secret not found, so could not initialize the Bolt app." ) self._name: str = name or inspect.stack()[1].filename.split( os.path.sep)[-1] self._signing_secret: str = signing_secret client_id = client_id or os.environ.get("SLACK_CLIENT_ID", None) client_secret = client_secret or os.environ.get( "SLACK_CLIENT_SECRET", None) scopes = scopes or os.environ.get("SLACK_SCOPES", "").split(",") user_scopes = user_scopes or os.environ.get("SLACK_USER_SCOPES", "").split(",") redirect_uri = redirect_uri or os.environ.get("SLACK_REDIRECT_URI", None) oauth_install_path = oauth_install_path or os.environ.get( "SLACK_INSTALL_PATH", "/slack/install") oauth_redirect_uri_path = oauth_redirect_uri_path or os.environ.get( "SLACK_REDIRECT_URI_PATH", "/slack/oauth_redirect") self._verification_token: Optional[ str] = verification_token or os.environ.get( "SLACK_VERIFICATION_TOKEN", None) self._framework_logger = get_bolt_logger(App) self._token: Optional[str] = token if client is not None: self._client = client self._token = client.token if token is not None: self._framework_logger.warning( "As you gave client as well, the bot token will be unused." ) else: self._client = create_web_client( token) # NOTE: the token here can be None self._installation_store: Optional[ InstallationStore] = installation_store self._oauth_state_store: Optional[OAuthStateStore] = oauth_state_store self._oauth_state_cookie_name = oauth_state_cookie_name self._oauth_state_expiration_seconds = oauth_state_expiration_seconds self._oauth_flow: Optional[OAuthFlow] = None self._authorization_test_enabled = authorization_test_enabled if oauth_flow: self._oauth_flow = oauth_flow if self._installation_store is None: self._installation_store = self._oauth_flow.installation_store if self._oauth_state_store is None: self._oauth_state_store = self._oauth_flow.oauth_state_store if self._oauth_flow._client is None: self._oauth_flow._client = self._client else: if client_id is not None and client_secret is not None: # The OAuth flow support is enabled if self._installation_store is None and self._oauth_state_store is None: # use the default ones self._installation_store = FileInstallationStore( client_id=client_id, ) self._oauth_state_store = FileOAuthStateStore( expiration_seconds=self. _oauth_state_expiration_seconds, client_id=client_id, ) if (self._installation_store is not None and self._oauth_state_store is None): raise ValueError( f"Configure an appropriate OAuthStateStore for {self._installation_store}" ) self._oauth_flow = OAuthFlow( client=create_web_client(), logger=self._framework_logger, # required storage implementations installation_store=self._installation_store, oauth_state_store=self._oauth_state_store, oauth_state_cookie_name=self._oauth_state_cookie_name, oauth_state_expiration_seconds=self. _oauth_state_expiration_seconds, # used for oauth.v2.access calls client_id=client_id, client_secret=client_secret, # installation url parameters scopes=scopes, user_scopes=user_scopes, redirect_uri=redirect_uri, # path in this app install_path=oauth_install_path, redirect_uri_path=oauth_redirect_uri_path, # urls after callback success_url=oauth_success_url, failure_url=oauth_failure_url, ) if self._installation_store is not None and self._token is not None: self._token = None self._framework_logger.warning( "As you gave installation_store as well, the bot token will be unused." ) self._middleware_list: List[Union[Callable, Middleware]] = [] self._listeners: List[Listener] = [] self._listener_executor = ThreadPoolExecutor( max_workers=5) # TODO: shutdown self._process_before_response = process_before_response self._init_middleware_list_done = False self._init_middleware_list()