diff --git a/elementary/clients/slack/client.py b/elementary/clients/slack/client.py index f66a9b969..0c2814157 100644 --- a/elementary/clients/slack/client.py +++ b/elementary/clients/slack/client.py @@ -1,7 +1,9 @@ import json +import ssl from abc import ABC, abstractmethod from typing import Dict, List, Optional, Tuple, Union +import certifi import requests from ratelimit import limits, sleep_and_retry from slack_sdk import WebClient, WebhookClient @@ -25,8 +27,9 @@ class SlackClient(ABC): def __init__( self, tracking: Optional[Tracking] = None, + ssl_context: Optional[ssl.SSLContext] = None, ): - self.client = self._initial_client() + self.client = self._initial_client(ssl_context) self.tracking = tracking self._initial_retry_handlers() self.email_to_user_id_cache: Dict[str, str] = {} @@ -38,19 +41,38 @@ def create_client( if not config.has_slack: return None if config.slack_token: - logger.debug("Creating Slack client with token.") - return SlackWebClient(token=config.slack_token, tracking=tracking) + logger.debug( + "Creating Slack client with token (system CA? = %s).", + config.use_system_ca_files, + ) + ssl_context = ( + None + if config.use_system_ca_files + else ssl.create_default_context(cafile=certifi.where()) + ) + return SlackWebClient( + token=config.slack_token, tracking=tracking, ssl_context=ssl_context + ) elif config.slack_webhook: - logger.debug("Creating Slack client with webhook.") + logger.debug( + "Creating Slack client with webhook (system CA? = %s).", + config.use_system_ca_files, + ) + ssl_context = ( + ssl.create_default_context(cafile=certifi.where()) + if not config.use_system_ca_files + else None + ) return SlackWebhookClient( webhook=config.slack_webhook, is_workflow=config.is_slack_workflow, tracking=tracking, + ssl_context=ssl_context, ) return None @abstractmethod - def _initial_client(self): + def _initial_client(self, ssl_context: Optional[ssl.SSLContext]): raise NotImplementedError def _initial_retry_handlers(self): @@ -85,12 +107,13 @@ def __init__( self, token: str, tracking: Optional[Tracking] = None, + ssl_context: Optional[ssl.SSLContext] = None, ): self.token = token - super().__init__(tracking) + super().__init__(tracking, ssl_context) - def _initial_client(self): - return WebClient(token=self.token) + def _initial_client(self, ssl_context: Optional[ssl.SSLContext]): + return WebClient(token=self.token, ssl=ssl_context) @sleep_and_retry @limits(calls=1, period=ONE_SECOND) @@ -231,16 +254,22 @@ def __init__( webhook: str, is_workflow: bool, tracking: Optional[Tracking] = None, + ssl_context: Optional[ssl.SSLContext] = None, ): self.webhook = webhook self.is_workflow = is_workflow - super().__init__(tracking) + super().__init__(tracking, ssl_context) - def _initial_client(self): + def _initial_client(self, ssl_context: Optional[ssl.SSLContext]): if self.is_workflow: + # Workflow webhooks do not support the ssl_context parameter. + # requests.Session() uses the requests default CA bundle (certifi). return requests.Session() + return WebhookClient( - url=self.webhook, default_headers={"Content-type": "application/json"} + url=self.webhook, + default_headers={"Content-type": "application/json"}, + ssl=ssl_context, ) @sleep_and_retry diff --git a/elementary/config/config.py b/elementary/config/config.py index b8e56143c..b15ff9c43 100644 --- a/elementary/config/config.py +++ b/elementary/config/config.py @@ -76,6 +76,7 @@ def __init__( run_dbt_deps_if_needed: Optional[bool] = None, project_name: Optional[str] = None, quiet_logs: Optional[bool] = None, + use_system_ca_files: bool = True, ): self.config_dir = config_dir self.profiles_dir = profiles_dir @@ -223,6 +224,8 @@ def __init__( quiet_logs, config.get("quiet_logs"), False ) + self.use_system_ca_files = use_system_ca_files + def _load_configuration(self) -> dict: if not os.path.exists(self.config_dir): os.makedirs(self.config_dir) diff --git a/elementary/monitor/cli.py b/elementary/monitor/cli.py index 9b47133ff..0550b434b 100644 --- a/elementary/monitor/cli.py +++ b/elementary/monitor/cli.py @@ -75,6 +75,11 @@ def decorator(func): default=None, help="The Slack token for your workspace.", )(func) + func = click.option( + "--use-system-ca-files/--no-use-system-ca-files", + default=True, + help="Whether to use the system CA files for SSL connections or the ones provided by certify (see https://pypi.org/project/certifi).", + )(func) if cmd in (Command.REPORT, Command.SEND_REPORT): func = click.option( "--exclude-elementary-models", @@ -331,6 +336,7 @@ def monitor( teams_webhook, maximum_columns_in_alert_samples, quiet_logs, + use_system_ca_files, ): """ Get alerts on failures in dbt jobs. @@ -365,6 +371,7 @@ def monitor( teams_webhook=teams_webhook, maximum_columns_in_alert_samples=maximum_columns_in_alert_samples, quiet_logs=quiet_logs, + use_system_ca_files=use_system_ca_files, ) anonymous_tracking = AnonymousCommandLineTracking(config) anonymous_tracking.set_env("use_select", bool(select)) @@ -692,6 +699,7 @@ def send_report( include, target_path, quiet_logs, + use_system_ca_files, ): """ Generate and send the report to an external platform. @@ -735,6 +743,7 @@ def send_report( env=env, project_name=project_name, quiet_logs=quiet_logs, + use_system_ca_files=use_system_ca_files, ) anonymous_tracking = AnonymousCommandLineTracking(config) anonymous_tracking.set_env("use_select", bool(select))