Fixing up notification testing (#575)

This commit is contained in:
kevgliss 2016-12-08 11:33:40 -08:00 committed by GitHub
parent be1415fbd4
commit a4b32b0d31
16 changed files with 173 additions and 99 deletions

View File

@ -207,7 +207,8 @@ class CertificateNotificationOutputSchema(LemurOutputSchema):
name = fields.String()
owner = fields.Email()
user = fields.Nested(UserNestedOutputSchema)
replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
validity_end = ArrowDateTime(attribute='not_after')
replaced = fields.Nested(CertificateNestedOutputSchema, many=True)
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])

View File

@ -24,6 +24,7 @@ def send_expiration_notifications():
This function will check for upcoming certificate expiration,
and send out notification emails at given intervals.
"""
sent = 0
for plugin in plugins.all(plugin_type='notification'):
notifications = database.db.session.query(Notification)\
.filter(Notification.plugin_name == plugin.slug)\
@ -36,16 +37,18 @@ def send_expiration_notifications():
data = certificate_notification_output_schema.dump(certificate).data
messages.append((data, n.options))
for data, targets, options in messages:
for data, options in messages:
try:
plugin.send('expiration', data, targets, options)
plugin.send('expiration', data, [data['owner']], options)
metrics.send('expiration_notification_sent', 'counter', 1)
sent += 1
except Exception as e:
metrics.send('expiration_notification_failure', 'counter', 1)
current_app.logger.exception(e)
return sent
def send_rotation_notification(certificate):
def send_rotation_notification(certificate, notification_plugin=None):
"""
Sends a report to certificate owners when their certificate as been
rotated.
@ -53,12 +56,13 @@ def send_rotation_notification(certificate):
:param certificate:
:return:
"""
plugin = plugins.get(current_app.config.get('LEMUR_DEFAULT_NOTIFICATION_PLUGIN'))
if not notification_plugin:
notification_plugin = plugins.get(current_app.config.get('LEMUR_DEFAULT_NOTIFICATION_PLUGIN'))
data = certificate_notification_output_schema.dump(certificate).data
try:
plugin.send('rotation', data, [data.owner])
notification_plugin.send('rotation', data, [data['owner']])
metrics.send('rotation_notification_sent', 'counter', 1)
except Exception as e:
metrics.send('rotation_notification_failure', 'counter', 1)

View File

@ -16,7 +16,7 @@ class NotificationPlugin(Plugin):
"""
type = 'notification'
def send(self):
def send(self, notification_type, message, targets, options, **kwargs):
raise NotImplementedError
@ -48,5 +48,5 @@ class ExpirationNotificationPlugin(NotificationPlugin):
def options(self):
return list(self.default_options) + self.additional_options
def send(self):
def send(self, notification_type, message, targets, options, **kwargs):
raise NotImplementedError

View File

@ -85,10 +85,10 @@ class EmailNotificationPlugin(ExpirationNotificationPlugin):
raise InvalidConfiguration('Email sender type {0} is not recognized.')
@staticmethod
def send(template_name, message, targets, **kwargs):
subject = 'Lemur: Expiration Notification'
def send(notification_type, message, targets, options, **kwargs):
subject = 'Lemur: {0} Notification'.format(notification_type.capitalize())
body = render_html(template_name, message)
body = render_html(notification_type, message)
s_type = current_app.config.get("LEMUR_EMAIL_SENDER", 'ses').lower()

View File

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

View File

@ -43,7 +43,7 @@
<tr>
<td width="32px"></td>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:24px;color:#ffffff;line-height:1.25">
Your certificate(s) have been rotated!
Your certificate has been rotated!
</td>
<td width="32px"></td>
</tr>
@ -74,44 +74,31 @@
<br>This is a Lemur certificate rotation notice. Your certificate has been re-issued and re-provisioned.
</td>
</tr>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020;line-height:1.5">
Impacted Certificate:
</td>
</tr>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#202020;line-height:1.5">
<table border="0" cellspacing="0" cellpadding="0"
style="margin-top:48px;margin-bottom:48px">
<tbody>
{% for message in messages %}
<tr valign="middle">
<td width="32px"></td>
<td width="16px"></td>
<td style="line-height:1.2">
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.name }}</span>
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ certificate.name }}</span>
<br>
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">
{{ message.endpoints | length }} Endpoints
<br>{{ message.owner }}
<br>{{ message.not_after | time }}
<a href="https://{{ hostname }}/#/certificates/{{ message.name }}" target="_blank">Details</a>
<br>{{ certificate.owner }}
<br>{{ certificate.validityEnd | time }}
<a href="https://{{ hostname }}/#/certificates/{{ certificate.name }}" target="_blank">Details</a>
</span>
</td>
</tr>
{% if not loop.last %}
<tr valign="middle">
<td width="32px" height="24px"></td>
</tr>
{% endif %}
{% endfor %}
</tbody>
</table>
</td>
</tr>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020;line-height:1.5">
Impacted Endpoints:
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:15px;color:#202020;line-height:1.5">
Replaced by:
</td>
</tr>
<tr>
@ -119,18 +106,43 @@
<table border="0" cellspacing="0" cellpadding="0"
style="margin-top:48px;margin-bottom:48px">
<tbody>
{% for message in messages %}
<tr valign="middle">
<td width="32px"></td>
<td width="16px"></td>
<td style="line-height:1.2">
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ message.name }}</span>
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ certificate.replaced.name }}</span>
<br>
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">
{{ message.endpoints | length }} Endpoints
<br>{{ message.owner }}
<br>{{ message.not_after | time }}
<a href="https://{{ hostname }}/#/certificates/{{ message.name }}" target="_blank">Details</a>
<br>{{ certificate.replaced[0].owner }}
<br>{{ certificate.replaced[0].validityEnd | time }}
<a href="https://{{ hostname }}/#/certificates/{{ certificate.replaced[0].name }}" target="_blank">Details</a>
</span>
</td>
</tr>
</tbody>
</table>
</td>
</tr>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:15px;color:#202020;line-height:1.5">
Endpoints Rotated:
</td>
</tr>
<tr>
<td style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#202020;line-height:1.5">
<table border="0" cellspacing="0" cellpadding="0"
style="margin-top:48px;margin-bottom:48px">
<tbody>
{% for endpoint in certificate.endpoints %}
<tr valign="middle">
<td width="32px"></td>
<td width="16px"></td>
<td style="line-height:1.2">
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:20px;color:#202020">{{ endpoint.name }}</span>
<br>
<span style="font-family:Roboto-Regular,Helvetica,Arial,sans-serif;font-size:13px;color:#727272">
<br>{{ endpoint.dnsname }} | {{ endpoint.port }}
<a href="https://{{ hostname }}/#/endpoints/{{ endpoint.name }}" target="_blank">Details</a>
</span>
</td>
</tr>

View File

@ -0,0 +1 @@
from lemur.tests.conftest import * # noqa

View File

@ -1,12 +1,34 @@
import os
from lemur.plugins.lemur_email.templates.config import env
from lemur.tests.factories import CertificateFactory
def test_render():
messages = [{
'name': 'a-really-really-long-certificate-name',
'owner': 'bob@example.com',
'not_after': '2015-12-14 23:59:59'
}] * 10
dir_path = os.path.dirname(os.path.realpath(__file__))
def test_render(certificate, endpoint):
from lemur.certificates.schemas import certificate_notification_output_schema
new_cert = CertificateFactory()
new_cert.replaces.append(certificate)
certificates = [certificate_notification_output_schema.dump(certificate).data]
template = env.get_template('{}.html'.format('expiration'))
body = template.render(dict(messages=messages, hostname='lemur.test.example.com'))
with open(os.path.join(dir_path, 'expiration-rendered.html'), 'w') as f:
body = template.render(dict(certificates=certificates, hostname='lemur.test.example.com'))
f.write(body)
template = env.get_template('{}.html'.format('rotation'))
certificate.endpoints.append(endpoint)
with open(os.path.join(dir_path, 'rotation-rendered.html'), 'w') as f:
body = template.render(
dict(
certificate=certificate_notification_output_schema.dump(certificate).data,
hostname='lemur.test.example.com'
)
)
f.write(body)

View File

@ -23,28 +23,28 @@ def create_certificate_url(name):
)
def create_expiration_attachments(messages):
def create_expiration_attachments(certificates):
attachments = []
for message in messages:
for certificate in certificates:
attachments.append({
'title': message['name'],
'title_link': create_certificate_url(message['name']),
'title': certificate['name'],
'title_link': create_certificate_url(certificate['name']),
'color': 'danger',
'fallback': '',
'fields': [
{
'title': 'Owner',
'value': message['owner'],
'value': certificate['owner'],
'short': True
},
{
'title': 'Expires',
'value': arrow.get(message['not_after']).format('dddd, MMMM D, YYYY'),
'value': arrow.get(certificate['validityEnd']).format('dddd, MMMM D, YYYY'),
'short': True
},
{
'title': 'Endpoints Detected',
'value': len(message['endpoints']),
'value': len(certificate['endpoints']),
'short': True
}
],
@ -54,6 +54,37 @@ def create_expiration_attachments(messages):
return attachments
def create_rotation_attachments(certificate):
return {
'title': certificate['name'],
'title_link': create_certificate_url(certificate['name']),
'fields': [
{
{
'title': 'Owner',
'value': certificate['owner'],
'short': True
},
{
'title': 'Expires',
'value': arrow.get(certificate['validityEnd']).format('dddd, MMMM D, YYYY'),
'short': True
},
{
'title': 'Replaced By',
'value': len(certificate['replaced'][0]['name']),
'short': True
},
{
'title': 'Endpoints Rotated',
'value': len(certificate['endpoints']),
'short': True
}
}
]
}
class SlackNotificationPlugin(ExpirationNotificationPlugin):
title = 'Slack'
slug = 'slack-notification'
@ -85,25 +116,31 @@ class SlackNotificationPlugin(ExpirationNotificationPlugin):
},
]
def send(self, event_type, message, targets, options, **kwargs):
def send(self, notification_type, message, targets, options, **kwargs):
"""
A typical check can be performed using the notify command:
`lemur notify`
"""
if event_type == 'expiration':
attachments = None
if notification_type == 'expiration':
attachments = create_expiration_attachments(message)
elif notification_type == 'rotation':
attachments = create_rotation_attachments(message)
if not attachments:
raise Exception('Unable to create message attachments')
body = {
'text': 'Lemur Expiration Notification',
'text': 'Lemur {0} Notification'.format(notification_type.capitalize()),
'attachments': attachments,
'channel': self.get_option('recipients', options),
'username': self.get_option('username', options)
}
r = requests.post(self.get_option('webhook', options), json.dumps(body))
if r.status_code not in [200]:
raise Exception('Failed to send message')
current_app.logger.error("Slack response: {0} Message Body: {1}".format(r.status_code, body))

View File

@ -2,8 +2,8 @@
def test_formatting(certificate):
from lemur.plugins.lemur_slack.plugin import create_expiration_attachments
from lemur.notifications.service import _get_message_data
data = [_get_message_data(certificate)]
from lemur.certificates.schemas import certificate_notification_output_schema
data = [certificate_notification_output_schema.dump(certificate).data]
attachment = {
'title': certificate.name,

View File

@ -1,9 +0,0 @@
import unittest
class LemurTestCase(unittest.TestCase):
pass
class LemurPluginTestCase(LemurTestCase):
pass

View File

@ -1,16 +1,12 @@
import os
import pytest
from flask import current_app
from flask_principal import identity_changed, Identity
from lemur import create_app
from lemur.database import db as _db
from lemur.auth.service import create_token
from .factories import AuthorityFactory, NotificationFactory, DestinationFactory, \
CertificateFactory, UserFactory, RoleFactory, SourceFactory
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory
def pytest_runtest_setup(item):
@ -114,6 +110,14 @@ def certificate(session):
return c
@pytest.fixture
def endpoint(session):
s = SourceFactory()
e = EndpointFactory(source=s)
session.commit()
return e
@pytest.fixture
def role(session):
r = RoleFactory()
@ -171,17 +175,3 @@ def source_plugin():
from .plugins.source_plugin import TestSourcePlugin
register(TestSourcePlugin)
return TestSourcePlugin
@pytest.yield_fixture(scope="function")
def logged_in_user(session, app):
with app.test_request_context():
identity_changed.send(current_app._get_current_object(), identity=Identity(1))
yield
@pytest.yield_fixture(scope="function")
def logged_in_admin(session, app):
with app.test_request_context():
identity_changed.send(current_app._get_current_object(), identity=Identity(2))
yield

View File

@ -240,6 +240,7 @@ class EndpointFactory(BaseFactory):
type = FuzzyChoice(['elb'])
active = True
port = FuzzyInteger(0, high=65535)
dnsname = 'endpoint.example.com'
policy = SubFactory(PolicyFactory)
certificate = SubFactory(CertificateFactory)
source = SubFactory(SourceFactory)

View File

@ -13,5 +13,5 @@ class TestNotificationPlugin(NotificationPlugin):
super(TestNotificationPlugin, self).__init__(*args, **kwargs)
@staticmethod
def send(event_type, message, targets, options, **kwargs):
def send(notification_type, message, targets, options, **kwargs):
return

View File

@ -354,7 +354,7 @@ def test_get_account_number(client):
assert get_account_number(arn) == '11111111'
def test_mint_certificate(issuer_plugin, authority, logged_in_admin):
def test_mint_certificate(issuer_plugin, authority):
from lemur.certificates.service import mint
cert_body, private_key, chain = mint(authority=authority, csr=CSR_STR)
assert cert_body == INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR
@ -372,7 +372,7 @@ def test_create_certificate(issuer_plugin, authority, user):
assert cert.name == 'ACustomName1'
def test_reissue_certificate(issuer_plugin, authority, certificate, logged_in_admin):
def test_reissue_certificate(issuer_plugin, authority, certificate):
from lemur.certificates.service import reissue_certificate
new_cert = reissue_certificate(certificate)
assert new_cert

View File

@ -3,6 +3,8 @@ from freezegun import freeze_time
from datetime import timedelta
from moto import mock_ses
def test_needs_notification(app, certificate, notification):
from lemur.notifications.messaging import needs_notification
@ -21,11 +23,24 @@ def test_needs_notification(app, certificate, notification):
assert needs_notification(certificate)
@pytest.skip
def test_send_expiration_notification():
assert False
@mock_ses
def test_send_expiration_notification(certificate, notification, notification_plugin):
from lemur.notifications.messaging import send_expiration_notifications
notification.options = [{'name': 'interval', 'value': 10}, {'name': 'unit', 'value': 'days'}]
certificate.notifications.append(notification)
delta = certificate.not_after - timedelta(days=10)
with freeze_time(delta.datetime):
sent = send_expiration_notifications()
assert sent == 1
certificate.notify = False
sent = send_expiration_notifications()
assert sent == 0
@pytest.skip
def test_send_rotation_notification():
assert False
@mock_ses
def test_send_rotation_notification(notification_plugin, certificate):
from lemur.notifications.messaging import send_rotation_notification
send_rotation_notification(certificate, notification_plugin=notification_plugin)