diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 072cb653..ea9616bc 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -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=[]) diff --git a/lemur/notifications/messaging.py b/lemur/notifications/messaging.py index cfff37b7..49f4dbac 100644 --- a/lemur/notifications/messaging.py +++ b/lemur/notifications/messaging.py @@ -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) diff --git a/lemur/plugins/bases/notification.py b/lemur/plugins/bases/notification.py index 36e94ef3..d1b63c44 100644 --- a/lemur/plugins/bases/notification.py +++ b/lemur/plugins/bases/notification.py @@ -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 diff --git a/lemur/plugins/lemur_email/plugin.py b/lemur/plugins/lemur_email/plugin.py index 65e2ee09..c98cd2a2 100644 --- a/lemur/plugins/lemur_email/plugin.py +++ b/lemur/plugins/lemur_email/plugin.py @@ -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() diff --git a/lemur/plugins/lemur_email/templates/expiration.html b/lemur/plugins/lemur_email/templates/expiration.html index 969d229f..8c2f8eab 100644 --- a/lemur/plugins/lemur_email/templates/expiration.html +++ b/lemur/plugins/lemur_email/templates/expiration.html @@ -79,18 +79,18 @@ - {% for message in messages %} + {% for certificate in certificates %} diff --git a/lemur/plugins/lemur_email/templates/rotation.html b/lemur/plugins/lemur_email/templates/rotation.html index 8e84a2ea..52abc84a 100644 --- a/lemur/plugins/lemur_email/templates/rotation.html +++ b/lemur/plugins/lemur_email/templates/rotation.html @@ -43,7 +43,7 @@ @@ -74,44 +74,31 @@
This is a Lemur certificate rotation notice. Your certificate has been re-issued and re-provisioned. - - - - @@ -119,18 +106,43 @@
- {{ message.name }} + {{ certificate.name }}
- {{ message.endpoints | length }} Endpoints -
{{ message.owner }} -
{{ message.not_after | time }} - Details + {{ certificate.endpoints | length }} Endpoints +
{{ certificate.owner }} +
{{ certificate.validityEnd | time }} + Details
- Your certificate(s) have been rotated! + Your certificate has been rotated!
- Impacted Certificate: -
- {% for message in messages %} - {% if not loop.last %} - - - - {% endif %} - {% endfor %}
- {{ message.name }} + {{ certificate.name }}
- {{ message.endpoints | length }} Endpoints -
{{ message.owner }} -
{{ message.not_after | time }} - Details +
{{ certificate.owner }} +
{{ certificate.validityEnd | time }} + Details
- Impacted Endpoints: + + Replaced by:
- {% for message in messages %} + + + + + + +
+ {{ certificate.replaced.name }} +
+ +
{{ certificate.replaced[0].owner }} +
{{ certificate.replaced[0].validityEnd | time }} + Details +
+
+ + + + + Endpoints Rotated: + + + + + + + {% for endpoint in certificate.endpoints %} diff --git a/lemur/plugins/lemur_email/tests/conftest.py b/lemur/plugins/lemur_email/tests/conftest.py new file mode 100644 index 00000000..0e1cd89f --- /dev/null +++ b/lemur/plugins/lemur_email/tests/conftest.py @@ -0,0 +1 @@ +from lemur.tests.conftest import * # noqa diff --git a/lemur/plugins/lemur_email/tests/test_email.py b/lemur/plugins/lemur_email/tests/test_email.py index ea7fc04d..821e4da7 100644 --- a/lemur/plugins/lemur_email/tests/test_email.py +++ b/lemur/plugins/lemur_email/tests/test_email.py @@ -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) diff --git a/lemur/plugins/lemur_slack/plugin.py b/lemur/plugins/lemur_slack/plugin.py index 3db831b3..225b73ea 100644 --- a/lemur/plugins/lemur_slack/plugin.py +++ b/lemur/plugins/lemur_slack/plugin.py @@ -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)) diff --git a/lemur/plugins/lemur_slack/tests/test_slack.py b/lemur/plugins/lemur_slack/tests/test_slack.py index b5d8dcfa..140208ed 100644 --- a/lemur/plugins/lemur_slack/tests/test_slack.py +++ b/lemur/plugins/lemur_slack/tests/test_slack.py @@ -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, diff --git a/lemur/tests/__init__.py b/lemur/tests/__init__.py index c2cb93b4..e69de29b 100644 --- a/lemur/tests/__init__.py +++ b/lemur/tests/__init__.py @@ -1,9 +0,0 @@ -import unittest - - -class LemurTestCase(unittest.TestCase): - pass - - -class LemurPluginTestCase(LemurTestCase): - pass diff --git a/lemur/tests/conftest.py b/lemur/tests/conftest.py index 95863cf3..cbb7224c 100644 --- a/lemur/tests/conftest.py +++ b/lemur/tests/conftest.py @@ -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 diff --git a/lemur/tests/factories.py b/lemur/tests/factories.py index a9023368..064337dd 100644 --- a/lemur/tests/factories.py +++ b/lemur/tests/factories.py @@ -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) diff --git a/lemur/tests/plugins/notification_plugin.py b/lemur/tests/plugins/notification_plugin.py index 4ebba3df..ad393d60 100644 --- a/lemur/tests/plugins/notification_plugin.py +++ b/lemur/tests/plugins/notification_plugin.py @@ -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 diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 43ffd8a5..e74e6dc4 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -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 diff --git a/lemur/tests/test_messaging.py b/lemur/tests/test_messaging.py index 73bd442c..ca51fdc2 100644 --- a/lemur/tests/test_messaging.py +++ b/lemur/tests/test_messaging.py @@ -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)
- {{ message.name }} + {{ endpoint.name }}
- {{ message.endpoints | length }} Endpoints -
{{ message.owner }} -
{{ message.not_after | time }} - Details +
{{ endpoint.dnsname }} | {{ endpoint.port }} + Details