feat: adds TLS certificate validation option for SMTP (#21272)

This commit is contained in:
Daniel Vaz Gaspar 2022-09-01 10:51:34 +01:00 committed by GitHub
parent 994f327157
commit 9fd752057e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 52 additions and 16 deletions

View File

@ -126,6 +126,7 @@ SLACK_API_TOKEN = "xoxb-"
# Email configuration # Email configuration
SMTP_HOST = "smtp.sendgrid.net" #change to your host SMTP_HOST = "smtp.sendgrid.net" #change to your host
SMTP_STARTTLS = True SMTP_STARTTLS = True
SMTP_SSL_SERVER_AUTH = True # If your using an SMTP server with a valid certificate
SMTP_SSL = False SMTP_SSL = False
SMTP_USER = "your_user" SMTP_USER = "your_user"
SMTP_PORT = 2525 # your port eg. 587 SMTP_PORT = 2525 # your port eg. 587

View File

@ -987,7 +987,9 @@ SMTP_USER = "superset"
SMTP_PORT = 25 SMTP_PORT = 25
SMTP_PASSWORD = "superset" SMTP_PASSWORD = "superset"
SMTP_MAIL_FROM = "superset@superset.com" 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 ENABLE_CHUNK_ENCODING = False
# Whether to bump the logging level to ERROR on the flask_appbuilder package # Whether to bump the logging level to ERROR on the flask_appbuilder package

View File

@ -27,6 +27,7 @@ import platform
import re import re
import signal import signal
import smtplib import smtplib
import ssl
import tempfile import tempfile
import threading import threading
import traceback import traceback
@ -994,23 +995,28 @@ def send_mime_email(
smtp_password = config["SMTP_PASSWORD"] smtp_password = config["SMTP_PASSWORD"]
smtp_starttls = config["SMTP_STARTTLS"] smtp_starttls = config["SMTP_STARTTLS"]
smtp_ssl = config["SMTP_SSL"] smtp_ssl = config["SMTP_SSL"]
smpt_ssl_server_auth = config["SMTP_SSL_SERVER_AUTH"]
if not dryrun: 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 = ( smtp = (
smtplib.SMTP_SSL(smtp_host, smtp_port) smtplib.SMTP_SSL(smtp_host, smtp_port, context=ssl_context)
if smtp_ssl if smtp_ssl
else smtplib.SMTP(smtp_host, smtp_port) else smtplib.SMTP(smtp_host, smtp_port)
) )
if smtp_starttls: if smtp_starttls:
smtp.starttls() smtp.starttls(context=ssl_context)
if smtp_user and smtp_password: if smtp_user and smtp_password:
smtp.login(smtp_user, smtp_password) smtp.login(smtp_user, smtp_password)
logger.debug("Sent an email to %s", str(e_to)) logger.debug("Sent an email to %s", str(e_to))
smtp.sendmail(e_from, e_to, mime_msg.as_string()) smtp.sendmail(e_from, e_to, mime_msg.as_string())
smtp.quit() smtp.quit()
else:
logger.info("Dryrun enabled, email notification content is below:")
logger.info(mime_msg.as_string())
def get_email_address_list(address_string: str) -> List[str]: def get_email_address_list(address_string: str) -> List[str]:

View File

@ -17,6 +17,7 @@
# under the License. # under the License.
"""Unit tests for email service in Superset""" """Unit tests for email service in Superset"""
import logging import logging
import ssl
import tempfile import tempfile
import unittest import unittest
from email.mime.application import MIMEApplication from email.mime.application import MIMEApplication
@ -175,9 +176,35 @@ class TestEmailSmtp(SupersetTestCase):
utils.send_mime_email("from", "to", MIMEMultipart(), app.config, dryrun=False) utils.send_mime_email("from", "to", MIMEMultipart(), app.config, dryrun=False)
assert not mock_smtp.called assert not mock_smtp.called
mock_smtp_ssl.assert_called_with( 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_SSL")
@mock.patch("smtplib.SMTP") @mock.patch("smtplib.SMTP")
def test_send_mime_noauth(self, mock_smtp, mock_smtp_ssl): def test_send_mime_noauth(self, mock_smtp, mock_smtp_ssl):