lemur/lemur/notifications/messaging.py

296 lines
9.2 KiB
Python
Raw Normal View History

"""
.. module: lemur.notifications.messaging
:platform: Unix
:copyright: (c) 2018 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
2020-10-17 01:21:43 +02:00
import sys
from collections import defaultdict
from datetime import timedelta
from itertools import groupby
import arrow
from flask import current_app
from sqlalchemy import and_
from lemur import database
from lemur.certificates.models import Certificate
from lemur.certificates.schemas import certificate_notification_output_schema
from lemur.common.utils import windowed_query
from lemur.constants import FAILURE_METRIC_STATUS, SUCCESS_METRIC_STATUS
from lemur.extensions import metrics, sentry
2018-07-27 23:15:14 +02:00
from lemur.pending_certificates.schemas import pending_certificate_output_schema
from lemur.plugins import plugins
from lemur.plugins.utils import get_plugin_option
def get_certificates(exclude=None):
"""
2020-10-16 19:40:11 +02:00
Finds all certificates that are eligible for expiration notifications.
:param exclude:
:return:
"""
now = arrow.utcnow()
max = now + timedelta(days=90)
2019-05-16 16:57:02 +02:00
q = (
database.db.session.query(Certificate)
.filter(Certificate.not_after <= max)
.filter(Certificate.notify == True)
.filter(Certificate.expired == False)
) # noqa
exclude_conditions = []
if exclude:
for e in exclude:
2019-05-16 16:57:02 +02:00
exclude_conditions.append(~Certificate.name.ilike("%{}%".format(e)))
q = q.filter(and_(*exclude_conditions))
certs = []
2019-06-06 22:35:45 +02:00
for c in windowed_query(q, Certificate.id, 10000):
if needs_notification(c):
certs.append(c)
return certs
def get_eligible_certificates(exclude=None):
"""
2020-10-16 19:40:11 +02:00
Finds all certificates that are eligible for certificate expiration notification.
Returns the set of all eligible certificates, grouped by owner, with a list of applicable notifications.
:param exclude:
:return:
"""
certificates = defaultdict(dict)
certs = get_certificates(exclude=exclude)
# group by owner
for owner, items in groupby(certs, lambda x: x.owner):
notification_groups = []
for certificate in items:
notifications = needs_notification(certificate)
if notifications:
for notification in notifications:
notification_groups.append((notification, certificate))
# group by notification
for notification, items in groupby(notification_groups, lambda x: x[0].label):
certificates[owner][notification] = list(items)
return certificates
2020-10-16 19:40:11 +02:00
def send_plugin_notification(event_type, data, recipients, notification):
"""
Executes the plugin and handles failure.
:param event_type:
:param data:
2020-10-16 19:40:11 +02:00
:param recipients:
:param notification:
:return:
"""
2020-10-17 01:21:43 +02:00
function = f"{__name__}.{sys._getframe().f_code.co_name}"
log_data = {
"function": function,
2020-10-20 20:48:54 +02:00
"message": f"Sending expiration notification for to recipients {recipients}",
2020-10-17 01:30:21 +02:00
"notification_type": "expiration",
2020-10-20 20:48:54 +02:00
"certificate_targets": recipients,
2020-10-17 01:21:43 +02:00
}
status = FAILURE_METRIC_STATUS
try:
2020-10-16 19:40:11 +02:00
notification.plugin.send(event_type, data, recipients, notification.options)
status = SUCCESS_METRIC_STATUS
except Exception as e:
2020-10-20 20:48:54 +02:00
log_data["message"] = f"Unable to send expiration notification to recipients {recipients}"
2020-10-17 01:21:43 +02:00
current_app.logger.error(log_data, exc_info=True)
sentry.captureException()
2019-05-16 16:57:02 +02:00
metrics.send(
"notification",
"counter",
1,
metric_tags={"status": status, "event_type": event_type},
)
if status == SUCCESS_METRIC_STATUS:
return True
def send_expiration_notifications(exclude):
"""
This function will check for upcoming certificate expiration,
and send out notification emails at given intervals.
"""
success = failure = 0
# security team gets all
2019-05-16 16:57:02 +02:00
security_email = current_app.config.get("LEMUR_SECURITY_TEAM_EMAIL")
security_data = []
for owner, notification_group in get_eligible_certificates(exclude=exclude).items():
for notification_label, certificates in notification_group.items():
notification_data = []
notification = certificates[0][0]
for data in certificates:
n, certificate = data
2019-05-16 16:57:02 +02:00
cert_data = certificate_notification_output_schema.dump(
certificate
).data
notification_data.append(cert_data)
security_data.append(cert_data)
2020-10-16 19:40:11 +02:00
if send_default_notification(
"expiration", notification_data, [owner], notification.options
2019-05-16 16:57:02 +02:00
):
success += 1
else:
failure += 1
2020-10-20 20:48:54 +02:00
recipients = notification.plugin.filter_recipients(notification.options, security_email + [owner])
2020-10-16 19:40:11 +02:00
if send_plugin_notification(
"expiration",
notification_data,
recipients,
notification,
2019-05-16 16:57:02 +02:00
):
2020-10-16 19:40:11 +02:00
success += 1
else:
failure += 1
if send_default_notification(
"expiration", security_data, security_email, notification.options
2019-05-16 16:57:02 +02:00
):
success += 1
else:
failure += 1
return success, failure
2020-10-16 19:40:11 +02:00
def send_default_notification(notification_type, data, targets, notification_options=None):
"""
2020-10-16 19:40:11 +02:00
Sends a report to the specified target via the default notification plugin. Applicable for any notification_type.
At present, "default" means email, as the other notification plugins do not support dynamically configured targets.
2020-10-16 19:40:11 +02:00
:param notification_type:
:param data:
:param targets:
:param notification_options:
:return:
"""
2020-10-17 01:21:43 +02:00
function = f"{__name__}.{sys._getframe().f_code.co_name}"
log_data = {
"function": function,
2020-10-20 20:48:54 +02:00
"message": f"Sending notification for certificate data {data}",
"notification_type": notification_type,
2020-10-17 01:21:43 +02:00
}
status = FAILURE_METRIC_STATUS
2020-10-16 19:40:11 +02:00
notification_plugin = plugins.get(
current_app.config.get("LEMUR_DEFAULT_NOTIFICATION_PLUGIN", "email-notification")
)
try:
2020-10-16 19:40:11 +02:00
# we need the notification.options here because the email templates utilize the interval/unit info
notification_plugin.send(notification_type, data, targets, notification_options)
status = SUCCESS_METRIC_STATUS
except Exception as e:
2020-10-20 20:48:54 +02:00
log_data["message"] = f"Unable to send {notification_type} notification for certificate data {data} " \
f"to target {targets}"
current_app.logger.error(log_data, exc_info=True)
sentry.captureException()
2019-05-16 16:57:02 +02:00
metrics.send(
"notification",
"counter",
1,
2020-10-16 19:40:11 +02:00
metric_tags={"status": status, "event_type": notification_type},
2019-05-16 16:57:02 +02:00
)
2018-07-27 23:15:14 +02:00
if status == SUCCESS_METRIC_STATUS:
return True
2020-10-16 19:40:11 +02:00
def send_rotation_notification(certificate):
data = certificate_notification_output_schema.dump(certificate).data
return send_default_notification("rotation", data, [data["owner"]])
2019-05-16 16:57:02 +02:00
def send_pending_failure_notification(
2020-10-16 19:40:11 +02:00
pending_cert, notify_owner=True, notify_security=True
2019-05-16 16:57:02 +02:00
):
2018-07-27 23:15:14 +02:00
"""
Sends a report to certificate owners when their pending certificate failed to be created.
:param pending_cert:
2020-10-16 19:40:11 +02:00
:param notify_owner:
:param notify_security:
2018-07-27 23:15:14 +02:00
:return:
"""
data = pending_certificate_output_schema.dump(pending_cert).data
2019-05-16 16:57:02 +02:00
data["security_email"] = current_app.config.get("LEMUR_SECURITY_TEAM_EMAIL")
2018-07-27 23:15:14 +02:00
2020-10-16 19:40:11 +02:00
notify_owner_success = False
2018-07-27 23:15:14 +02:00
if notify_owner:
2020-10-16 19:40:11 +02:00
notify_owner_success = send_default_notification("failed", data, [data["owner"]], pending_cert)
2018-07-27 23:15:14 +02:00
2020-10-16 19:40:11 +02:00
notify_security_success = False
2018-07-27 23:15:14 +02:00
if notify_security:
2020-10-16 19:40:11 +02:00
notify_security_success = send_default_notification("failed", data, data["security_email"], pending_cert)
2020-10-16 19:40:11 +02:00
return notify_owner_success or notify_security_success
def needs_notification(certificate):
"""
2020-10-16 19:40:11 +02:00
Determine if notifications for a given certificate should currently be sent.
For each notification configured for the cert, verifies it is active, properly configured,
and that the configured expiration period is currently met.
:param certificate:
:return:
"""
now = arrow.utcnow()
days = (certificate.not_after - now).days
notifications = []
for notification in certificate.notifications:
if not notification.active or not notification.options:
2020-10-16 19:40:11 +02:00
continue
2019-05-16 16:57:02 +02:00
interval = get_plugin_option("interval", notification.options)
unit = get_plugin_option("unit", notification.options)
2019-05-16 16:57:02 +02:00
if unit == "weeks":
interval *= 7
2019-05-16 16:57:02 +02:00
elif unit == "months":
interval *= 30
2019-05-16 16:57:02 +02:00
elif unit == "days": # it's nice to be explicit about the base unit
pass
else:
2019-05-16 16:57:02 +02:00
raise Exception(
2020-10-20 00:12:48 +02:00
f"Invalid base unit for expiration interval: {unit}"
2019-05-16 16:57:02 +02:00
)
2020-10-20 20:48:54 +02:00
print(f"Does cert {certificate.name} need a notification {notification.label}? Actual: {days}, "
f"configured: {interval}") # TODO REMOVE
if days == interval:
notifications.append(notification)
return notifications