diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index fcf89243..742d7455 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -80,6 +80,7 @@ def get_or_increase_name(name): class Certificate(db.Model): __tablename__ = 'certificates' id = Column(Integer, primary_key=True) + external_id = Column(String(128)) owner = Column(String(128), nullable=False) name = Column(String(128), unique=True) description = Column(String(1024)) @@ -162,6 +163,7 @@ class Certificate(db.Model): self.signing_algorithm = defaults.signing_algorithm(cert) self.bits = defaults.bitstrength(cert) self.serial = defaults.serial(cert) + self.external_id = kwargs.get('external_id') for domain in defaults.domains(cert): self.domains.append(Domain(name=domain)) diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 0cb99cf6..9547a373 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -172,6 +172,7 @@ class CertificateCloneSchema(LemurOutputSchema): class CertificateOutputSchema(LemurOutputSchema): id = fields.Integer() + external_id = fields.String() bits = fields.Integer() body = fields.String() chain = fields.String() @@ -253,6 +254,10 @@ class CertificateNotificationOutputSchema(LemurOutputSchema): endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[]) +class CertificateRevokeSchema(LemurInputSchema): + comments = fields.String() + + certificate_input_schema = CertificateInputSchema() certificate_output_schema = CertificateOutputSchema() certificates_output_schema = CertificateOutputSchema(many=True) @@ -260,3 +265,4 @@ certificate_upload_input_schema = CertificateUploadInputSchema() certificate_export_input_schema = CertificateExportInputSchema() certificate_edit_input_schema = CertificateEditInputSchema() certificate_notification_output_schema = CertificateNotificationOutputSchema() +certificate_revoke_schema = CertificateRevokeSchema() diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 7340bd71..20d4faec 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -180,8 +180,8 @@ def mint(**kwargs): private_key = None csr_imported.send(authority=authority, csr=csr) - cert_body, cert_chain = issuer.create_certificate(csr, kwargs) - return cert_body, private_key, cert_chain, + cert_body, cert_chain, external_id = issuer.create_certificate(csr, kwargs) + return cert_body, private_key, cert_chain, external_id def import_certificate(**kwargs): @@ -234,10 +234,11 @@ def create(**kwargs): """ Creates a new certificate. """ - cert_body, private_key, cert_chain = mint(**kwargs) + cert_body, private_key, cert_chain, external_id = mint(**kwargs) kwargs['body'] = cert_body kwargs['private_key'] = private_key kwargs['chain'] = cert_chain + kwargs['external_id'] = external_id roles = create_certificate_roles(**kwargs) diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 2e2f7ccb..540490d0 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -18,8 +18,16 @@ from lemur.auth.service import AuthenticatedResource from lemur.auth.permissions import AuthorityPermission, CertificatePermission from lemur.certificates import service -from lemur.certificates.schemas import certificate_input_schema, certificate_output_schema, \ - certificate_upload_input_schema, certificates_output_schema, certificate_export_input_schema, certificate_edit_input_schema +from lemur.plugins.base import plugins +from lemur.certificates.schemas import ( + certificate_input_schema, + certificate_output_schema, + certificate_upload_input_schema, + certificates_output_schema, + certificate_export_input_schema, + certificate_edit_input_schema, + certificate_revoke_schema +) from lemur.roles import service as role_service from lemur.logs import service as log_service @@ -944,6 +952,73 @@ class CertificateExport(AuthenticatedResource): return dict(extension=extension, passphrase=passphrase, data=base64.b64encode(data).decode('utf-8')) +class CertificateRevoke(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(CertificateRevoke, self).__init__() + + @validate_schema(certificate_revoke_schema, None) + def put(self, certificate_id, data=None): + """ + .. http:put:: /certificates/1/revoke + + Revoke a certificate + + **Example request**: + + .. sourcecode:: http + + POST /certificates/1/revoke HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + { + "comments": "Certificate no longer needed" + } + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + { + 'id': 1 + } + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + + """ + cert = service.get(certificate_id) + + if not cert: + return dict(message="Cannot find specified certificate"), 404 + + # allow creators + if g.current_user != cert.user: + owner_role = role_service.get_by_name(cert.owner) + permission = CertificatePermission(owner_role, [x.name for x in cert.roles]) + + if not permission.can(): + return dict(message='You are not authorized to revoke this certificate.'), 403 + + if not cert.external_id: + return dict(message='Cannot revoke certificate. No external id found.'), 400 + + if cert.endpoints: + return dict(message='Cannot revoke certificate. Endpoints are deployed with the given certificate.'), 403 + + plugin = plugins.get(cert.authority.plugin_name) + plugin.revoke_certificate(cert, data) + log_service.create(g.current_user, 'revoke_cert', certificate=cert) + return dict(id=cert.id) + + +api.add_resource(CertificateRevoke, '/certificates//revoke', endpoint='revokeCertificate') api.add_resource(CertificatesList, '/certificates', endpoint='certificates') api.add_resource(Certificates, '/certificates/', endpoint='certificate') api.add_resource(CertificatesStats, '/certificates/stats', endpoint='certificateStats') diff --git a/lemur/logs/models.py b/lemur/logs/models.py index fe69b715..f634ba9b 100644 --- a/lemur/logs/models.py +++ b/lemur/logs/models.py @@ -18,6 +18,6 @@ class Log(db.Model): __tablename__ = 'logs' id = Column(Integer, primary_key=True) certificate_id = Column(Integer, ForeignKey('certificates.id')) - log_type = Column(Enum('key_view', 'create_cert', 'update_cert', name='log_type'), nullable=False) + log_type = Column(Enum('key_view', 'create_cert', 'update_cert', 'revoke_cert', name='log_type'), nullable=False) logged_at = Column(ArrowType(), PassiveDefault(func.now()), nullable=False) user_id = Column(Integer, ForeignKey('users.id'), nullable=False) diff --git a/lemur/migrations/env.py b/lemur/migrations/env.py index 0a038e6c..63425041 100644 --- a/lemur/migrations/env.py +++ b/lemur/migrations/env.py @@ -3,6 +3,9 @@ from alembic import context from sqlalchemy import engine_from_config, pool from logging.config import fileConfig +import alembic_autogenerate_enums + + # this is the Alembic Config object, which provides # access to the values within the .ini file in use. config = context.config diff --git a/lemur/migrations/versions/1ae8e3104db8_.py b/lemur/migrations/versions/1ae8e3104db8_.py index 2189ed0d..3cb3bb9e 100644 --- a/lemur/migrations/versions/1ae8e3104db8_.py +++ b/lemur/migrations/versions/1ae8e3104db8_.py @@ -14,28 +14,8 @@ from alembic import op def upgrade(): - connection = None - - if not op.get_context().as_sql: - connection = op.get_bind() - connection.execution_options(isolation_level='AUTOCOMMIT') - - op.execute("ALTER TYPE log_type ADD VALUE 'create_cert'") - op.execute("ALTER TYPE log_type ADD VALUE 'update_cert'") - - if connection is not None: - connection.execution_options(isolation_level='READ_COMMITTED') + op.sync_enum_values('public', 'log_type', ['key_view'], ['create_cert', 'key_view', 'update_cert']) def downgrade(): - connection = None - - if not op.get_context().as_sql: - connection = op.get_bind() - connection.execution_options(isolation_level='AUTOCOMMIT') - - op.execute("ALTER TYPE log_type DROP VALUE 'create_cert'") - op.execute("ALTER TYPE log_type DROP VALUE 'update_cert'") - - if connection is not None: - connection.execution_options(isolation_level='READ_COMMITTED') + op.sync_enum_values('public', 'log_type', ['create_cert', 'key_view', 'update_cert'], ['key_view']) diff --git a/lemur/migrations/versions/b29e2c4bf8c9_.py b/lemur/migrations/versions/b29e2c4bf8c9_.py new file mode 100644 index 00000000..19835e09 --- /dev/null +++ b/lemur/migrations/versions/b29e2c4bf8c9_.py @@ -0,0 +1,28 @@ +"""Adds external ID checking and modifying enum + +Revision ID: b29e2c4bf8c9 +Revises: 1ae8e3104db8 +Create Date: 2017-09-26 10:50:35.740367 + +""" + +# revision identifiers, used by Alembic. +revision = 'b29e2c4bf8c9' +down_revision = '1ae8e3104db8' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.add_column('certificates', sa.Column('external_id', sa.String(128), nullable=True)) + op.sync_enum_values('public', 'log_type', ['create_cert', 'key_view', 'update_cert'], ['create_cert', 'key_view', 'revoke_cert', 'update_cert']) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.sync_enum_values('public', 'log_type', ['create_cert', 'key_view', 'revoke_cert', 'update_cert'], ['create_cert', 'key_view', 'update_cert']) + op.drop_column('certificates', 'external_id') + # ### end Alembic commands ### diff --git a/lemur/plugins/bases/issuer.py b/lemur/plugins/bases/issuer.py index 7ae0662c..add16a16 100644 --- a/lemur/plugins/bases/issuer.py +++ b/lemur/plugins/bases/issuer.py @@ -21,3 +21,6 @@ class IssuerPlugin(Plugin): def create_authority(self, options): raise NotImplementedError + + def revoke_certificate(self, certificate, comments): + raise NotImplementedError diff --git a/lemur/plugins/lemur_cfssl/plugin.py b/lemur/plugins/lemur_cfssl/plugin.py index c1912364..2cddc8a1 100644 --- a/lemur/plugins/lemur_cfssl/plugin.py +++ b/lemur/plugins/lemur_cfssl/plugin.py @@ -49,7 +49,8 @@ class CfsslIssuerPlugin(IssuerPlugin): response_json = json.loads(response.content.decode('utf_8')) cert = response_json['result']['certificate'] - return cert, current_app.config.get('CFSSL_INTERMEDIATE'), + # TODO add external ID + return cert, current_app.config.get('CFSSL_INTERMEDIATE'), None @staticmethod def create_authority(options): diff --git a/lemur/plugins/lemur_cryptography/plugin.py b/lemur/plugins/lemur_cryptography/plugin.py index 3723c51f..23c691b2 100644 --- a/lemur/plugins/lemur_cryptography/plugin.py +++ b/lemur/plugins/lemur_cryptography/plugin.py @@ -187,7 +187,7 @@ class CryptographyIssuerPlugin(IssuerPlugin): """ current_app.logger.debug("Issuing new cryptography certificate with options: {0}".format(options)) cert_pem, chain_cert_pem = issue_certificate(csr, options) - return cert_pem, chain_cert_pem + return cert_pem, chain_cert_pem, None @staticmethod def create_authority(options): diff --git a/lemur/plugins/lemur_digicert/plugin.py b/lemur/plugins/lemur_digicert/plugin.py index 6a80fc7d..94330f23 100644 --- a/lemur/plugins/lemur_digicert/plugin.py +++ b/lemur/plugins/lemur_digicert/plugin.py @@ -312,7 +312,17 @@ class DigiCertIssuerPlugin(IssuerPlugin): # retrieve certificate certificate_url = "{0}/services/v2/certificate/{1}/download/format/pem_all".format(base_url, certificate_id) end_entity, intermediate, root = pem.parse(self.session.get(certificate_url).content) - return "\n".join(str(end_entity).splitlines()), "\n".join(str(intermediate).splitlines()) + return "\n".join(str(end_entity).splitlines()), "\n".join(str(intermediate).splitlines()), certificate_id + + def revoke_certificate(self, certificate, comments): + """Revoke a Digicert certificate.""" + base_url = current_app.config.get('DIGICERT_URL') + + # make certificate revoke request + create_url = '{0}/certificate/{1}/revoke'.format(base_url, certificate.external_id) + metrics.send('digicert_revoke_certificate', 'counter', 1) + response = self.session.put(create_url, data=json.dumps({'comments': comments})) + return handle_response(response) @staticmethod def create_authority(options): @@ -379,7 +389,22 @@ class DigiCertCISIssuerPlugin(IssuerPlugin): self.session.headers.pop('Accept') end_entity = pem.parse(certificate_pem)[0] - return "\n".join(str(end_entity).splitlines()), current_app.config.get('DIGICERT_CIS_INTERMEDIATE') + return "\n".join(str(end_entity).splitlines()), current_app.config.get('DIGICERT_CIS_INTERMEDIATE'), data['id'] + + def revoke_certificate(self, certificate, comments): + """Revoke a Digicert certificate.""" + base_url = current_app.config.get('DIGICERT_CIS_URL') + + # make certificate revoke request + revoke_url = '{0}/platform/cis/certificate/{1}/revoke'.format(base_url, certificate.external_id) + metrics.send('digicert_revoke_certificate_success', 'counter', 1) + response = self.session.put(revoke_url, data=json.dumps({'comments': comments})) + + if response.status_code != 204: + metrics.send('digicert_revoke_certificate_failure', 'counter', 1) + raise Exception('Failed to revoke certificate.') + + metrics.send('digicert_revoke_certificate_success', 'counter', 1) @staticmethod def create_authority(options): diff --git a/lemur/plugins/lemur_digicert/tests/test_digicert.py b/lemur/plugins/lemur_digicert/tests/test_digicert.py index 9719c4bf..3493b7fa 100644 --- a/lemur/plugins/lemur_digicert/tests/test_digicert.py +++ b/lemur/plugins/lemur_digicert/tests/test_digicert.py @@ -171,7 +171,7 @@ ghi adapter.register_uri('GET', 'mock://www.digicert.com/services/v2/certificate/cert123/download/format/pem_all', text=pem_fixture) subject.session.mount('mock', adapter) - cert, intermediate = subject.create_certificate("", {'common_name': 'test.com'}) + cert, intermediate, external_id = subject.create_certificate("", {'common_name': 'test.com'}) assert cert == "-----BEGIN CERTIFICATE-----\nabc\n-----END CERTIFICATE-----" assert intermediate == "-----BEGIN CERTIFICATE-----\ndef\n-----END CERTIFICATE-----" diff --git a/lemur/plugins/lemur_verisign/plugin.py b/lemur/plugins/lemur_verisign/plugin.py index 88de9d29..1e32c4e4 100644 --- a/lemur/plugins/lemur_verisign/plugin.py +++ b/lemur/plugins/lemur_verisign/plugin.py @@ -195,7 +195,8 @@ class VerisignIssuerPlugin(IssuerPlugin): response = self.session.post(url, data=data) cert = handle_response(response.content)['Response']['Certificate'] - return cert, current_app.config.get('VERISIGN_INTERMEDIATE'), + # TODO add external id + return cert, current_app.config.get('VERISIGN_INTERMEDIATE'), None @staticmethod def create_authority(options): diff --git a/lemur/static/app/angular/app.js b/lemur/static/app/angular/app.js index 3de47b73..c19cb37f 100644 --- a/lemur/static/app/angular/app.js +++ b/lemur/static/app/angular/app.js @@ -58,6 +58,25 @@ }); }); + lemur.directive('compareTo', function() { + return { + require: 'ngModel', + scope: { + otherModelValue: '=compareTo' + }, + link: function(scope, element, attributes, ngModel) { + + ngModel.$validators.compareTo = function(modelValue) { + return modelValue === scope.otherModelValue; + }; + + scope.$watch('otherModelValue', function() { + ngModel.$validate(); + }); + } + }; + }); + lemur.service('MomentService', function () { this.diffMoment = function (start, end) { if (end !== 'None') { diff --git a/lemur/static/app/angular/certificates/certificate/certificate.js b/lemur/static/app/angular/certificates/certificate/certificate.js index 7cf8ca00..fdd773fd 100644 --- a/lemur/static/app/angular/certificates/certificate/certificate.js +++ b/lemur/static/app/angular/certificates/certificate/certificate.js @@ -328,4 +328,36 @@ angular.module('lemur') $scope.authorityService = AuthorityService; $scope.destinationService = DestinationService; $scope.notificationService = NotificationService; +}) + +.controller('CertificateRevokeController', function ($scope, $uibModalInstance, CertificateApi, CertificateService, LemurRestangular, NotificationService, toaster, revokeId) { + CertificateApi.get(revokeId).then(function (certificate) { + $scope.certificate = certificate; + }); + + $scope.cancel = function () { + $uibModalInstance.dismiss('cancel'); + }; + + $scope.revoke = function (certificate) { + CertificateService.revoke(certificate).then( + function () { + toaster.pop({ + type: 'success', + title: certificate.name, + body: 'Successfully revoked!' + }); + $uibModalInstance.close(); + }, + function (response) { + toaster.pop({ + type: 'error', + title: certificate.name, + body: 'lemur-bad-request', + bodyOutputType: 'directive', + directiveData: response.data, + timeout: 100000 + }); + }); + }; }); diff --git a/lemur/static/app/angular/certificates/certificate/revoke.tpl.html b/lemur/static/app/angular/certificates/certificate/revoke.tpl.html new file mode 100644 index 00000000..d91c7989 --- /dev/null +++ b/lemur/static/app/angular/certificates/certificate/revoke.tpl.html @@ -0,0 +1,48 @@ + + + diff --git a/lemur/static/app/angular/certificates/services.js b/lemur/static/app/angular/certificates/services.js index 8efacf42..13b0a524 100644 --- a/lemur/static/app/angular/certificates/services.js +++ b/lemur/static/app/angular/certificates/services.js @@ -258,5 +258,9 @@ angular.module('lemur') return certificate.customPOST(certificate.exportOptions, 'export'); }; + CertificateService.revoke = function (certificate) { + return certificate.customPUT(certificate.externalId, 'revoke'); + }; + return CertificateService; }); diff --git a/lemur/static/app/angular/certificates/view/view.js b/lemur/static/app/angular/certificates/view/view.js index cc817d2f..0008dd64 100644 --- a/lemur/static/app/angular/certificates/view/view.js +++ b/lemur/static/app/angular/certificates/view/view.js @@ -190,4 +190,19 @@ angular.module('lemur') } }); }; + + $scope.revoke = function (certificateId) { + $uibModal.open({ + animation: true, + controller: 'CertificateRevokeController', + templateUrl: '/angular/certificates/certificate/revoke.tpl.html', + size: 'lg', + backdrop: 'static', + resolve: { + revokeId: function () { + return certificateId; + } + } + }); + }; }); diff --git a/lemur/static/app/angular/certificates/view/view.tpl.html b/lemur/static/app/angular/certificates/view/view.tpl.html index c23dd781..5c1e73ca 100644 --- a/lemur/static/app/angular/certificates/view/view.tpl.html +++ b/lemur/static/app/angular/certificates/view/view.tpl.html @@ -60,6 +60,7 @@
  • Edit
  • Clone
  • Export
  • +
  • Revoke
  • @@ -188,27 +189,21 @@ Chain - +
    {{ certificate.chain }}
    Public Certificate - +
    {{ certificate.body }}
    Private Key - +
    {{ certificate.privateKey }}
    diff --git a/lemur/tests/plugins/issuer_plugin.py b/lemur/tests/plugins/issuer_plugin.py index 72ab55c4..c131de2e 100644 --- a/lemur/tests/plugins/issuer_plugin.py +++ b/lemur/tests/plugins/issuer_plugin.py @@ -15,7 +15,7 @@ class TestIssuerPlugin(IssuerPlugin): super(TestIssuerPlugin, self).__init__(*args, **kwargs) def create_certificate(self, csr, issuer_options): - return INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR + return INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR, None @staticmethod def create_authority(options): diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 0c7a7f0a..a33f4b07 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -430,7 +430,7 @@ def test_get_account_number(client): def test_mint_certificate(issuer_plugin, authority): from lemur.certificates.service import mint - cert_body, private_key, chain = mint(authority=authority, csr=CSR_STR) + cert_body, private_key, chain, external_id = mint(authority=authority, csr=CSR_STR) assert cert_body == INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR diff --git a/setup.py b/setup.py index 283b9445..e8c1fe0b 100644 --- a/setup.py +++ b/setup.py @@ -65,8 +65,9 @@ install_requires = [ 'pem==17.1.0', 'raven[flask]==6.2.1', 'jinja2==2.9.6', - 'pyldap==2.4.37', # required by ldap auth provider - 'paramiko==2.3.1' # required for lemur_linuxdst plugin + 'pyldap==2.4.37', # required by ldap auth provider + 'paramiko==2.3.1', # required for lemur_linuxdst plugin + 'alembic-autogenerate-enums==0.0.2' ] tests_require = [