Initial support for notification plugins closes #8, closes #9, closes #7, closes #4, closes #16

This commit is contained in:
kevgliss
2015-07-29 17:13:06 -07:00
parent 1191fbe6c2
commit 1e748a64d7
43 changed files with 1659 additions and 582 deletions

View File

@ -1,3 +1,4 @@
from .destination import DestinationPlugin # noqa
from .issuer import IssuerPlugin # noqa
from .source import SourcePlugin # noqa
from .notification import NotificationPlugin, ExpirationNotificationPlugin # noqa

View File

@ -0,0 +1,52 @@
"""
.. module: lemur.bases.notification
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from lemur.plugins.base import Plugin
class NotificationPlugin(Plugin):
"""
This is the base class from which all of the supported
issuers will inherit from.
"""
type = 'notification'
def send(self):
raise NotImplementedError
class ExpirationNotificationPlugin(NotificationPlugin):
"""
This is the base class for all expiration notification plugins.
It contains some default options that are needed for all expiration
notification plugins.
"""
default_options = [
{
'name': 'interval',
'type': 'int',
'required': True,
'validation': '^\d+$',
'helpMessage': 'Number of days to be alert before expiration.',
},
{
'name': 'unit',
'type': 'select',
'required': True,
'validation': '',
'available': ['days', 'weeks', 'months'],
'helpMessage': 'Interval unit',
}
]
@property
def options(self):
return list(self.default_options) + self.additional_options
def send(self):
raise NotImplementedError

View File

@ -61,7 +61,7 @@ class AWSSourcePlugin(SourcePlugin):
options = [
{
'name': 'accountNumber',
'type': 'int',
'type': 'str',
'required': True,
'validation': '/^[0-9]{12,12}$/',
'helpMessage': 'Must be a valid AWS account number!',

View File

@ -0,0 +1,5 @@
try:
VERSION = __import__('pkg_resources') \
.get_distribution(__name__).version
except Exception, e:
VERSION = 'unknown'

View File

@ -0,0 +1,76 @@
"""
.. module: lemur.plugins.lemur_aws.aws
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import boto.ses
from flask import current_app
from flask_mail import Message
from lemur.extensions import smtp_mail
from lemur.plugins.bases import ExpirationNotificationPlugin
from lemur.plugins import lemur_email as email
from lemur.plugins.lemur_email.templates.config import env
def find_value(name, options):
for o in options:
if o.get(name):
return o['value']
class EmailNotificationPlugin(ExpirationNotificationPlugin):
title = 'Email'
slug = 'email-notification'
description = 'Sends expiration email notifications'
version = email.VERSION
author = 'Kevin Glisson'
author_url = 'https://github.com/netflix/lemur'
additional_options = [
{
'name': 'recipients',
'type': 'str',
'required': True,
'validation': '^([\w+-.%]+@[\w-.]+\.[A-Za-z]{2,4},?)+$',
'helpMessage': 'Comma delimited list of email addresses',
},
]
@staticmethod
def send(event_type, message, targets, options, **kwargs):
"""
Configures all Lemur email messaging
:param event_type:
:param options:
"""
subject = 'Notification: Lemur'
if event_type == 'expiration':
subject = 'Notification: SSL Certificate Expiration '
# jinja template depending on type
template = env.get_template('{}.html'.format(event_type))
body = template.render(**kwargs)
s_type = current_app.config.get("LEMUR_EMAIL_SENDER").lower()
if s_type == 'ses':
conn = boto.connect_ses()
conn.send_email(current_app.config.get("LEMUR_EMAIL"), subject, body, targets, format='html')
elif s_type == 'smtp':
msg = Message(subject, recipients=targets)
msg.body = "" # kinda a weird api for sending html emails
msg.html = body
smtp_mail.send(msg)
else:
current_app.logger.error("No mail carrier specified, notification emails were not able to be sent!")

View File

@ -0,0 +1,4 @@
from jinja2 import Environment, PackageLoader
loader = PackageLoader('lemur')
env = Environment(loader=loader)

View File

@ -0,0 +1,150 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="initial-scale=1.0"> <!-- So that mobile webkit will display zoomed in -->
<meta name="format-detection" content="telephone=no"> <!-- disable auto telephone linking in iOS -->
<title>Lemur</title>
<style type="text/css">
/* Resets: see reset.css for details */
.ReadMsgBody { width: 100%; background-color: #ebebeb;}
.ExternalClass {width: 100%; background-color: #ebebeb;}
.ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height:100%;}
body {-webkit-text-size-adjust:none; -ms-text-size-adjust:none;}
body {margin:0; padding:0;}
table {border-spacing:0;}
table td {border-collapse:collapse;}
.yshortcuts a {border-bottom: none !important;}
/* Constrain email width for small screens */
@media screen and (max-width: 600px) {
table[class="container"] {
width: 95% !important;
}
}
/* Give content more room on mobile */
@media screen and (max-width: 480px) {
td[class="container-padding"] {
padding-left: 12px !important;
padding-right: 12px !important;
}
}
</style>
</head>
<body style="margin:0; padding:10px 0;" bgcolor="#ebebeb" leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
<br>
<!-- 100% wrapper (grey background) -->
<table border="0" width="100%" height="100%" cellpadding="50" cellspacing="0" bgcolor="#ebebeb">
<tr>
<td align="center" valign="top" bgcolor="#ebebeb" style="background-color: #ebebeb;">
<!-- 600px container (white background) -->
<table border="0" width="600" cellpadding="0" cellspacing="0" class="container" bgcolor="#ffffff">
<tr>
<td class="container-padding" bgcolor="#ffffff" style="background-color: #ffffff; padding-left: 30px; padding-right: 30px; font-size: 14px; line-height: 20px; font-family: Helvetica, sans-serif; color: #333;">
<br />
<div style="font-weight: bold; font-size: 18px; line-height: 24px; color: #202d3b">
<span style="color: #29abe0">Notice: Your SSL certificates are expiring!</span>
<hr />
</div>
Lemur, Netflix's SSL management portal has noticed that the following certificates are expiring soon, if you rely on these certificates
you should create new certificates to replace the certificates that are expiring. Visit https://lemur.netflix.com/#/certificates/create to reissue them.
</td>
</tr>
{% for message in messages %}
<tr>
<td class="container-padding" bgcolor="#ffffff" style="background-color: #ffffff; padding-left: 30px; padding-right: 30px; font-size: 14px; line-height: 20px; font-family: Helvetica, sans-serif; color: #333;">
<hr />
<table width="540">
<tr>
<td><strong>Name</strong></td>
</tr>
<tr>
<td>{{ message.name }}</td>
<tr>
<td><strong>Owner</strong></td>
</tr>
<tr>
<td>{{ message.owner }}</td>
</tr>
<tr>
<td><strong>Creator</strong></td>
</tr>
<tr>
<td>{{ message.creator }}</td>
</tr>
<tr>
<td><strong>Not Before</strong></td>
</tr>
<tr>
<td>{{ message.not_before }}</td>
</tr>
<tr>
<td><strong>Not After</strong></td>
</tr>
<tr>
<td>{{ message.not_after }}</td>
</tr>
<tr>
<td><strong>Associated Domains</strong></td>
</tr>
{% if message.domains %}
{% for name in message.domains %}
<tr>
<td>{{ name }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td>Unknown</td>
</tr>
{% endif %}
<tr>
<td><strong>Associated ELBs</strong></td>
</tr>
{% if message.listeners %}
{% for name in message.listeners %}
<tr>
<td>{{ name }}</td>
</tr>
{% endfor %}
{% else %}
<tr>
<td>None</td>
</tr>
{% endif %}
<tr>
<td><strong>Potentially Superseded by</strong> (Lemur's best guess)</td>
</tr>
{% if message.superseded %}
{% for name in message.superseded %}
<tr><td>{{ name }}</td></tr>
{% endfor %}
{% else %}
<tr><td>Unknown</td></tr>
{% endif %}
<tr><td></td></tr>
</table>
</td>
</tr>
{% endfor %}
</table>
<!--/600px container -->
</td>
</tr>
<tr>
<td style="padding-top: 0px" align="center" valign="top">
<em style="font-style:italic; font-size: 12px; color: #aaa;">Lemur is broken regularly by <a style="color: #29abe0; text-decoration: none;" href="mailto:secops@netflix.com">Security Operations</a></em>
</td>
</tr>
</table>
<!--/100% wrapper-->
<br>
<br>
</body>
</html>

View File

@ -65,13 +65,13 @@ class PluginsList(AuthenticatedResource):
"id": 2,
"accountNumber": 222222222,
"label": "account2",
"comments": "this is a thing"
"description": "this is a thing"
},
{
"id": 1,
"accountNumber": 11111111111,
"label": "account1",
"comments": "this is a thing"
"description": "this is a thing"
},
]
"total": 2
@ -80,19 +80,24 @@ class PluginsList(AuthenticatedResource):
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
self.reqparse.add_argument('type', type=str, location='args')
args = self.reqparse.parse_args()
if args['type']:
return list(plugins.all(plugin_type=args['type']))
return plugins.all()
class PluginsTypeList(AuthenticatedResource):
""" Defines the 'plugins' endpoint """
class Plugins(AuthenticatedResource):
""" Defines the the 'plugins' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(PluginsTypeList, self).__init__()
super(Plugins, self).__init__()
@marshal_items(FIELDS)
def get(self, plugin_type):
def get(self, name):
"""
.. http:get:: /plugins/issuer
.. http:get:: /plugins/<name>
The current plugin list
@ -100,7 +105,7 @@ class PluginsTypeList(AuthenticatedResource):
.. sourcecode:: http
GET /plugins/issuer HTTP/1.1
GET /plugins HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
@ -113,27 +118,16 @@ class PluginsTypeList(AuthenticatedResource):
Content-Type: text/javascript
{
"items": [
{
"id": 2,
"accountNumber": 222222222,
"label": "account2",
"comments": "this is a thing"
},
{
"id": 1,
"accountNumber": 11111111111,
"label": "account1",
"comments": "this is a thing"
},
]
"total": 2
"accountNumber": 222222222,
"label": "account2",
"description": "this is a thing"
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
return list(plugins.all(plugin_type=plugin_type))
return plugins.get(name)
api.add_resource(PluginsList, '/plugins', endpoint='plugins')
api.add_resource(PluginsTypeList, '/plugins/<plugin_type>', endpoint='pluginType')
api.add_resource(Plugins, '/plugins/<name>', endpoint='pluginName')