Miscellaneous notification fixes and tests

This commit is contained in:
Jasmine Schladen 2020-10-16 15:13:12 -07:00
parent 67c184a97c
commit 60bb0037f0
8 changed files with 157 additions and 61 deletions

View File

@ -101,6 +101,9 @@ def send_notification(event_type, data, targets, notification):
notification.plugin.send(event_type, data, targets, notification.options) notification.plugin.send(event_type, data, targets, notification.options)
status = SUCCESS_METRIC_STATUS status = SUCCESS_METRIC_STATUS
except Exception as e: except Exception as e:
current_app.logger.error(
"Unable to send notification to {}.".format(targets), exc_info=True
)
sentry.captureException() sentry.captureException()
metrics.send( metrics.send(
@ -190,13 +193,13 @@ def send_rotation_notification(certificate, notification_plugin=None):
status = FAILURE_METRIC_STATUS status = FAILURE_METRIC_STATUS
if not notification_plugin: if not notification_plugin:
notification_plugin = plugins.get( notification_plugin = plugins.get(
current_app.config.get("LEMUR_DEFAULT_NOTIFICATION_PLUGIN") current_app.config.get("LEMUR_DEFAULT_NOTIFICATION_PLUGIN", "email-notification")
) )
data = certificate_notification_output_schema.dump(certificate).data data = certificate_notification_output_schema.dump(certificate).data
try: try:
notification_plugin.send("rotation", data, [data["owner"]]) notification_plugin.send("rotation", data, [data["owner"]], [])
status = SUCCESS_METRIC_STATUS status = SUCCESS_METRIC_STATUS
except Exception as e: except Exception as e:
current_app.logger.error( current_app.logger.error(
@ -290,7 +293,7 @@ def needs_notification(certificate):
for notification in certificate.notifications: for notification in certificate.notifications:
if not notification.active or not notification.options: if not notification.active or not notification.options:
return continue
interval = get_plugin_option("interval", notification.options) interval = get_plugin_option("interval", notification.options)
unit = get_plugin_option("unit", notification.options) unit = get_plugin_option("unit", notification.options)

View File

@ -19,14 +19,16 @@ from lemur.plugins import lemur_email as email
from lemur.plugins.lemur_email.templates.config import env from lemur.plugins.lemur_email.templates.config import env
def render_html(template_name, message): def render_html(template_name, options, certificates):
""" """
Renders the html for our email notification. Renders the html for our email notification.
:param template_name: :param template_name:
:param message: :param options:
:param certificates:
:return: :return:
""" """
message = {"options": options, "certificates": certificates}
template = env.get_template("{}.html".format(template_name)) template = env.get_template("{}.html".format(template_name))
return template.render( return template.render(
dict(message=message, hostname=current_app.config.get("LEMUR_HOSTNAME")) dict(message=message, hostname=current_app.config.get("LEMUR_HOSTNAME"))
@ -100,8 +102,7 @@ class EmailNotificationPlugin(ExpirationNotificationPlugin):
subject = "Lemur: {0} Notification".format(notification_type.capitalize()) subject = "Lemur: {0} Notification".format(notification_type.capitalize())
data = {"options": options, "certificates": message} body = render_html(notification_type, options, message)
body = render_html(notification_type, data)
s_type = current_app.config.get("LEMUR_EMAIL_SENDER", "ses").lower() s_type = current_app.config.get("LEMUR_EMAIL_SENDER", "ses").lower()

View File

@ -83,12 +83,12 @@
<td width="32px"></td> <td width="32px"></td>
<td width="16px"></td> <td width="16px"></td>
<td style="line-height:1.2"> <td style="line-height:1.2">
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ certificate.name }}</span> <span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.certificates.name }}</span>
<br> <br>
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272"> <span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">
<br>{{ certificate.owner }} <br>{{ message.certificates.owner }}
<br>{{ certificate.validityEnd | time }} <br>{{ message.certificates.validityEnd | time }}
<a href="https://{{ hostname }}/#/certificates/{{ certificate.name }}" target="_blank">Details</a> <a href="https://{{ hostname }}/#/certificates/{{ message.certificates.name }}" target="_blank">Details</a>
</span> </span>
</td> </td>
</tr> </tr>
@ -110,12 +110,12 @@
<td width="32px"></td> <td width="32px"></td>
<td width="16px"></td> <td width="16px"></td>
<td style="line-height:1.2"> <td style="line-height:1.2">
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ certificate.replacedBy[0].name }}</span> <span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.certificates.name }}</span>
<br> <br>
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272"> <span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">
<br>{{ certificate.replacedBy[0].owner }} <br>{{ message.certificates.owner }}
<br>{{ certificate.replacedBy[0].validityEnd | time }} <br>{{ message.certificates.validityEnd | time }}
<a href="https://{{ hostname }}/#/certificates/{{ certificate.replacedBy[0].name }}" target="_blank">Details</a> <a href="https://{{ hostname }}/#/certificates/{{ message.certificates.name }}" target="_blank">Details</a>
</span> </span>
</td> </td>
</tr> </tr>
@ -133,7 +133,7 @@
<table border="0" cellspacing="0" cellpadding="0" <table border="0" cellspacing="0" cellpadding="0"
style="margin-top:48px;margin-bottom:48px"> style="margin-top:48px;margin-bottom:48px">
<tbody> <tbody>
{% for endpoint in certificate.endpoints %} {% for endpoint in message.certificates.endpoints %}
<tr valign="middle"> <tr valign="middle">
<td width="32px"></td> <td width="32px"></td>
<td width="16px"></td> <td width="16px"></td>

View File

@ -1,36 +1,83 @@
import os import os
from lemur.plugins.lemur_email.templates.config import env from datetime import timedelta
import arrow
import boto3
from moto import mock_ses
from lemur.certificates.schemas import certificate_notification_output_schema
from lemur.plugins.lemur_email.plugin import render_html
from lemur.tests.factories import CertificateFactory from lemur.tests.factories import CertificateFactory
dir_path = os.path.dirname(os.path.realpath(__file__)) dir_path = os.path.dirname(os.path.realpath(__file__))
def test_render(certificate, endpoint): @mock_ses
from lemur.certificates.schemas import certificate_notification_output_schema def verify_sender_email():
ses_client = boto3.client("ses", region_name="us-east-1")
ses_client.verify_email_identity(EmailAddress="lemur@example.com")
def get_options():
return [
{"name": "interval", "value": 10},
{"name": "unit", "value": "days"},
]
def test_render_expiration(certificate, endpoint):
new_cert = CertificateFactory() new_cert = CertificateFactory()
new_cert.replaces.append(certificate) new_cert.replaces.append(certificate)
data = { assert render_html("expiration", get_options(), [certificate_notification_output_schema.dump(certificate).data])
"certificates": [certificate_notification_output_schema.dump(certificate).data],
"options": [
{"name": "interval", "value": 10},
{"name": "unit", "value": "days"},
],
}
template = env.get_template("{}.html".format("expiration"))
body = template.render(dict(message=data, hostname="lemur.test.example.com"))
template = env.get_template("{}.html".format("rotation"))
def test_render_rotation(certificate, endpoint):
certificate.endpoints.append(endpoint) certificate.endpoints.append(endpoint)
body = template.render( assert render_html("rotation", get_options(), certificate_notification_output_schema.dump(certificate).data)
dict(
certificate=certificate_notification_output_schema.dump(certificate).data,
hostname="lemur.test.example.com", def test_render_rotation_failure(pending_certificate):
) assert render_html("failed", get_options(), certificate_notification_output_schema.dump(pending_certificate).data)
)
@mock_ses
def test_send_expiration_notification():
from lemur.notifications.messaging import send_expiration_notifications
from lemur.tests.factories import CertificateFactory
from lemur.tests.factories import NotificationFactory
now = arrow.utcnow()
in_ten_days = now + timedelta(days=10, hours=1) # a bit more than 10 days since we'll check in the future
certificate = CertificateFactory()
notification = NotificationFactory(plugin_name="email-notification")
certificate.not_after = in_ten_days
certificate.notifications.append(notification)
certificate.notifications[0].options = get_options()
verify_sender_email()
assert send_expiration_notifications([]) == (2, 0)
@mock_ses
def test_send_rotation_notification(endpoint, source_plugin):
from lemur.notifications.messaging import send_rotation_notification
from lemur.deployment.service import rotate_certificate
new_certificate = CertificateFactory()
rotate_certificate(endpoint, new_certificate)
assert endpoint.certificate == new_certificate
verify_sender_email()
assert send_rotation_notification(new_certificate)
@mock_ses
def test_send_pending_failure_notification(user, pending_certificate, async_issuer_plugin):
from lemur.notifications.messaging import send_pending_failure_notification
verify_sender_email()
assert send_pending_failure_notification(pending_certificate)

View File

@ -58,26 +58,19 @@ def create_rotation_attachments(certificate):
"title": certificate["name"], "title": certificate["name"],
"title_link": create_certificate_url(certificate["name"]), "title_link": create_certificate_url(certificate["name"]),
"fields": [ "fields": [
{"title": "Owner", "value": certificate["owner"], "short": True},
{ {
{"title": "Owner", "value": certificate["owner"], "short": True}, "title": "Expires",
{ "value": arrow.get(certificate["validityEnd"]).format(
"title": "Expires", "dddd, MMMM D, YYYY"
"value": arrow.get(certificate["validityEnd"]).format( ),
"dddd, MMMM D, YYYY" "short": True,
), },
"short": True, {
}, "title": "Endpoints Rotated",
{ "value": len(certificate["endpoints"]),
"title": "Replaced By", "short": True,
"value": len(certificate["replaced"][0]["name"]), },
"short": True,
},
{
"title": "Endpoints Rotated",
"value": len(certificate["endpoints"]),
"short": True,
},
}
], ],
} }

View File

@ -21,3 +21,48 @@ def test_formatting(certificate):
} }
assert attachment == create_expiration_attachments(data)[0] assert attachment == create_expiration_attachments(data)[0]
def get_options():
return [
{"name": "interval", "value": 10},
{"name": "unit", "value": "days"},
]
# Currently disabled as we have no good way to mock Slack webhooks
# def test_send_expiration_notification():
# from lemur.notifications.messaging import send_expiration_notifications
# from lemur.tests.factories import CertificateFactory
#
# now = arrow.utcnow()
# in_ten_days = now + timedelta(days=10, hours=1) # a bit more than 10 days since we'll check in the future
# certificate = CertificateFactory()
# notification = NotificationFactory(plugin_name="slack-notification")
#
# certificate.not_after = in_ten_days
# certificate.notifications.append(notification)
# certificate.notifications[0].options = get_options()
#
# assert send_expiration_notifications([]) == (2, 0)
# Currently disabled as we have no good way to mock Slack webhooks
# def test_send_rotation_notification(endpoint, source_plugin):
# from lemur.notifications.messaging import send_rotation_notification
# from lemur.deployment.service import rotate_certificate
#
# notification = NotificationFactory(plugin_name="slack-notification")
#
# new_certificate = CertificateFactory()
# rotate_certificate(endpoint, new_certificate)
# assert endpoint.certificate == new_certificate
#
# assert send_rotation_notification(new_certificate, notification_plugin=notification.plugin)
# Currently disabled as the Slack plugin doesn't support this type of notification
# def test_send_pending_failure_notification(user, pending_certificate, async_issuer_plugin):
# from lemur.notifications.messaging import send_pending_failure_notification
#
# assert send_pending_failure_notification(pending_certificate, notification_plugin=plugins.get("slack-notification"))

View File

@ -46,7 +46,7 @@ LEMUR_ALLOWED_DOMAINS = [
# Lemur currently only supports SES for sending email, this address # Lemur currently only supports SES for sending email, this address
# needs to be verified # needs to be verified
LEMUR_EMAIL = "" LEMUR_EMAIL = "lemur@example.com"
LEMUR_SECURITY_TEAM_EMAIL = ["security@example.com"] LEMUR_SECURITY_TEAM_EMAIL = ["security@example.com"]
LEMUR_HOSTNAME = "lemur.example.com" LEMUR_HOSTNAME = "lemur.example.com"

View File

@ -1,8 +1,8 @@
from datetime import timedelta
import arrow
import pytest import pytest
from freezegun import freeze_time from freezegun import freeze_time
from datetime import timedelta
import arrow
from moto import mock_ses from moto import mock_ses
@ -105,4 +105,11 @@ def test_send_expiration_notification_with_no_notifications(
def test_send_rotation_notification(notification_plugin, certificate): def test_send_rotation_notification(notification_plugin, certificate):
from lemur.notifications.messaging import send_rotation_notification from lemur.notifications.messaging import send_rotation_notification
send_rotation_notification(certificate, notification_plugin=notification_plugin) assert send_rotation_notification(certificate, notification_plugin=notification_plugin)
@mock_ses
def test_send_pending_failure_notification(notification_plugin, async_issuer_plugin, pending_certificate):
from lemur.notifications.messaging import send_pending_failure_notification
assert send_pending_failure_notification(pending_certificate, notification_plugin=notification_plugin)