Esempio n. 1
0
    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
Esempio n. 2
0
    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
Esempio n. 3
0
    def __init__(
            self,
            *,
            client: Optional[WebClient] = None,
            logger: Optional[Logger] = None,
            settings: Optional[OAuthSettings] = None,
            oauth_state_bucket_name: Optional[str] = None,  # required
            installation_bucket_name: Optional[str] = None,  # required
    ):
        logger = logger or logging.getLogger(__name__)
        settings = settings or OAuthSettings(
            client_id=os.environ["SLACK_CLIENT_ID"],
            client_secret=os.environ["SLACK_CLIENT_SECRET"],
        )
        oauth_state_bucket_name = (oauth_state_bucket_name
                                   or os.environ["SLACK_STATE_S3_BUCKET_NAME"])
        installation_bucket_name = (
            installation_bucket_name
            or os.environ["SLACK_INSTALLATION_S3_BUCKET_NAME"])
        self.s3_client = boto3.client("s3")
        if settings.state_store is None or not isinstance(
                settings.state_store, AmazonS3OAuthStateStore):
            settings.state_store = AmazonS3OAuthStateStore(
                logger=logger,
                s3_client=self.s3_client,
                bucket_name=oauth_state_bucket_name,
                expiration_seconds=settings.state_expiration_seconds,
            )

        if settings.installation_store is None or not isinstance(
                settings.installation_store, AmazonS3InstallationStore):
            settings.installation_store = AmazonS3InstallationStore(
                logger=logger,
                s3_client=self.s3_client,
                bucket_name=installation_bucket_name,
                client_id=settings.client_id,
            )

        # Set up authorize function to surely use this installation_store.
        # When a developer use a settings initialized outside this constructor,
        # the settings may already have pre-defined authorize.
        # In this case, the /slack/events endpoint doesn't work along with the OAuth flow.
        settings.authorize = InstallationStoreAuthorize(
            logger=logger,
            installation_store=settings.installation_store,
            bot_only=settings.installation_store_bot_only,
        )

        OAuthFlow.__init__(self,
                           client=client,
                           logger=logger,
                           settings=settings)
Esempio n. 4
0
 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
Esempio n. 5
0
 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
Esempio n. 6
0
    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)
Esempio n. 7
0
 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
Esempio n. 8
0
 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
Esempio n. 9
0
 def test_instantiation(self):
     oauth_flow = OAuthFlow.sqlite3(
         database="./logs/test_db",
         client_id="111.222",
         client_secret="xxx",
         scopes=["chat:write", "commands"],
     )
     assert oauth_flow is not None
     assert oauth_flow.logger is not None
     assert oauth_flow.client is not None
Esempio n. 10
0
 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_handle_installation(self):
     oauth_flow = OAuthFlow.sqlite3(
         client=WebClient(base_url=self.mock_api_server_base_url),
         database="./logs/test_db",
         client_id="111.222",
         client_secret="xxx",
         scopes=["chat:write", "commands"],
     )
     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 "https://slack.com/oauth/v2/authorize?state=" in resp.body
Esempio n. 12
0
    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
Esempio n. 13
0
 def test_handle_callback_invalid_state(self):
     oauth_flow = OAuthFlow.sqlite3(
         client=WebClient(base_url=self.mock_api_server_base_url),
         database="./logs/test_db",
         client_id="111.222",
         client_secret="xxx",
         scopes=["chat:write", "commands"],
     )
     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
Esempio n. 14
0
    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.sqlite3(
            client=WebClient(base_url=self.mock_api_server_base_url),
            database="./logs/test_db",
            client_id="111.222",
            client_secret="xxx",
            scopes=["chat:write", "commands"],
            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"
Esempio n. 15
0
 def test_handle_callback(self):
     oauth_flow = OAuthFlow.sqlite3(
         client=WebClient(base_url=self.mock_api_server_base_url),
         database="./logs/test_db",
         client_id="111.222",
         client_secret="xxx",
         scopes=["chat:write", "commands"],
         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
Esempio n. 16
0
# ------------------------------------------------
# instead of slack_bolt in requirements.txt
import sys

sys.path.insert(1, "..")
# ------------------------------------------------

import logging

logging.basicConfig(level=logging.DEBUG)

from slack_bolt import App
from slack_bolt.oauth import OAuthFlow

app = App(oauth_flow=OAuthFlow.sqlite3(database="./slackapp.db"))


@app.event("app_mention")
def handle_app_mentions(payload, say, logger):
    logger.info(payload)
    say("What's up?")


if __name__ == "__main__":
    app.start(3000)

# pip install slack_bolt
# export SLACK_SIGNING_SECRET=***
# export SLACK_BOT_TOKEN=xoxb-***
# export SLACK_CLIENT_ID=111.111
# export SLACK_CLIENT_SECRET=***
Esempio n. 17
0
    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()
Esempio n. 18
0
    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()