You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@superset.apache.org by el...@apache.org on 2022/09/20 20:50:27 UTC

[superset] 17/29: feat: adds TLS certificate validation option for SMTP (#21272)

This is an automated email from the ASF dual-hosted git repository.

elizabeth pushed a commit to branch 2.0-test
in repository https://gitbox.apache.org/repos/asf/superset.git

commit 2b760d07757442c10320826af224265f564df146
Author: Daniel Vaz Gaspar <da...@gmail.com>
AuthorDate: Thu Sep 1 10:51:34 2022 +0100

    feat: adds TLS certificate validation option for SMTP (#21272)
    
    (cherry picked from commit 9fd752057eb261b0e5db87636836fd30579ffce6)
---
 docs/docs/installation/alerts-reports.mdx |  1 +
 superset/config.py                        |  4 +++-
 superset/utils/core.py                    | 34 ++++++++++++++++++-------------
 tests/integration_tests/email_tests.py    | 29 +++++++++++++++++++++++++-
 4 files changed, 52 insertions(+), 16 deletions(-)

diff --git a/docs/docs/installation/alerts-reports.mdx b/docs/docs/installation/alerts-reports.mdx
index a7491ad03e..a86f14893e 100644
--- a/docs/docs/installation/alerts-reports.mdx
+++ b/docs/docs/installation/alerts-reports.mdx
@@ -126,6 +126,7 @@ SLACK_API_TOKEN = "xoxb-"
 # Email configuration
 SMTP_HOST = "smtp.sendgrid.net" #change to your host
 SMTP_STARTTLS = True
+SMTP_SSL_SERVER_AUTH = True # If your using an SMTP server with a valid certificate
 SMTP_SSL = False
 SMTP_USER = "your_user"
 SMTP_PORT = 2525 # your port eg. 587
diff --git a/superset/config.py b/superset/config.py
index 8ab257e511..bae75fed6e 100644
--- a/superset/config.py
+++ b/superset/config.py
@@ -956,7 +956,9 @@ SMTP_USER = "superset"
 SMTP_PORT = 25
 SMTP_PASSWORD = "superset"
 SMTP_MAIL_FROM = "superset@superset.com"
-
+# If True creates a default SSL context with ssl.Purpose.CLIENT_AUTH using the
+# default system root CA certificates.
+SMTP_SSL_SERVER_AUTH = False
 ENABLE_CHUNK_ENCODING = False
 
 # Whether to bump the logging level to ERROR on the flask_appbuilder package
diff --git a/superset/utils/core.py b/superset/utils/core.py
index 6c90837959..652014b7de 100644
--- a/superset/utils/core.py
+++ b/superset/utils/core.py
@@ -27,6 +27,7 @@ import platform
 import re
 import signal
 import smtplib
+import ssl
 import tempfile
 import threading
 import traceback
@@ -980,23 +981,28 @@ def send_mime_email(
     smtp_password = config["SMTP_PASSWORD"]
     smtp_starttls = config["SMTP_STARTTLS"]
     smtp_ssl = config["SMTP_SSL"]
+    smpt_ssl_server_auth = config["SMTP_SSL_SERVER_AUTH"]
 
-    if not dryrun:
-        smtp = (
-            smtplib.SMTP_SSL(smtp_host, smtp_port)
-            if smtp_ssl
-            else smtplib.SMTP(smtp_host, smtp_port)
-        )
-        if smtp_starttls:
-            smtp.starttls()
-        if smtp_user and smtp_password:
-            smtp.login(smtp_user, smtp_password)
-        logger.debug("Sent an email to %s", str(e_to))
-        smtp.sendmail(e_from, e_to, mime_msg.as_string())
-        smtp.quit()
-    else:
+    if dryrun:
         logger.info("Dryrun enabled, email notification content is below:")
         logger.info(mime_msg.as_string())
+        return
+
+    # Default ssl context is SERVER_AUTH using the default system
+    # root CA certificates
+    ssl_context = ssl.create_default_context() if smpt_ssl_server_auth else None
+    smtp = (
+        smtplib.SMTP_SSL(smtp_host, smtp_port, context=ssl_context)
+        if smtp_ssl
+        else smtplib.SMTP(smtp_host, smtp_port)
+    )
+    if smtp_starttls:
+        smtp.starttls(context=ssl_context)
+    if smtp_user and smtp_password:
+        smtp.login(smtp_user, smtp_password)
+    logger.debug("Sent an email to %s", str(e_to))
+    smtp.sendmail(e_from, e_to, mime_msg.as_string())
+    smtp.quit()
 
 
 def get_email_address_list(address_string: str) -> List[str]:
diff --git a/tests/integration_tests/email_tests.py b/tests/integration_tests/email_tests.py
index d6c46a08d9..68e4aaf71e 100644
--- a/tests/integration_tests/email_tests.py
+++ b/tests/integration_tests/email_tests.py
@@ -17,6 +17,7 @@
 # under the License.
 """Unit tests for email service in Superset"""
 import logging
+import ssl
 import tempfile
 import unittest
 from email.mime.application import MIMEApplication
@@ -144,9 +145,35 @@ class TestEmailSmtp(SupersetTestCase):
         utils.send_mime_email("from", "to", MIMEMultipart(), app.config, dryrun=False)
         assert not mock_smtp.called
         mock_smtp_ssl.assert_called_with(
-            app.config["SMTP_HOST"], app.config["SMTP_PORT"]
+            app.config["SMTP_HOST"], app.config["SMTP_PORT"], context=None
         )
 
+    @mock.patch("smtplib.SMTP_SSL")
+    @mock.patch("smtplib.SMTP")
+    def test_send_mime_ssl_server_auth(self, mock_smtp, mock_smtp_ssl):
+        app.config["SMTP_SSL"] = True
+        app.config["SMTP_SSL_SERVER_AUTH"] = True
+        mock_smtp.return_value = mock.Mock()
+        mock_smtp_ssl.return_value = mock.Mock()
+        utils.send_mime_email("from", "to", MIMEMultipart(), app.config, dryrun=False)
+        assert not mock_smtp.called
+        mock_smtp_ssl.assert_called_with(
+            app.config["SMTP_HOST"], app.config["SMTP_PORT"], context=mock.ANY
+        )
+        called_context = mock_smtp_ssl.call_args.kwargs["context"]
+        self.assertEqual(called_context.verify_mode, ssl.CERT_REQUIRED)
+
+    @mock.patch("smtplib.SMTP")
+    def test_send_mime_tls_server_auth(self, mock_smtp):
+        app.config["SMTP_STARTTLS"] = True
+        app.config["SMTP_SSL_SERVER_AUTH"] = True
+        mock_smtp.return_value = mock.Mock()
+        mock_smtp.return_value.starttls.return_value = mock.Mock()
+        utils.send_mime_email("from", "to", MIMEMultipart(), app.config, dryrun=False)
+        mock_smtp.return_value.starttls.assert_called_with(context=mock.ANY)
+        called_context = mock_smtp.return_value.starttls.call_args.kwargs["context"]
+        self.assertEqual(called_context.verify_mode, ssl.CERT_REQUIRED)
+
     @mock.patch("smtplib.SMTP_SSL")
     @mock.patch("smtplib.SMTP")
     def test_send_mime_noauth(self, mock_smtp, mock_smtp_ssl):