From 572e790832ef1b1924c768c6caab12bde322a9ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebasti=C3=A1n=20Zaffarano?= Date: Wed, 5 Feb 2025 15:49:00 +0100 Subject: [PATCH 1/4] Add optional ssl config flag --- elementary/clients/slack/client.py | 49 +++++++++++++++++++++++------- elementary/config/config.py | 3 ++ elementary/monitor/cli.py | 9 ++++++ 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/elementary/clients/slack/client.py b/elementary/clients/slack/client.py index f66a9b969..2cf82ac5b 100644 --- a/elementary/clients/slack/client.py +++ b/elementary/clients/slack/client.py @@ -1,8 +1,10 @@ import json +import ssl from abc import ABC, abstractmethod from typing import Dict, List, Optional, Tuple, Union import requests +import certifi from ratelimit import limits, sleep_and_retry from slack_sdk import WebClient, WebhookClient from slack_sdk.errors import SlackApiError @@ -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,20 @@ 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: 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..18cbcbb09 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 @@ -222,6 +223,8 @@ def __init__( self.quiet_logs = self._first_not_none( 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): 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)) From ed661581235fec7088de36f78d82cd729afeefe3 Mon Sep 17 00:00:00 2001 From: Itamar Hartstein Date: Mon, 9 Feb 2026 22:12:01 +0200 Subject: [PATCH 2/4] fix: honor SSL context for Slack workflow webhooks - Update _initial_client in SlackWebhookClient to configure requests.Session with SSL verification - When ssl_context is provided, set session.verify to certifi CA bundle - Add warning when workflow webhooks cannot fully honor --use-system-ca-files setting - Ensures SSL context is respected for workflow webhook requests --- elementary/clients/slack/client.py | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/elementary/clients/slack/client.py b/elementary/clients/slack/client.py index 2cf82ac5b..0afedc214 100644 --- a/elementary/clients/slack/client.py +++ b/elementary/clients/slack/client.py @@ -3,8 +3,8 @@ from abc import ABC, abstractmethod from typing import Dict, List, Optional, Tuple, Union -import requests import certifi +import requests from ratelimit import limits, sleep_and_retry from slack_sdk import WebClient, WebhookClient from slack_sdk.errors import SlackApiError @@ -67,7 +67,7 @@ def create_client( webhook=config.slack_webhook, is_workflow=config.is_slack_workflow, tracking=tracking, - ssl_context=ssl_context + ssl_context=ssl_context, ) return None @@ -262,7 +262,18 @@ def __init__( def _initial_client(self, ssl_context: Optional[ssl.SSLContext]): if self.is_workflow: - return requests.Session() + session = requests.Session() + if ssl_context is not None: + # For workflow webhooks, requests.Session doesn't directly support ssl.SSLContext, + # so we configure it to use certifi's CA bundle instead. + # The ssl_context parameter indicates that certifi should be used (not system CA files). + logger.warning( + "Workflow webhooks use requests.Session which doesn't fully support SSLContext. " + "Using certifi CA bundle for SSL verification instead of --use-system-ca-files setting." + ) + session.verify = certifi.where() + # If ssl_context is None, use system CA files (requests default behavior) + return session return WebhookClient( url=self.webhook, From 2c8f0e4111a63fd3950b9fb99ab92510876d1ed5 Mon Sep 17 00:00:00 2001 From: Itamar Hartstein Date: Tue, 10 Feb 2026 00:09:54 +0200 Subject: [PATCH 3/4] precommit fix --- elementary/config/config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/elementary/config/config.py b/elementary/config/config.py index 18cbcbb09..b15ff9c43 100644 --- a/elementary/config/config.py +++ b/elementary/config/config.py @@ -223,7 +223,7 @@ def __init__( self.quiet_logs = self._first_not_none( quiet_logs, config.get("quiet_logs"), False ) - + self.use_system_ca_files = use_system_ca_files def _load_configuration(self) -> dict: From 76877dc1b2073a66e222ba7577568843a341f970 Mon Sep 17 00:00:00 2001 From: Itamar Hartstein Date: Tue, 10 Feb 2026 00:14:46 +0200 Subject: [PATCH 4/4] fix is_workflow logic --- elementary/clients/slack/client.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/elementary/clients/slack/client.py b/elementary/clients/slack/client.py index 0afedc214..0c2814157 100644 --- a/elementary/clients/slack/client.py +++ b/elementary/clients/slack/client.py @@ -262,18 +262,9 @@ def __init__( def _initial_client(self, ssl_context: Optional[ssl.SSLContext]): if self.is_workflow: - session = requests.Session() - if ssl_context is not None: - # For workflow webhooks, requests.Session doesn't directly support ssl.SSLContext, - # so we configure it to use certifi's CA bundle instead. - # The ssl_context parameter indicates that certifi should be used (not system CA files). - logger.warning( - "Workflow webhooks use requests.Session which doesn't fully support SSLContext. " - "Using certifi CA bundle for SSL verification instead of --use-system-ca-files setting." - ) - session.verify = certifi.where() - # If ssl_context is None, use system CA files (requests default behavior) - return session + # 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,