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

@ -21,6 +21,7 @@ from lemur.listeners.views import mod as listeners_bp
from lemur.certificates.views import mod as certificates_bp
from lemur.status.views import mod as status_bp
from lemur.plugins.views import mod as plugins_bp
from lemur.notifications.views import mod as notifications_bp
LEMUR_BLUEPRINTS = (
users_bp,
@ -34,6 +35,7 @@ LEMUR_BLUEPRINTS = (
certificates_bp,
status_bp,
plugins_bp,
notifications_bp,
)

View File

@ -15,7 +15,7 @@ from lemur.authorities.models import Authority
from lemur.roles import service as role_service
from lemur.roles.models import Role
import lemur.certificates.service as cert_service
from lemur.certificates.models import Certificate
from lemur.plugins.base import plugins
@ -42,10 +42,6 @@ def create(kwargs):
"""
Create a new authority.
:param name: name of the authority
:param roles: roles that are allowed to use this authority
:param options: available options for authority
:param description:
:rtype : Authority
:return:
"""
@ -55,7 +51,9 @@ def create(kwargs):
kwargs['creator'] = g.current_user.email
cert_body, intermediate, issuer_roles = issuer.create_authority(kwargs)
cert = cert_service.save_cert(cert_body, None, intermediate, [])
cert = Certificate(cert_body, chain=intermediate)
cert.owner = kwargs['ownerEmail']
cert.description = "This is the ROOT certificate for the {0} certificate authority".format(kwargs.get('caName'))
cert.user = g.current_user
# we create and attach any roles that the issuer gives us

View File

@ -13,16 +13,17 @@ from cryptography.hazmat.backends import default_backend
from flask import current_app
from sqlalchemy.orm import relationship
from sqlalchemy import Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy_utils import EncryptedType
from lemur.database import db
from lemur.plugins.base import plugins
from lemur.domains.models import Domain
from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE
from lemur.models import certificate_associations, certificate_destination_associations
from lemur.models import certificate_associations, certificate_destination_associations, certificate_notification_associations
def create_name(issuer, not_before, not_after, subject, san):
@ -147,7 +148,7 @@ def cert_get_issuer(cert):
"""
delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum())
try:
issuer = str(cert.subject.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
issuer = str(cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)[0].value)
return issuer.translate(None, delchars)
except Exception as e:
current_app.logger.error("Unable to get issuer! {0}".format(e))
@ -203,8 +204,6 @@ class Certificate(db.Model):
owner = Column(String(128))
body = Column(Text())
private_key = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY')))
challenge = Column(EncryptedType(String, os.environ.get('LEMUR_ENCRYPTION_KEY'))) # TODO deprecate
csr_config = Column(Text()) # TODO deprecate
status = Column(String(128))
deleted = Column(Boolean, index=True)
name = Column(String(128))
@ -221,7 +220,8 @@ class Certificate(db.Model):
date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False)
user_id = Column(Integer, ForeignKey('users.id'))
authority_id = Column(Integer, ForeignKey('authorities.id'))
accounts = relationship("Destination", secondary=certificate_destination_associations, backref='certificate')
notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate')
destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate')
domains = relationship("Domain", secondary=certificate_associations, backref="certificate")
elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate')
@ -272,3 +272,10 @@ class Certificate(db.Model):
def as_dict(self):
return {c.name: getattr(self, c.name) for c in self.__table__.columns}
@event.listens_for(Certificate.destinations, 'append')
def update_destinations(target, value, initiator):
destination_plugin = plugins.get(value.plugin_name)
destination_plugin.upload(target.body, target.private_key, target.chain, value.options)

View File

@ -15,6 +15,7 @@ from lemur.plugins.base import plugins
from lemur.certificates.models import Certificate
from lemur.destinations.models import Destination
from lemur.notifications.models import Notification
from lemur.authorities.models import Authority
from lemur.roles.models import Role
@ -75,7 +76,7 @@ def find_duplicates(cert_body):
return Certificate.query.filter_by(body=cert_body).all()
def update(cert_id, owner, active):
def update(cert_id, owner, description, active, destinations, notifications):
"""
Updates a certificate.
@ -87,6 +88,11 @@ def update(cert_id, owner, active):
cert = get(cert_id)
cert.owner = owner
cert.active = active
cert.description = description
database.update_list(cert, 'notifications', Notification, notifications)
database.update_list(cert, 'destinations', Destination, destinations)
return database.update(cert)
@ -106,7 +112,8 @@ def mint(issuer_options):
issuer_options['creator'] = g.user.email
cert_body, cert_chain = issuer.create_certificate(csr, issuer_options)
cert = save_cert(cert_body, private_key, cert_chain, issuer_options.get('destinations'))
cert = Certificate(cert_body, private_key, cert_chain)
cert.user = g.user
cert.authority = authority
database.update(cert)
@ -139,43 +146,25 @@ def import_certificate(**kwargs):
if kwargs.get('user'):
cert.user = kwargs.get('user')
if kwargs.get('destination'):
cert.destinations.append(kwargs.get('destination'))
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
cert = database.create(cert)
return cert
def save_cert(cert_body, private_key, cert_chain, destinations):
"""
Determines if the certificate needs to be uploaded to AWS or other services.
:param cert_body:
:param private_key:
:param cert_chain:
:param destinations:
"""
cert = Certificate(cert_body, private_key, cert_chain)
# we should save them to any destination that is requested
for destination in destinations:
destination_plugin = plugins.get(destination['plugin']['slug'])
destination_plugin.upload(cert, private_key, cert_chain, destination['plugin']['pluginOptions'])
return cert
def upload(**kwargs):
"""
Allows for pre-made certificates to be imported into Lemur.
"""
cert = save_cert(
cert = Certificate(
kwargs.get('public_cert'),
kwargs.get('private_key'),
kwargs.get('intermediate_cert'),
kwargs.get('destinations')
)
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
cert.owner = kwargs['owner']
cert = database.create(cert)
g.user.certificates.append(cert)
@ -189,10 +178,18 @@ def create(**kwargs):
cert, private_key, cert_chain = mint(kwargs)
cert.owner = kwargs['owner']
database.update_list(cert, 'destinations', Destination, kwargs.get('destinations'))
database.create(cert)
cert.description = kwargs['description']
g.user.certificates.append(cert)
database.update(g.user)
# do this after the certificate has already been created because if it fails to upload to the third party
# we do not want to lose the certificate information.
database.update_list(cert, 'notifications', Notification, kwargs.get('notifications'))
database.update(cert)
return cert
@ -207,6 +204,7 @@ def render(args):
time_range = args.pop('time_range')
destination_id = args.pop('destination_id')
notification_id = args.pop('notification_id', None)
show = args.pop('show')
# owner = args.pop('owner')
# creator = args.pop('creator') # TODO we should enabling filtering by owner
@ -248,6 +246,9 @@ def render(args):
if destination_id:
query = query.filter(Certificate.destinations.any(Destination.id == destination_id))
if notification_id:
query = query.filter(Certificate.notifications.any(Notification.id == notification_id))
if time_range:
to = arrow.now().replace(weeks=+time_range).format('YYYY-MM-DD')
now = arrow.now().format('YYYY-MM-DD')
@ -284,11 +285,17 @@ def create_csr(csr_config):
x509.BasicConstraints(ca=False, path_length=None), critical=True,
)
# for k, v in csr_config.get('extensions', {}).items():
# if k == 'subAltNames':
# builder = builder.add_extension(
# x509.SubjectAlternativeName([x509.DNSName(n) for n in v]), critical=True,
# )
for k, v in csr_config.get('extensions', {}).items():
if k == 'subAltNames':
# map types to their x509 objects
general_names = []
for name in v['names']:
if name['nameType'] == 'DNSName':
general_names.append(x509.DNSName(name['value']))
builder = builder.add_extension(
x509.SubjectAlternativeName(general_names), critical=True
)
# TODO support more CSR options, none of the authorities support these atm
# builder.add_extension(

View File

@ -271,7 +271,7 @@ class CertificatesList(AuthenticatedResource):
"""
self.reqparse.add_argument('extensions', type=dict, location='json')
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('elbs', type=list, location='json')
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
self.reqparse.add_argument('owner', type=str, location='json')
self.reqparse.add_argument('validityStart', type=str, location='json') # TODO validate
self.reqparse.add_argument('validityEnd', type=str, location='json') # TODO validate
@ -329,7 +329,8 @@ class CertificatesUpload(AuthenticatedResource):
"publicCert": "---Begin Public...",
"intermediateCert": "---Begin Public...",
"privateKey": "---Begin Private..."
"destinations": []
"destinations": [],
"notifications": []
}
**Example response**:
@ -371,6 +372,7 @@ class CertificatesUpload(AuthenticatedResource):
self.reqparse.add_argument('owner', type=str, required=True, location='json')
self.reqparse.add_argument('publicCert', type=pem_str, required=True, dest='public_cert', location='json')
self.reqparse.add_argument('destinations', type=list, default=[], dest='destinations', location='json')
self.reqparse.add_argument('notifications', type=list, default=[], dest='notifications', location='json')
self.reqparse.add_argument('intermediateCert', type=pem_str, dest='intermediate_cert', location='json')
self.reqparse.add_argument('privateKey', type=private_key_str, dest='private_key', location='json')
@ -523,6 +525,8 @@ class Certificates(AuthenticatedResource):
{
"owner": "jimbob@example.com",
"active": false
"notifications": [],
"destinations": []
}
**Example response**:
@ -549,7 +553,7 @@ class Certificates(AuthenticatedResource):
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"cn": "example.com",
"status": "unknown"
"status": "unknown",
}
:reqheader Authorization: OAuth token to authenticate
@ -558,6 +562,9 @@ class Certificates(AuthenticatedResource):
"""
self.reqparse.add_argument('active', type=bool, location='json')
self.reqparse.add_argument('owner', type=str, location='json')
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('destinations', type=list, default=[], location='json')
self.reqparse.add_argument('notifications', type=list, default=[], location='json')
args = self.reqparse.parse_args()
cert = service.get(certificate_id)
@ -565,13 +572,96 @@ class Certificates(AuthenticatedResource):
permission = UpdateCertificatePermission(certificate_id, hasattr(role, 'id'))
if permission.can():
return service.update(certificate_id, args['owner'], args['active'])
return service.update(
certificate_id,
args['owner'],
args['description'],
args['active'],
args['destinations'],
args['notifications']
)
return dict(message='You are not authorized to update this certificate'), 403
class NotificationCertificatesList(AuthenticatedResource):
""" Defines the 'certificates' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(NotificationCertificatesList, self).__init__()
@marshal_items(FIELDS)
def get(self, notification_id):
"""
.. http:get:: /notifications/1/certificates
The current list of certificates for a given notification
**Example request**:
.. sourcecode:: http
GET /notifications/1/certificates HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [
{
"id": 1,
"name": "cert1",
"description": "this is cert1",
"bits": 2048,
"deleted": false,
"issuer": "ExampeInc.",
"serial": "123450",
"chain": "-----Begin ...",
"body": "-----Begin ...",
"san": true,
"owner": 'bob@example.com",
"active": true,
"notBefore": "2015-06-05T17:09:39",
"notAfter": "2015-06-10T17:09:39",
"cn": "example.com",
"status": "unknown"
}
]
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
parser.add_argument('timeRange', type=int, dest='time_range', location='args')
parser.add_argument('owner', type=bool, location='args')
parser.add_argument('id', type=str, location='args')
parser.add_argument('active', type=bool, location='args')
parser.add_argument('destinationId', type=int, dest="destination_id", location='args')
parser.add_argument('creator', type=str, location='args')
parser.add_argument('show', type=str, location='args')
args = parser.parse_args()
args['notification_id'] = notification_id
return service.render(args)
api.add_resource(CertificatesList, '/certificates', endpoint='certificates')
api.add_resource(Certificates, '/certificates/<int:certificate_id>', endpoint='certificate')
api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats')
api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload')
api.add_resource(CertificatePrivateKey, '/certificates/<int:certificate_id>/key', endpoint='privateKeyCertificates')
api.add_resource(NotificationCertificatesList, '/notifications/<int:notification_id>/certificates', endpoint='notificationCertificates')

View File

@ -14,7 +14,6 @@ from lemur.auth.service import AuthenticatedResource
from lemur.auth.permissions import admin_permission
from lemur.common.utils import paginated_parser, marshal_items
from lemur.plugins.views import FIELDS as PLUGIN_FIELDS
mod = Blueprint('destinations', __name__)
api = Api(mod)
@ -22,7 +21,8 @@ api = Api(mod)
FIELDS = {
'description': fields.String,
'plugin': fields.Nested(PLUGIN_FIELDS, attribute='plugin'),
'destinationOptions': fields.Raw(attribute='options'),
'pluginName': fields.String(attribute='plugin_name'),
'label': fields.String,
'id': fields.Integer,
}
@ -60,19 +60,23 @@ class DestinationsList(AuthenticatedResource):
{
"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
"destinationOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-destination",
"id": 3,
"description": "test",
"label": "test"
}
],
"total": 1
}
:query sortBy: field to sort on
@ -104,9 +108,20 @@ class DestinationsList(AuthenticatedResource):
Accept: application/json, text/javascript
{
"accountNumber": 11111111111,
"label": "account1,
"comments": "this is a thing"
"destinationOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-destination",
"id": 3,
"description": "test",
"label": "test"
}
**Example response**:
@ -118,15 +133,24 @@ class DestinationsList(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"accountNumber": 11111111111,
"label": "account1",
"comments": "this is a thing"
"destinationOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-destination",
"id": 3,
"description": "test",
"label": "test"
}
:arg accountNumber: aws account number
:arg label: human readable account label
:arg comments: some description about the account
:arg description: some description about the account
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
@ -167,10 +191,20 @@ class Destinations(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"accountNumber": 11111111111,
"label": "account1",
"comments": "this is a thing"
"destinationOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-destination",
"id": 3,
"description": "test",
"label": "test"
}
:reqheader Authorization: OAuth token to authenticate
@ -194,6 +228,22 @@ class Destinations(AuthenticatedResource):
Host: example.com
Accept: application/json, text/javascript
{
"destinationOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-destination",
"id": 3,
"description": "test",
"label": "test"
}
**Example response**:
@ -204,24 +254,34 @@ class Destinations(AuthenticatedResource):
Content-Type: text/javascript
{
"id": 1,
"accountNumber": 11111111111,
"label": "labelChanged",
"comments": "this is a thing"
"destinationOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-destination",
"id": 3,
"description": "test",
"label": "test"
}
:arg accountNumber: aws account number
:arg label: human readable account label
:arg comments: some description about the account
:arg description: some description about the account
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('pluginOptions', type=dict, location='json', required=True)
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
args = self.reqparse.parse_args()
return service.update(destination_id, args['label'], args['options'], args['description'])
return service.update(destination_id, args['label'], args['plugin']['pluginOptions'], args['description'])
@admin_permission.require(http_exception=403)
def delete(self, destination_id):
@ -257,6 +317,28 @@ class CertificateDestinations(AuthenticatedResource):
Vary: Accept
Content-Type: text/javascript
{
"items": [
{
"destinationOptions": [
{
"name": "accountNumber",
"required": true,
"value": 111111111112,
"helpMessage": "Must be a valid AWS account number!",
"validation": "/^[0-9]{12,12}$/",
"type": "int"
}
],
"pluginName": "aws-destination",
"id": 3,
"description": "test",
"label": "test"
}
],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1

View File

@ -32,6 +32,8 @@ from lemur.destinations.models import Destination # noqa
from lemur.domains.models import Domain # noqa
from lemur.elbs.models import ELB # noqa
from lemur.listeners.models import Listener # noqa
from lemur.notifications.models import Notification # noqa
manager = Manager(create_app)
manager.add_option('-c', '--config', dest='config')

View File

@ -25,6 +25,12 @@ certificate_destination_associations = db.Table('certificate_destination_associa
ForeignKey('certificates.id', ondelete='cascade'))
)
certificate_notification_associations = db.Table('certificate_notification_associations',
Column('notification_id', Integer,
ForeignKey('notifications.id', ondelete='cascade')),
Column('certificate_id', Integer,
ForeignKey('certificates.id', ondelete='cascade'))
)
roles_users = db.Table('roles_users',
Column('user_id', Integer, ForeignKey('users.id')),
Column('role_id', Integer, ForeignKey('roles.id'))

View File

@ -1,218 +0,0 @@
"""
.. module: lemur.notifications
: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 ssl
import socket
import arrow
import boto.ses
from flask import current_app
from flask_mail import Message
from lemur import database
from lemur.certificates.models import Certificate
from lemur.domains.models import Domain
from lemur.templates.config import env
from lemur.extensions import smtp_mail
NOTIFICATION_INTERVALS = [30, 15, 5, 2]
def _get_domain_certificate(name):
"""
Fetch the SSL certificate currently hosted at a given domain (if any) and
compare it against our all of our know certificates to determine if a new
SSL certificate has already been deployed
:param name:
:return:
"""
query = database.session_query(Certificate)
try:
pub_key = ssl.get_server_certificate((name, 443))
return query.filter(Certificate.body == pub_key.strip()).first()
except socket.gaierror as e:
current_app.logger.info(str(e))
def _find_superseded(domains):
"""
Here we try to fetch any domain in the certificate to see if we can resolve it
and to try and see if it is currently serving the certificate we are
alerting on
:param domains:
:return:
"""
query = database.session_query(Certificate)
ss_list = []
for domain in domains:
dc = _get_domain_certificate(domain.name)
if dc:
ss_list.append(dc)
current_app.logger.info("Trying to resolve {0}".format(domain.name))
query = query.filter(Certificate.domains.any(Domain.name.in_([x.name for x in domains])))
query = query.filter(Certificate.active == True) # noqa
query = query.filter(Certificate.not_after >= arrow.utcnow().format('YYYY-MM-DD'))
ss_list.extend(query.all())
return ss_list
def send_expiration_notifications():
"""
This function will check for upcoming certificate expiration,
and send out notification emails at given intervals.
"""
notifications = 0
certs = _get_expiring_certs()
alerts = []
for cert in certs:
if _is_eligible_for_notifications(cert):
data = _get_message_data(cert)
recipients = _get_message_recipients(cert)
alerts.append((data, recipients))
roll_ups = _create_roll_ups(alerts)
for messages, recipients in roll_ups:
notifications += 1
send("Certificate Expiration", dict(messages=messages), 'event', recipients)
print notifications
current_app.logger.info("Lemur has sent {0} certification notifications".format(notifications))
def _get_message_recipients(cert):
"""
Determine who the recipients of the certificate expiration should be
:param cert:
:return:
"""
recipients = []
if current_app.config.get('SECURITY_TEAM_EMAIL'):
recipients.extend(current_app.config.get('SECURITY_TEAM_EMAIL'))
recipients.append(cert.owner)
if cert.user:
recipients.append(cert.user.email)
return list(set(recipients))
def _get_message_data(cert):
"""
Parse our the certification information needed for our notification
:param cert:
:return:
"""
cert_dict = cert.as_dict()
cert_dict['domains'] = [x .name for x in cert.domains]
cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert.domains) if cert.name != x]))
return cert_dict
def _get_expiring_certs(outlook=30):
"""
Find all the certificates expiring within a given outlook
:param outlook: int days to look forward
:return:
"""
now = arrow.utcnow()
query = database.session_query(Certificate)
attr = Certificate.not_after
# get all certs expiring in the next 30 days
to = now.replace(days=+outlook).format('YYYY-MM-DD')
certs = []
for cert in query.filter(attr <= to).filter(attr >= now.format('YYYY-MM-DD')).all():
if _is_eligible_for_notifications(cert):
certs.append(cert)
return certs
def _is_eligible_for_notifications(cert, intervals=None):
"""
Determine if notifications for a given certificate should
currently be sent
:param cert:
:param intervals: list of days to alert on
:return:
"""
now = arrow.utcnow()
if cert.active:
days = (cert.not_after - now.naive).days
if not intervals:
intervals = NOTIFICATION_INTERVALS
if days in intervals:
return cert
def _create_roll_ups(messages):
"""
Take all of the messages that should be sent and provide
a roll up to the same set if the recipients are the same
:param messages:
"""
roll_ups = []
for message_data, recipients in messages:
for m, r in roll_ups:
if r == recipients:
m.append(message_data)
current_app.logger.info(
"Sending email expiration alert about {0} to {1}".format(
message_data['name'], ",".join(recipients)))
break
else:
roll_ups.append(([message_data], recipients))
return roll_ups
def send(subject, data, email_type, recipients):
"""
Configures all Lemur email messaging
:param subject:
:param data:
:param email_type:
:param recipients:
"""
# jinja template depending on type
template = env.get_template('{}.html'.format(email_type))
body = template.render(**data)
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, recipients, format='html')
elif s_type == 'smtp':
msg = Message(subject, recipients=recipients)
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,29 @@
"""
.. module: lemur.notifications.models
: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 sqlalchemy.orm import relationship
from sqlalchemy import Integer, String, Column, Boolean, Text
from sqlalchemy_utils import JSONType
from lemur.database import db
from lemur.plugins.base import plugins
from lemur.models import certificate_notification_associations
class Notification(db.Model):
__tablename__ = 'notifications'
id = Column(Integer, primary_key=True)
label = Column(String(128))
description = Column(Text())
options = Column(JSONType)
active = Column(Boolean, default=True)
plugin_name = Column(String(32))
certificates = relationship("Certificate", secondary=certificate_notification_associations, passive_deletes=True, backref="notification", cascade='all,delete')
@property
def plugin(self):
return plugins.get(self.plugin_name)

View File

@ -0,0 +1,254 @@
"""
.. module: lemur.notifications
: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 ssl
import socket
import arrow
from flask import current_app
from lemur import database
from lemur.domains.models import Domain
from lemur.notifications.models import Notification
from lemur.certificates.models import Certificate
from lemur.certificates import service as cert_service
from lemur.plugins.base import plugins
def _get_message_data(cert):
"""
Parse our the certification information needed for our notification
:param cert:
:return:
"""
cert_dict = cert.as_dict()
cert_dict['creator'] = cert.user.email
cert_dict['domains'] = [x .name for x in cert.domains]
cert_dict['superseded'] = list(set([x.name for x in find_superseded(cert.domains) if cert.name != x]))
return cert_dict
def _deduplicate(messages):
"""
Take all of the messages that should be sent and provide
a roll up to the same set if the recipients are the same
"""
roll_ups = []
for targets, data in messages:
for m, r in roll_ups:
if r == targets:
m.append(data)
current_app.logger.info(
"Sending expiration alert about {0} to {1}".format(
data['name'], ",".join(targets)))
break
else:
roll_ups.append(([data], targets, data.plugin_options))
return roll_ups
def send_expiration_notifications():
"""
This function will check for upcoming certificate expiration,
and send out notification emails at given intervals.
"""
notifications = 0
for plugin_name, notifications in database.get_all(Notification, 'active', field='status').group_by(Notification.plugin_name):
notifications += 1
messages = _deduplicate(notifications)
plugin = plugins.get(plugin_name)
for data, targets, options in messages:
plugin.send('expiration', data, targets, options)
current_app.logger.info("Lemur has sent {0} certification notifications".format(notifications))
def get_domain_certificate(name):
"""
Fetch the SSL certificate currently hosted at a given domain (if any) and
compare it against our all of our know certificates to determine if a new
SSL certificate has already been deployed
:param name:
:return:
"""
try:
pub_key = ssl.get_server_certificate((name, 443))
return cert_service.find_duplicates(pub_key.strip())
except socket.gaierror as e:
current_app.logger.info(str(e))
def find_superseded(domains):
"""
Here we try to fetch any domain in the certificate to see if we can resolve it
and to try and see if it is currently serving the certificate we are
alerting on.
:param domains:
:return:
"""
query = database.session_query(Certificate)
ss_list = []
for domain in domains:
dc = get_domain_certificate(domain.name)
if dc:
ss_list.append(dc)
current_app.logger.info("Trying to resolve {0}".format(domain.name))
query = query.filter(Certificate.domains.any(Domain.name.in_([x.name for x in domains])))
query = query.filter(Certificate.active == True) # noqa
query = query.filter(Certificate.not_after >= arrow.utcnow().format('YYYY-MM-DD'))
ss_list.extend(query.all())
return ss_list
def _is_eligible_for_notifications(cert):
"""
Determine if notifications for a given certificate should
currently be sent
:param cert:
:return:
"""
now = arrow.utcnow()
days = (cert.not_after - now.naive).days
for notification in cert.notifications:
interval = notification.options['interval']
unit = notification.options['unit']
if unit == 'weeks':
interval *= 7
elif unit == 'months':
interval *= 30
elif unit == 'days': # it's nice to be explicit about the base unit
pass
else:
raise Exception("Invalid base unit for expiration interval: {0}".format(unit))
if days == interval:
return cert
def create(label, plugin_name, options, description, certificates):
"""
Creates a new destination, that can then be used as a destination for certificates.
:param label: Notification common name
:param plugin_name:
:param options:
:param description:
:rtype : Notification
:return:
"""
notification = Notification(label=label, options=options, plugin_name=plugin_name, description=description)
notification = database.update_list(notification, 'certificates', Certificate, certificates)
return database.create(notification)
def update(notification_id, label, options, description, certificates):
"""
Updates an existing destination.
:param label: Notification common name
:param options:
:param description:
:rtype : Notification
:return:
"""
notification = get(notification_id)
notification.label = label
notification.options = options
notification.description = description
notification = database.update_list(notification, 'certificates', Certificate, certificates)
return database.update(notification)
def delete(notification_id):
"""
Deletes an notification.
:param notification_id: Lemur assigned ID
"""
database.delete(get(notification_id))
def get(notification_id):
"""
Retrieves an notification by it's lemur assigned ID.
:param notification_id: Lemur assigned ID
:rtype : Notification
:return:
"""
return database.get(Notification, notification_id)
def get_by_label(label):
"""
Retrieves a notification by it's label
:param label:
:return:
"""
return database.get(Notification, label, field='label')
def get_all():
"""
Retrieves all notification currently known by Lemur.
:return:
"""
query = database.session_query(Notification)
return database.find_all(query, Notification, {}).all()
def render(args):
sort_by = args.pop('sort_by')
sort_dir = args.pop('sort_dir')
page = args.pop('page')
count = args.pop('count')
filt = args.pop('filter')
certificate_id = args.pop('certificate_id', None)
if certificate_id:
query = database.session_query(Notification).join(Certificate, Notification.certificate)
query = query.filter(Certificate.id == certificate_id)
else:
query = database.session_query(Notification)
if filt:
terms = filt.split(';')
if terms[0] == 'active' and terms[1] == 'false':
query = query.filter(Notification.active == False) # noqa
elif terms[0] == 'active' and terms[1] == 'true':
query = query.filter(Notification.active == True) # noqa
else:
query = database.filter(query, Notification, terms)
query = database.find_all(query, Notification, args)
if sort_by and sort_dir:
query = database.sort(query, Notification, sort_by, sort_dir)
return database.paginate(query, page, count)

View File

@ -0,0 +1,455 @@
"""
.. module: lemur.notifications.views
:platform: Unix
:synopsis: This module contains all of the accounts view code.
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from flask import Blueprint
from flask.ext.restful import Api, reqparse, fields
from lemur.notifications import service
from lemur.auth.service import AuthenticatedResource
from lemur.common.utils import paginated_parser, marshal_items
mod = Blueprint('notifications', __name__)
api = Api(mod)
FIELDS = {
'description': fields.String,
'notificationOptions': fields.Raw(attribute='options'),
'pluginName': fields.String(attribute='plugin_name'),
'label': fields.String,
'active': fields.Boolean,
'id': fields.Integer,
}
class NotificationsList(AuthenticatedResource):
""" Defines the 'notifications' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(NotificationsList, self).__init__()
@marshal_items(FIELDS)
def get(self):
"""
.. http:get:: /notifications
The current account list
**Example request**:
.. sourcecode:: http
GET /notifications HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [
{
"description": "An example",
"notificationOptions": [
{
"name": "interval",
"required": true,
"value": 5,
"helpMessage": "Number of days to be alert before expiration.",
"validation": "^\\d+$",
"type": "int"
},
{
"available": [
"days",
"weeks",
"months"
],
"name": "unit",
"required": true,
"value": "weeks",
"helpMessage": "Interval unit",
"validation": "",
"type": "select"
},
{
"name": "recipients",
"required": true,
"value": "kglisson@netflix.com,example@netflix.com",
"helpMessage": "Comma delimited list of email addresses",
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
"type": "str"
}
],
"label": "example",
"pluginName": "email-notification",
"active": true,
"id": 2
}
],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
parser = paginated_parser.copy()
args = parser.parse_args()
return service.render(args)
@marshal_items(FIELDS)
def post(self):
"""
.. http:post:: /notifications
Creates a new account
**Example request**:
.. sourcecode:: http
POST /notifications HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
{
"description": "a test",
"notificationOptions": [
{
"name": "interval",
"required": true,
"value": 5,
"helpMessage": "Number of days to be alert before expiration.",
"validation": "^\\d+$",
"type": "int"
},
{
"available": [
"days",
"weeks",
"months"
],
"name": "unit",
"required": true,
"value": "weeks",
"helpMessage": "Interval unit",
"validation": "",
"type": "select"
},
{
"name": "recipients",
"required": true,
"value": "kglisson@netflix.com,example@netflix.com",
"helpMessage": "Comma delimited list of email addresses",
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
"type": "str"
}
],
"label": "test",
"pluginName": "email-notification",
"active": true,
"id": 2
}
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"description": "a test",
"notificationOptions": [
{
"name": "interval",
"required": true,
"value": 5,
"helpMessage": "Number of days to be alert before expiration.",
"validation": "^\\d+$",
"type": "int"
},
{
"available": [
"days",
"weeks",
"months"
],
"name": "unit",
"required": true,
"value": "weeks",
"helpMessage": "Interval unit",
"validation": "",
"type": "select"
},
{
"name": "recipients",
"required": true,
"value": "kglisson@netflix.com,example@netflix.com",
"helpMessage": "Comma delimited list of email addresses",
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
"type": "str"
}
],
"label": "test",
"pluginName": "email-notification",
"active": true,
"id": 2
}
:arg accountNumber: aws account number
:arg label: human readable account label
:arg comments: some description about the account
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
self.reqparse.add_argument('description', type=str, location='json')
self.reqparse.add_argument('certificates', type=list, default=[], location='json')
args = self.reqparse.parse_args()
return service.create(
args['label'],
args['plugin']['slug'],
args['plugin']['pluginOptions'],
args['description'],
args['certificates']
)
class Notifications(AuthenticatedResource):
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(Notifications, self).__init__()
@marshal_items(FIELDS)
def get(self, notification_id):
"""
.. http:get:: /notifications/1
Get a specific account
**Example request**:
.. sourcecode:: http
GET /notifications/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"description": "a test",
"notificationOptions": [
{
"name": "interval",
"required": true,
"value": 5,
"helpMessage": "Number of days to be alert before expiration.",
"validation": "^\\d+$",
"type": "int"
},
{
"available": [
"days",
"weeks",
"months"
],
"name": "unit",
"required": true,
"value": "weeks",
"helpMessage": "Interval unit",
"validation": "",
"type": "select"
},
{
"name": "recipients",
"required": true,
"value": "kglisson@netflix.com,example@netflix.com",
"helpMessage": "Comma delimited list of email addresses",
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
"type": "str"
}
],
"label": "test",
"pluginName": "email-notification",
"active": true,
"id": 2
}
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
return service.get(notification_id)
@marshal_items(FIELDS)
def put(self, notification_id):
"""
.. http:put:: /notifications/1
Updates an account
**Example request**:
.. sourcecode:: http
POST /notifications/1 HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"id": 1,
"accountNumber": 11111111111,
"label": "labelChanged",
"comments": "this is a thing"
}
:arg accountNumber: aws account number
:arg label: human readable account label
:arg comments: some description about the account
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
self.reqparse.add_argument('label', type=str, location='json', required=True)
self.reqparse.add_argument('plugin', type=dict, location='json', required=True)
self.reqparse.add_argument('certificates', type=list, default=[], location='json')
self.reqparse.add_argument('description', type=str, location='json')
args = self.reqparse.parse_args()
return service.update(
notification_id,
args['label'],
args['plugin']['pluginOptions'],
args['description'],
args['certificates']
)
def delete(self, notification_id):
service.delete(notification_id)
return {'result': True}
class CertificateNotifications(AuthenticatedResource):
""" Defines the 'certificate/<int:certificate_id/notifications'' endpoint """
def __init__(self):
super(CertificateNotifications, self).__init__()
@marshal_items(FIELDS)
def get(self, certificate_id):
"""
.. http:get:: /certificates/1/notifications
The current account list for a given certificates
**Example request**:
.. sourcecode:: http
GET /certificates/1/notifications HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [
{
"description": "An example",
"notificationOptions": [
{
"name": "interval",
"required": true,
"value": 555,
"helpMessage": "Number of days to be alert before expiration.",
"validation": "^\\d+$",
"type": "int"
},
{
"available": [
"days",
"weeks",
"months"
],
"name": "unit",
"required": true,
"value": "weeks",
"helpMessage": "Interval unit",
"validation": "",
"type": "select"
},
{
"name": "recipients",
"required": true,
"value": "kglisson@netflix.com,example@netflix.com",
"helpMessage": "Comma delimited list of email addresses",
"validation": "^([\\w+-.%]+@[\\w-.]+\\.[A-Za-z]{2,4},?)+$",
"type": "str"
}
],
"label": "example",
"pluginName": "email-notification",
"active": true,
"id": 2
}
],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: acs or desc
:query page: int. default is 1
:query filter: key value pair. format is k=v;
:query limit: limit number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
"""
parser = paginated_parser.copy()
args = parser.parse_args()
args['certificate_id'] = certificate_id
return service.render(args)
api.add_resource(NotificationsList, '/notifications', endpoint='notifications')
api.add_resource(Notifications, '/notifications/<int:notification_id>', endpoint='notification')
api.add_resource(CertificateNotifications, '/certificates/<int:certificate_id>/notifications',
endpoint='certificateNotifications')

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

@ -72,6 +72,12 @@
<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>

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')

View File

@ -2,15 +2,29 @@
angular.module('lemur')
.controller('AuthorityEditController', function ($scope, $routeParams, AuthorityApi, AuthorityService, RoleService){
AuthorityApi.get($routeParams.id).then(function (authority) {
.controller('AuthorityEditController', function ($scope, $modalInstance, AuthorityApi, AuthorityService, RoleService, editId){
AuthorityApi.get(editId).then(function (authority) {
AuthorityService.getRoles(authority);
$scope.authority = authority;
});
$scope.authorityService = AuthorityService;
$scope.save = AuthorityService.update;
$scope.roleService = RoleService;
$scope.save = function (authority) {
AuthorityService.update(authority).then(
function () {
$modalInstance.close();
},
function () {
}
);
};
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
})
.controller('AuthorityCreateController', function ($scope, $modalInstance, AuthorityService, LemurRestangular, RoleService, PluginService, WizardHandler) {
@ -25,7 +39,7 @@ angular.module('lemur')
});
};
PluginService.get('issuer').then(function (plugins) {
PluginService.getByType('issuer').then(function (plugins) {
$scope.plugins = plugins;
});

View File

@ -1,44 +1,41 @@
<h2 class="featurette-heading">Edit</span> Authority <span class="text-muted"><small>Chain of command
</small></span></h2>
<div class="panel panel-default">
<div class="panel-heading">
<a href="#/authorities" class="btn btn-danger pull-right">Cancel</a>
<div class="clearfix"></div>
</div>
<div class="panel-body">
<form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="authority.selectedRole" placeholder="Role Name"
typeahead="role.name for role in roleService.findRoleByName($viewValue)" typeahead-loading="loadingRoles"
class="form-control input-md" typeahead-on-select="authority.attachRole($item)" typeahead-min-wait="50"
tooltip="Roles control which authorities a user can issue certificates from"
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="roles.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ authority.roles.length || 0 }}</span>
</button>
</span>
</div>
<table ng-show="authority.roles" class="table">
<tr ng-repeat="role in authority.roles track by $index">
<td><a class="btn btn-sm btn-info" href="#/roles/{{ role.id }}/edit">{{ role.name }}</a></td>
<td><span class="text-muted">{{ role.description }}</span></td>
<td>
<button type="button" ng-click="authority.removeRole($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
</table>
</div>
</div>
</form>
</div>
<div class="panel-footer">
<button ng-click="save(authority)" class="btn btn-success pull-right">Save</button>
<div class="clearfix"></div>
</div>
<div class="modal-header">
<div class="modal-title">
<div class="modal-header">Edit Authority <span class="text-muted"><small>chain of command!</small></span></div>
</div>
<div class="modal-body">
<form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group">
<label class="control-label col-sm-2">
Roles
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="authority.selectedRole" placeholder="Role Name"
typeahead="role.name for role in roleService.findRoleByName($viewValue)" typeahead-loading="loadingRoles"
class="form-control input-md" typeahead-on-select="authority.attachRole($item)" typeahead-min-wait="50"
tooltip="Roles control which authorities a user can issue certificates from"
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="roles.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ authority.roles.length || 0 }}</span>
</button>
</span>
</div>
<table ng-show="authority.roles" class="table">
<tr ng-repeat="role in authority.roles track by $index">
<td><a class="btn btn-sm btn-info" href="#/roles/{{ role.id }}/edit">{{ role.name }}</a></td>
<td><span class="text-muted">{{ role.description }}</span></td>
<td>
<button type="button" ng-click="authority.removeRole($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
</table>
</div>
</div>
</form>
</div>
<div class="modal-footer">
<button ng-click="save(authority)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>
</div>

View File

@ -65,6 +65,19 @@ angular.module('lemur')
});
};
AuthorityService.findActiveAuthorityByName = function (filterValue) {
return AuthorityApi.getList({'filter[name]': filterValue})
.then(function (authorities) {
var activeAuthorities = [];
_.each(authorities, function (authority) {
if (authority.active) {
activeAuthorities.push(authority);
}
});
return activeAuthorities;
});
};
AuthorityService.create = function (authority) {
authority.attachSubAltName();
return AuthorityApi.post(authority).then(
@ -86,7 +99,7 @@ angular.module('lemur')
};
AuthorityService.update = function (authority) {
authority.put().then(
return authority.put().then(
function () {
toaster.pop({
type: 'success',
@ -105,13 +118,13 @@ angular.module('lemur')
};
AuthorityService.getRoles = function (authority) {
authority.getList('roles').then(function (roles) {
return authority.getList('roles').then(function (roles) {
authority.roles = roles;
});
};
AuthorityService.updateActive = function (authority) {
authority.put().then(
return authority.put().then(
function () {
toaster.pop({
type: 'success',

View File

@ -46,7 +46,7 @@ angular.module('lemur')
$scope.edit = function (authorityId) {
var modalInstance = $modal.open({
animation: true,
templateUrl: '/angular/authorities/authority/authorityWizard.tpl.html',
templateUrl: '/angular/authorities/authority/authorityEdit.tpl.html',
controller: 'AuthorityEditController',
size: 'lg',
resolve: {

View File

@ -1,17 +1,28 @@
'use strict';
angular.module('lemur')
.controller('CertificateEditController', function ($scope, $routeParams, CertificateApi, CertificateService, MomentService) {
CertificateApi.get($routeParams.id).then(function (certificate) {
.controller('CertificateEditController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, NotificationService, editId) {
CertificateApi.get(editId).then(function (certificate) {
CertificateService.getNotifications(certificate);
CertificateService.getDestinations(certificate);
$scope.certificate = certificate;
});
$scope.momentService = MomentService;
$scope.save = CertificateService.update;
$scope.cancel = function () {
$modalInstance.dismiss('cancel');
};
$scope.save = function (certificate) {
CertificateService.update(certificate).then(function () {
$modalInstance.close();
});
};
$scope.destinationService = DestinationService;
$scope.notificationService = NotificationService;
})
.controller('CertificateCreateController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, ELBService, AuthorityService, PluginService, MomentService, WizardHandler, LemurRestangular) {
.controller('CertificateCreateController', function ($scope, $modalInstance, CertificateApi, CertificateService, DestinationService, ELBService, AuthorityService, PluginService, MomentService, WizardHandler, LemurRestangular, NotificationService) {
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
$scope.create = function (certificate) {
@ -77,11 +88,12 @@ angular.module('lemur')
};
PluginService.get('destination').then(function (plugins) {
PluginService.getByType('destination').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.elbService = ELBService;
$scope.authorityService = AuthorityService;
$scope.destinationService = DestinationService;
$scope.notificationService = NotificationService;
});

View File

@ -7,14 +7,11 @@
<wz-step title="Tracking" canexit="trackingForm.$valid">
<ng-include src="'angular/certificates/certificate/tracking.tpl.html'"></ng-include>
</wz-step>
<wz-step title="Options" canenter="enterValidation">
<ng-include src="'angular/certificates/certificate/options.tpl.html'"></ng-include>
</wz-step>
<wz-step title="Distinguished Name" canenter="exitTracking" canexit="exitDN">
<ng-include src="'angular/certificates/certificate/distinguishedName.tpl.html'"></ng-include>
</wz-step>
<wz-step title="Destinations" canenter="enterValidation">
<ng-include src="'angular/certificates/certificate/destinations.tpl.html'"></ng-include>
<wz-step title="Options" canenter="enterValidation">
<ng-include src="'angular/certificates/certificate/options.tpl.html'"></ng-include>
</wz-step>
</wizard>
</div>

View File

@ -3,26 +3,26 @@
Destinations
</label>
<div class="col-sm-10">
<div class="input-group">
<input type="text" ng-model="certificate.selectedDestination" placeholder="AWS, TheSecret..."
typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachDestination($item)" typeahead-min-wait="50"
tooltip="Lemur can upload certificates to any pre-defined destination"
tooltip-trigger="focus" tooltip-placement="top">
<div class="input-group">
<input type="text" ng-model="certificate.selectedDestination" placeholder="AWS, TheSecret..."
typeahead="destination.label for destination in destinationService.findDestinationsByName($viewValue)" typeahead-loading="loadingDestinations"
class="form-control input-md" typeahead-on-select="certificate.attachDestination($item)" typeahead-min-wait="50"
tooltip="Lemur can upload certificates to any pre-defined destination"
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="destinations.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ certificate.destinations.length || 0 }}</span>
<span class="badge">{{ certificate.destinations.length || 0 }}</span>
</button>
</span>
</div>
<table class="table">
<tr ng-repeat="destination in certificate.destinations track by $index">
<td><a class="btn btn-sm btn-info" href="#/destinations/{{ destination.id }}/certificates">{{ destination.label }}</a></td>
<td><span class="text-muted">{{ destination.description }}</span></td>
<td>
<button type="button" ng-click="certificate.removeDestination($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
</table>
</div>
<table class="table">
<tr ng-repeat="destination in certificate.destinations track by $index">
<td><a class="btn btn-sm btn-info" href="#/destinations/{{ destination.id }}/certificates">{{ destination.label }}</a></td>
<td><span class="text-muted">{{ destination.description }}</span></td>
<td>
<button type="button" ng-click="certificate.removeDestination($index)" class="btn btn-danger btn-sm pull-right">Remove</button>
</td>
</tr>
</table>
</div>
</div>

View File

@ -1,83 +1,84 @@
<form name="trackingForm" novalidate>
<div class="form-horizontal">
<div class="form-group"
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
<label class="control-label col-sm-2">
Owner
</label>
<div class="col-sm-10">
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@netflix.com" tooltip="This is the certificates team distribution list or main point of contact" class="form-control" required/>
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate owner</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.description.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.description.$dirty}">
<label class="control-label col-sm-2">
Description
</label>
<div class="col-sm-10">
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea>
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.selectedAuthority.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.selectedAuthority.$dirty}">
<label class="control-label col-sm-2">
Certificate Authority
</label>
<div class="col-sm-10">
<div class="input-group col-sm-12">
<input name="selectedAuthority" tooltip="If you are unsure which authority you need; you most likely want to use 'verisign'" type="text" ng-model="certificate.selectedAuthority" placeholder="Authority Name" typeahead-on-select="certificate.attachAuthority($item)"
typeahead="authority.name for authority in authorityService.findAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities"
class="form-control" typeahead-wait-ms="100" typeahead-template-url="angular/authorities/authority/select.tpl.html" required>
<div class="form-horizontal">
<div class="form-group"
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
<label class="control-label col-sm-2">
Owner
</label>
<div class="col-sm-10">
<input type="email" name="ownerEmail" ng-model="certificate.owner" placeholder="TeamDL@netflix.com" tooltip="This is the certificates team distribution list or main point of contact" class="form-control" required/>
<p ng-show="trackingForm.ownerEmail.$invalid && !trackingForm.ownerEmail.$pristine" class="help-block">You must enter an Certificate owner</p>
</div>
</div>
</div>
</div>
<div ng-show="certificate.authority" class="form-group">
<label class="control-label col-sm-2">
Certificate Template
</label>
<div class="col-sm-10">
<select class="form-control" ng-change="certificate.useTemplate()" name="certificateTemplate" ng-model="certificate.template" ng-options="template.name for template in templates"></select>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
<label class="control-label col-sm-2">
Common Name
</label>
<div class="col-sm-10">
<input name="commonName" tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next panel" ng-model="certificate.commonName" placeholder="Common Name" class="form-control" required/>
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" tooltip="If no date is selected Lemur attempts to issue a 2 year certificate">
Validity Range <span class="glyphicon glyphicon-question-sign"></span>
</label>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Starting Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="$parent.openNotBefore.isOpen" min-date="certificate.authority.notBefore" max-date="certificate.authority.maxDate" ng-model="certificate.validityStart" />
<div class="form-group"
ng-class="{'has-error': trackingForm.description.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.description.$dirty}">
<label class="control-label col-sm-2">
Description
</label>
<div class="col-sm-10">
<textarea name="description" ng-model="certificate.description" placeholder="Something elegant" class="form-control" ng-pattern="/^[\w\-\s]+$/" required></textarea>
<p ng-show="trackingForm.description.$invalid && !trackingForm.description.$pristine" class="help-block">You must give a short description about this authority will be used for, this description should only include alphanumeric characters</p>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.selectedAuthority.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.selectedAuthority.$dirty}">
<label class="control-label col-sm-2">
Certificate Authority
</label>
<div class="col-sm-10">
<div class="input-group col-sm-12">
<input name="selectedAuthority" tooltip="If you are unsure which authority you need; you most likely want to use 'verisign'" type="text" ng-model="certificate.selectedAuthority" placeholder="Authority Name" typeahead-on-select="certificate.attachAuthority($item)"
typeahead="authority.name for authority in authorityService.findActiveAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities"
class="form-control" typeahead-wait-ms="100" typeahead-template-url="angular/authorities/authority/select.tpl.html" required>
</div>
</div>
</div>
<div ng-show="certificate.authority" class="form-group">
<label class="control-label col-sm-2">
Certificate Template
</label>
<div class="col-sm-10">
<select class="form-control" ng-change="certificate.useTemplate()" name="certificateTemplate" ng-model="certificate.template" ng-options="template.name for template in templates"></select>
</div>
</div>
<div class="form-group"
ng-class="{'has-error': trackingForm.commonName.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.commonName.$dirty}">
<label class="control-label col-sm-2">
Common Name
</label>
<div class="col-sm-10">
<input name="commonName" tooltip="If you need a certificate with multiple domains enter your primary domain here and the rest under 'Subject Alternate Names' in the next panel" ng-model="certificate.commonName" placeholder="Common Name" class="form-control" required/>
<p ng-show="trackingForm.commonName.$invalid && !trackingForm.commonName.$pristine" class="help-block">You must enter a common name</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2" tooltip="If no date is selected Lemur attempts to issue a 2 year certificate">
Validity Range <span class="glyphicon glyphicon-question-sign"></span>
</label>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Starting Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="$parent.openNotBefore.isOpen" min-date="certificate.authority.notBefore" max-date="certificate.authority.maxDate" ng-model="certificate.validityStart" />
<span class="input-group-btn">
<button class="btn btn-default" ng-click="openNotBefore($event)"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
</div>
<span style="padding-top: 15px" class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Ending Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="$parent.openNotAfter.isOpen" min-date="certificate.authority.notBefore" max-date="certificate.authority.maxDate" ng-model="certificate.validityEnd" />
</div>
</div>
</div>
<span style="padding-top: 15px" class="text-center col-sm-2"><label><span class="glyphicon glyphicon-resize-horizontal"></span></label></span>
<div class="col-sm-4">
<div>
<div class="input-group">
<input tooltip="Ending Date (yyyy/MM/dd)" class="form-control" datepicker-popup="yyyy/MM/dd" is-open="$parent.openNotAfter.isOpen" min-date="certificate.authority.notBefore" max-date="certificate.authority.maxDate" ng-model="certificate.validityEnd" />
<span class="input-group-btn">
<button class="btn btn-default" ng-click="openNotAfter($event)"><i class="glyphicon glyphicon-calendar"></i></button>
</span>
</div>
</div>
</div>
</div>
</div>
</div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</div>
</div>
</div>
</form>

View File

@ -2,14 +2,15 @@
angular.module('lemur')
.controller('CertificateUploadController', function ($scope, $modalInstance, CertificateService, LemurRestangular, DestinationService, ELBService, PluginService) {
.controller('CertificateUploadController', function ($scope, $modalInstance, CertificateService, LemurRestangular, DestinationService, NotificationService, ELBService, PluginService) {
$scope.certificate = LemurRestangular.restangularizeElement(null, {}, 'certificates');
$scope.upload = CertificateService.upload;
$scope.destinationService = DestinationService;
$scope.notificationService = NotificationService;
$scope.elbService = ELBService;
PluginService.get('destination').then(function (plugins) {
PluginService.getByType('destination').then(function (plugins) {
$scope.plugins = plugins;
});

View File

@ -61,6 +61,7 @@
class="help-block">Enter a valid certificate.</p>
</div>
</div>
<div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</form>
</div>

View File

@ -67,6 +67,16 @@ angular.module('lemur')
removeDestination: function (index) {
this.destinations.splice(index, 1);
},
attachNotification: function (notification) {
this.selectedNotification = null;
if (this.notifications === undefined) {
this.notifications = [];
}
this.notifications.push(notification);
},
removeNotification: function (index) {
this.notifications.splice(index, 1);
},
attachELB: function (elb) {
this.selectedELB = null;
if (this.elbs === undefined) {
@ -89,7 +99,7 @@ angular.module('lemur')
});
return LemurRestangular.all('certificates');
})
.service('CertificateService', function ($location, CertificateApi, toaster) {
.service('CertificateService', function ($location, CertificateApi, LemurRestangular, toaster) {
var CertificateService = this;
CertificateService.findCertificatesByName = function (filterValue) {
return CertificateApi.getList({'filter[name]': filterValue})
@ -120,7 +130,7 @@ angular.module('lemur')
};
CertificateService.update = function (certificate) {
certificate.put().then(function () {
return LemurRestangular.copy(certificate).put().then(function () {
toaster.pop({
type: 'success',
title: certificate.name,
@ -131,7 +141,7 @@ angular.module('lemur')
};
CertificateService.upload = function (certificate) {
CertificateApi.customPOST(certificate, 'upload').then(
return CertificateApi.customPOST(certificate, 'upload').then(
function () {
toaster.pop({
type: 'success',
@ -150,7 +160,7 @@ angular.module('lemur')
};
CertificateService.loadPrivateKey = function (certificate) {
certificate.customGET('key').then(
return certificate.customGET('key').then(
function (response) {
if (response.key === null) {
toaster.pop({
@ -172,43 +182,49 @@ angular.module('lemur')
};
CertificateService.getAuthority = function (certificate) {
certificate.customGET('authority').then(function (authority) {
return certificate.customGET('authority').then(function (authority) {
certificate.authority = authority;
});
};
CertificateService.getCreator = function (certificate) {
certificate.customGET('creator').then(function (creator) {
return certificate.customGET('creator').then(function (creator) {
certificate.creator = creator;
});
};
CertificateService.getDestinations = function (certificate) {
certificate.getList('destinations').then(function (destinations) {
return certificate.getList('destinations').then(function (destinations) {
certificate.destinations = destinations;
});
};
CertificateService.getNotifications = function (certificate) {
return certificate.getList('notifications').then(function (notifications) {
certificate.notifications = notifications;
});
};
CertificateService.getListeners = function (certificate) {
certificate.getList('listeners').then(function (listeners) {
return certificate.getList('listeners').then(function (listeners) {
certificate.listeners = listeners;
});
};
CertificateService.getELBs = function (certificate) {
certificate.getList('listeners').then(function (elbs) {
return certificate.getList('listeners').then(function (elbs) {
certificate.elbs = elbs;
});
};
CertificateService.getDomains = function (certificate) {
certificate.getList('domains').then(function (domains) {
return certificate.getList('domains').then(function (domains) {
certificate.domains = domains;
});
};
CertificateService.updateActive = function (certificate) {
certificate.put().then(
return certificate.put().then(
function () {
toaster.pop({
type: 'success',

View File

@ -27,7 +27,7 @@ angular.module('lemur')
_.each(data, function (certificate) {
CertificateService.getDomains(certificate);
CertificateService.getDestinations(certificate);
CertificateService.getListeners(certificate);
CertificateService.getNotifications(certificate);
CertificateService.getAuthority(certificate);
CertificateService.getCreator(certificate);
});
@ -74,6 +74,24 @@ angular.module('lemur')
});
};
$scope.edit = function (certificateId) {
var modalInstance = $modal.open({
animation: true,
controller: 'CertificateEditController',
templateUrl: '/angular/certificates/certificate/edit.tpl.html',
size: 'lg',
resolve: {
editId: function () {
return certificateId;
}
}
});
modalInstance.result.then(function () {
$scope.certificateTable.reload();
});
};
$scope.import = function () {
var modalInstance = $modal.open({
animation: true,

View File

@ -30,11 +30,6 @@
<li><span class="text-muted">{{ certificate.owner }}</span></li>
</ul>
</td>
<td data-title="'Destinations'" filter="{ 'destination': 'select' }" filter-date="getDestinationDropDown()">
<div class="btn-group">
<a href="#/destinations/{{ destination.id }}/edit" class="btn btn-sm btn-default" ng-repeat="account in certificate.destinations">{{ destination.label }}</a>
</div>
</td>
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()">
<form>
<switch ng-change="certificateService.updateActive(certificate)" id="status" name="status" ng-model="certificate.active" class="green small"></switch>
@ -47,70 +42,80 @@
{{ certificate.cn }}
</td>
<td data-title="''">
<div class="btn-group-vertical pull-right">
<button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1" butn-checkbox-false="0">View</button>
<div class="btn-group pull-right">
<button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1" butn-checkbox-false="0">More</button>
<button class="btn btn-sm btn-warning" ng-click="edit(certificate.id)">Edit</button>
</div>
</td>
</tr>
<tr class="warning" ng-show="certificate.toggle" ng-repeat-end>
<td colspan="5">
<div class="col-md-6">
<ul class="list-group">
<li class="list-group-item">
<strong>Creator</strong>
<span class="pull-right">
{{ certificate.creator.email }}
</span>
</li>
<li class="list-group-item">
<strong>Not Before</strong>
<span class="pull-right" tooltip="{{ certificate.notBefore }}">
{{ momentService.createMoment(certificate.notBefore) }}
</span>
</li>
<li class="list-group-item">
<strong>Not After</strong>
<span class="pull-right" tooltip="{{ certificate.notAfter }}">
{{ momentService.createMoment(certificate.notAfter) }}
</span>
</li>
<li class="list-group-item">
<strong>San</strong>
<span class="pull-right">
<i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i>
<i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i>
</span>
</li>
<li class="list-group-item">
<strong>Bits</strong>
<span class="pull-right">{{ certificate.bits }}</span>
</li>
<li class="list-group-item">
<strong>Serial</strong>
<span class="pull-right">{{ certificate.serial }}</span>
</li>
<li tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked" class="list-group-item">
<strong>Validity</strong>
<span class="pull-right">
<span ng-show="!certificate.status" class="label label-warning">Unknown</span>
<span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
<span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span>
</span>
</li>
<li class="list-group-item">
<strong>Description</strong>
<span class="pull-right">{{ certificate.description }}</span>
</li>
</ul>
<h4>Domains</h4>
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
</div>
<h4 ng-show="certificate.destinations.total">ARNs</h4>
<ul class="list-group">
<li class="list-group-item" ng-repeat="arn in certificate.arns">{{ arn }}</li>
</ul>
</div>
<td colspan="6">
<tabset justified="true" class="col-md-6">
<tab heading="Basic Info">
<ul class="list-group">
<li class="list-group-item">
<strong>Creator</strong>
<span class="pull-right">
{{ certificate.creator.email }}
</span>
</li>
<li class="list-group-item">
<strong>Not Before</strong>
<span class="pull-right" tooltip="{{ certificate.notBefore }}">
{{ momentService.createMoment(certificate.notBefore) }}
</span>
</li>
<li class="list-group-item">
<strong>Not After</strong>
<span class="pull-right" tooltip="{{ certificate.notAfter }}">
{{ momentService.createMoment(certificate.notAfter) }}
</span>
</li>
<li class="list-group-item">
<strong>San</strong>
<span class="pull-right">
<i class="glyphicon glyphicon-ok" ng-show="certificate.san"></i>
<i class="glyphicon glyphicon-remove" ng-show="!certificate.san"></i>
</span>
</li>
<li class="list-group-item">
<strong>Bits</strong>
<span class="pull-right">{{ certificate.bits }}</span>
</li>
<li class="list-group-item">
<strong>Serial</strong>
<span class="pull-right">{{ certificate.serial }}</span>
</li>
<li tooltip="Lemur will attempt to check a certificates validity, this is used to track whether a certificate as been revoked" class="list-group-item">
<strong>Validity</strong>
<span class="pull-right">
<span ng-show="!certificate.status" class="label label-warning">Unknown</span>
<span ng-show="certificate.status == 'revoked'" class="label label-danger">Revoked</span>
<span ng-show="certificate.status == 'valid'" class="label label-success">Valid</span>
</span>
</li>
<li class="list-group-item">
<strong>Description</strong>
<span class="pull-right">{{ certificate.description }}</span>
</li>
</ul>
</tab>
<tab heading="Notifications">
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="notification in certificate.notifications">{{ notification.label }}</a>
</div>
</tab>
<tab heading="Destinations">
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="destination in certificate.destinations">{{ destination.label }}</a>
</div>
</tab>
<tab heading="Domains">
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
</div>
</tab>
</tabset>
<tabset justified="true" class="col-md-6">
<tab heading="Chain">
<p>

View File

@ -5,7 +5,7 @@ angular.module('lemur')
.controller('DestinationsCreateController', function ($scope, $modalInstance, PluginService, DestinationService, LemurRestangular){
$scope.destination = LemurRestangular.restangularizeElement(null, {}, 'destinations');
PluginService.get('destination').then(function (plugins) {
PluginService.getByType('destination').then(function (plugins) {
$scope.plugins = plugins;
});
$scope.save = function (destination) {
@ -24,8 +24,14 @@ angular.module('lemur')
$scope.destination = destination;
});
PluginService.get('destination').then(function (plugins) {
$scope.plugins = plugins;
PluginService.getByType('destination').then(function (plugins) {
$scope.plugins = plugins;
_.each($scope.plugins, function (plugin) {
if (plugin.slug == $scope.destination.pluginName) {
plugin.pluginOptions = $scope.destination.destinationOptions;
$scope.destination.plugin = plugin;
};
});
});
$scope.save = function (destination) {

View File

@ -3,7 +3,7 @@ angular.module('lemur')
.service('DestinationApi', function (LemurRestangular) {
return LemurRestangular.all('destinations');
})
.service('DestinationService', function ($location, DestinationApi, toaster) {
.service('DestinationService', function ($location, DestinationApi, PluginService, toaster) {
var DestinationService = this;
DestinationService.findDestinationsByName = function (filterValue) {
return DestinationApi.getList({'filter[label]': filterValue})
@ -49,5 +49,11 @@ angular.module('lemur')
});
});
};
DestinationService.getPlugin = function (destination) {
return PluginService.getByName(destination.pluginName).then(function (plugin) {
destination.plugin = plugin;
});
};
return DestinationService;
});

View File

@ -23,6 +23,9 @@ angular.module('lemur')
getData: function ($defer, params) {
DestinationApi.getList(params.url()).then(
function (data) {
_.each(data, function (destination) {
DestinationService.getPlugin(destination);
});
params.total(data.total);
$defer.resolve(data);
}

View File

@ -1,14 +1,28 @@
'use strict';
angular.module('lemur')
.service('PluginApi', function (LemurRestangular) {
return LemurRestangular.all('plugins');
})
.service('PluginService', function (PluginApi) {
var PluginService = this;
PluginService.get = function (type) {
return PluginApi.customGETLIST(type).then(function (plugins) {
return plugins;
});
};
});
.service('PluginApi', function (LemurRestangular) {
return LemurRestangular.all('plugins');
})
.service('PluginService', function (PluginApi) {
var PluginService = this;
PluginService.get = function () {
return PluginApi.getList().then(function (plugins) {
return plugins;
});
};
PluginService.getByType = function (type) {
return PluginApi.getList({'type': type}).then(function (plugins) {
return plugins;
});
};
PluginService.getByName = function (pluginName) {
return PluginApi.customGET(pluginName).then(function (plugin) {
return plugin;
});
};
return PluginService;
});

View File

@ -55,11 +55,11 @@
class="form-control input-md" typeahead-on-select="user.attachRole($item)" typeahead-min-wait="50"
tooltip="Roles control which authorities a user can issue certificates from"
tooltip-trigger="focus" tooltip-placement="top">
<span class="input-group-btn">
<button ng-model="roles.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ user.roles.total || 0 }}</span>
</button>
</span>
<span class="input-group-btn">
<button ng-model="roles.show" class="btn btn-md btn-default" btn-checkbox btn-checkbox-true="1" btn-checkbox-false="0">
<span class="badge">{{ user.roles.total || 0 }}</span>
</button>
</span>
</div>
<table ng-show="user.roles" class="table">
<tr ng-repeat="role in user.roles track by $index">

View File

@ -49,13 +49,20 @@
</div>
<div class="navbar-collapse collapse" ng-controller="LoginController">
<ul class="nav navbar-nav navbar-left">
<li data-match-route="/dashboard"><a href="/#/dashboard">Dashboard</a></li>
<li data-match-route="/certificates"><a href="/#/certificates">Certificates</a></li>
<li data-match-route="/authorities"><a href="/#/authorities">Authorities</a></li>
<li data-match-route="/domains"><a href="/#/domains">Domains</a></li>
<li><a href="/#/roles">Roles</a></li>
<li><a href="/#/users">Users</a></li>
<li><a href="/#/dashboard">Dashboard</a></li>
<li><a href="/#/certificates">Certificates</a></li>
<li><a href="/#/authorities">Authorities</a></li>
<li><a href="/#/notifications">Notifications</a></li>
<li><a href="/#/destinations">Destinations</a></li>
<li></li>
<li class="dropdown" dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle" dropdown-toggle>Settings <span class="caret"></span></a>
<ul class="dropdown-menu">
<li><a href="/#/roles">Roles</a></li>
<li><a href="/#/users">Users</a></li>
<li><a href="/#/domains">Domains</a></li>
</ul>
</li>
</ul>
<ul ng-show="!currentUser.username" class="nav navbar-nav navbar-right">
<li><a href="/#/login">Login</a></li>
@ -63,7 +70,7 @@
<ul ng-show="currentUser.username" class="nav navbar-nav navbar-right">
<li class="dropdown" dropdown on-toggle="toggled(open)">
<a href class="dropdown-toggle profile-nav" dropdown-toggle>
{{ currentUser.username }}<img ng-show="currentUser.profileImage" src="{{ currentUser.profileImage }}" class="profile img-circle">
{{ currentUser.username }}<img ng-if="currentUser.profileImage" src="{{ currentUser.profileImage }}" class="profile img-circle">
</a>
<ul class="dropdown-menu">
<li><a ng-click="logout()">Logout</a></li>

View File

@ -0,0 +1,117 @@
from lemur.notifications.service import * # noqa
from lemur.notifications.views import * # noqa
def test_crud(session):
notification = create('testnotify', 'email-notification', {}, 'notify1', [])
assert notification.id > 0
notification = update(notification.id, 'testnotify2', {}, 'notify2', [])
assert notification.label == 'testnotify2'
assert len(get_all()) == 1
delete(1)
assert len(get_all()) == 0
def test_notification_get(client):
assert client.get(api.url_for(Notifications, notification_id=1)).status_code == 401
def test_notification_post(client):
assert client.post(api.url_for(Notifications, notification_id=1), data={}).status_code == 405
def test_notification_put(client):
assert client.put(api.url_for(Notifications, notification_id=1), data={}).status_code == 401
def test_notification_delete(client):
assert client.delete(api.url_for(Notifications, notification_id=1)).status_code == 401
def test_notification_patch(client):
assert client.patch(api.url_for(Notifications, notification_id=1), data={}).status_code == 405
VALID_USER_HEADER_TOKEN = {
'Authorization': 'Basic ' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MzUyMzMzNjksInN1YiI6MSwiZXhwIjoxNTIxNTQ2OTY5fQ.1qCi0Ip7mzKbjNh0tVd3_eJOrae3rNa_9MCVdA4WtQI'}
def test_auth_notification_get(client):
assert client.get(api.url_for(Notifications, notification_id=1), headers=VALID_USER_HEADER_TOKEN).status_code == 200
def test_auth_notification_post_(client):
assert client.post(api.url_for(Notifications, notification_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 405
def test_auth_notification_put(client):
assert client.put(api.url_for(Notifications, notification_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 400
def test_auth_notification_delete(client):
assert client.delete(api.url_for(Notifications, notification_id=1), headers=VALID_USER_HEADER_TOKEN).status_code == 200
def test_auth_notification_patch(client):
assert client.patch(api.url_for(Notifications, notification_id=1), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 405
VALID_ADMIN_HEADER_TOKEN = {
'Authorization': 'Basic ' + 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE0MzUyNTAyMTgsInN1YiI6MiwiZXhwIjoxNTIxNTYzODE4fQ.6mbq4-Ro6K5MmuNiTJBB153RDhlM5LGJBjI7GBKkfqA'}
def test_admin_notification_get(client):
assert client.get(api.url_for(Notifications, notification_id=1), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 200
def test_admin_notification_post(client):
assert client.post(api.url_for(Notifications, notification_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 405
def test_admin_notification_put(client):
assert client.put(api.url_for(Notifications, notification_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 400
def test_admin_notification_delete(client):
assert client.delete(api.url_for(Notifications, notification_id=1), headers=VALID_ADMIN_HEADER_TOKEN).status_code == 200
def test_admin_notification_patch(client):
assert client.patch(api.url_for(Notifications, notification_id=1), data={}, headers=VALID_ADMIN_HEADER_TOKEN).status_code == 405
def test_notifications_get(client):
assert client.get(api.url_for(NotificationsList)).status_code == 401
def test_notifications_post(client):
assert client.post(api.url_for(NotificationsList), data={}).status_code == 401
def test_notifications_put(client):
assert client.put(api.url_for(NotificationsList), data={}).status_code == 405
def test_notifications_delete(client):
assert client.delete(api.url_for(NotificationsList)).status_code == 405
def test_notifications_patch(client):
assert client.patch(api.url_for(NotificationsList), data={}).status_code == 405
def test_auth_notifications_get(client):
assert client.get(api.url_for(NotificationsList), headers=VALID_USER_HEADER_TOKEN).status_code == 200
def test_auth_notifications_post(client):
assert client.post(api.url_for(NotificationsList), data={}, headers=VALID_USER_HEADER_TOKEN).status_code == 400
def test_admin_notifications_get(client):
resp = client.get(api.url_for(NotificationsList), headers=VALID_ADMIN_HEADER_TOKEN)
assert resp.status_code == 200
assert resp.json == {'items': [], 'total': 0}

View File

@ -138,6 +138,7 @@ setup(
'cloudca_source = lemur.plugins.lemur_cloudca.plugin:CloudCASourcePlugin'
'aws_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin'
'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin'
],
},
classifiers=[