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

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

View File

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

View File

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

View File

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