diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 7939349a..d629d327 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -22,7 +22,8 @@ from lemur.domains.models import Domain from lemur.constants import SAN_NAMING_TEMPLATE, DEFAULT_NAMING_TEMPLATE from lemur.models import certificate_associations, certificate_source_associations, \ - certificate_destination_associations, certificate_notification_associations + certificate_destination_associations, certificate_notification_associations, \ + certificate_replacement_associations def create_name(issuer, not_before, not_after, subject, san): @@ -32,6 +33,11 @@ def create_name(issuer, not_before, not_after, subject, san): useful information such as Common Name, Validation dates, and Issuer. + :param san: + :param subject: + :param not_after: + :param issuer: + :param not_before: :rtype : str :return: """ @@ -231,6 +237,11 @@ class Certificate(db.Model): authority_id = Column(Integer, ForeignKey('authorities.id')) notifications = relationship("Notification", secondary=certificate_notification_associations, backref='certificate') destinations = relationship("Destination", secondary=certificate_destination_associations, backref='certificate') + replaces = relationship("Certificate", + secondary=certificate_replacement_associations, + primaryjoin=id == certificate_replacement_associations.c.certificate_id, # noqa + secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa + backref='replaced') sources = relationship("Source", secondary=certificate_source_associations, backref='certificate') domains = relationship("Domain", secondary=certificate_associations, backref="certificate") @@ -280,11 +291,44 @@ class Certificate(db.Model): """ return "arn:aws:iam::{}:server-certificate/{}".format(account_number, self.name) - 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): + """ + Attempt to upload the new certificate to the new destination + + :param target: + :param value: + :param initiator: + :return: + """ destination_plugin = plugins.get(value.plugin_name) destination_plugin.upload(target.name, target.body, target.private_key, target.chain, value.options) + + +@event.listens_for(Certificate.replaces, 'append') +def update_replacement(target, value, initiator): + """ + When a certificate is marked as 'replaced' it is then marked as in-active + + :param target: + :param value: + :param initiator: + :return: + """ + value.active = False + + +@event.listens_for(Certificate, 'before_update') +def protect_active(mapper, connection, target): + """ + When a certificate has a replacement do not allow it to be marked as 'active' + + :param connection: + :param mapper: + :param target: + :return: + """ + if target.active: + if target.replaced: + raise Exception("Cannot mark certificate as active, certificate has been marked as replaced.") diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 9e5db6ad..7da66e7c 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -76,13 +76,16 @@ def find_duplicates(cert_body): return Certificate.query.filter_by(body=cert_body).all() -def update(cert_id, owner, description, active, destinations, notifications): +def update(cert_id, owner, description, active, destinations, notifications, replaces): """ - Updates a certificate. - + Updates a certificate :param cert_id: :param owner: + :param description: :param active: + :param destinations: + :param notifications: + :param replaces: :return: """ from lemur.notifications import service as notification_service @@ -104,6 +107,7 @@ def update(cert_id, owner, description, active, destinations, notifications): cert.notifications = new_notifications database.update_list(cert, 'destinations', Destination, destinations) + database.update_list(cert, 'replaces', Certificate, replaces) cert.owner = owner @@ -165,6 +169,7 @@ def import_certificate(**kwargs): notification_name = 'DEFAULT_SECURITY' notifications = notification_service.create_default_expiration_notifications(notification_name, current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')) + database.update_list(cert, 'replaces', Certificate, kwargs['replacements']) cert.notifications = notifications cert = database.create(cert) @@ -194,8 +199,8 @@ def upload(**kwargs): g.user.certificates.append(cert) database.update_list(cert, 'destinations', Destination, kwargs.get('destinations')) - database.update_list(cert, 'notifications', Notification, kwargs.get('notifications')) + database.update_list(cert, 'replaces', Certificate, kwargs['replacements']) # create default notifications for this certificate if none are provided notifications = [] @@ -228,7 +233,7 @@ def create(**kwargs): # 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, 'destinations', Destination, kwargs.get('destinations')) - + database.update_list(cert, 'replaces', Certificate, kwargs['replacements']) database.update_list(cert, 'notifications', Notification, kwargs.get('notifications')) # create default notifications for this certificate if none are provided diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 8918b5a0..08f81853 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -269,7 +269,10 @@ class CertificatesList(AuthenticatedResource): }, "commonName": "test", "validityStart": "2015-06-05T07:00:00.000Z", - "validityEnd": "2015-06-16T07:00:00.000Z" + "validityEnd": "2015-06-16T07:00:00.000Z", + "replacements": [ + {'id': 123} + ] } **Example response**: @@ -317,6 +320,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('notifications', type=list, default=[], location='json') + self.reqparse.add_argument('replacements', type=list, default=[], location='json') 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('authority', type=valid_authority, location='json', required=True) @@ -375,6 +379,7 @@ class CertificatesUpload(AuthenticatedResource): "privateKey": "---Begin Private..." "destinations": [], "notifications": [], + "replacements": [], "name": "cert1" } @@ -419,8 +424,9 @@ class CertificatesUpload(AuthenticatedResource): self.reqparse.add_argument('owner', type=str, required=True, location='json') self.reqparse.add_argument('name', type=str, 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('destinations', type=list, default=[], location='json') + self.reqparse.add_argument('notifications', type=list, default=[], location='json') + self.reqparse.add_argument('replacements', type=list, default=[], 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') @@ -575,7 +581,8 @@ class Certificates(AuthenticatedResource): "owner": "jimbob@example.com", "active": false "notifications": [], - "destinations": [] + "destinations": [], + "replacements": [] } **Example response**: @@ -614,6 +621,7 @@ class Certificates(AuthenticatedResource): 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=notification_list, default=[], location='json') + self.reqparse.add_argument('replacements', type=list, default=[], location='json') args = self.reqparse.parse_args() cert = service.get(certificate_id) @@ -628,7 +636,8 @@ class Certificates(AuthenticatedResource): args['description'], args['active'], args['destinations'], - args['notifications'] + args['notifications'], + args['replacements'] ) return dict(message='You are not authorized to update this certificate'), 403 @@ -711,9 +720,65 @@ class NotificationCertificatesList(AuthenticatedResource): return service.render(args) +class CertificatesReplacementsList(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(CertificatesReplacementsList, self).__init__() + + @marshal_items(FIELDS) + def get(self, certificate_id): + """ + .. http:get:: /certificates/1/replacements + + One certificate + + **Example request**: + + .. sourcecode:: http + + GET /certificates/1/replacements 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, + "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", + "signingAlgorithm": "sha2", + "cn": "example.com", + "status": "unknown" + }] + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + return service.get(certificate_id).replaces + + api.add_resource(CertificatesList, '/certificates', endpoint='certificates') api.add_resource(Certificates, '/certificates/', endpoint='certificate') api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats') api.add_resource(CertificatesUpload, '/certificates/upload', endpoint='certificateUpload') api.add_resource(CertificatePrivateKey, '/certificates//key', endpoint='privateKeyCertificates') api.add_resource(NotificationCertificatesList, '/notifications//certificates', endpoint='notificationCertificates') +api.add_resource(CertificatesReplacementsList, '/certificates//replacements', endpoint='replacements') diff --git a/lemur/models.py b/lemur/models.py index 2ca4f6dd..09566e7e 100644 --- a/lemur/models.py +++ b/lemur/models.py @@ -36,6 +36,14 @@ certificate_notification_associations = db.Table('certificate_notification_assoc Column('certificate_id', Integer, ForeignKey('certificates.id', ondelete='cascade')) ) + +certificate_replacement_associations = db.Table('certificate_replacement_associations', + Column('replaced_certificate_id', Integer, + ForeignKey('certificates.id', ondelete='cascade')), + Column('certificate_id', Integer, + ForeignKey('certificates.id', ondelete='cascade')) + ) + roles_users = db.Table('roles_users', Column('user_id', Integer, ForeignKey('users.id')), Column('role_id', Integer, ForeignKey('roles.id')) diff --git a/lemur/static/app/angular/authorities/view/view.tpl.html b/lemur/static/app/angular/authorities/view/view.tpl.html index 9d64f174..22f5063e 100644 --- a/lemur/static/app/angular/authorities/view/view.tpl.html +++ b/lemur/static/app/angular/authorities/view/view.tpl.html @@ -35,7 +35,7 @@ -
+
Permalink
+
diff --git a/lemur/static/app/angular/certificates/certificate/tracking.tpl.html b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html index cf337f0f..490e68a5 100644 --- a/lemur/static/app/angular/certificates/certificate/tracking.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html @@ -1,84 +1,118 @@
-
-
- -
- -

You must enter an Certificate owner

-
-
-
- -
- -

You must give a short description about this authority will be used for.

-
-
-
- -
-
- -
-
-
-
- -
- -
-
-
- -
- -

You must enter a common name and it must be less than 64 characters

-
-
-
- -
-
-
- - - - -
-
-
- -
-
-
- - - - -
-
-
-
-
-
+
+
+ + +
+ + +

You must + enter an Certificate owner

+
+
+ + +
+ + +

You + must give a short description about this authority will be used for.

+
+
+
+ + +
+
+ +
+
+
+
+ + +
+ +
+
+
+ + +
+ + +

You must + enter a common name and it must be less than 64 characters

+
+
+
+ + +
+
+
+ + + + +
+
+
+ + +
+
+
+ + + + +
+
+
+
+
+
+
+
diff --git a/lemur/static/app/angular/certificates/certificate/upload.js b/lemur/static/app/angular/certificates/certificate/upload.js index cf8bd11b..a7a29837 100644 --- a/lemur/static/app/angular/certificates/certificate/upload.js +++ b/lemur/static/app/angular/certificates/certificate/upload.js @@ -8,6 +8,7 @@ angular.module('lemur') $scope.destinationService = DestinationService; $scope.notificationService = NotificationService; + $scope.certificateService = CertificateService; PluginService.getByType('destination').then(function (plugins) { $scope.plugins = plugins; diff --git a/lemur/static/app/angular/certificates/certificate/upload.tpl.html b/lemur/static/app/angular/certificates/certificate/upload.tpl.html index f97d8d67..75159f09 100644 --- a/lemur/static/app/angular/certificates/certificate/upload.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/upload.tpl.html @@ -80,6 +80,7 @@ class="help-block">Enter a valid certificate.

+
diff --git a/lemur/static/app/angular/certificates/services.js b/lemur/static/app/angular/certificates/services.js index 47faf9cb..39755866 100644 --- a/lemur/static/app/angular/certificates/services.js +++ b/lemur/static/app/angular/certificates/services.js @@ -67,6 +67,16 @@ angular.module('lemur') removeDestination: function (index) { this.destinations.splice(index, 1); }, + attachReplacement: function (replacement) { + this.selectedReplacement = null; + if (this.replacements === undefined) { + this.replacements = []; + } + this.replacements.push(replacement); + }, + removeReplacement: function (index) { + this.replacements.splice(index, 1); + }, attachNotification: function (notification) { this.selectedNotification = null; if (this.notifications === undefined) { @@ -149,6 +159,12 @@ angular.module('lemur') }); }; + CertificateService.getReplacements = function (certificate) { + return certificate.getList('replacements').then(function (replacements) { + certificate.replacements = replacements; + }); + }; + CertificateService.getDefaults = function (certificate) { return DefaultService.get().then(function (defaults) { certificate.country = defaults.country; diff --git a/lemur/static/app/angular/certificates/view/view.js b/lemur/static/app/angular/certificates/view/view.js index c987a901..392614dd 100644 --- a/lemur/static/app/angular/certificates/view/view.js +++ b/lemur/static/app/angular/certificates/view/view.js @@ -36,6 +36,7 @@ angular.module('lemur') CertificateService.getDomains(certificate); CertificateService.getDestinations(certificate); CertificateService.getNotifications(certificate); + CertificateService.getReplacements(certificate); CertificateService.getAuthority(certificate); CertificateService.getCreator(certificate); }); @@ -101,6 +102,7 @@ angular.module('lemur') body: 'Unable to update! ' + response.data.message, timeout: 100000 }); + certificate.active = false; }); }; $scope.getCertificateStatus = function () { diff --git a/lemur/static/app/angular/certificates/view/view.tpl.html b/lemur/static/app/angular/certificates/view/view.tpl.html index ea5761ec..3d628efa 100644 --- a/lemur/static/app/angular/certificates/view/view.tpl.html +++ b/lemur/static/app/angular/certificates/view/view.tpl.html @@ -26,8 +26,8 @@
    -
  • {{ certificate.name }}
  • -
  • {{ certificate.owner }}
  • +
  • {{ ::certificate.name }}
  • +
  • {{ ::certificate.owner }}
@@ -37,10 +37,10 @@ - {{ certificate.authority.name || certificate.issuer }} + {{ ::certificate.authority.name || certificate.issuer }} - {{ certificate.cn }} + {{ ::certificate.cn }}
@@ -61,19 +61,19 @@
  • Creator - {{ certificate.creator.email }} + {{ ::certificate.creator.email }}
  • Not Before - - {{ momentService.createMoment(certificate.notBefore) }} + + {{ ::momentService.createMoment(certificate.notBefore) }}
  • Not After - - {{ momentService.createMoment(certificate.notAfter) }} + + {{ ::momentService.createMoment(certificate.notAfter) }}
  • @@ -85,15 +85,15 @@
  • Bits - {{ certificate.bits }} + {{ ::certificate.bits }}
  • Signing Algorithm - {{ certificate.signingAlgorithm }} + {{ ::certificate.signingAlgorithm }}
  • Serial - {{ certificate.serial }} + {{ ::certificate.serial }}
  • Description -

    {{ certificate.description }}

    +

    {{ ::certificate.description }}

  • @@ -115,8 +115,8 @@ Notifications
    • - {{ notification.label }} - {{ notification.description}} + {{ ::notification.label }} + {{ ::notification.description}}
    @@ -124,18 +124,27 @@ Destinations
    • - {{ destination.label }} - {{ destination.description }} + {{ ::destination.label }} + {{ ::destination.description }}
    Domains + + Replaces +
      +
    • + {{ ::replacement.name }} +

      {{ ::replacement.description}}

      +
    • +
    +
    @@ -145,7 +154,7 @@ tooltip="Copy chain to clipboard" tooltip-trigger="mouseenter" clipboard text="certificate.chain"> -
    {{ certificate.chain }}
    +
    {{ ::certificate.chain }}
    @@ -154,7 +163,7 @@ tooltip="Copy certificate to clipboard" tooltip-trigger="mouseenter" clipboard text="certificate.body"> -
    {{ certificate.body }}
    +
    {{ ::certificate.body }}
    @@ -163,7 +172,7 @@ tooltip="Copy key to clipboard" tooltip-trigger="mouseenter" clipboard text="certificate.privateKey"> -
    {{ certificate.privateKey }}
    +
    {{ ::certificate.privateKey }}