Fixing up notification testing (#575)
This commit is contained in:
parent
be1415fbd4
commit
a4b32b0d31
|
@ -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=[])
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
from lemur.tests.conftest import * # noqa
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -1,9 +0,0 @@
|
|||
import unittest
|
||||
|
||||
|
||||
class LemurTestCase(unittest.TestCase):
|
||||
pass
|
||||
|
||||
|
||||
class LemurPluginTestCase(LemurTestCase):
|
||||
pass
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue