Merge pull request #27 from kevgliss/editCertificate

Initial support for notification plugins closes #8, closes #9, closes…
This commit is contained in:
kevgliss 2015-07-29 22:12:21 -07:00
commit 11a6294162
45 changed files with 1686 additions and 605 deletions

View File

@ -1,6 +1,9 @@
Lemur Lemur
***** *****
[![Build Status](https://magnum.travis-ci.com/Netflix/lemur.svg?token=i5tcyx3z4N7pEVTxTeuP&branch=master)](https://magnum.travis-ci.com/Netflix/lemur)
Lemur manages SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults. Lemur manages SSL certificate creation. It provides a central portal for developers to issuer their own SSL certificates with 'sane' defaults.
It works on CPython 2.7. It is known It works on CPython 2.7. It is known

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,218 +0,0 @@
"""
.. module: lemur.notifications
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
import ssl
import socket
import arrow
import boto.ses
from flask import current_app
from flask_mail import Message
from lemur import database
from lemur.certificates.models import Certificate
from lemur.domains.models import Domain
from lemur.templates.config import env
from lemur.extensions import smtp_mail
NOTIFICATION_INTERVALS = [30, 15, 5, 2]
def _get_domain_certificate(name):
"""
Fetch the SSL certificate currently hosted at a given domain (if any) and
compare it against our all of our know certificates to determine if a new
SSL certificate has already been deployed
:param name:
:return:
"""
query = database.session_query(Certificate)
try:
pub_key = ssl.get_server_certificate((name, 443))
return query.filter(Certificate.body == pub_key.strip()).first()
except socket.gaierror as e:
current_app.logger.info(str(e))
def _find_superseded(domains):
"""
Here we try to fetch any domain in the certificate to see if we can resolve it
and to try and see if it is currently serving the certificate we are
alerting on
:param domains:
:return:
"""
query = database.session_query(Certificate)
ss_list = []
for domain in domains:
dc = _get_domain_certificate(domain.name)
if dc:
ss_list.append(dc)
current_app.logger.info("Trying to resolve {0}".format(domain.name))
query = query.filter(Certificate.domains.any(Domain.name.in_([x.name for x in domains])))
query = query.filter(Certificate.active == True) # noqa
query = query.filter(Certificate.not_after >= arrow.utcnow().format('YYYY-MM-DD'))
ss_list.extend(query.all())
return ss_list
def send_expiration_notifications():
"""
This function will check for upcoming certificate expiration,
and send out notification emails at given intervals.
"""
notifications = 0
certs = _get_expiring_certs()
alerts = []
for cert in certs:
if _is_eligible_for_notifications(cert):
data = _get_message_data(cert)
recipients = _get_message_recipients(cert)
alerts.append((data, recipients))
roll_ups = _create_roll_ups(alerts)
for messages, recipients in roll_ups:
notifications += 1
send("Certificate Expiration", dict(messages=messages), 'event', recipients)
print notifications
current_app.logger.info("Lemur has sent {0} certification notifications".format(notifications))
def _get_message_recipients(cert):
"""
Determine who the recipients of the certificate expiration should be
:param cert:
:return:
"""
recipients = []
if current_app.config.get('SECURITY_TEAM_EMAIL'):
recipients.extend(current_app.config.get('SECURITY_TEAM_EMAIL'))
recipients.append(cert.owner)
if cert.user:
recipients.append(cert.user.email)
return list(set(recipients))
def _get_message_data(cert):
"""
Parse our the certification information needed for our notification
:param cert:
:return:
"""
cert_dict = cert.as_dict()
cert_dict['domains'] = [x .name for x in cert.domains]
cert_dict['superseded'] = list(set([x.name for x in _find_superseded(cert.domains) if cert.name != x]))
return cert_dict
def _get_expiring_certs(outlook=30):
"""
Find all the certificates expiring within a given outlook
:param outlook: int days to look forward
:return:
"""
now = arrow.utcnow()
query = database.session_query(Certificate)
attr = Certificate.not_after
# get all certs expiring in the next 30 days
to = now.replace(days=+outlook).format('YYYY-MM-DD')
certs = []
for cert in query.filter(attr <= to).filter(attr >= now.format('YYYY-MM-DD')).all():
if _is_eligible_for_notifications(cert):
certs.append(cert)
return certs
def _is_eligible_for_notifications(cert, intervals=None):
"""
Determine if notifications for a given certificate should
currently be sent
:param cert:
:param intervals: list of days to alert on
:return:
"""
now = arrow.utcnow()
if cert.active:
days = (cert.not_after - now.naive).days
if not intervals:
intervals = NOTIFICATION_INTERVALS
if days in intervals:
return cert
def _create_roll_ups(messages):
"""
Take all of the messages that should be sent and provide
a roll up to the same set if the recipients are the same
:param messages:
"""
roll_ups = []
for message_data, recipients in messages:
for m, r in roll_ups:
if r == recipients:
m.append(message_data)
current_app.logger.info(
"Sending email expiration alert about {0} to {1}".format(
message_data['name'], ",".join(recipients)))
break
else:
roll_ups.append(([message_data], recipients))
return roll_ups
def send(subject, data, email_type, recipients):
"""
Configures all Lemur email messaging
:param subject:
:param data:
:param email_type:
:param recipients:
"""
# jinja template depending on type
template = env.get_template('{}.html'.format(email_type))
body = template.render(**data)
s_type = current_app.config.get("LEMUR_EMAIL_SENDER").lower()
if s_type == 'ses':
conn = boto.connect_ses()
conn.send_email(current_app.config.get("LEMUR_EMAIL"), subject, body, recipients, format='html')
elif s_type == 'smtp':
msg = Message(subject, recipients=recipients)
msg.body = "" # kinda a weird api for sending html emails
msg.html = body
smtp_mail.send(msg)
else:
current_app.logger.error("No mail carrier specified, notification emails were not able to be sent!")

View File

@ -0,0 +1,29 @@
"""
.. module: lemur.notifications.models
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
"""
from sqlalchemy.orm import relationship
from sqlalchemy import Integer, String, Column, Boolean, Text
from sqlalchemy_utils import JSONType
from lemur.database import db
from lemur.plugins.base import plugins
from lemur.models import certificate_notification_associations
class Notification(db.Model):
__tablename__ = 'notifications'
id = Column(Integer, primary_key=True)
label = Column(String(128))
description = Column(Text())
options = Column(JSONType)
active = Column(Boolean, default=True)
plugin_name = Column(String(32))
certificates = relationship("Certificate", secondary=certificate_notification_associations, passive_deletes=True, backref="notification", cascade='all,delete')
@property
def plugin(self):
return plugins.get(self.plugin_name)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -72,6 +72,12 @@
<tr> <tr>
<td>{{ message.owner }}</td> <td>{{ message.owner }}</td>
</tr> </tr>
<tr>
<td><strong>Creator</strong></td>
</tr>
<tr>
<td>{{ message.creator }}</td>
</tr>
<tr> <tr>
<td><strong>Not Before</strong></td> <td><strong>Not Before</strong></td>
</tr> </tr>

View File

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

View File

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

View File

@ -1,11 +1,8 @@
<h2 class="featurette-heading">Edit</span> Authority <span class="text-muted"><small>Chain of command <div class="modal-header">
</small></span></h2> <div class="modal-title">
<div class="panel panel-default"> <div class="modal-header">Edit Authority <span class="text-muted"><small>chain of command!</small></span></div>
<div class="panel-heading">
<a href="#/authorities" class="btn btn-danger pull-right">Cancel</a>
<div class="clearfix"></div>
</div> </div>
<div class="panel-body"> <div class="modal-body">
<form name="createForm" class="form-horizontal" role="form" novalidate> <form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group"> <div class="form-group">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
@ -37,8 +34,8 @@
</div> </div>
</form> </form>
</div> </div>
<div class="panel-footer"> <div class="modal-footer">
<button ng-click="save(authority)" class="btn btn-success pull-right">Save</button> <button ng-click="save(authority)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save</button>
<div class="clearfix"></div> <button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div> </div>
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

@ -28,7 +28,7 @@
<div class="col-sm-10"> <div class="col-sm-10">
<div class="input-group col-sm-12"> <div class="input-group col-sm-12">
<input name="selectedAuthority" tooltip="If you are unsure which authority you need; you most likely want to use 'verisign'" type="text" ng-model="certificate.selectedAuthority" placeholder="Authority Name" typeahead-on-select="certificate.attachAuthority($item)" <input name="selectedAuthority" tooltip="If you are unsure which authority you need; you most likely want to use 'verisign'" type="text" ng-model="certificate.selectedAuthority" placeholder="Authority Name" typeahead-on-select="certificate.attachAuthority($item)"
typeahead="authority.name for authority in authorityService.findAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities" typeahead="authority.name for authority in authorityService.findActiveAuthorityByName($viewValue)" typeahead-loading="loadingAuthorities"
class="form-control" typeahead-wait-ms="100" typeahead-template-url="angular/authorities/authority/select.tpl.html" required> class="form-control" typeahead-wait-ms="100" typeahead-template-url="angular/authorities/authority/select.tpl.html" required>
</div> </div>
</div> </div>
@ -77,7 +77,8 @@
</div> </div>
</div> </div>
</div> </div>
</div> <div ng-include="'angular/certificates/certificate/notifications.tpl.html'"></div>
<div ng-include="'angular/certificates/certificate/destinations.tpl.html'"></div>
</div> </div>
</form> </form>

View File

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

View File

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

View File

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

View File

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

View File

@ -30,11 +30,6 @@
<li><span class="text-muted">{{ certificate.owner }}</span></li> <li><span class="text-muted">{{ certificate.owner }}</span></li>
</ul> </ul>
</td> </td>
<td data-title="'Destinations'" filter="{ 'destination': 'select' }" filter-date="getDestinationDropDown()">
<div class="btn-group">
<a href="#/destinations/{{ destination.id }}/edit" class="btn btn-sm btn-default" ng-repeat="account in certificate.destinations">{{ destination.label }}</a>
</div>
</td>
<td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()"> <td data-title="'Active'" filter="{ 'active': 'select' }" filter-data="getCertificateStatus()">
<form> <form>
<switch ng-change="certificateService.updateActive(certificate)" id="status" name="status" ng-model="certificate.active" class="green small"></switch> <switch ng-change="certificateService.updateActive(certificate)" id="status" name="status" ng-model="certificate.active" class="green small"></switch>
@ -47,14 +42,16 @@
{{ certificate.cn }} {{ certificate.cn }}
</td> </td>
<td data-title="''"> <td data-title="''">
<div class="btn-group-vertical pull-right"> <div class="btn-group pull-right">
<button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1" butn-checkbox-false="0">View</button> <button ng-model="certificate.toggle" class="btn btn-sm btn-info" btn-checkbox btn-checkbox-true="1" butn-checkbox-false="0">More</button>
<button class="btn btn-sm btn-warning" ng-click="edit(certificate.id)">Edit</button>
</div> </div>
</td> </td>
</tr> </tr>
<tr class="warning" ng-show="certificate.toggle" ng-repeat-end> <tr class="warning" ng-show="certificate.toggle" ng-repeat-end>
<td colspan="5"> <td colspan="6">
<div class="col-md-6"> <tabset justified="true" class="col-md-6">
<tab heading="Basic Info">
<ul class="list-group"> <ul class="list-group">
<li class="list-group-item"> <li class="list-group-item">
<strong>Creator</strong> <strong>Creator</strong>
@ -102,15 +99,23 @@
<span class="pull-right">{{ certificate.description }}</span> <span class="pull-right">{{ certificate.description }}</span>
</li> </li>
</ul> </ul>
<h4>Domains</h4> </tab>
<tab heading="Notifications">
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="notification in certificate.notifications">{{ notification.label }}</a>
</div>
</tab>
<tab heading="Destinations">
<div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="destination in certificate.destinations">{{ destination.label }}</a>
</div>
</tab>
<tab heading="Domains">
<div class="list-group"> <div class="list-group">
<a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a> <a href="#/domains/{{ domain.id }}" class="list-group-item" ng-repeat="domain in certificate.domains">{{ domain.name }}</a>
</div> </div>
<h4 ng-show="certificate.destinations.total">ARNs</h4> </tab>
<ul class="list-group"> </tabset>
<li class="list-group-item" ng-repeat="arn in certificate.arns">{{ arn }}</li>
</ul>
</div>
<tabset justified="true" class="col-md-6"> <tabset justified="true" class="col-md-6">
<tab heading="Chain"> <tab heading="Chain">
<p> <p>

View File

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

View File

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

View File

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

View File

@ -6,9 +6,23 @@ angular.module('lemur')
}) })
.service('PluginService', function (PluginApi) { .service('PluginService', function (PluginApi) {
var PluginService = this; var PluginService = this;
PluginService.get = function (type) { PluginService.get = function () {
return PluginApi.customGETLIST(type).then(function (plugins) { return PluginApi.getList().then(function (plugins) {
return plugins; return plugins;
}); });
}; };
PluginService.getByType = function (type) {
return PluginApi.getList({'type': type}).then(function (plugins) {
return plugins;
});
};
PluginService.getByName = function (pluginName) {
return PluginApi.customGET(pluginName).then(function (plugin) {
return plugin;
});
};
return PluginService;
}); });

View File

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

View File

@ -27,7 +27,7 @@ def test_create_basic_csr():
country=u'US', country=u'US',
state=u'CA', state=u'CA',
location=u'A place', location=u'A place',
extensions=dict(subAltNames=[u'test.example.com', u'test2.example.com']) extensions=dict(names=dict(subAltNames=[u'test.example.com', u'test2.example.com']))
) )
csr, pem = create_csr(csr_config) csr, pem = create_csr(csr_config)
@ -56,15 +56,15 @@ def test_cert_is_san():
from lemur.tests.certs import INTERNAL_VALID_SAN_CERT, INTERNAL_VALID_LONG_CERT from lemur.tests.certs import INTERNAL_VALID_SAN_CERT, INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_is_san from lemur.certificates.models import cert_is_san
assert cert_is_san(INTERNAL_VALID_LONG_CERT) == None assert cert_is_san(INTERNAL_VALID_LONG_CERT) == None # noqa
assert cert_is_san(INTERNAL_VALID_SAN_CERT) == True assert cert_is_san(INTERNAL_VALID_SAN_CERT) == True # noqa
def test_cert_is_wildcard(): def test_cert_is_wildcard():
from lemur.tests.certs import INTERNAL_VALID_WILDCARD_CERT, INTERNAL_VALID_LONG_CERT from lemur.tests.certs import INTERNAL_VALID_WILDCARD_CERT, INTERNAL_VALID_LONG_CERT
from lemur.certificates.models import cert_is_wildcard from lemur.certificates.models import cert_is_wildcard
assert cert_is_wildcard(INTERNAL_VALID_WILDCARD_CERT) == True assert cert_is_wildcard(INTERNAL_VALID_WILDCARD_CERT) == True # noqa
assert cert_is_wildcard(INTERNAL_VALID_LONG_CERT) == None assert cert_is_wildcard(INTERNAL_VALID_LONG_CERT) == None # noqa
def test_cert_get_bitstrength(): def test_cert_get_bitstrength():

View File

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

View File

@ -22,27 +22,27 @@ from subprocess import check_output
ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__))) ROOT = os.path.realpath(os.path.join(os.path.dirname(__file__)))
install_requires = [ install_requires = [
'Flask>=0.10.1', 'Flask==0.10.1',
'Flask-RESTful>=0.3.3', 'Flask-RESTful==0.3.3',
'Flask-SQLAlchemy>=1.0.5', 'Flask-SQLAlchemy==2.0',
'Flask-Script>=2.0.5', 'Flask-Script==2.0.5',
'Flask-Migrate>=1.4.0', 'Flask-Migrate==1.4.0',
'Flask-Bcrypt>=0.6.2', 'Flask-Bcrypt==0.6.2',
'Flask-Principal>=0.4.0', 'Flask-Principal==0.4.0',
'Flask-Mail==0.9.1', 'Flask-Mail==0.9.1',
'SQLAlchemy-Utils>=0.30.11', 'SQLAlchemy-Utils==0.30.11',
'BeautifulSoup4', 'BeautifulSoup4',
'requests>=2.7.0', 'requests==2.7.0',
'psycopg2>=2.6.1', 'psycopg2==2.6.1',
'arrow>=0.5.4', 'arrow==0.5.4',
'boto>=2.38.0', # we might make this optional 'boto==2.38.0', # we might make this optional
'six>=1.9.0', 'six==1.9.0',
'gunicorn>=19.3.0', 'gunicorn==19.3.0',
'pycrypto>=2.6.1', 'pycrypto==2.6.1',
'cryptography>=1.0dev', 'cryptography>=1.0dev',
'pyopenssl>=0.15.1', 'pyopenssl==0.15.1',
'pyjwt>=1.0.1', 'pyjwt==1.0.1',
'xmltodict>=0.9.2' 'xmltodict==0.9.2'
] ]
tests_require = [ tests_require = [
@ -138,6 +138,7 @@ setup(
'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_destination = lemur.plugins.lemur_aws.plugin:AWSDestinationPlugin',
'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin' 'aws_source = lemur.plugins.lemur_aws.plugin:AWSSourcePlugin'
'email_notification = lemur.plugins.lemur_email.plugin:EmailNotificationPlugin'
], ],
}, },
classifiers=[ classifiers=[