diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 3eaba746..34305cc2 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -227,6 +227,10 @@ class Certificate(db.Model): def location(self): return defaults.location(self.parsed_cert) + @property + def distinguished_name(self): + return self.parsed_cert.subject.rfc4514_string() + @property def key_type(self): if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey): diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 6b457086..946bd541 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -206,6 +206,7 @@ class CertificateOutputSchema(LemurOutputSchema): cn = fields.String() common_name = fields.String(attribute='cn') + distinguished_name = fields.String() not_after = fields.DateTime() validity_end = ArrowDateTime(attribute='not_after') diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 54c60924..948c44d6 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -6,6 +6,7 @@ .. moduleauthor:: Kevin Glisson """ import base64 +import arrow from builtins import str from flask import Blueprint, make_response, jsonify, g @@ -660,6 +661,51 @@ class Certificates(AuthenticatedResource): log_service.create(g.current_user, 'update_cert', certificate=cert) return cert + def delete(self, certificate_id, data=None): + """ + .. http:delete:: /certificates/1 + + Delete a certificate + + **Example request**: + + .. sourcecode:: http + + DELETE /certificates/1 HTTP/1.1 + Host: example.com + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + + :reqheader Authorization: OAuth token to authenticate + :statuscode 204: no error + :statuscode 403: unauthenticated + :statusoode 404: certificate not found + + """ + 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 delete this certificate'), 403 + + if arrow.get(cert.not_after) > arrow.utcnow(): + return dict(message='Certificate is still valid, only expired certificates can be deleted'), 412 + + service.update(certificate_id, deleted=True) + log_service.create(g.current_user, 'delete_cert', certificate=cert) + return '', 204 + class NotificationCertificatesList(AuthenticatedResource): """ Defines the 'certificates' endpoint """ diff --git a/lemur/logs/models.py b/lemur/logs/models.py index d4239e59..9f982c24 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', 'revoke_cert', name='log_type'), nullable=False) + log_type = Column(Enum('key_view', 'create_cert', 'update_cert', 'revoke_cert', 'delete_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/manage.py b/lemur/manage.py index b972e8a5..184b9aa6 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -273,10 +273,11 @@ class CreateUser(Command): Option('-u', '--username', dest='username', required=True), Option('-e', '--email', dest='email', required=True), Option('-a', '--active', dest='active', default=True), - Option('-r', '--roles', dest='roles', action='append', default=[]) + Option('-r', '--roles', dest='roles', action='append', default=[]), + Option('-p', '--password', dest='password', default=None) ) - def run(self, username, email, active, roles): + def run(self, username, email, active, roles, password): role_objs = [] for r in roles: role_obj = role_service.get_by_name(r) @@ -286,14 +287,16 @@ class CreateUser(Command): sys.stderr.write("[!] Cannot find role {0}\n".format(r)) sys.exit(1) - password1 = prompt_pass("Password") - password2 = prompt_pass("Confirm Password") + if not password: + password1 = prompt_pass("Password") + password2 = prompt_pass("Confirm Password") + password = password1 - if password1 != password2: - sys.stderr.write("[!] Passwords do not match!\n") - sys.exit(1) + if password1 != password2: + sys.stderr.write("[!] Passwords do not match!\n") + sys.exit(1) - user_service.create(username, password1, email, active, None, role_objs) + user_service.create(username, password, email, active, None, role_objs) sys.stdout.write("[+] Created new user: {0}\n".format(username)) diff --git a/lemur/migrations/versions/9f79024fe67b_.py b/lemur/migrations/versions/9f79024fe67b_.py new file mode 100644 index 00000000..ad22d5f3 --- /dev/null +++ b/lemur/migrations/versions/9f79024fe67b_.py @@ -0,0 +1,22 @@ +""" Add delete_cert to log_type enum + +Revision ID: 9f79024fe67b +Revises: ee827d1e1974 +Create Date: 2019-01-03 15:36:59.181911 + +""" + +# revision identifiers, used by Alembic. +revision = '9f79024fe67b' +down_revision = 'ee827d1e1974' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.sync_enum_values('public', 'log_type', ['create_cert', 'key_view', 'revoke_cert', 'update_cert'], ['create_cert', 'delete_cert', 'key_view', 'revoke_cert', 'update_cert']) + + +def downgrade(): + op.sync_enum_values('public', 'log_type', ['create_cert', 'delete_cert', 'key_view', 'revoke_cert', 'update_cert'], ['create_cert', 'key_view', 'revoke_cert', 'update_cert']) diff --git a/lemur/plugins/lemur_acme/dyn.py b/lemur/plugins/lemur_acme/dyn.py index 9bab3a65..5d419f7f 100644 --- a/lemur/plugins/lemur_acme/dyn.py +++ b/lemur/plugins/lemur_acme/dyn.py @@ -5,7 +5,7 @@ import dns.exception import dns.name import dns.query import dns.resolver -from dyn.tm.errors import DynectCreateError +from dyn.tm.errors import DynectCreateError, DynectGetError from dyn.tm.session import DynectSession from dyn.tm.zones import Node, Zone, get_all_zones from flask import current_app @@ -119,7 +119,11 @@ def delete_txt_record(change_id, account_number, domain, token): zone = Zone(zone_name) node = Node(zone_name, fqdn) - all_txt_records = node.get_all_records_by_type('TXT') + try: + all_txt_records = node.get_all_records_by_type('TXT') + except DynectGetError: + # No Text Records remain or host is not in the zone anymore because all records have been deleted. + return for txt_record in all_txt_records: if txt_record.txtdata == ("{}".format(token)): current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn)) diff --git a/lemur/plugins/lemur_cfssl/plugin.py b/lemur/plugins/lemur_cfssl/plugin.py index 030f290a..4bfefc85 100644 --- a/lemur/plugins/lemur_cfssl/plugin.py +++ b/lemur/plugins/lemur_cfssl/plugin.py @@ -10,6 +10,9 @@ import json import requests +import base64 +import hmac +import hashlib from flask import current_app @@ -48,6 +51,21 @@ class CfsslIssuerPlugin(IssuerPlugin): data = {'certificate_request': csr} data = json.dumps(data) + try: + hex_key = current_app.config.get('CFSSL_KEY') + key = bytes.fromhex(hex_key) + except (ValueError, NameError): + # unable to find CFSSL_KEY in config, continue using normal sign method + pass + else: + data = data.encode() + + token = base64.b64encode(hmac.new(key, data, digestmod=hashlib.sha256).digest()) + data = base64.b64encode(data) + + data = json.dumps({'token': token.decode('utf-8'), 'request': data.decode('utf-8')}) + + url = "{0}{1}".format(current_app.config.get('CFSSL_URL'), '/api/v1/cfssl/authsign') response = self.session.post(url, data=data.encode(encoding='utf_8', errors='strict')) if response.status_code > 399: metrics.send('cfssl_create_certificate_failure', 'counter', 1) diff --git a/lemur/static/app/angular/certificates/view/view.tpl.html b/lemur/static/app/angular/certificates/view/view.tpl.html index ba17ffa6..28b4e08e 100644 --- a/lemur/static/app/angular/certificates/view/view.tpl.html +++ b/lemur/static/app/angular/certificates/view/view.tpl.html @@ -83,6 +83,8 @@
+
Distinguished Name
+
{{ certificate.distinguishedName }}
Certificate Authority
{{ certificate.authority ? certificate.authority.name : "Imported" }} ({{ certificate.issuer }})
Serial
diff --git a/lemur/tests/conftest.py b/lemur/tests/conftest.py index 9a48eb94..3f5fa2d8 100644 --- a/lemur/tests/conftest.py +++ b/lemur/tests/conftest.py @@ -15,7 +15,8 @@ from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \ CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \ - RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, CryptoAuthorityFactory + RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, InvalidCertificateFactory, \ + CryptoAuthorityFactory def pytest_runtest_setup(item): @@ -168,6 +169,15 @@ def pending_certificate(session): return p +@pytest.fixture +def invalid_certificate(session): + u = UserFactory() + a = AsyncAuthorityFactory() + i = InvalidCertificateFactory(user=u, authority=a) + session.commit() + return i + + @pytest.fixture def admin_user(session): u = UserFactory() diff --git a/lemur/tests/factories.py b/lemur/tests/factories.py index 3717c64d..a4af3d43 100644 --- a/lemur/tests/factories.py +++ b/lemur/tests/factories.py @@ -20,7 +20,7 @@ from lemur.policies.models import RotationPolicy from lemur.api_keys.models import ApiKey from .vectors import SAN_CERT_STR, SAN_CERT_KEY, CSR_STR, INTERMEDIATE_CERT_STR, ROOTCA_CERT_STR, INTERMEDIATE_KEY, \ - WILDCARD_CERT_KEY + WILDCARD_CERT_KEY, INVALID_CERT_STR class BaseFactory(SQLAlchemyModelFactory): @@ -137,6 +137,11 @@ class CACertificateFactory(CertificateFactory): private_key = INTERMEDIATE_KEY +class InvalidCertificateFactory(CertificateFactory): + body = INVALID_CERT_STR + private_key = '' + + class AuthorityFactory(BaseFactory): """Authority factory.""" name = Sequence(lambda n: 'authority{0}'.format(n)) diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index a1df1c0d..3ee7f84e 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -619,6 +619,12 @@ def test_certificate_get_body(client): response_body = client.get(api.url_for(Certificates, certificate_id=1), headers=VALID_USER_HEADER_TOKEN).json assert response_body['serial'] == '211983098819107449768450703123665283596' assert response_body['serialHex'] == '9F7A75B39DAE4C3F9524C68B06DA6A0C' + assert response_body['distinguishedName'] == ('CN=LemurTrust Unittests Class 1 CA 2018,' + 'O=LemurTrust Enterprises Ltd,' + 'OU=Unittesting Operations Center,' + 'C=EE,' + 'ST=N/A,' + 'L=Earth') @pytest.mark.parametrize("token,status", [ @@ -647,15 +653,26 @@ def test_certificate_put_with_data(client, certificate, issuer_plugin): @pytest.mark.parametrize("token,status", [ - (VALID_USER_HEADER_TOKEN, 405), - (VALID_ADMIN_HEADER_TOKEN, 405), - (VALID_ADMIN_API_TOKEN, 405), - ('', 405) + (VALID_USER_HEADER_TOKEN, 403), + (VALID_ADMIN_HEADER_TOKEN, 412), + (VALID_ADMIN_API_TOKEN, 412), + ('', 401) ]) def test_certificate_delete(client, token, status): assert client.delete(api.url_for(Certificates, certificate_id=1), headers=token).status_code == status +@pytest.mark.parametrize("token,status", [ + (VALID_USER_HEADER_TOKEN, 403), + (VALID_ADMIN_HEADER_TOKEN, 204), + (VALID_ADMIN_API_TOKEN, 204), + ('', 401) +]) +def test_invalid_certificate_delete(client, invalid_certificate, token, status): + assert client.delete( + api.url_for(Certificates, certificate_id=invalid_certificate.id), headers=token).status_code == status + + @pytest.mark.parametrize("token,status", [ (VALID_USER_HEADER_TOKEN, 405), (VALID_ADMIN_HEADER_TOKEN, 405), diff --git a/requirements-dev.txt b/requirements-dev.txt index 21156588..ac35f3e9 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -4,14 +4,14 @@ # # pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in # -aspy.yaml==1.1.1 # via pre-commit +aspy.yaml==1.1.2 # via pre-commit bleach==3.1.0 # via readme-renderer certifi==2018.11.29 # via requests cfgv==1.4.0 # via pre-commit chardet==3.0.4 # via requests docutils==0.14 # via readme-renderer flake8==3.5.0 -identify==1.1.8 # via pre-commit +identify==1.2.1 # via pre-commit idna==2.8 # via requests importlib-metadata==0.8 # via pre-commit importlib-resources==1.0.2 # via pre-commit @@ -19,19 +19,19 @@ invoke==1.2.0 mccabe==0.6.1 # via flake8 nodeenv==1.3.3 pkginfo==1.5.0.1 # via twine -pre-commit==1.14.0 +pre-commit==1.14.2 pycodestyle==2.3.1 # via flake8 pyflakes==1.6.0 # via flake8 pygments==2.3.1 # via readme-renderer pyyaml==3.13 # via aspy.yaml, pre-commit readme-renderer==24.0 # via twine -requests-toolbelt==0.8.0 # via twine +requests-toolbelt==0.9.0 # via twine requests==2.21.0 # via requests-toolbelt, twine six==1.12.0 # via bleach, cfgv, pre-commit, readme-renderer toml==0.10.0 # via pre-commit -tqdm==4.29.0 # via twine +tqdm==4.30.0 # via twine twine==1.12.1 urllib3==1.24.1 # via requests -virtualenv==16.2.0 # via pre-commit +virtualenv==16.3.0 # via pre-commit webencodings==0.5.1 # via bleach zipp==0.3.3 # via importlib-metadata diff --git a/requirements-docs.txt b/requirements-docs.txt index f3182456..15085766 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -4,28 +4,28 @@ # # pip-compile --no-index --output-file requirements-docs.txt requirements-docs.in # -acme==0.30.0 +acme==0.30.2 alabaster==0.7.12 # via sphinx alembic-autogenerate-enums==0.0.2 -alembic==1.0.5 -amqp==2.3.2 +alembic==1.0.7 +amqp==2.4.0 aniso8601==4.1.0 arrow==0.13.0 asn1crypto==0.24.0 asyncpool==1.0 babel==2.6.0 # via sphinx -bcrypt==3.1.5 +bcrypt==3.1.6 billiard==3.5.0.5 blinker==1.4 -boto3==1.9.76 -botocore==1.12.76 +boto3==1.9.86 +botocore==1.12.86 celery[redis]==4.2.1 certifi==2018.11.29 cffi==1.11.5 chardet==3.0.4 click==7.0 cloudflare==2.1.0 -cryptography==2.4.2 +cryptography==2.5 dnspython3==1.15.0 dnspython==1.15.0 docutils==0.14 @@ -54,22 +54,22 @@ lockfile==0.12.2 mako==1.0.7 markupsafe==1.1.0 marshmallow-sqlalchemy==0.15.0 -marshmallow==2.17.0 +marshmallow==2.18.0 mock==2.0.0 ndg-httpsclient==0.5.1 -packaging==18.0 # via sphinx +packaging==19.0 # via sphinx paramiko==2.4.2 pbr==5.1.1 pem==18.2.0 -psycopg2==2.7.6.1 -pyasn1-modules==0.2.3 +psycopg2==2.7.7 +pyasn1-modules==0.2.4 pyasn1==0.4.5 pycparser==2.19 pygments==2.3.1 # via sphinx pyjwt==1.7.1 pynacl==1.3.0 -pyopenssl==18.0.0 -pyparsing==2.3.0 # via packaging +pyopenssl==19.0.0 +pyparsing==2.3.1 # via packaging pyrfc3339==1.1 python-dateutil==2.7.5 python-editor==1.0.3 @@ -77,7 +77,7 @@ pytz==2018.9 pyyaml==3.13 raven[flask]==6.10.0 redis==2.10.6 -requests-toolbelt==0.8.0 +requests-toolbelt==0.9.0 requests[security]==2.21.0 retrying==1.3.3 s3transfer==0.1.13 @@ -87,9 +87,9 @@ sphinx-rtd-theme==0.4.2 sphinx==1.8.3 sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-websupport==1.1.0 # via sphinx -sqlalchemy-utils==0.33.10 -sqlalchemy==1.2.15 -tabulate==0.8.2 +sqlalchemy-utils==0.33.11 +sqlalchemy==1.2.17 +tabulate==0.8.3 urllib3==1.24.1 vine==1.2.0 werkzeug==0.14.1 diff --git a/requirements-tests.txt b/requirements-tests.txt index 490d74d1..c326e951 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,30 +8,30 @@ asn1crypto==0.24.0 # via cryptography atomicwrites==1.2.1 # via pytest attrs==18.2.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.9.76 # via moto +boto3==1.9.86 # via moto boto==2.49.0 # via moto -botocore==1.12.76 # via boto3, moto, s3transfer +botocore==1.12.86 # via boto3, moto, s3transfer certifi==2018.11.29 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests click==7.0 # via flask coverage==4.5.2 -cryptography==2.4.2 # via moto +cryptography==2.5 # via moto docker-pycreds==0.4.0 # via docker -docker==3.6.0 # via moto +docker==3.7.0 # via moto docutils==0.14 # via botocore ecdsa==0.13 # via python-jose factory-boy==2.11.1 -faker==1.0.1 +faker==1.0.2 flask==1.0.2 # via pytest-flask freezegun==0.3.11 future==0.17.1 # via python-jose -idna==2.8 # via cryptography, requests +idna==2.8 # via requests itsdangerous==1.1.0 # via flask jinja2==2.10 # via flask, moto jmespath==0.9.3 # via boto3, botocore jsondiff==1.1.1 # via moto -jsonpickle==1.0 # via aws-xray-sdk +jsonpickle==1.1 # via aws-xray-sdk markupsafe==1.1.0 # via jinja2 mock==2.0.0 # via moto more-itertools==5.0.0 # via pytest @@ -42,11 +42,11 @@ pluggy==0.8.1 # via pytest py==1.7.0 # via pytest pyaml==18.11.0 # via moto pycparser==2.19 # via cffi -pycryptodome==3.7.2 # via python-jose -pyflakes==2.0.0 +pycryptodome==3.7.3 # via python-jose +pyflakes==2.1.0 pytest-flask==0.14.0 pytest-mock==1.10.0 -pytest==4.1.0 +pytest==4.1.1 python-dateutil==2.7.5 # via botocore, faker, freezegun, moto python-jose==2.0.2 # via moto pytz==2018.9 # via moto @@ -60,5 +60,5 @@ text-unidecode==1.2 # via faker urllib3==1.24.1 # via botocore, requests websocket-client==0.54.0 # via docker werkzeug==0.14.1 # via flask, moto, pytest-flask -wrapt==1.11.0 # via aws-xray-sdk +wrapt==1.11.1 # via aws-xray-sdk xmltodict==0.11.0 # via moto diff --git a/requirements.txt b/requirements.txt index bc72db0a..c595e509 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,26 +4,26 @@ # # pip-compile --no-index --output-file requirements.txt requirements.in # -acme==0.30.0 +acme==0.30.2 alembic-autogenerate-enums==0.0.2 -alembic==1.0.5 # via flask-migrate -amqp==2.3.2 # via kombu +alembic==1.0.7 # via flask-migrate +amqp==2.4.0 # via kombu aniso8601==4.1.0 # via flask-restful arrow==0.13.0 asn1crypto==0.24.0 # via cryptography asyncpool==1.0 -bcrypt==3.1.5 # via flask-bcrypt, paramiko +bcrypt==3.1.6 # via flask-bcrypt, paramiko billiard==3.5.0.5 # via celery blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.9.76 -botocore==1.12.76 +boto3==1.9.86 +botocore==1.12.86 celery[redis]==4.2.1 certifi==2018.11.29 cffi==1.11.5 # via bcrypt, cryptography, pynacl chardet==3.0.4 # via requests click==7.0 # via flask cloudflare==2.1.0 -cryptography==2.4.2 +cryptography==2.5 dnspython3==1.15.0 dnspython==1.15.0 # via dnspython3 docutils==0.14 # via botocore @@ -39,7 +39,7 @@ flask-sqlalchemy==2.3.2 flask==1.0.2 future==0.17.1 gunicorn==19.9.0 -idna==2.8 # via cryptography, requests +idna==2.8 # via requests inflection==0.3.1 itsdangerous==1.1.0 # via flask jinja2==2.10 @@ -51,19 +51,19 @@ lockfile==0.12.2 mako==1.0.7 # via alembic markupsafe==1.1.0 # via jinja2, mako marshmallow-sqlalchemy==0.15.0 -marshmallow==2.17.0 +marshmallow==2.18.0 mock==2.0.0 # via acme ndg-httpsclient==0.5.1 paramiko==2.4.2 pbr==5.1.1 # via mock pem==18.2.0 -psycopg2==2.7.6.1 -pyasn1-modules==0.2.3 # via python-ldap +psycopg2==2.7.7 +pyasn1-modules==0.2.4 # via python-ldap pyasn1==0.4.5 # via ndg-httpsclient, paramiko, pyasn1-modules, python-ldap pycparser==2.19 # via cffi pyjwt==1.7.1 pynacl==1.3.0 # via paramiko -pyopenssl==18.0.0 +pyopenssl==19.0.0 pyrfc3339==1.1 # via acme python-dateutil==2.7.5 # via alembic, arrow, botocore python-editor==1.0.3 # via alembic @@ -72,14 +72,14 @@ pytz==2018.9 # via acme, celery, flask-restful, pyrfc3339 pyyaml==3.13 # via cloudflare raven[flask]==6.10.0 redis==2.10.6 -requests-toolbelt==0.8.0 # via acme +requests-toolbelt==0.9.0 # via acme requests[security]==2.21.0 retrying==1.3.3 s3transfer==0.1.13 # via boto3 six==1.12.0 -sqlalchemy-utils==0.33.10 -sqlalchemy==1.2.15 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils -tabulate==0.8.2 +sqlalchemy-utils==0.33.11 +sqlalchemy==1.2.17 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils +tabulate==0.8.3 urllib3==1.24.1 # via botocore, requests vine==1.2.0 # via amqp werkzeug==0.14.1 # via flask