From e247d635fca923355db865382f55d54c2c0ed099 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Sat, 1 Aug 2015 15:29:34 -0700 Subject: [PATCH 01/15] Adding backend code for sources models --- lemur/__init__.py | 3 + lemur/certificates/models.py | 5 +- lemur/elbs/models.py | 44 --- lemur/elbs/service.py | 124 ------ lemur/elbs/views.py | 78 ---- lemur/listeners/__init__.py | 0 lemur/listeners/models.py | 42 -- lemur/listeners/service.py | 159 -------- lemur/listeners/views.py | 128 ------- lemur/manage.py | 8 +- lemur/migrations/versions/1ff763f5b80b_.py | 42 ++ lemur/migrations/versions/4c8915e461b3_.py | 41 ++ lemur/migrations/versions/4dc5ddd111b8_.py | 31 ++ lemur/models.py | 19 +- lemur/{elbs => sources}/__init__.py | 0 lemur/sources/models.py | 29 ++ lemur/sources/service.py | 107 ++++++ lemur/{certificates => sources}/sync.py | 2 +- lemur/sources/views.py | 359 ++++++++++++++++++ .../certificates/certificate/edit.tpl.html | 38 ++ .../certificate/notifications.tpl.html | 28 ++ .../notification/notification.js | 56 +++ .../notification/notification.tpl.html | 87 +++++ .../app/angular/notifications/services.js | 106 ++++++ .../app/angular/notifications/view/view.js | 96 +++++ .../angular/notifications/view/view.tpl.html | 52 +++ 26 files changed, 1096 insertions(+), 588 deletions(-) delete mode 100644 lemur/elbs/models.py delete mode 100644 lemur/elbs/service.py delete mode 100644 lemur/elbs/views.py delete mode 100644 lemur/listeners/__init__.py delete mode 100644 lemur/listeners/models.py delete mode 100644 lemur/listeners/service.py delete mode 100644 lemur/listeners/views.py create mode 100644 lemur/migrations/versions/1ff763f5b80b_.py create mode 100644 lemur/migrations/versions/4c8915e461b3_.py create mode 100644 lemur/migrations/versions/4dc5ddd111b8_.py rename lemur/{elbs => sources}/__init__.py (100%) create mode 100644 lemur/sources/models.py create mode 100644 lemur/sources/service.py rename lemur/{certificates => sources}/sync.py (98%) create mode 100644 lemur/sources/views.py create mode 100644 lemur/static/app/angular/certificates/certificate/edit.tpl.html create mode 100644 lemur/static/app/angular/certificates/certificate/notifications.tpl.html create mode 100644 lemur/static/app/angular/notifications/notification/notification.js create mode 100644 lemur/static/app/angular/notifications/notification/notification.tpl.html create mode 100644 lemur/static/app/angular/notifications/services.js create mode 100644 lemur/static/app/angular/notifications/view/view.js create mode 100644 lemur/static/app/angular/notifications/view/view.tpl.html diff --git a/lemur/__init__.py b/lemur/__init__.py index 43c7e0df..32d65e06 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -22,6 +22,8 @@ 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 +from lemur.sources.views import mod as sources_bp + LEMUR_BLUEPRINTS = ( users_bp, @@ -36,6 +38,7 @@ LEMUR_BLUEPRINTS = ( status_bp, plugins_bp, notifications_bp, + sources_bp ) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index d876a3da..012b5566 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -23,7 +23,9 @@ 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, certificate_notification_associations + +from lemur.models import certificate_associations, certificate_source_associations, \ + certificate_destination_associations, certificate_notification_associations def create_name(issuer, not_before, not_after, subject, san): @@ -222,6 +224,7 @@ class Certificate(db.Model): authority_id = Column(Integer, ForeignKey('authorities.id')) notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate') destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate') + sources = relationship("Source", secondary=certificate_source_associations, backref='certificate') domains = relationship("Domain", secondary=certificate_associations, backref="certificate") elb_listeners = relationship("Listener", lazy='dynamic', backref='certificate') diff --git a/lemur/elbs/models.py b/lemur/elbs/models.py deleted file mode 100644 index d57a9f1f..00000000 --- a/lemur/elbs/models.py +++ /dev/null @@ -1,44 +0,0 @@ -""" -.. module: lemur.elbs.models - :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. - -.. moduleauthor:: Kevin Glisson -""" -from sqlalchemy import Column, BigInteger, String, DateTime, PassiveDefault, func -from sqlalchemy.orm import relationship - -from lemur.database import db -from lemur.listeners.models import Listener - - -class ELB(db.Model): - __tablename__ = 'elbs' - id = Column(BigInteger, primary_key=True) - # account_id = Column(BigInteger, ForeignKey("accounts.id"), index=True) - region = Column(String(32)) - name = Column(String(128)) - vpc_id = Column(String(128)) - scheme = Column(String(128)) - dns_name = Column(String(128)) - listeners = relationship("Listener", backref='elb', cascade="all, delete, delete-orphan") - date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) - - def __init__(self, elb_obj=None): - if elb_obj: - self.region = elb_obj.connection.region.name - self.name = elb_obj.name - self.vpc_id = elb_obj.vpc_id - self.scheme = elb_obj.scheme - self.dns_name = elb_obj.dns_name - for listener in elb_obj.listeners: - self.listeners.append(Listener(listener)) - - def as_dict(self): - return {c.name: getattr(self, c.name) for c in self.__table__.columns} - - def serialize(self): - blob = self.as_dict() - del blob['date_created'] - return blob diff --git a/lemur/elbs/service.py b/lemur/elbs/service.py deleted file mode 100644 index d00110bf..00000000 --- a/lemur/elbs/service.py +++ /dev/null @@ -1,124 +0,0 @@ -""" -.. module: lemur.elbs.service - :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. - -.. moduleauthor:: Kevin Glisson - -""" -from sqlalchemy import func -from sqlalchemy.sql import and_ - -from lemur import database -from lemur.elbs.models import ELB -from lemur.listeners.models import Listener - - -def get_all(account_id, elb_name): - """ - Retrieves all ELBs in a given account - - :param account_id: - :param elb_name: - :rtype : Elb - :return: - """ - query = database.session_query(ELB) - return query.filter(and_(ELB.name == elb_name, ELB.account_id == account_id)).all() - - -def get_by_region_and_account(region, account_id): - query = database.session_query(ELB) - return query.filter(and_(ELB.region == region, ELB.account_id == account_id)).all() - - -def get_all_elbs(): - """ - Get all ELBs that Lemur knows about - - :rtype : list - :return: - """ - return ELB.query.all() - - -def get(elb_id): - """ - Retrieve an ELB with a give ID - - :rtype : Elb - :param elb_id: - :return: - """ - return database.get(ELB, elb_id) - - -def create(account, elb): - """ - Create a new ELB - - :param account: - :param elb: - """ - elb = ELB(elb) - account.elbs.append(elb) - database.create(elb) - - -def delete(elb_id): - """ - Delete an ELB - - :param elb_id: - """ - database.delete(get(elb_id)) - - -def render(args): - query = database.session_query(ELB) - - sort_by = args.pop('sort_by') - sort_dir = args.pop('sort_dir') - page = args.pop('page') - count = args.pop('count') - filt = args.pop('filter') - active = args.pop('active') - certificate_id = args.pop('certificate_id') - - if certificate_id: - query.filter(ELB.listeners.any(Listener.certificate_id == certificate_id)) - - if active == 'true': - query = query.filter(ELB.listeners.any()) - - if filt: - terms = filt.split(';') - query = database.filter(query, ELB, terms) - - query = database.find_all(query, ELB, args) - - if sort_by and sort_dir: - query = database.sort(query, ELB, sort_by, sort_dir) - - return database.paginate(query, page, count) - - -def stats(**kwargs): - attr = getattr(ELB, kwargs.get('metric')) - query = database.db.session.query(attr, func.count(attr)) - - if kwargs.get('account_id'): - query = query.filter(ELB.account_id == kwargs.get('account_id')) - - if kwargs.get('active') == 'true': - query = query.join(ELB.listeners) - query = query.filter(Listener.certificate_id != None) # noqa - - items = query.group_by(attr).all() - - results = [] - for key, count in items: - if key: - results.append({"key": key, "y": count}) - return results diff --git a/lemur/elbs/views.py b/lemur/elbs/views.py deleted file mode 100644 index 214d28e2..00000000 --- a/lemur/elbs/views.py +++ /dev/null @@ -1,78 +0,0 @@ -""" -.. module: lemur.elbs.service - :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. - -.. moduleauthor:: Kevin Glisson - -""" -from flask import Blueprint -from flask.ext.restful import reqparse, Api, fields -from lemur.elbs import service -from lemur.auth.service import AuthenticatedResource - -from lemur.common.utils import marshal_items, paginated_parser - - -mod = Blueprint('elbs', __name__) -api = Api(mod) - - -FIELDS = { - 'name': fields.String, - 'id': fields.Integer, - 'region': fields.String, - 'scheme': fields.String, - 'accountId': fields.Integer(attribute='account_id'), - 'vpcId': fields.String(attribute='vpc_id') -} - - -class ELBsList(AuthenticatedResource): - """ Defines the 'elbs' endpoint """ - def __init__(self): - super(ELBsList, self).__init__() - - @marshal_items(FIELDS) - def get(self): - parser = paginated_parser.copy() - parser.add_argument('owner', type=str, location='args') - parser.add_argument('id', type=str, location='args') - parser.add_argument('accountId', type=str, dest='account_id', location='args') - parser.add_argument('certificateId', type=str, dest='certificate_id', location='args') - parser.add_argument('active', type=str, default='true', location='args') - - args = parser.parse_args() - return service.render(args) - - -class ELBsStats(AuthenticatedResource): - def __init__(self): - self.reqparse = reqparse.RequestParser() - super(ELBsStats, self).__init__() - - def get(self): - self.reqparse.add_argument('metric', type=str, location='args') - self.reqparse.add_argument('accountId', dest='account_id', location='args') - self.reqparse.add_argument('active', type=str, default='true', location='args') - - args = self.reqparse.parse_args() - - items = service.stats(**args) - return {"items": items, "total": len(items)} - - -class ELBs(AuthenticatedResource): - def __init__(self): - self.reqparse = reqparse.RequestParser() - super(ELBs, self).__init__() - - @marshal_items(FIELDS) - def get(self, elb_id): - return service.get(elb_id) - - -api.add_resource(ELBsList, '/elbs', endpoint='elbs') -api.add_resource(ELBs, '/elbs/', endpoint='elb') -api.add_resource(ELBsStats, '/elbs/stats', endpoint='elbsStats') diff --git a/lemur/listeners/__init__.py b/lemur/listeners/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/lemur/listeners/models.py b/lemur/listeners/models.py deleted file mode 100644 index b72c9b48..00000000 --- a/lemur/listeners/models.py +++ /dev/null @@ -1,42 +0,0 @@ -""" -.. module: lemur.elbs.models - :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. - -.. moduleauthor:: Kevin Glisson - -""" -from sqlalchemy import Column, Integer, BigInteger, String, ForeignKey, DateTime, PassiveDefault, func - -from lemur.database import db -from lemur.certificates import service as cert_service -from lemur.certificates.models import Certificate, get_name_from_arn - - -class Listener(db.Model): - __tablename__ = 'listeners' - id = Column(BigInteger, primary_key=True) - certificate_id = Column(Integer, ForeignKey(Certificate.id), index=True) - elb_id = Column(BigInteger, ForeignKey("elbs.id"), index=True) - instance_port = Column(Integer) - instance_protocol = Column(String(16)) - load_balancer_port = Column(Integer) - load_balancer_protocol = Column(String(16)) - date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) - - def __init__(self, listener): - self.load_balancer_port = listener.load_balancer_port - self.load_balancer_protocol = listener.protocol - self.instance_port = listener.instance_port - self.instance_protocol = listener.instance_protocol - if listener.ssl_certificate_id not in ["Invalid-Certificate", None]: - self.certificate_id = cert_service.get_by_name(get_name_from_arn(listener.ssl_certificate_id)).id - - def as_dict(self): - return {c.name: getattr(self, c.name) for c in self.__table__.columns} - - def serialize(self): - blob = self.as_dict() - del blob['date_created'] - return blob diff --git a/lemur/listeners/service.py b/lemur/listeners/service.py deleted file mode 100644 index 6f2ae596..00000000 --- a/lemur/listeners/service.py +++ /dev/null @@ -1,159 +0,0 @@ -""" -.. module: lemur.listeners.service - :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. - -.. moduleauthor:: Kevin Glisson - -""" -from sqlalchemy import func - -from lemur import database - -from lemur.exceptions import CertificateUnavailable - -from lemur.elbs.models import ELB -from lemur.listeners.models import Listener -from lemur.elbs import service as elb_service -from lemur.certificates import service as certificate_service - -# from lemur.common.services.aws.elb import update_listeners, create_new_listeners, delete_listeners - - -def verify_attachment(certificate_id, elb_account_number): - """ - Ensures that the certificate we want ot attach to our listener is - in the same account as our listener. - - :rtype : Certificate - :param certificate_id: - :param elb_account_number: - :return: :raise CertificateUnavailable: - """ - cert = certificate_service.get(certificate_id) - - # we need to ensure that the specified cert is in our account - for account in cert.accounts: - if account.account_number == elb_account_number: - break - else: - raise CertificateUnavailable - return cert - - -def get(listener_id): - return database.get(Listener, listener_id) - - -def create(elb_id, instance_protocol, instance_port, load_balancer_port, load_balancer_protocol, certificate_id=None): - listener = Listener(elb_id, - instance_port, - instance_protocol, - load_balancer_port, - load_balancer_protocol - ) - - elb = elb_service.get(elb_id) - elb.listeners.append(listener) - account_number = elb.account.account_number - - cert = verify_attachment(certificate_id, account_number) - listener_tuple = (load_balancer_port, instance_port, load_balancer_protocol, cert.get_art(account_number),) - # create_new_listeners(account_number, elb.region, elb.name, [listener_tuple]) - - return {'message': 'Listener has been created'} - - -def update(listener_id, **kwargs): - listener = get(listener_id) - - # if the lb_port has changed we need to make sure we are deleting - # the listener on the old port to avoid listener duplication - ports = [] - if listener.load_balancer_port != kwargs.get('load_balancer_port'): - ports.append(listener.load_balancer_port) - else: - ports.append(kwargs.get('load_balancer_port')) - - certificate_id = kwargs.get('certificate_id') - - listener.instance_port = kwargs.get('instance_port') - listener.instance_protocol = kwargs.get('instance_protocol') - listener.load_balancer_port = kwargs.get('load_balancer_port') - listener.load_balancer_protocol = kwargs.get('load_balancer_protocol') - - elb = listener.elb - account_number = listener.elb.account.account_number - - arn = None - if certificate_id: - cert = verify_attachment(certificate_id, account_number) - cert.elb_listeners.append(listener) - arn = cert.get_arn(account_number) - - # remove certificate that is no longer wanted - if listener.certificate and not certificate_id: - listener.certificate.remove() - - database.update(listener) - listener_tuple = (listener.load_balancer_port, listener.instance_port, listener.load_balancer_protocol, arn,) - # update_listeners(account_number, elb.region, elb.name, [listener_tuple], ports) - - return {'message': 'Listener has been updated'} - - -def delete(listener_id): - # first try to delete the listener in aws - listener = get(listener_id) - # delete_listeners(listener.elb.account.account_number, listener.elb.region, listener.elb.name, [listener.load_balancer_port]) - # cleanup operation in lemur - database.delete(listener) - - -def render(args): - query = database.session_query(Listener) - - 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) - elb_id = args.pop('elb_id', None) - - if certificate_id: - query = database.get_all(Listener, certificate_id, field='certificate_id') - - if elb_id: - query = query.filter(Listener.elb_id == elb_id) - - if filt: - terms = filt.split(';') - query = database.filter(query, Listener, terms) - - query = database.find_all(query, Listener, args) - - if sort_by and sort_dir: - query = database.sort(query, Listener, sort_by, sort_dir) - - return database.paginate(query, page, count) - - -def stats(**kwargs): - attr = getattr(Listener, kwargs.get('metric')) - query = database.db.session.query(attr, func.count(attr)) - query = query.join(Listener.elb) - - if kwargs.get('account_id'): - query = query.filter(ELB.account_id == kwargs.get('account_id')) - - if kwargs.get('active') == 'true': - query = query.filter(Listener.certificate_id != None) # noqa - - items = query.group_by(attr).all() - results = [] - for key, count in items: - if key: - results.append({"key": key, "y": count}) - return results diff --git a/lemur/listeners/views.py b/lemur/listeners/views.py deleted file mode 100644 index b603d827..00000000 --- a/lemur/listeners/views.py +++ /dev/null @@ -1,128 +0,0 @@ -""" -.. module: lemur.listeners.service - :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. - -.. moduleauthor:: Kevin Glisson - -""" -from flask import Blueprint -from flask.ext.restful import reqparse, Api, fields - -from lemur.listeners import service -from lemur.auth.service import AuthenticatedResource -from lemur.auth.permissions import admin_permission -from lemur.common.utils import marshal_items, paginated_parser - - -mod = Blueprint('listeners', __name__) -api = Api(mod) - - -FIELDS = { - 'id': fields.Integer, - 'elbId': fields.Integer(attribute="elb_id"), - 'certificateId': fields.Integer(attribute="certificate_id"), - 'instancePort': fields.Integer(attribute="instance_port"), - 'instanceProtocol': fields.String(attribute="instance_protocol"), - 'loadBalancerPort': fields.Integer(attribute="load_balancer_port"), - 'loadBalancerProtocol': fields.String(attribute="load_balancer_protocol") -} - - -class ListenersList(AuthenticatedResource): - def __init__(self): - super(ListenersList, self).__init__() - - @marshal_items(FIELDS) - def get(self): - parser = paginated_parser.copy() - parser.add_argument('certificateId', type=int, dest='certificate_id', location='args') - args = parser.parse_args() - return service.render(args) - - -class ListenersCertificateList(AuthenticatedResource): - def __init__(self): - super(ListenersCertificateList, self).__init__() - - @marshal_items(FIELDS) - def get(self, certificate_id): - parser = paginated_parser.copy() - args = parser.parse_args() - args['certificate_id'] = certificate_id - return service.render(args) - - -class ListenersELBList(AuthenticatedResource): - def __init__(self): - super(ListenersELBList, self).__init__() - - @marshal_items(FIELDS) - def get(self, elb_id): - parser = paginated_parser.copy() - args = parser.parse_args() - args['elb_id'] = elb_id - return service.render(args) - - -class ListenersStats(AuthenticatedResource): - def __init__(self): - self.reqparse = reqparse.RequestParser() - super(ListenersStats, self).__init__() - - def get(self): - self.reqparse.add_argument('metric', type=str, location='args') - self.reqparse.add_argument('accountId', dest='account_id', location='args') - self.reqparse.add_argument('active', type=str, default='true', location='args') - - args = self.reqparse.parse_args() - - items = service.stats(**args) - return {"items": items, "total": len(items)} - - -class Listeners(AuthenticatedResource): - def __init__(self): - super(Listeners, self).__init__() - - @marshal_items(FIELDS) - def get(self, listener_id): - return service.get(listener_id) - - @admin_permission.require(http_exception=403) - @marshal_items(FIELDS) - def post(self): - self.reqparse.add_argument('elbId', type=str, dest='elb_id', required=True, location='json') - self.reqparse.add_argument('instanceProtocol', type=str, dest='instance_protocol', required=True, location='json') - self.reqparse.add_argument('instancePort', type=int, dest='instance_port', required=True, location='json') - self.reqparse.add_argument('loadBalancerProtocol', type=str, dest='load_balancer_protocol', required=True, location='json') - self.reqparse.add_argument('loadBalancerPort', type=int, dest='load_balancer_port', required=True, location='json') - self.reqparse.add_argument('certificateId', type=int, dest='certificate_id', location='json') - - args = self.reqparse.parse_args() - return service.create(**args) - - @admin_permission.require(http_exception=403) - @marshal_items(FIELDS) - def put(self, listener_id): - self.reqparse.add_argument('instanceProtocol', type=str, dest='instance_protocol', required=True, location='json') - self.reqparse.add_argument('instancePort', type=int, dest='instance_port', required=True, location='json') - self.reqparse.add_argument('loadBalancerProtocol', type=str, dest='load_balancer_protocol', required=True, location='json') - self.reqparse.add_argument('loadBalancerPort', type=int, dest='load_balancer_port', required=True, location='json') - self.reqparse.add_argument('certificateId', type=int, dest='certificate_id', location='json') - - args = self.reqparse.parse_args() - return service.update(listener_id, **args) - - @admin_permission.require(http_exception=403) - def delete(self, listener_id): - return service.delete(listener_id) - - -api.add_resource(ListenersList, '/listeners', endpoint='listeners') -api.add_resource(Listeners, '/listeners/', endpoint='listener') -api.add_resource(ListenersStats, '/listeners/stats', endpoint='listenersStats') -api.add_resource(ListenersCertificateList, '/certificates//listeners', endpoint='listenersCertificates') -api.add_resource(ListenersELBList, '/elbs//listeners', endpoint='elbListeners') diff --git a/lemur/manage.py b/lemur/manage.py index 21929980..df2dd120 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -19,7 +19,7 @@ from lemur.certificates import service as cert_service from lemur.plugins.base import plugins from lemur.certificates.verify import verify_string -from lemur.certificates import sync +from lemur.sources import sync from lemur import create_app @@ -33,6 +33,7 @@ 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 +from lemur.sources.models import Source # noqa manager = Manager(create_app) @@ -183,12 +184,11 @@ class Sync(Command): run on a periodic basis and updates the Lemur datastore with the information it discovers. """ + + # TODO create these commands dynamically option_list = [ Group( Option('-a', '--all', action="store_true"), - Option('-b', '--aws', action="store_true"), - Option('-d', '--cloudca', action="store_true"), - Option('-s', '--source', action="store_true"), exclusive=True, required=True ) ] diff --git a/lemur/migrations/versions/1ff763f5b80b_.py b/lemur/migrations/versions/1ff763f5b80b_.py new file mode 100644 index 00000000..e4a9af22 --- /dev/null +++ b/lemur/migrations/versions/1ff763f5b80b_.py @@ -0,0 +1,42 @@ +"""Adding in models for certificate sources + +Revision ID: 1ff763f5b80b +Revises: 4dc5ddd111b8 +Create Date: 2015-08-01 15:24:20.412725 + +""" + +# revision identifiers, used by Alembic. +revision = '1ff763f5b80b' +down_revision = '4dc5ddd111b8' + +from alembic import op +import sqlalchemy as sa + +import sqlalchemy_utils + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('sources', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('label', sa.String(length=32), nullable=True), + sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('plugin_name', sa.String(length=32), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('certificate_source_associations', + sa.Column('source_id', sa.Integer(), nullable=True), + sa.Column('certificate_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['source_id'], ['destinations.id'], ondelete='cascade') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('certificate_source_associations') + op.drop_table('sources') + ### end Alembic commands ### diff --git a/lemur/migrations/versions/4c8915e461b3_.py b/lemur/migrations/versions/4c8915e461b3_.py new file mode 100644 index 00000000..f67d837f --- /dev/null +++ b/lemur/migrations/versions/4c8915e461b3_.py @@ -0,0 +1,41 @@ +"""Adding notifications + +Revision ID: 4c8915e461b3 +Revises: 3b718f59b8ce +Create Date: 2015-07-24 14:34:57.316273 + +""" + +# revision identifiers, used by Alembic. +revision = '4c8915e461b3' +down_revision = '3b718f59b8ce' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects import postgresql + +import sqlalchemy_utils + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('notifications', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('label', sa.String(length=128), nullable=True), + sa.Column('description', sa.Text(), nullable=True), + sa.Column('options', sqlalchemy_utils.types.json.JSONType(), nullable=True), + sa.Column('active', sa.Boolean(), nullable=True), + sa.Column('plugin_name', sa.String(length=32), nullable=True), + sa.PrimaryKeyConstraint('id') + ) + op.drop_column(u'certificates', 'challenge') + op.drop_column(u'certificates', 'csr_config') + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.add_column(u'certificates', sa.Column('csr_config', sa.TEXT(), autoincrement=False, nullable=True)) + op.add_column(u'certificates', sa.Column('challenge', postgresql.BYTEA(), autoincrement=False, nullable=True)) + op.drop_table('notifications') + ### end Alembic commands ### diff --git a/lemur/migrations/versions/4dc5ddd111b8_.py b/lemur/migrations/versions/4dc5ddd111b8_.py new file mode 100644 index 00000000..8330744d --- /dev/null +++ b/lemur/migrations/versions/4dc5ddd111b8_.py @@ -0,0 +1,31 @@ +"""Creating a one-to-many relationship for notifications + +Revision ID: 4dc5ddd111b8 +Revises: 4c8915e461b3 +Create Date: 2015-07-24 15:02:04.398262 + +""" + +# revision identifiers, used by Alembic. +revision = '4dc5ddd111b8' +down_revision = '4c8915e461b3' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('certificate_notification_associations', + sa.Column('notification_id', sa.Integer(), nullable=True), + sa.Column('certificate_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ondelete='cascade'), + sa.ForeignKeyConstraint(['notification_id'], ['notifications.id'], ondelete='cascade') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('certificate_notification_associations') + ### end Alembic commands ### diff --git a/lemur/models.py b/lemur/models.py index 10ee07be..761c2da9 100644 --- a/lemur/models.py +++ b/lemur/models.py @@ -8,9 +8,7 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ - from sqlalchemy import Column, Integer, ForeignKey - from lemur.database import db certificate_associations = db.Table('certificate_associations', @@ -25,12 +23,19 @@ certificate_destination_associations = db.Table('certificate_destination_associa ForeignKey('certificates.id', ondelete='cascade')) ) +certificate_source_associations = db.Table('certificate_source_associations', + Column('source_id', Integer, + ForeignKey('destinations.id', ondelete='cascade')), + Column('certificate_id', Integer, + 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')) - ) + 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')) diff --git a/lemur/elbs/__init__.py b/lemur/sources/__init__.py similarity index 100% rename from lemur/elbs/__init__.py rename to lemur/sources/__init__.py diff --git a/lemur/sources/models.py b/lemur/sources/models.py new file mode 100644 index 00000000..85beaeea --- /dev/null +++ b/lemur/sources/models.py @@ -0,0 +1,29 @@ +""" +.. module: lemur.sources.models + :platform: unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +import copy +from sqlalchemy import Column, Integer, String, Text +from sqlalchemy_utils import JSONType +from lemur.database import db + +from lemur.plugins.base import plugins + + +class Source(db.Model): + __tablename__ = 'sources' + id = Column(Integer, primary_key=True) + label = Column(String(32)) + options = Column(JSONType) + description = Column(Text()) + plugin_name = Column(String(32)) + + @property + def plugin(self): + p = plugins.get(self.plugin_name) + c = copy.deepcopy(p) + c.options = self.options + return c diff --git a/lemur/sources/service.py b/lemur/sources/service.py new file mode 100644 index 00000000..dd7eaa1a --- /dev/null +++ b/lemur/sources/service.py @@ -0,0 +1,107 @@ +""" +.. module: lemur.sources.service + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from lemur import database +from lemur.sources.models import Source +from lemur.certificates.models import Certificate + + +def create(label, plugin_name, options, description=None): + """ + Creates a new source, that can then be used as a source for certificates. + + :param label: Source common name + :param description: + :rtype : Source + :return: New source + """ + source = Source(label=label, options=options, plugin_name=plugin_name, description=description) + return database.create(source) + + +def update(source_id, label, options, description): + """ + Updates an existing source. + + :param source_id: Lemur assigned ID + :param label: Source common name + :rtype : Source + :return: + """ + source = get(source_id) + + source.label = label + source.options = options + source.description = description + + return database.update(source) + + +def delete(source_id): + """ + Deletes an source. + + :param source_id: Lemur assigned ID + """ + database.delete(get(source_id)) + + +def get(source_id): + """ + Retrieves an source by it's lemur assigned ID. + + :param source_id: Lemur assigned ID + :rtype : Source + :return: + """ + return database.get(Source, source_id) + + +def get_by_label(label): + """ + Retrieves a source by it's label + + :param label: + :return: + """ + return database.get(Source, label, field='label') + + +def get_all(): + """ + Retrieves all source currently known by Lemur. + + :return: + """ + query = database.session_query(Source) + return database.find_all(query, Source, {}).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(Source).join(Certificate, Source.certificate) + query = query.filter(Certificate.id == certificate_id) + else: + query = database.session_query(Source) + + if filt: + terms = filt.split(';') + query = database.filter(query, Source, terms) + + query = database.find_all(query, Source, args) + + if sort_by and sort_dir: + query = database.sort(query, Source, sort_by, sort_dir) + + return database.paginate(query, page, count) diff --git a/lemur/certificates/sync.py b/lemur/sources/sync.py similarity index 98% rename from lemur/certificates/sync.py rename to lemur/sources/sync.py index b91af6b1..5b37897f 100644 --- a/lemur/certificates/sync.py +++ b/lemur/sources/sync.py @@ -1,5 +1,5 @@ """ -.. module: sync +.. module: lemur.sources.sync :platform: Unix :synopsis: This module contains various certificate syncing operations. Because of the nature of the SSL environment there are multiple ways diff --git a/lemur/sources/views.py b/lemur/sources/views.py new file mode 100644 index 00000000..807054cc --- /dev/null +++ b/lemur/sources/views.py @@ -0,0 +1,359 @@ +""" +.. module: lemur.sources.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 +""" +from flask import Blueprint +from flask.ext.restful import Api, reqparse, fields +from lemur.sources import service + +from lemur.auth.service import AuthenticatedResource +from lemur.auth.permissions import admin_permission +from lemur.common.utils import paginated_parser, marshal_items + + +mod = Blueprint('sources', __name__) +api = Api(mod) + + +FIELDS = { + 'description': fields.String, + 'sourceOptions': fields.Raw(attribute='options'), + 'pluginName': fields.String(attribute='plugin_name'), + 'label': fields.String, + 'id': fields.Integer, +} + + +class SourcesList(AuthenticatedResource): + """ Defines the 'sources' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(SourcesList, self).__init__() + + @marshal_items(FIELDS) + def get(self): + """ + .. http:get:: /sources + + The current account list + + **Example request**: + + .. sourcecode:: http + + GET /sources 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": [ + { + "sourceOptions": [ + { + "name": "accountNumber", + "required": true, + "value": 111111111112, + "helpMessage": "Must be a valid AWS account number!", + "validation": "/^[0-9]{12,12}$/", + "type": "int" + } + ], + "pluginName": "aws-source", + "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 + :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) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def post(self): + """ + .. http:post:: /sources + + Creates a new account + + **Example request**: + + .. sourcecode:: http + + POST /sources HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "sourceOptions": [ + { + "name": "accountNumber", + "required": true, + "value": 111111111112, + "helpMessage": "Must be a valid AWS account number!", + "validation": "/^[0-9]{12,12}$/", + "type": "int" + } + ], + "pluginName": "aws-source", + "id": 3, + "description": "test", + "label": "test" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "sourceOptions": [ + { + "name": "accountNumber", + "required": true, + "value": 111111111112, + "helpMessage": "Must be a valid AWS account number!", + "validation": "/^[0-9]{12,12}$/", + "type": "int" + } + ], + "pluginName": "aws-source", + "id": 3, + "description": "test", + "label": "test" + } + + :arg label: human readable account label + :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('plugin', type=dict, location='json', required=True) + self.reqparse.add_argument('description', type=str, location='json') + + args = self.reqparse.parse_args() + return service.create(args['label'], args['plugin']['slug'], args['plugin']['pluginOptions'], args['description']) + + +class Sources(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Sources, self).__init__() + + @marshal_items(FIELDS) + def get(self, source_id): + """ + .. http:get:: /sources/1 + + Get a specific account + + **Example request**: + + .. sourcecode:: http + + GET /sources/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 + + { + "sourceOptions": [ + { + "name": "accountNumber", + "required": true, + "value": 111111111112, + "helpMessage": "Must be a valid AWS account number!", + "validation": "/^[0-9]{12,12}$/", + "type": "int" + } + ], + "pluginName": "aws-source", + "id": 3, + "description": "test", + "label": "test" + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + """ + return service.get(source_id) + + @admin_permission.require(http_exception=403) + @marshal_items(FIELDS) + def put(self, source_id): + """ + .. http:put:: /sources/1 + + Updates an account + + **Example request**: + + .. sourcecode:: http + + POST /sources/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "sourceOptions": [ + { + "name": "accountNumber", + "required": true, + "value": 111111111112, + "helpMessage": "Must be a valid AWS account number!", + "validation": "/^[0-9]{12,12}$/", + "type": "int" + } + ], + "pluginName": "aws-source", + "id": 3, + "description": "test", + "label": "test" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + "sourceOptions": [ + { + "name": "accountNumber", + "required": true, + "value": 111111111112, + "helpMessage": "Must be a valid AWS account number!", + "validation": "/^[0-9]{12,12}$/", + "type": "int" + } + ], + "pluginName": "aws-source", + "id": 3, + "description": "test", + "label": "test" + } + + :arg accountNumber: aws account number + :arg label: human readable account label + :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('plugin', type=dict, location='json', required=True) + self.reqparse.add_argument('description', type=str, location='json') + + args = self.reqparse.parse_args() + return service.update(source_id, args['label'], args['plugin']['pluginOptions'], args['description']) + + @admin_permission.require(http_exception=403) + def delete(self, source_id): + service.delete(source_id) + return {'result': True} + + +class CertificateSources(AuthenticatedResource): + """ Defines the 'certificate/', endpoint='account') +api.add_resource(CertificateSources, '/certificates//sources', + endpoint='certificateSources') diff --git a/lemur/static/app/angular/certificates/certificate/edit.tpl.html b/lemur/static/app/angular/certificates/certificate/edit.tpl.html new file mode 100644 index 00000000..a3cf3888 --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/edit.tpl.html @@ -0,0 +1,38 @@ + diff --git a/lemur/static/app/angular/certificates/certificate/notifications.tpl.html b/lemur/static/app/angular/certificates/certificate/notifications.tpl.html new file mode 100644 index 00000000..5f8136ac --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/notifications.tpl.html @@ -0,0 +1,28 @@ +
+ +
+
+ + + + +
+ + + + + + +
{{ notification.label }}{{ notification.description }} + +
+
+
diff --git a/lemur/static/app/angular/notifications/notification/notification.js b/lemur/static/app/angular/notifications/notification/notification.js new file mode 100644 index 00000000..a8135c22 --- /dev/null +++ b/lemur/static/app/angular/notifications/notification/notification.js @@ -0,0 +1,56 @@ +'use strict'; + +angular.module('lemur') + + .controller('NotificationsCreateController', function ($scope, $modalInstance, PluginService, NotificationService, CertificateService, LemurRestangular){ + $scope.notification = LemurRestangular.restangularizeElement(null, {}, 'notifications'); + + PluginService.getByType('notification').then(function (plugins) { + $scope.plugins = plugins; + }); + $scope.save = function (notification) { + NotificationService.create(notification).then( + function () { + $modalInstance.close(); + }, + function () { + + } + ); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + + $scope.certificateService = CertificateService; + }) + + .controller('NotificationsEditController', function ($scope, $modalInstance, NotificationService, NotificationApi, PluginService, CertificateService, editId) { + NotificationApi.get(editId).then(function (notification) { + $scope.notification = notification; + NotificationService.getCertificates(notification); + }); + + PluginService.getByType('notification').then(function (plugins) { + $scope.plugins = plugins; + _.each($scope.plugins, function (plugin) { + if (plugin.slug == $scope.notification.pluginName) { + plugin.pluginOptions = $scope.notification.notificationOptions; + $scope.notification.plugin = plugin; + }; + }); + }); + + $scope.save = function (notification) { + NotificationService.update(notification).then(function () { + $modalInstance.close(); + }); + }; + + $scope.cancel = function () { + $modalInstance.dismiss('cancel'); + }; + + $scope.certificateService = CertificateService; + }); diff --git a/lemur/static/app/angular/notifications/notification/notification.tpl.html b/lemur/static/app/angular/notifications/notification/notification.tpl.html new file mode 100644 index 00000000..8289ae5b --- /dev/null +++ b/lemur/static/app/angular/notifications/notification/notification.tpl.html @@ -0,0 +1,87 @@ + + diff --git a/lemur/static/app/angular/notifications/services.js b/lemur/static/app/angular/notifications/services.js new file mode 100644 index 00000000..9bb6affa --- /dev/null +++ b/lemur/static/app/angular/notifications/services.js @@ -0,0 +1,106 @@ +'use strict'; +angular.module('lemur') + .service('NotificationApi', function (LemurRestangular) { + LemurRestangular.extendModel('notifications', function (obj) { + return angular.extend(obj, { + attachCertificate: function (certificate) { + this.selectedCertificate = null; + if (this.certificates === undefined) { + this.certificates = []; + } + this.certificates.push(certificate); + }, + removeCertificate: function (index) { + this.certificate.splice(index, 1); + } + }); + }); + return LemurRestangular.all('notifications'); + }) + .service('NotificationService', function ($location, NotificationApi, PluginService, toaster) { + var NotificationService = this; + NotificationService.findNotificationsByName = function (filterValue) { + return NotificationApi.getList({'filter[label]': filterValue}) + .then(function (notifications) { + return notifications; + }); + }; + + NotificationService.getCertificates = function (notification) { + notification.getList('certificates').then(function (certificates) { + notification.certificates = certificates; + }); + }; + + NotificationService.getPlugin = function (notification) { + return PluginService.getByName(notification.pluginName).then(function (plugin) { + notification.plugin = plugin; + }); + }; + + + NotificationService.loadMoreCertificates = function (notification, page) { + notification.getList('certificates', {page: page}).then(function (certificates) { + _.each(certificates, function (certificate) { + notification.roles.push(certificate); + }); + }); + }; + + NotificationService.create = function (notification) { + return NotificationApi.post(notification).then( + function () { + toaster.pop({ + type: 'success', + title: notification.label, + body: 'Successfully created!' + }); + $location.path('notifications'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: notification.label, + body: 'Was not created! ' + response.data.message + }); + }); + }; + + NotificationService.update = function (notification) { + return notification.put().then( + function () { + toaster.pop({ + type: 'success', + title: notification.label, + body: 'Successfully updated!' + }); + $location.path('notifications'); + }, + function (response) { + toaster.pop({ + type: 'error', + title: notification.label, + body: 'Was not updated! ' + response.data.message + }); + }); + }; + + NotificationService.updateActive = function (notification) { + notification.put().then( + function () { + toaster.pop({ + type: 'success', + title: notification.name, + body: 'Successfully updated!' + }); + }, + function (response) { + toaster.pop({ + type: 'error', + title: notification.name, + body: 'Was not updated! ' + response.data.message + }); + }); + }; + return NotificationService; + }); diff --git a/lemur/static/app/angular/notifications/view/view.js b/lemur/static/app/angular/notifications/view/view.js new file mode 100644 index 00000000..ae269e67 --- /dev/null +++ b/lemur/static/app/angular/notifications/view/view.js @@ -0,0 +1,96 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($routeProvider) { + $routeProvider.when('/notifications', { + templateUrl: '/angular/notifications/view/view.tpl.html', + controller: 'NotificationsViewController' + }); + }) + + .controller('NotificationsViewController', function ($q, $scope, $modal, NotificationApi, NotificationService, ngTableParams, toaster) { + $scope.filter = {}; + $scope.notificationsTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + NotificationApi.getList(params.url()).then( + function (data) { + _.each(data, function (notification) { + NotificationService.getPlugin(notification); + }); + params.total(data.total); + $defer.resolve(data); + } + ); + } + }); + + $scope.getNotificationStatus = function () { + var def = $q.defer(); + def.resolve([{'title': 'Active', 'id': true}, {'title': 'Inactive', 'id': false}]); + return def; + }; + + $scope.remove = function (notification) { + notification.remove().then( + function () { + $scope.notificationsTable.reload(); + }, + function (response) { + toaster.pop({ + type: 'error', + title: 'Opps', + body: 'I see what you did there' + response.data.message + }); + } + ); + }; + + $scope.edit = function (notificationId) { + var modalInstance = $modal.open({ + animation: true, + templateUrl: '/angular/notifications/notification/notification.tpl.html', + controller: 'NotificationsEditController', + size: 'lg', + resolve: { + editId: function () { + return notificationId; + } + } + }); + + modalInstance.result.then(function () { + $scope.notificationsTable.reload(); + }); + + }; + + $scope.create = function () { + var modalInstance = $modal.open({ + animation: true, + controller: 'NotificationsCreateController', + templateUrl: '/angular/notifications/notification/notification.tpl.html', + size: 'lg' + }); + + modalInstance.result.then(function () { + $scope.notificationsTable.reload(); + }); + + }; + + $scope.toggleFilter = function (params) { + params.settings().$scope.show_filter = !params.settings().$scope.show_filter; + }; + + $scope.notificationService = NotificationService; + + }); diff --git a/lemur/static/app/angular/notifications/view/view.tpl.html b/lemur/static/app/angular/notifications/view/view.tpl.html new file mode 100644 index 00000000..1335e5e3 --- /dev/null +++ b/lemur/static/app/angular/notifications/view/view.tpl.html @@ -0,0 +1,52 @@ +
+
+

Notifications + you have to speak up son!

+
+
+
+ +
+
+ +
+
+
+
+ + + + + + + + + +
+
    +
  • {{ notification.label }}
  • +
  • {{ notification.description }}
  • +
+
+
    +
  • {{ notification.plugin.title }}
  • +
  • {{ notification.plugin.description }}
  • +
+
+
+ +
+
+
+ + +
+
+
+
+
+
From abf21d29315504969e32f2199d8f2c176058addf Mon Sep 17 00:00:00 2001 From: kevgliss Date: Sat, 1 Aug 2015 15:37:47 -0700 Subject: [PATCH 02/15] Adding in frontend javascript for sources --- lemur/static/app/angular/elbs/elb/elb.js | 3 - .../static/app/angular/elbs/elb/elb.tpl.html | 10 -- lemur/static/app/angular/elbs/services.js | 59 -------- lemur/static/app/angular/elbs/view/view.js | 34 ----- .../app/angular/elbs/view/view.tpl.html | 128 ------------------ .../static/app/angular/listeners/services.js | 37 ----- lemur/static/app/index.html | 1 + 7 files changed, 1 insertion(+), 271 deletions(-) delete mode 100644 lemur/static/app/angular/elbs/elb/elb.js delete mode 100644 lemur/static/app/angular/elbs/elb/elb.tpl.html delete mode 100644 lemur/static/app/angular/elbs/services.js delete mode 100644 lemur/static/app/angular/elbs/view/view.js delete mode 100644 lemur/static/app/angular/elbs/view/view.tpl.html delete mode 100644 lemur/static/app/angular/listeners/services.js diff --git a/lemur/static/app/angular/elbs/elb/elb.js b/lemur/static/app/angular/elbs/elb/elb.js deleted file mode 100644 index d35be162..00000000 --- a/lemur/static/app/angular/elbs/elb/elb.js +++ /dev/null @@ -1,3 +0,0 @@ -/** - * Created by kglisson on 1/19/15. - */ diff --git a/lemur/static/app/angular/elbs/elb/elb.tpl.html b/lemur/static/app/angular/elbs/elb/elb.tpl.html deleted file mode 100644 index d0658f25..00000000 --- a/lemur/static/app/angular/elbs/elb/elb.tpl.html +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/lemur/static/app/angular/elbs/services.js b/lemur/static/app/angular/elbs/services.js deleted file mode 100644 index 83d98dc5..00000000 --- a/lemur/static/app/angular/elbs/services.js +++ /dev/null @@ -1,59 +0,0 @@ -'use strict'; - -angular.module('lemur') - .service('ELBApi', function (LemurRestangular) { - LemurRestangular.extendModel('elbs', function (obj) { - return angular.extend(obj, { - attachListener: function (listener) { - if (this.listeners === undefined) { - this.listeners = []; - } - this.listeners.push(listener); - }, - removeListener: function (index) { - this.listeners.splice(index, 1); - } - }); - }); - return LemurRestangular.all('elbs'); - }) - .service('ELBService', function ($location, ELBApi, toaster) { - var ELBService = this; - ELBService.findELBByName = function (filterValue) { - return ELBApi.getList({'filter[name]': filterValue}) - .then(function (elbs) { - return elbs; - }); - }; - - ELBService.getListeners = function (elb) { - elb.getList('listeners').then(function (listeners) { - elb.listeners = listeners; - }); - return elb; - }; - - ELBService.create = function (elb) { - ELBApi.post(elb).then(function () { - toaster.pop({ - type: 'success', - title: 'ELB ' + elb.name, - body: 'Has been successfully created!' - }); - $location.path('elbs'); - }); - }; - - ELBService.update = function (elb) { - elb.put().then(function () { - toaster.pop({ - type: 'success', - title: 'ELB ' + elb.name, - body: 'Has been successfully updated!' - }); - $location.path('elbs'); - }); - }; - - return ELBService; - }); diff --git a/lemur/static/app/angular/elbs/view/view.js b/lemur/static/app/angular/elbs/view/view.js deleted file mode 100644 index 6a7bd825..00000000 --- a/lemur/static/app/angular/elbs/view/view.js +++ /dev/null @@ -1,34 +0,0 @@ -'use strict'; - -angular.module('lemur') - .config(function config($routeProvider) { - $routeProvider.when('/elbs', { - templateUrl: '/angular/elbs/view/view.tpl.html', - controller: 'ELBViewController' - }); - }) - - .controller('ELBViewController', function ($scope, ELBApi, ELBService, ngTableParams) { - $scope.filter = {}; - $scope.elbsTable = new ngTableParams({ - page: 1, // show first page - count: 10, // count per page - sorting: { - id: 'desc' // initial sorting - }, - filter: $scope.filter - }, { - total: 0, // length of data - getData: function ($defer, params) { - ELBApi.getList(params.url()) - .then(function (data) { - params.total(data.total); - $defer.resolve(data); - }); - } - }); - - $scope.toggleFilter = function (params) { - params.settings().$scope.show_filter = !params.settings().$scope.show_filter; - }; - }); diff --git a/lemur/static/app/angular/elbs/view/view.tpl.html b/lemur/static/app/angular/elbs/view/view.tpl.html deleted file mode 100644 index b3e4dad5..00000000 --- a/lemur/static/app/angular/elbs/view/view.tpl.html +++ /dev/null @@ -1,128 +0,0 @@ -
-
-

ELBs - Bring Balance to the Force

-
-
-
-
-
- -
-
-
-
- - - - - - - - - - - - - - -
-
{{ elb.name }}
-
-
{{ elb.account.label }} -
-
-
{{ elb.region }}
-
-
- - -
-
-
- - -
-
-
- - -
-
- - - - - - - - - - - - - - - - - - - - - -
- Certificate NameInstance PortInstance ProtocolLoad Balancer PortLoad Balancer Protocol
-
- -
-
-
- - -
-
-
-
-
- -
-
-
- -
-
-
- -
-
-
- -
-
- -
-
-
-
-
diff --git a/lemur/static/app/angular/listeners/services.js b/lemur/static/app/angular/listeners/services.js deleted file mode 100644 index ef141c17..00000000 --- a/lemur/static/app/angular/listeners/services.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -angular.module('lemur') - .service('ListenerApi', function (LemurRestangular) { - return LemurRestangular.all('listeners'); - }) - .service('ListenerService', function ($location, ListenerApi) { - var ListenerService = this; - ListenerService.findListenerByName = function (filterValue) { - return ListenerApi.getList({'filter[name]': filterValue}) - .then(function (roles) { - return roles; - }); - }; - - ListenerService.create = function (role) { - ListenerApi.post(role).then(function () { - toaster.pop({ - type: 'success', - title: 'Listener ' + role.name, - body: 'Has been successfully created!' - }); - $location.path('roles/view'); - }); - }; - - ListenerService.update = function (role) { - role.put().then(function () { - toaster.pop({ - type: 'success', - title: 'Listener ' + role.name, - body: 'Has been successfully updated!' - }); - $location.path('roles/view'); - }); - }; - }); diff --git a/lemur/static/app/index.html b/lemur/static/app/index.html index 3ef93dd7..46dcd3a9 100644 --- a/lemur/static/app/index.html +++ b/lemur/static/app/index.html @@ -54,6 +54,7 @@
  • Authorities
  • Notifications
  • Destinations
  • +
  • Sources
  • +
    + +
    + +

    You must give a short description about this authority will be used for, this description should only include alphanumeric characters

    +
    +
    diff --git a/lemur/static/app/angular/certificates/services.js b/lemur/static/app/angular/certificates/services.js index db9c1223..9344a3e9 100644 --- a/lemur/static/app/angular/certificates/services.js +++ b/lemur/static/app/angular/certificates/services.js @@ -77,18 +77,8 @@ angular.module('lemur') removeNotification: function (index) { this.notifications.splice(index, 1); }, - attachELB: function (elb) { - this.selectedELB = null; - if (this.elbs === undefined) { - this.elbs = []; - } - this.elbs.push(elb); - }, - removeELB: function (index) { - this.elbs.splice(index, 1); - }, findDuplicates: function () { - DomainService.findDomainByName(this.extensions.subAltNames[0]).then(function (domains) { //We should do a better job of searchin multiple domains + DomainService.findDomainByName(this.extensions.subAltNames[0]).then(function (domains) { //We should do a better job of searching for multiple domains this.duplicates = domains.total; }); }, @@ -205,18 +195,6 @@ angular.module('lemur') }); }; - CertificateService.getListeners = function (certificate) { - return certificate.getList('listeners').then(function (listeners) { - certificate.listeners = listeners; - }); - }; - - CertificateService.getELBs = function (certificate) { - return certificate.getList('listeners').then(function (elbs) { - certificate.elbs = elbs; - }); - }; - CertificateService.getDomains = function (certificate) { return certificate.getList('domains').then(function (domains) { certificate.domains = domains; diff --git a/lemur/static/app/angular/certificates/view/view.tpl.html b/lemur/static/app/angular/certificates/view/view.tpl.html index 563b4263..b19f14cd 100644 --- a/lemur/static/app/angular/certificates/view/view.tpl.html +++ b/lemur/static/app/angular/certificates/view/view.tpl.html @@ -101,14 +101,20 @@ - +
      +
    • + {{ notification.label }} + {{ notification.description}} +
    • +
    - +
      +
    • + {{ destination.label }} + {{ destination.description }} +
    • +
    From c9e9a9ed7c667378631a7bb1b631505230459520 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Sun, 2 Aug 2015 07:45:10 -0700 Subject: [PATCH 09/15] Fixing upload description --- lemur/certificates/views.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 47511eaa..db8772aa 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -369,6 +369,7 @@ class CertificatesUpload(AuthenticatedResource): :statuscode 403: unauthenticated :statuscode 200: no error """ + self.reqparse.add_argument('description', type=str, location='json') 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') From cdb3814469d4c3f39d3439d3d5a178696794e70e Mon Sep 17 00:00:00 2001 From: kevgliss Date: Sun, 2 Aug 2015 09:14:27 -0700 Subject: [PATCH 10/15] Fixing notification deduplication and roll up --- lemur/certificates/models.py | 1 - lemur/certificates/service.py | 13 ----- lemur/destinations/service.py | 26 ++++++++- lemur/destinations/views.py | 16 +++++- lemur/manage.py | 17 ++++++ lemur/notifications/service.py | 55 +++++++++++++------ lemur/plugins/base/v1.py | 11 +++- lemur/plugins/lemur_email/plugin.py | 6 -- lemur/plugins/service.py | 7 +++ .../static/app/angular/dashboard/dashboard.js | 10 ++-- .../app/angular/dashboard/dashboard.tpl.html | 11 ++++ lemur/tests/test_notifications.py | 20 +++++++ 12 files changed, 144 insertions(+), 49 deletions(-) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index cbe37019..ef7cbdc6 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -279,5 +279,4 @@ class Certificate(db.Model): @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) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 8eb2c1e9..237f145f 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -18,7 +18,6 @@ from lemur.destinations.models import Destination from lemur.notifications.models import Notification from lemur.authorities.models import Authority - from lemur.roles.models import Role from cryptography import x509 @@ -400,14 +399,6 @@ def stats(**kwargs): :param kwargs: :return: """ - query = database.session_query(Certificate) - - if kwargs.get('active') == 'true': - query = query.filter(Certificate.elb_listeners.any()) - - if kwargs.get('destination_id'): - query = query.filter(Certificate.destinations.any(Destination.id == kwargs.get('destination_id'))) - if kwargs.get('metric') == 'not_after': start = arrow.utcnow() end = start.replace(weeks=+32) @@ -420,10 +411,6 @@ def stats(**kwargs): attr = getattr(Certificate, kwargs.get('metric')) query = database.db.session.query(attr, func.count(attr)) - # TODO this could be cleaned up - if kwargs.get('active') == 'true': - query = query.filter(Certificate.elb_listeners.any()) - items = query.group_by(attr).all() keys = [] diff --git a/lemur/destinations/service.py b/lemur/destinations/service.py index 38dc600f..f27a138c 100644 --- a/lemur/destinations/service.py +++ b/lemur/destinations/service.py @@ -5,6 +5,8 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ +from sqlalchemy import func + from lemur import database from lemur.destinations.models import Destination from lemur.certificates.models import Certificate @@ -28,9 +30,8 @@ def update(destination_id, label, options, description): Updates an existing destination. :param destination_id: Lemur assigned ID - :param destination_number: AWS assigned ID :param label: Destination common name - :param comments: + :param description: :rtype : Destination :return: """ @@ -107,3 +108,24 @@ def render(args): query = database.sort(query, Destination, sort_by, sort_dir) return database.paginate(query, page, count) + + +def stats(**kwargs): + """ + Helper that defines some useful statistics about destinations. + + :param kwargs: + :return: + """ + attr = getattr(Destination, kwargs.get('metric')) + query = database.db.session.query(attr, func.count(attr)) + + items = query.group_by(attr).all() + + keys = [] + values = [] + for key, count in items: + keys.append(key) + values.append(count) + + return {'labels': keys, 'values': values} diff --git a/lemur/destinations/views.py b/lemur/destinations/views.py index 55ff7071..21e7886e 100644 --- a/lemur/destinations/views.py +++ b/lemur/destinations/views.py @@ -353,7 +353,21 @@ class CertificateDestinations(AuthenticatedResource): return service.render(args) +class DestinationsStats(AuthenticatedResource): + """ Defines the 'certificates' stats endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(DestinationsStats, self).__init__() + + def get(self): + self.reqparse.add_argument('metric', type=str, location='args') + args = self.reqparse.parse_args() + items = service.stats(**args) + return dict(items=items, total=len(items)) + + api.add_resource(DestinationsList, '/destinations', endpoint='destinations') -api.add_resource(Destinations, '/destinations/', endpoint='account') +api.add_resource(Destinations, '/destinations/', endpoint='destination') api.add_resource(CertificateDestinations, '/certificates//destinations', endpoint='certificateDestinations') +api.add_resource(DestinationsStats, '/destinations/stats', endpoint='destinationStats') diff --git a/lemur/manage.py b/lemur/manage.py index 1ad37291..4f5794ac 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -222,6 +222,23 @@ def sync_sources(labels, view): sync_lock.release() +@manager.command +def notify(): + """ + Runs Lemur's notification engine, that looks for expired certificates and sends + notifications out to those that bave subscribed to them. + + :return: + """ + sys.stdout.write("Starting to notify subscribers about expiring certificates!\n") + count = notification_service.send_expiration_notifications() + sys.stdout.write( + "Finished notifying subscribers about expiring certificates! Sent {count} notifications!\n".format( + count=count + ) + ) + + class InitializeApp(Command): """ This command will bootstrap our database with any destinations as diff --git a/lemur/notifications/service.py b/lemur/notifications/service.py index 0ffdc52d..9653f957 100644 --- a/lemur/notifications/service.py +++ b/lemur/notifications/service.py @@ -34,7 +34,7 @@ def _get_message_data(cert): 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])) + cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert) if cert.name != x])) return cert_dict @@ -44,8 +44,13 @@ def _deduplicate(messages): 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: + for data, options in messages: + targets = [] + for o in options: + if o.get('name') == 'recipients': + targets = o['value'].split(',') + + for m, r, o in roll_ups: if r == targets: m.append(data) current_app.logger.info( @@ -53,7 +58,7 @@ def _deduplicate(messages): data['name'], ",".join(targets))) break else: - roll_ups.append(([data], targets, data.plugin_options)) + roll_ups.append(([data], targets, options)) return roll_ups @@ -62,21 +67,30 @@ def send_expiration_notifications(): This function will check for upcoming certificate expiration, and send out notification emails at given intervals. """ - notifications = 0 + sent = 0 - for plugin_name, notifications in database.get_all(Notification, True, field='active').group_by(Notification.plugin_name): - notifications += 1 + for plugin in plugins.all(plugin_type='notification'): + notifications = database.db.session.query(Notification)\ + .filter(Notification.plugin_name == plugin.slug)\ + .filter(Notification.active == True).all() # noqa - messages = _deduplicate(notifications) - plugin = plugins.get(plugin_name) + messages = [] + for n in notifications: + for c in n.certificates: + if _is_eligible_for_notifications(c): + messages.append((_get_message_data(c), n.options)) + + messages = _deduplicate(messages) for data, targets, options in messages: + sent += 1 plugin.send('expiration', data, targets, options) - current_app.logger.info("Lemur has sent {0} certification notifications".format(notifications)) + current_app.logger.info("Lemur has sent {0} certification notifications".format(sent)) + return sent -def get_domain_certificate(name): +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 @@ -92,7 +106,7 @@ def get_domain_certificate(name): current_app.logger.info(str(e)) -def find_superseded(domains): +def _find_superseded(cert): """ 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 @@ -103,17 +117,22 @@ def find_superseded(domains): """ query = database.session_query(Certificate) ss_list = [] - for domain in domains: - dc = get_domain_certificate(domain.name) - if dc: - ss_list.append(dc) + + # determine what is current host at our domains + for domain in cert.domains: + dups = _get_domain_certificate(domain.name) + for c in dups: + if c.body != cert.body: + ss_list.append(dups) + 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]))) + # look for other certificates that may not be hosted but cover the same domains + query = query.filter(Certificate.domains.any(Domain.name.in_([x.name for x in cert.domains]))) query = query.filter(Certificate.active == True) # noqa query = query.filter(Certificate.not_after >= arrow.utcnow().format('YYYY-MM-DD')) + query = query.filter(Certificate.body != cert.body) ss_list.extend(query.all()) - return ss_list diff --git a/lemur/plugins/base/v1.py b/lemur/plugins/base/v1.py index ce378b98..7026c99f 100644 --- a/lemur/plugins/base/v1.py +++ b/lemur/plugins/base/v1.py @@ -101,13 +101,18 @@ class IPlugin(local): Returns a list of tuples pointing to various resources for this plugin. >>> def get_resource_links(self): >>> return [ - >>> ('Documentation', 'http://sentry.readthedocs.org'), - >>> ('Bug Tracker', 'https://github.com/getsentry/sentry/issues'), - >>> ('Source', 'https://github.com/getsentry/sentry'), + >>> ('Documentation', 'http://lemury.readthedocs.org'), + >>> ('Bug Tracker', 'https://github.com/Netflix/lemur/issues'), + >>> ('Source', 'https://github.com/Netflix/lemur'), >>> ] """ return self.resource_links + def get_option(self, name, options): + for o in options: + if o.get(name): + return o['value'] + class Plugin(IPlugin): """ diff --git a/lemur/plugins/lemur_email/plugin.py b/lemur/plugins/lemur_email/plugin.py index cecc6e37..90c53a67 100644 --- a/lemur/plugins/lemur_email/plugin.py +++ b/lemur/plugins/lemur_email/plugin.py @@ -19,12 +19,6 @@ 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' diff --git a/lemur/plugins/service.py b/lemur/plugins/service.py index e69de29b..33965963 100644 --- a/lemur/plugins/service.py +++ b/lemur/plugins/service.py @@ -0,0 +1,7 @@ +""" +.. module: service + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" diff --git a/lemur/static/app/angular/dashboard/dashboard.js b/lemur/static/app/angular/dashboard/dashboard.js index f0d94eab..630e0439 100644 --- a/lemur/static/app/angular/dashboard/dashboard.js +++ b/lemur/static/app/angular/dashboard/dashboard.js @@ -11,11 +11,6 @@ angular.module('lemur') var baseAccounts = LemurRestangular.all('accounts'); - baseAccounts.getList() - .then(function (data) { - $scope.accounts = data; - }); - $scope.colours = [ { fillColor: 'rgba(41, 171, 224, 0.2)', @@ -89,4 +84,9 @@ angular.module('lemur') .then(function (data) { $scope.expiring = {labels: data.items.labels, values: [data.items.values]}; }); + + LemurRestangular.all('destinations').customGET('stats', {metric: 'certificates'}) + .then(function (data) { + $scope.destinations = {labels: data.items.labels, values: [data.items.values]}; + }); }); diff --git a/lemur/static/app/angular/dashboard/dashboard.tpl.html b/lemur/static/app/angular/dashboard/dashboard.tpl.html index a00880a4..c8d2c6a6 100644 --- a/lemur/static/app/angular/dashboard/dashboard.tpl.html +++ b/lemur/static/app/angular/dashboard/dashboard.tpl.html @@ -36,6 +36,17 @@
    +
    +
    +
    +
    +

    Destinations

    +
    +
    + +
    +
    +
    diff --git a/lemur/tests/test_notifications.py b/lemur/tests/test_notifications.py index e66c1984..cfea8afa 100644 --- a/lemur/tests/test_notifications.py +++ b/lemur/tests/test_notifications.py @@ -115,3 +115,23 @@ 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} + + +def test_get_message_data(session): + assert 1 == 2 + + +def test_deduplicate(session): + assert 1 == 2 + + +def test_find_superseded(session): + assert 1 == 2 + + +def test_is_eligible_for_notifications(session): + assert 1 == 2 + + +def test_create_default_expiration_notifications(session): + assert 1 == 2 From 0360ccc666eca6693eaded116b12bfc482d31280 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 3 Aug 2015 09:49:33 -0700 Subject: [PATCH 11/15] Cleaning up some documentation --- docs/administration/index.rst | 3 --- .../internals/lemur.certificates.rst | 9 -------- docs/quickstart/index.rst | 21 ++++++++++++------- lemur/plugins/lemur_aws/elb.py | 4 ++-- lemur/tests/test_notifications.py | 20 ------------------ 5 files changed, 16 insertions(+), 41 deletions(-) diff --git a/docs/administration/index.rst b/docs/administration/index.rst index b609cbfe..e57dd5c0 100644 --- a/docs/administration/index.rst +++ b/docs/administration/index.rst @@ -150,9 +150,6 @@ Lemur supports sending certification expiration notifications through SES and SM LEMUR_SECURITY_TEAM_EMAIL = ['security@example.com'] -.. data:: - - Authority Options ----------------- diff --git a/docs/developer/internals/lemur.certificates.rst b/docs/developer/internals/lemur.certificates.rst index a19617c4..cf052148 100644 --- a/docs/developer/internals/lemur.certificates.rst +++ b/docs/developer/internals/lemur.certificates.rst @@ -28,15 +28,6 @@ certificates Package :undoc-members: :show-inheritance: -:mod:`sync` Module ------------------- - -.. automodule:: lemur.certificates.sync - :noindex: - :members: - :undoc-members: - :show-inheritance: - :mod:`verify` Module -------------------- diff --git a/docs/quickstart/index.rst b/docs/quickstart/index.rst index f3a04a63..034141fb 100644 --- a/docs/quickstart/index.rst +++ b/docs/quickstart/index.rst @@ -42,13 +42,13 @@ Finally, activate your virtualenv:: Installing build dependencies ----------------------------- -If installing Lemur on true bare Ubuntu OS you will need to grab the following packages so that Lemur can correctly build it's -dependencies. +If installing Lemur on truely bare Ubuntu OS you will need to grab the following packages so that Lemur can correctly build it's +dependencies:: $ sudo apt-get update $ sudo apt-get install nodejs-legacy python-pip libpq-dev python-dev build-essential libssl-dev libffi-dev nginx git supervisor -And optionally if your database is going to be on the same host as the webserver. +And optionally if your database is going to be on the same host as the webserver:: $ sudo apt-get install postgres @@ -110,7 +110,7 @@ Update your configuration Once created you will need to update the configuration file with information about your environment, such as which database to talk to, where keys are stores etc.. -.. Note:: If you are unVfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so: +.. Note:: If you are unfamiliar with with the SQLALCHEMY_DATABASE_URI string it can be broken up like so: postgresql://userame:password@databasefqdn:databaseport/databasename Setup Postgres @@ -119,7 +119,7 @@ Setup Postgres For production a dedicated database is recommended, for this guide we will assume postgres has been installed and is on the same machine that Lemur is installed on. -First, set a password for the postgres user. For this guide, we will use **lemur** as an example but you should use the database password generated for by Lemur.:: +First, set a password for the postgres user. For this guide, we will use **lemur** as an example but you should use the database password generated for by Lemur:: $ sudo -u postgres psql postgres # \password postgres @@ -139,10 +139,17 @@ Initializing Lemur Lemur provides a helpful command that will initialize your database for you. It creates a default user (lemur) that is used by Lemur to help associate certificates that do not currently have an owner. This is most commonly the case when -Lemur has discovered certificates from a third party resource. This is also a default user that can be used to +Lemur has discovered certificates from a third party source. This is also a default user that can be used to administer Lemur. -**Make note of the password used as this will be use to first login to the Lemur UI** +In addition to create a new User, Lemur also creates a few default email notifications. These notifications are based +on a few configuration options such as `LEMUR_SECURITY_TEAM_EMAIL` they basically garentee that every cerificate within +Lemur will send one expiration notification to the security team. + +Additional notifications can be created through the UI or API. +See :ref:`Creating Notifications ` and :ref:`Command Line Interface ` for details. + +**Make note of the password used as this will be used during first login to the Lemur UI** .. code-block:: bash diff --git a/lemur/plugins/lemur_aws/elb.py b/lemur/plugins/lemur_aws/elb.py index d71b5013..b263d473 100644 --- a/lemur/plugins/lemur_aws/elb.py +++ b/lemur/plugins/lemur_aws/elb.py @@ -1,9 +1,9 @@ """ -.. module:: elb +.. module: elb :synopsis: Module contains some often used and helpful classes that are used to deal with ELBs -.. moduleauthor:: Kevin Glisson (kglisson@netflix.com) +.. moduleauthor:: Kevin Glisson """ import boto.ec2 diff --git a/lemur/tests/test_notifications.py b/lemur/tests/test_notifications.py index cfea8afa..e66c1984 100644 --- a/lemur/tests/test_notifications.py +++ b/lemur/tests/test_notifications.py @@ -115,23 +115,3 @@ 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} - - -def test_get_message_data(session): - assert 1 == 2 - - -def test_deduplicate(session): - assert 1 == 2 - - -def test_find_superseded(session): - assert 1 == 2 - - -def test_is_eligible_for_notifications(session): - assert 1 == 2 - - -def test_create_default_expiration_notifications(session): - assert 1 == 2 From 7d169f7c4c2ab2efc5dbeb69bb7eeb84a6421aa1 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 3 Aug 2015 13:51:27 -0700 Subject: [PATCH 12/15] Fixing up some of the sync related code --- lemur/certificates/models.py | 2 +- lemur/certificates/service.py | 4 +- lemur/manage.py | 1 + lemur/notifications/service.py | 16 +++--- lemur/plugins/lemur_aws/iam.py | 10 ++-- lemur/plugins/lemur_aws/plugin.py | 13 ++--- lemur/sources/service.py | 50 +++++++++++++------ .../static/app/angular/dashboard/dashboard.js | 2 - .../notification/notification.js | 2 +- setup.py | 8 +-- 10 files changed, 66 insertions(+), 42 deletions(-) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index ef7cbdc6..83f3f690 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -279,4 +279,4 @@ class Certificate(db.Model): @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) + destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options) diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 237f145f..886ef165 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -135,10 +135,10 @@ def import_certificate(**kwargs): """ from lemur.users import service as user_service from lemur.notifications import service as notification_service - cert = Certificate(kwargs['public_certificate']) + cert = Certificate(kwargs['public_certificate'], chain=kwargs['intermediate_certificate']) # TODO future source plugins might have a better understanding of who the 'owner' is we should support this - cert.owner = kwargs.get('owner', current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')) + cert.owner = kwargs.get('owner', current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')[0]) cert.creator = kwargs.get('creator', user_service.get_by_email('lemur@nobody')) # NOTE existing certs may not follow our naming standard we will diff --git a/lemur/manage.py b/lemur/manage.py index 4f5794ac..3621159e 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -180,6 +180,7 @@ def sync_sources(labels, view): information it discovers. """ if view: + sys.stdout.write("Active", "Label", "Description") for source in source_service.get_all(): sys.stdout.write( "[{active}]\t{label}\t{description}!\n".format( diff --git a/lemur/notifications/service.py b/lemur/notifications/service.py index 9653f957..db40b53c 100644 --- a/lemur/notifications/service.py +++ b/lemur/notifications/service.py @@ -24,6 +24,12 @@ from lemur.certificates import service as cert_service from lemur.plugins.base import plugins +def get_options(name, options): + for o in options: + if o.get('name') == name: + return o + + def _get_message_data(cert): """ Parse our the certification information needed for our notification @@ -45,10 +51,8 @@ def _deduplicate(messages): """ roll_ups = [] for data, options in messages: - targets = [] - for o in options: - if o.get('name') == 'recipients': - targets = o['value'].split(',') + o = get_options('recipients', options) + targets = o['value'].split(',') for m, r, o in roll_ups: if r == targets: @@ -148,8 +152,8 @@ def _is_eligible_for_notifications(cert): days = (cert.not_after - now.naive).days for notification in cert.notifications: - interval = notification.options['interval'] - unit = notification.options['unit'] + interval = get_options('interval', notification.options)['value'] + unit = get_options('unit', notification.options)['value'] if unit == 'weeks': interval *= 7 diff --git a/lemur/plugins/lemur_aws/iam.py b/lemur/plugins/lemur_aws/iam.py index 9279c577..5e3bca0a 100644 --- a/lemur/plugins/lemur_aws/iam.py +++ b/lemur/plugins/lemur_aws/iam.py @@ -19,17 +19,17 @@ def get_name_from_arn(arn): return arn.split("/", 1)[1] -def upload_cert(account_number, cert, private_key, cert_chain=None): +def upload_cert(account_number, name, body, private_key, cert_chain=None): """ Upload a certificate to AWS :param account_number: - :param cert: + :param name: :param private_key: :param cert_chain: :return: """ - return assume_service(account_number, 'iam').upload_server_cert(cert.name, str(cert.body), str(private_key), + return assume_service(account_number, 'iam').upload_server_cert(name, str(body), str(private_key), cert_chain=str(cert_chain)) @@ -57,7 +57,7 @@ def get_all_server_certs(account_number): result = response['list_server_certificates_response']['list_server_certificates_result'] for cert in result['server_certificate_metadata_list']: - certs.append(cert) + certs.append(cert['arn']) if result['is_truncated'] == 'true': marker = result['marker'] @@ -72,7 +72,7 @@ def get_cert_from_arn(arn): :param arn: :return: """ - name = arn.split("/", 1)[1] + name = get_name_from_arn(arn) account_number = arn.split(":")[4] name = name.split("/")[-1] diff --git a/lemur/plugins/lemur_aws/plugin.py b/lemur/plugins/lemur_aws/plugin.py index 07b1f9a7..06c36d7f 100644 --- a/lemur/plugins/lemur_aws/plugin.py +++ b/lemur/plugins/lemur_aws/plugin.py @@ -13,7 +13,7 @@ from lemur.plugins import lemur_aws as aws def find_value(name, options): for o in options: - if o.get(name): + if o['name'] == name: return o['value'] @@ -41,8 +41,8 @@ class AWSDestinationPlugin(DestinationPlugin): # 'port': {'type': 'int'} # } - def upload(self, cert, private_key, cert_chain, options, **kwargs): - iam.upload_cert(find_value('accountNumber', options), cert, private_key, cert_chain=cert_chain) + def upload(self, name, body, private_key, cert_chain, options, **kwargs): + iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain) e = find_value('elb', options) if e: @@ -68,14 +68,15 @@ class AWSSourcePlugin(SourcePlugin): }, ] - def get_certificates(self, **kwargs): + def get_certificates(self, options, **kwargs): certs = [] - arns = elb.get_all_server_certs(kwargs['account_number']) + arns = iam.get_all_server_certs(find_value('accountNumber', options)) for arn in arns: - cert_body = iam.get_cert_from_arn(arn) + cert_body, cert_chain = iam.get_cert_from_arn(arn) cert_name = iam.get_name_from_arn(arn) cert = dict( public_certificate=cert_body, + intermediate_certificate=cert_chain, name=cert_name ) certs.append(cert) diff --git a/lemur/sources/service.py b/lemur/sources/service.py index e3f53094..b097696e 100644 --- a/lemur/sources/service.py +++ b/lemur/sources/service.py @@ -11,6 +11,7 @@ from lemur import database from lemur.sources.models import Source from lemur.certificates.models import Certificate from lemur.certificates import service as cert_service +from lemur.destinations import service as destination_service from lemur.plugins.base import plugins @@ -19,7 +20,7 @@ def _disassociate_certs_from_source(current_certificates, found_certificates, so missing = [] for cc in current_certificates: for fc in found_certificates: - if fc.body == cc.body: + if fc['public_certificate'] == cc.body: break else: missing.append(cc) @@ -36,6 +37,34 @@ def _disassociate_certs_from_source(current_certificates, found_certificates, so c.sources.delete(s) +def sync_create(certificate, source): + cert = cert_service.import_certificate(**certificate) + cert.sources.append(source) + sync_update_destination(cert, source) + database.update(cert) + + +def sync_update(certificate, source): + for s in certificate.sources: + if s.label == source.label: + break + else: + certificate.sources.append(source) + + sync_update_destination(certificate, source) + database.update(certificate) + + +def sync_update_destination(certificate, source): + dest = destination_service.get_by_label(source.label) + if dest: + for d in certificate.destinations: + if d.label == source.label: + break + else: + certificate.destinations.append(dest) + + def sync(labels=None): new, updated = 0, 0 c_certificates = cert_service.get_all_certs() @@ -46,30 +75,21 @@ def sync(labels=None): if source.label not in labels: continue - current_app.logger.error("Retrieving certificates from {0}".format(source.title)) + current_app.logger.error("Retrieving certificates from {0}".format(source.label)) s = plugins.get(source.plugin_name) certificates = s.get_certificates(source.options) for certificate in certificates: - exists = cert_service.find_duplicates(certificate) + exists = cert_service.find_duplicates(certificate['public_certificate']) if not exists: - cert = cert_service.import_certificate(**certificate) - cert.sources.append(source) - database.update(cert) - + sync_create(certificate, source) new += 1 # check to make sure that existing certificates have the current source associated with it - if len(exists) == 1: - for s in cert.sources: - if s.label == source.label: - break - else: - cert.sources.append(source) - + elif len(exists) == 1: + sync_update(exists[0], source) updated += 1 - else: current_app.logger.warning( "Multiple certificates found, attempt to deduplicate the following certificates: {0}".format( diff --git a/lemur/static/app/angular/dashboard/dashboard.js b/lemur/static/app/angular/dashboard/dashboard.js index 630e0439..69cfd554 100644 --- a/lemur/static/app/angular/dashboard/dashboard.js +++ b/lemur/static/app/angular/dashboard/dashboard.js @@ -9,8 +9,6 @@ angular.module('lemur') }) .controller('DashboardController', function ($scope, $rootScope, $filter, $location, LemurRestangular) { - var baseAccounts = LemurRestangular.all('accounts'); - $scope.colours = [ { fillColor: 'rgba(41, 171, 224, 0.2)', diff --git a/lemur/static/app/angular/notifications/notification/notification.js b/lemur/static/app/angular/notifications/notification/notification.js index 5df01e04..9495b6ec 100644 --- a/lemur/static/app/angular/notifications/notification/notification.js +++ b/lemur/static/app/angular/notifications/notification/notification.js @@ -38,7 +38,7 @@ angular.module('lemur') if (plugin.slug === $scope.notification.pluginName) { plugin.pluginOptions = $scope.notification.notificationOptions; $scope.notification.plugin = plugin; - }; + } }); }); diff --git a/setup.py b/setup.py index 5b315241..b508687f 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ install_requires = [ 'pyopenssl==0.15.1', 'pyjwt==1.0.1', 'xmltodict==0.9.2', - 'lockfile=0.10.2' + 'lockfile==0.10.2' ] tests_require = [ @@ -136,10 +136,10 @@ setup( 'lemur.plugins': [ 'verisign_issuer = lemur.plugins.lemur_verisign.plugin:VerisignIssuerPlugin', 'cloudca_issuer = lemur.plugins.lemur_cloudca.plugin:CloudCAIssuerPlugin', - 'cloudca_source = lemur.plugins.lemur_cloudca.plugin:CloudCASourcePlugin' + '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' + 'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin', + 'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin', ], }, classifiers=[ From a873e5c7ea0d629001df64f440464e4a7f7dc6f6 Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 3 Aug 2015 15:52:39 -0700 Subject: [PATCH 13/15] Lots of minor fixes --- lemur/authorities/service.py | 8 +++++ lemur/manage.py | 6 ++-- lemur/notifications/service.py | 33 ++++++++++++++----- lemur/plugins/lemur_aws/plugin.py | 2 +- .../destinations/destination/destination.js | 9 +++++ .../notification/notification.js | 9 +++++ .../app/angular/sources/source/source.js | 9 +++++ 7 files changed, 63 insertions(+), 13 deletions(-) diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index 0c831be8..23961ede 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -9,10 +9,12 @@ """ from flask import g +from flask import current_app from lemur import database from lemur.authorities.models import Authority from lemur.roles import service as role_service +from lemur.notifications import service as notification_service from lemur.roles.models import Role from lemur.certificates.models import Certificate @@ -56,9 +58,15 @@ def create(kwargs): cert.description = "This is the ROOT certificate for the {0} certificate authority".format(kwargs.get('caName')) cert.user = g.current_user + cert.notifications = notification_service.create_default_expiration_notifications( + 'DEFAULT_SECURITY', + current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL') + ) + # we create and attach any roles that the issuer gives us role_objs = [] for r in issuer_roles: + role = role_service.create( r['name'], password=r['password'], diff --git a/lemur/manage.py b/lemur/manage.py index 3621159e..8cee39e0 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -180,7 +180,7 @@ def sync_sources(labels, view): information it discovers. """ if view: - sys.stdout.write("Active", "Label", "Description") + sys.stdout.write("Active\tLabel\tDescription\n") for source in source_service.get_all(): sys.stdout.write( "[{active}]\t{label}\t{description}!\n".format( @@ -199,10 +199,10 @@ def sync_sources(labels, view): sync_lock.acquire(timeout=10) # wait up to 10 seconds if labels: - sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels)) + sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels)) labels = labels.split(",") else: - sys.stdout.write("[+] Starting to sync ALL sources!\n".format(labels)) + sys.stdout.write("[+] Starting to sync ALL sources!\n") sync(labels=labels) sys.stdout.write( diff --git a/lemur/notifications/service.py b/lemur/notifications/service.py index db40b53c..e1198df6 100644 --- a/lemur/notifications/service.py +++ b/lemur/notifications/service.py @@ -180,13 +180,22 @@ def create_default_expiration_notifications(name, recipients): """ options = [ { - 'name': 'recipients', - 'value': ','.join(recipients) + 'name': 'unit', + 'type': 'select', + 'required': True, + 'validation': '', + 'available': ['days', 'weeks', 'months'], + 'helpMessage': 'Interval unit', + 'value': 'days', }, { - 'name': 'unit', - 'value': 'days' - } + 'name': 'recipients', + 'type': 'str', + 'required': True, + 'validation': '^([\w+-.%]+@[\w-.]+\.[A-Za-z]{2,4},?)+$', + 'helpMessage': 'Comma delimited list of email addresses', + 'value': ','.join(recipients) + }, ] intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS") @@ -195,10 +204,16 @@ def create_default_expiration_notifications(name, recipients): for i in intervals: n = get_by_label("{name}_{interval}_DAY".format(name=name, interval=i)) if not n: - inter = [{ - 'name': 'interval', - 'value': i, - }] + inter = [ + { + 'name': 'interval', + 'type': 'int', + 'required': True, + 'validation': '^\d+$', + 'helpMessage': 'Number of days to be alert before expiration.', + 'value': i, + } + ] inter.extend(options) n = create( label="{name}_{interval}_DAY".format(name=name, interval=i), diff --git a/lemur/plugins/lemur_aws/plugin.py b/lemur/plugins/lemur_aws/plugin.py index 06c36d7f..0c6fc09a 100644 --- a/lemur/plugins/lemur_aws/plugin.py +++ b/lemur/plugins/lemur_aws/plugin.py @@ -29,7 +29,7 @@ class AWSDestinationPlugin(DestinationPlugin): options = [ { 'name': 'accountNumber', - 'type': 'int', + 'type': 'str', 'required': True, 'validation': '/^[0-9]{12,12}$/', 'helpMessage': 'Must be a valid AWS account number!', diff --git a/lemur/static/app/angular/destinations/destination/destination.js b/lemur/static/app/angular/destinations/destination/destination.js index 7bb6b66b..321eecfb 100644 --- a/lemur/static/app/angular/destinations/destination/destination.js +++ b/lemur/static/app/angular/destinations/destination/destination.js @@ -23,6 +23,15 @@ angular.module('lemur') .controller('DestinationsEditController', function ($scope, $modalInstance, DestinationService, DestinationApi, PluginService, editId) { DestinationApi.get(editId).then(function (destination) { $scope.destination = destination; + 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; + } + }); + }); }); PluginService.getByType('destination').then(function (plugins) { diff --git a/lemur/static/app/angular/notifications/notification/notification.js b/lemur/static/app/angular/notifications/notification/notification.js index 9495b6ec..7cb1da50 100644 --- a/lemur/static/app/angular/notifications/notification/notification.js +++ b/lemur/static/app/angular/notifications/notification/notification.js @@ -29,6 +29,15 @@ angular.module('lemur') .controller('NotificationsEditController', function ($scope, $modalInstance, NotificationService, NotificationApi, PluginService, CertificateService, editId) { NotificationApi.get(editId).then(function (notification) { $scope.notification = notification; + PluginService.getByType('notification').then(function (plugins) { + $scope.plugins = plugins; + _.each($scope.plugins, function (plugin) { + if (plugin.slug === $scope.notification.pluginName) { + plugin.pluginOptions = $scope.notification.notificationOptions; + $scope.notification.plugin = plugin; + } + }); + }); NotificationService.getCertificates(notification); }); diff --git a/lemur/static/app/angular/sources/source/source.js b/lemur/static/app/angular/sources/source/source.js index 7c75e3ad..b7378c77 100644 --- a/lemur/static/app/angular/sources/source/source.js +++ b/lemur/static/app/angular/sources/source/source.js @@ -23,6 +23,15 @@ angular.module('lemur') .controller('SourcesEditController', function ($scope, $modalInstance, SourceService, SourceApi, PluginService, editId) { SourceApi.get(editId).then(function (source) { $scope.source = source; + PluginService.getByType('source').then(function (plugins) { + $scope.plugins = plugins; + _.each($scope.plugins, function (plugin) { + if (plugin.slug === $scope.source.pluginName) { + plugin.pluginOptions = $scope.source.sourceOptions; + $scope.source.plugin = plugin; + } + }); + }); }); PluginService.getByType('source').then(function (plugins) { From 710b4d45bc6723f82ee3cba855e3534a27b5f44f Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 3 Aug 2015 16:10:00 -0700 Subject: [PATCH 14/15] Allowing notifications to be marked as in-active --- lemur/notifications/service.py | 3 ++- lemur/notifications/views.py | 4 ++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/lemur/notifications/service.py b/lemur/notifications/service.py index e1198df6..2c29d94d 100644 --- a/lemur/notifications/service.py +++ b/lemur/notifications/service.py @@ -243,7 +243,7 @@ def create(label, plugin_name, options, description, certificates): return database.create(notification) -def update(notification_id, label, options, description, certificates): +def update(notification_id, label, options, description, active, certificates): """ Updates an existing destination. @@ -258,6 +258,7 @@ def update(notification_id, label, options, description, certificates): notification.label = label notification.options = options notification.description = description + notification.active = active notification = database.update_list(notification, 'certificates', Certificate, certificates) return database.update(notification) diff --git a/lemur/notifications/views.py b/lemur/notifications/views.py index 884cea90..82bb3b86 100644 --- a/lemur/notifications/views.py +++ b/lemur/notifications/views.py @@ -110,6 +110,7 @@ class NotificationsList(AuthenticatedResource): :statuscode 200: no error """ parser = paginated_parser.copy() + parser.add_argument('active', type=bool, location='args') args = parser.parse_args() return service.render(args) @@ -346,6 +347,7 @@ class Notifications(AuthenticatedResource): """ 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('active', type=bool, location='json') self.reqparse.add_argument('certificates', type=list, default=[], location='json') self.reqparse.add_argument('description', type=str, location='json') @@ -355,6 +357,7 @@ class Notifications(AuthenticatedResource): args['label'], args['plugin']['pluginOptions'], args['description'], + args['active'], args['certificates'] ) @@ -444,6 +447,7 @@ class CertificateNotifications(AuthenticatedResource): :statuscode 200: no error """ parser = paginated_parser.copy() + parser.add_argument('active', type=bool, location='args') args = parser.parse_args() args['certificate_id'] = certificate_id return service.render(args) From 888e75e7f795f2544a27a77eb4455d51c2479c3f Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 3 Aug 2015 16:15:59 -0700 Subject: [PATCH 15/15] Fixing tests --- lemur/tests/test_notifications.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lemur/tests/test_notifications.py b/lemur/tests/test_notifications.py index e66c1984..78fe3053 100644 --- a/lemur/tests/test_notifications.py +++ b/lemur/tests/test_notifications.py @@ -6,7 +6,7 @@ def test_crud(session): notification = create('testnotify', 'email-notification', {}, 'notify1', []) assert notification.id > 0 - notification = update(notification.id, 'testnotify2', {}, 'notify2', []) + notification = update(notification.id, 'testnotify2', {}, 'notify2', True, []) assert notification.label == 'testnotify2' assert len(get_all()) == 1