diff --git a/elementary/clients/slack/client.py b/elementary/clients/slack/client.py index fb366d60b..10fa141e2 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 +import certifi from ratelimit import limits, sleep_and_retry from slack_sdk import WebClient, WebhookClient from slack_sdk.errors import SlackApiError @@ -24,8 +26,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] = {} @@ -37,15 +40,35 @@ 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.") - return SlackWebhookClient(webhook=config.slack_webhook, tracking=tracking) + 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, 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): @@ -79,12 +102,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) @@ -224,13 +248,16 @@ def __init__( self, webhook: str, tracking: Optional[Tracking] = None, + ssl_context: Optional[ssl.SSLContext] = None, ): self.webhook = webhook - super().__init__(tracking) + super().__init__(tracking, ssl_context) - def _initial_client(self): + def _initial_client(self, ssl_context: Optional[ssl.SSLContext]): 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 3eaefef46..a65aa7253 100644 --- a/elementary/config/config.py +++ b/elementary/config/config.py @@ -73,6 +73,7 @@ def __init__( env: str = DEFAULT_ENV, run_dbt_deps_if_needed: Optional[bool] = None, project_name: Optional[str] = None, + use_system_ca_files: bool = True, ): self.config_dir = config_dir self.profiles_dir = profiles_dir @@ -204,6 +205,8 @@ def __init__( "disable_elementary_version_check", 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 019293f1f..f31357edf 100644 --- a/elementary/monitor/cli.py +++ b/elementary/monitor/cli.py @@ -73,6 +73,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", @@ -304,6 +309,7 @@ def monitor( report_url, filters, teams_webhook, + use_system_ca_files, ): """ Get alerts on failures in dbt jobs. @@ -335,6 +341,7 @@ def monitor( slack_group_alerts_by=group_by, report_url=report_url, teams_webhook=teams_webhook, + use_system_ca_files=use_system_ca_files, ) anonymous_tracking = AnonymousCommandLineTracking(config) anonymous_tracking.set_env("use_select", bool(select)) @@ -652,6 +659,7 @@ def send_report( disable, include, target_path, + use_system_ca_files, ): """ Generate and send the report to an external platform. @@ -693,6 +701,7 @@ def send_report( report_url=report_url, env=env, project_name=project_name, + use_system_ca_files=use_system_ca_files, ) anonymous_tracking = AnonymousCommandLineTracking(config) anonymous_tracking.set_env("use_select", bool(select))