From f6a130b09d4b74d5427af46eef1c10e6a232b190 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Mon, 10 Sep 2018 09:13:31 -0700 Subject: [PATCH 1/5] Add more logging to messaging --- lemur/notifications/messaging.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/lemur/notifications/messaging.py b/lemur/notifications/messaging.py index cd4ff0f1..ddf8d875 100644 --- a/lemur/notifications/messaging.py +++ b/lemur/notifications/messaging.py @@ -165,6 +165,7 @@ def send_rotation_notification(certificate, notification_plugin=None): notification_plugin.send('rotation', data, [data['owner']]) status = SUCCESS_METRIC_STATUS except Exception as e: + current_app.logger.error('Unable to send notification to {}.'.format(data['owner']), exc_info=True) sentry.captureException() metrics.send('notification', 'counter', 1, metric_tags={'status': status, 'event_type': 'rotation'}) @@ -196,6 +197,8 @@ def send_pending_failure_notification(pending_cert, notify_owner=True, notify_se notification_plugin.send('failed', data, [data['owner']], pending_cert) status = SUCCESS_METRIC_STATUS except Exception as e: + current_app.logger.error('Unable to send pending failure notification to {}.'.format(data['owner']), + exc_info=True) sentry.captureException() if notify_security: @@ -203,6 +206,9 @@ def send_pending_failure_notification(pending_cert, notify_owner=True, notify_se notification_plugin.send('failed', data, data["security_email"], pending_cert) status = SUCCESS_METRIC_STATUS except Exception as e: + current_app.logger.error('Unable to send pending failure notification to ' + '{}.'.format(data['security_email']), + exc_info=True) sentry.captureException() metrics.send('notification', 'counter', 1, metric_tags={'status': status, 'event_type': 'rotation'}) From 7d42e4ce6770d857e17818b85c3620adc1746383 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Mon, 10 Sep 2018 10:34:47 -0700 Subject: [PATCH 2/5] Fix certificate import issues --- lemur/certificates/schemas.py | 3 ++- lemur/certificates/service.py | 2 +- lemur/common/defaults.py | 4 ++-- lemur/tests/test_certificates.py | 10 +++++----- lemur/tests/test_defaults.py | 2 +- lemur/tests/test_pending_certificates.py | 2 +- 6 files changed, 12 insertions(+), 11 deletions(-) diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index e62951bf..032a9175 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -235,8 +235,9 @@ class CertificateOutputSchema(LemurOutputSchema): class CertificateUploadInputSchema(CertificateCreationSchema): name = fields.String() + authority = fields.Nested(AssociatedAuthoritySchema, required=False) notify = fields.Boolean(missing=True) - + external_id = fields.String(missing=None, allow_none=True) private_key = fields.String(validate=validators.private_key) body = fields.String(required=True, validate=validators.public_certificate) chain = fields.String(validate=validators.public_certificate, missing=None, diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index a02c412c..13c0f8bf 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -233,7 +233,7 @@ def upload(**kwargs): kwargs['private_key'] = private_key.encode('utf-8') cert = Certificate(**kwargs) - + cert.authority = kwargs.get('authority') cert = database.create(cert) kwargs['creator'].certificates.append(cert) diff --git a/lemur/common/defaults.py b/lemur/common/defaults.py index 80079232..e9bbc6e6 100644 --- a/lemur/common/defaults.py +++ b/lemur/common/defaults.py @@ -232,8 +232,8 @@ def issuer(cert): delchars = ''.join(c for c in map(chr, range(256)) if not c.isalnum()) try: # Try organization name or fall back to CN - issuer = (cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME) - or cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME)) + issuer = (cert.issuer.get_attributes_for_oid(x509.OID_COMMON_NAME) or + cert.issuer.get_attributes_for_oid(x509.OID_ORGANIZATION_NAME)) issuer = str(issuer[0].value) for c in delchars: issuer = issuer.replace(c, "") diff --git a/lemur/tests/test_certificates.py b/lemur/tests/test_certificates.py index 103a0968..1a4d644b 100644 --- a/lemur/tests/test_certificates.py +++ b/lemur/tests/test_certificates.py @@ -61,7 +61,7 @@ def test_certificate_output_schema(session, certificate, issuer_plugin): # Make sure serialization parses the cert only once (uses cached 'parsed_cert' attribute) with patch('lemur.common.utils.parse_certificate', side_effect=utils.parse_certificate) as wrapper: data, errors = CertificateOutputSchema().dump(certificate) - assert data['issuer'] == 'LemurTrustEnterprisesLtd' + assert data['issuer'] == 'LemurTrustUnittestsClass1CA2018' assert wrapper.call_count == 1 @@ -455,8 +455,8 @@ def test_create_certificate(issuer_plugin, authority, user): cert = create(authority=authority, csr=CSR_STR, owner='joe@example.com', creator=user['user']) assert str(cert.not_after) == '2047-12-31T22:00:00+00:00' assert str(cert.not_before) == '2017-12-31T22:00:00+00:00' - assert cert.issuer == 'LemurTrustEnterprisesLtd' - assert cert.name == 'SAN-san.example.org-LemurTrustEnterprisesLtd-20171231-20471231-AFF2DB4F8D2D4D8E80FA382AE27C2333' + assert cert.issuer == 'LemurTrustUnittestsClass1CA2018' + assert cert.name == 'SAN-san.example.org-LemurTrustUnittestsClass1CA2018-20171231-20471231-AFF2DB4F8D2D4D8E80FA382AE27C2333' cert = create(authority=authority, csr=CSR_STR, owner='joe@example.com', name='ACustomName1', creator=user['user']) assert cert.name == 'ACustomName1' @@ -486,8 +486,8 @@ def test_import(user): cert = import_certificate(body=SAN_CERT_STR, chain=INTERMEDIATE_CERT_STR, private_key=SAN_CERT_KEY, creator=user['user']) assert str(cert.not_after) == '2047-12-31T22:00:00+00:00' assert str(cert.not_before) == '2017-12-31T22:00:00+00:00' - assert cert.issuer == 'LemurTrustEnterprisesLtd' - assert cert.name == 'SAN-san.example.org-LemurTrustEnterprisesLtd-20171231-20471231-AFF2DB4F8D2D4D8E80FA382AE27C2333-2' + assert cert.issuer == 'LemurTrustUnittestsClass1CA2018' + assert cert.name == 'SAN-san.example.org-LemurTrustUnittestsClass1CA2018-20171231-20471231-AFF2DB4F8D2D4D8E80FA382AE27C2333-2' cert = import_certificate(body=SAN_CERT_STR, chain=INTERMEDIATE_CERT_STR, private_key=SAN_CERT_KEY, owner='joe@example.com', name='ACustomName2', creator=user['user']) assert cert.name == 'ACustomName2' diff --git a/lemur/tests/test_defaults.py b/lemur/tests/test_defaults.py index c6c70684..918e1ab8 100644 --- a/lemur/tests/test_defaults.py +++ b/lemur/tests/test_defaults.py @@ -35,7 +35,7 @@ def test_cert_bitstrength(client): def test_cert_issuer(client): from lemur.common.defaults import issuer - assert issuer(INTERMEDIATE_CERT) == 'LemurTrustEnterprisesLtd' + assert issuer(INTERMEDIATE_CERT) == 'LemurTrustUnittestsRootCA2018' def test_text_to_slug(client): diff --git a/lemur/tests/test_pending_certificates.py b/lemur/tests/test_pending_certificates.py index 455a4068..567159e1 100644 --- a/lemur/tests/test_pending_certificates.py +++ b/lemur/tests/test_pending_certificates.py @@ -26,7 +26,7 @@ def test_create_pending(pending_certificate, user, session): from lemur.pending_certificates.service import create_certificate, get cert = {'body': WILDCARD_CERT_STR, 'chain': INTERMEDIATE_CERT_STR, - 'external_id': 54321} + 'external_id': '54321'} # Weird copy because the session behavior. pending_certificate is a valid object but the # return of vars(pending_certificate) is a sessionobject, and so nothing from the pending_cert From 23382b27775335264f9fd29ccfb1a1a31fbfb396 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Thu, 13 Sep 2018 10:35:54 -0700 Subject: [PATCH 3/5] Celery integration --- lemur/certificates/service.py | 46 ++++++------- lemur/common/celery.py | 119 ++++++++++++++++++++++++++++++++++ requirements-dev.txt | 10 +-- requirements-docs.txt | 18 +++-- requirements-tests.in | 2 +- requirements-tests.txt | 16 ++--- requirements.in | 4 +- requirements.txt | 14 ++-- 8 files changed, 181 insertions(+), 48 deletions(-) create mode 100644 lemur/common/celery.py diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 13c0f8bf..57592d56 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -5,32 +5,27 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ -import arrow - -from flask import current_app +from cryptography import x509 from sqlalchemy import func, or_, not_, cast, Integer -from cryptography import x509 +import arrow from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization +from flask import current_app from lemur import database -from lemur.extensions import metrics, sentry, signals -from lemur.plugins.base import plugins -from lemur.common.utils import generate_private_key, truthiness - -from lemur.roles.models import Role -from lemur.domains.models import Domain from lemur.authorities.models import Authority -from lemur.destinations.models import Destination from lemur.certificates.models import Certificate +from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema +from lemur.common.utils import generate_private_key, truthiness +from lemur.destinations.models import Destination +from lemur.domains.models import Domain +from lemur.extensions import metrics, sentry, signals from lemur.notifications.models import Notification from lemur.pending_certificates.models import PendingCertificate - -from lemur.certificates.schemas import CertificateOutputSchema, CertificateInputSchema - +from lemur.plugins.base import plugins from lemur.roles import service as role_service - +from lemur.roles.models import Role csr_created = signals.signal('csr_created', "CSR generated") csr_imported = signals.signal('csr_imported', "CSR imported from external source") @@ -95,8 +90,8 @@ def get_all_pending_cleaning(source): :param source: :return: """ - return Certificate.query.filter(Certificate.sources.any(id=source.id))\ - .filter(not_(Certificate.endpoints.any())).all() + return Certificate.query.filter(Certificate.sources.any(id=source.id)) \ + .filter(not_(Certificate.endpoints.any())).all() def get_all_pending_reissue(): @@ -109,8 +104,8 @@ def get_all_pending_reissue(): :return: """ - return Certificate.query.filter(Certificate.rotation == True)\ - .filter(not_(Certificate.replaced.any()))\ + return Certificate.query.filter(Certificate.rotation == True) \ + .filter(not_(Certificate.replaced.any())) \ .filter(Certificate.in_rotation_window == True).all() # noqa @@ -280,6 +275,11 @@ def create(**kwargs): if isinstance(cert, Certificate): certificate_issued.send(certificate=cert, authority=cert.authority) metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer)) + + if isinstance(cert, PendingCertificate) and cert.authority.plugin_name == 'acme-issuer': + # Call Celery to create acme-issuer (LetsEncrypt) certificates + from lemur.common.celery import fetch_acme_cert + fetch_acme_cert.delay(cert.id) return cert @@ -310,8 +310,8 @@ def render(args): if 'issuer' in terms: # we can't rely on issuer being correct in the cert directly so we combine queries - sub_query = database.session_query(Authority.id)\ - .filter(Authority.name.ilike(term))\ + sub_query = database.session_query(Authority.id) \ + .filter(Authority.name.ilike(term)) \ .subquery() query = query.filter( @@ -450,8 +450,8 @@ def stats(**kwargs): if kwargs.get('metric') == 'not_after': start = arrow.utcnow() end = start.replace(weeks=+32) - items = database.db.session.query(Certificate.issuer, func.count(Certificate.id))\ - .group_by(Certificate.issuer)\ + items = database.db.session.query(Certificate.issuer, func.count(Certificate.id)) \ + .group_by(Certificate.issuer) \ .filter(Certificate.not_after <= end.format('YYYY-MM-DD')) \ .filter(Certificate.not_after >= start.format('YYYY-MM-DD')).all() diff --git a/lemur/common/celery.py b/lemur/common/celery.py new file mode 100644 index 00000000..fc19df32 --- /dev/null +++ b/lemur/common/celery.py @@ -0,0 +1,119 @@ +""" +This module controls defines celery tasks and their applicable schedules. The celery beat server and workers will start +when invoked. + +When ran in development mode (LEMUR_CONFIG= 4: + error_log["message"] = "Deleting pending certificate" + send_pending_failure_notification(pending_cert, notify_owner=pending_cert.notify) + pending_certificate_service.delete(pending_certificate_service.cancel(pending_cert)) + else: + pending_certificate_service.increment_attempt(pending_cert) + pending_certificate_service.update( + cert.get("pending_cert").id, + status=str(cert.get("last_error")) + ) + # Add failed pending cert task back to queue + fetch_acme_cert.delay(id) + current_app.logger.error(error_log) + log_data["message"] = "Complete" + log_data["new"] = new + log_data["failed"] = failed + log_data["wrong_issuer"] = wrong_issuer + current_app.logger.debug(log_data) + print( + "[+] Certificates: New: {new} Failed: {failed} Not using ACME: {wrong_issuer}".format( + new=new, + failed=failed, + wrong_issuer=wrong_issuer + ) + ) diff --git a/requirements-dev.txt b/requirements-dev.txt index 9b8728ae..8c6b8b2f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -5,26 +5,26 @@ # pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in # aspy.yaml==1.1.1 # via pre-commit -cached-property==1.4.3 # via pre-commit +cached-property==1.5.1 # via pre-commit certifi==2018.8.24 # via requests cfgv==1.1.0 # via pre-commit chardet==3.0.4 # via requests flake8==3.5.0 -identify==1.1.4 # via pre-commit +identify==1.1.5 # via pre-commit idna==2.7 # via requests invoke==1.1.1 mccabe==0.6.1 # via flake8 nodeenv==1.3.2 pkginfo==1.4.2 # via twine -pre-commit==1.10.5 +pre-commit==1.11.0 pycodestyle==2.3.1 # via flake8 pyflakes==1.6.0 # via flake8 pyyaml==3.13 # via aspy.yaml, pre-commit requests-toolbelt==0.8.0 # via twine requests==2.19.1 # via requests-toolbelt, twine six==1.11.0 # via cfgv, pre-commit -toml==0.9.4 # via pre-commit -tqdm==4.25.0 # via twine +toml==0.9.6 # via pre-commit +tqdm==4.26.0 # via twine twine==1.11.0 urllib3==1.23 # via requests virtualenv==16.0.0 # via pre-commit diff --git a/requirements-docs.txt b/requirements-docs.txt index f3278b66..7e8d2d5e 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -4,19 +4,22 @@ # # pip-compile --no-index --output-file requirements-docs.txt requirements-docs.in # -acme==0.26.1 +acme==0.27.1 alabaster==0.7.11 # via sphinx alembic-autogenerate-enums==0.0.2 alembic==1.0.0 +amqp==2.3.2 aniso8601==3.0.2 arrow==0.12.1 asn1crypto==0.24.0 asyncpool==1.0 babel==2.6.0 # via sphinx bcrypt==3.1.4 +billiard==3.5.0.4 blinker==1.4 -boto3==1.8.1 -botocore==1.11.1 +boto3==1.7.79 +botocore==1.10.84 +celery[redis]==4.2.1 certifi==2018.8.24 cffi==1.11.5 chardet==3.0.4 @@ -39,13 +42,14 @@ flask==0.12 future==0.16.0 gunicorn==19.9.0 idna==2.7 -imagesize==1.0.0 # via sphinx +imagesize==1.1.0 # via sphinx inflection==0.3.1 itsdangerous==0.24 jinja2==2.10 jmespath==0.9.3 josepy==1.1.0 jsonlines==1.2.0 +kombu==4.2.1 lockfile==0.12.2 mako==1.0.7 markupsafe==1.0 @@ -72,6 +76,7 @@ python-editor==1.0.3 pytz==2018.5 pyyaml==3.13 raven[flask]==6.9.0 +redis==2.10.6 requests-toolbelt==0.8.0 requests[security]==2.19.1 retrying==1.3.3 @@ -79,12 +84,13 @@ s3transfer==0.1.13 six==1.11.0 snowballstemmer==1.2.1 # via sphinx sphinx-rtd-theme==0.4.1 -sphinx==1.7.7 +sphinx==1.8.0 sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-websupport==1.1.0 # via sphinx sqlalchemy-utils==0.33.3 -sqlalchemy==1.2.10 +sqlalchemy==1.2.11 tabulate==0.8.2 urllib3==1.23 +vine==1.1.4 werkzeug==0.14.1 xmltodict==0.11.0 diff --git a/requirements-tests.in b/requirements-tests.in index 02a2b0ae..efb4570a 100644 --- a/requirements-tests.in +++ b/requirements-tests.in @@ -4,7 +4,7 @@ coverage factory-boy Faker freezegun -moto +moto==1.3.4 # Issue with moto: https://github.com/spulec/moto/issues/1813 nose pyflakes pytest diff --git a/requirements-tests.txt b/requirements-tests.txt index a34bf1fd..1e67cbf3 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -5,12 +5,12 @@ # pip-compile --no-index --output-file requirements-tests.txt requirements-tests.in # asn1crypto==0.24.0 # via cryptography -atomicwrites==1.2.0 # via pytest -attrs==18.1.0 # via pytest +atomicwrites==1.2.1 # via pytest +attrs==18.2.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.8.1 # via moto +boto3==1.9.3 # via moto boto==2.49.0 # via moto -botocore==1.11.1 # via boto3, moto, s3transfer +botocore==1.12.3 # via boto3, moto, s3transfer certifi==2018.8.24 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests @@ -40,14 +40,14 @@ moto==1.3.4 nose==1.3.7 pbr==4.2.0 # via mock pluggy==0.7.1 # via pytest -py==1.5.4 # via pytest +py==1.6.0 # via pytest pyaml==17.12.1 # via moto pycparser==2.18 # via cffi pycryptodome==3.6.6 # via python-jose pyflakes==2.0.0 -pytest-flask==0.10.0 +pytest-flask==0.12.0 pytest-mock==1.10.0 -pytest==3.7.3 +pytest==3.8.0 python-dateutil==2.7.3 # via botocore, faker, freezegun, moto python-jose==2.0.2 # via moto pytz==2018.5 # via moto @@ -59,7 +59,7 @@ s3transfer==0.1.13 # via boto3 six==1.11.0 # via cryptography, docker, docker-pycreds, faker, freezegun, mock, more-itertools, moto, pytest, python-dateutil, python-jose, requests-mock, responses, websocket-client text-unidecode==1.2 # via faker urllib3==1.23 # via botocore, requests -websocket-client==0.51.0 # via docker +websocket-client==0.53.0 # via docker werkzeug==0.14.1 # via flask, moto, pytest-flask wrapt==1.10.11 # via aws-xray-sdk xmltodict==0.11.0 # via moto diff --git a/requirements.in b/requirements.in index 9d77bee2..a2b920c3 100644 --- a/requirements.in +++ b/requirements.in @@ -4,7 +4,9 @@ acme alembic-autogenerate-enums arrow asyncpool -boto3==1.7.79 +boto3==1.7.79 # Issue with moto: https://github.com/spulec/moto/issues/1813 +botocore== 1.10.84 # Issue with moto: https://github.com/spulec/moto/issues/1813 +celery[redis] certifi CloudFlare cryptography diff --git a/requirements.txt b/requirements.txt index 6da0f82b..43ebdb63 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,17 +4,20 @@ # # pip-compile --no-index --output-file requirements.txt requirements.in # -acme==0.26.1 +acme==0.27.1 alembic-autogenerate-enums==0.0.2 alembic==1.0.0 # via flask-migrate +amqp==2.3.2 # via kombu aniso8601==3.0.2 # via flask-restful arrow==0.12.1 asn1crypto==0.24.0 # via cryptography asyncpool==1.0 bcrypt==3.1.4 # via flask-bcrypt, paramiko +billiard==3.5.0.4 # via celery blinker==1.4 # via flask-mail, flask-principal, raven boto3==1.7.79 -botocore==1.10.84 # via boto3, s3transfer +botocore==1.10.84 +celery[redis]==4.2.1 certifi==2018.8.24 cffi==1.11.5 # via bcrypt, cryptography, pynacl chardet==3.0.4 # via requests @@ -43,6 +46,7 @@ jinja2==2.10 jmespath==0.9.3 # via boto3, botocore josepy==1.1.0 # via acme jsonlines==1.2.0 # via cloudflare +kombu==4.2.1 # via celery lockfile==0.12.2 mako==1.0.7 # via alembic markupsafe==1.0 # via jinja2, mako @@ -64,17 +68,19 @@ pyrfc3339==1.1 # via acme python-dateutil==2.7.3 # via alembic, arrow, botocore python-editor==1.0.3 # via alembic python-ldap==3.1.0 -pytz==2018.5 # via acme, flask-restful, pyrfc3339 +pytz==2018.5 # via acme, celery, flask-restful, pyrfc3339 pyyaml==3.13 # via cloudflare raven[flask]==6.9.0 +redis==2.10.6 # via celery requests-toolbelt==0.8.0 # via acme requests[security]==2.19.1 retrying==1.3.3 s3transfer==0.1.13 # via boto3 six==1.11.0 -sqlalchemy-utils==0.33.3 +sqlalchemy-utils==0.33.4 sqlalchemy==1.2.11 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils tabulate==0.8.2 urllib3==1.23 # via requests +vine==1.1.4 # via amqp werkzeug==0.14.1 # via flask xmltodict==0.11.0 From 563f0fb9b2fd2f4fcfbb532a62c90e7992edc86d Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Mon, 17 Sep 2018 10:52:12 -0700 Subject: [PATCH 4/5] Celery refactoring, celery beat job in configuration --- lemur/certificates/service.py | 9 ++------ lemur/common/celery.py | 16 +++++++++++++++ lemur/migrations/versions/9392b9f9a805_.py | 24 ++++++++++++++++++++++ lemur/pending_certificates/models.py | 19 +++++++++-------- lemur/pending_certificates/schemas.py | 24 +++++++++++----------- requirements-dev.txt | 2 +- requirements-docs.txt | 2 +- requirements-tests.txt | 6 +++--- 8 files changed, 70 insertions(+), 32 deletions(-) create mode 100644 lemur/migrations/versions/9392b9f9a805_.py diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 57592d56..0bd50694 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -5,13 +5,12 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ -from cryptography import x509 -from sqlalchemy import func, or_, not_, cast, Integer - import arrow +from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives import hashes, serialization from flask import current_app +from sqlalchemy import func, or_, not_, cast, Integer from lemur import database from lemur.authorities.models import Authority @@ -276,10 +275,6 @@ def create(**kwargs): certificate_issued.send(certificate=cert, authority=cert.authority) metrics.send('certificate_issued', 'counter', 1, metric_tags=dict(owner=cert.owner, issuer=cert.issuer)) - if isinstance(cert, PendingCertificate) and cert.authority.plugin_name == 'acme-issuer': - # Call Celery to create acme-issuer (LetsEncrypt) certificates - from lemur.common.celery import fetch_acme_cert - fetch_acme_cert.delay(cert.id) return cert diff --git a/lemur/common/celery.py b/lemur/common/celery.py index fc19df32..1858b4b5 100644 --- a/lemur/common/celery.py +++ b/lemur/common/celery.py @@ -8,7 +8,9 @@ command: celery -A lemur.common.celery worker --loglevel=info -l DEBUG -B """ import copy +import datetime import sys +from datetime import timezone from celery import Celery from flask import current_app @@ -117,3 +119,17 @@ def fetch_acme_cert(id): wrong_issuer=wrong_issuer ) ) + + +@celery.task() +def fetch_all_pending_acme_certs(): + """Instantiate celery workers to resolve all pending Acme certificates""" + pending_certs = pending_certificate_service.get_pending_certs('all') + + # We only care about certs using the acme-issuer plugin + for cert in pending_certs: + cert_authority = get_authority(cert.authority_id) + if cert_authority.plugin_name == 'acme-issuer': + if cert.last_updated == cert.date_created or datetime.datetime.now( + timezone.utc) - cert.last_updated > datetime.timedelta(minutes=3): + fetch_acme_cert.delay(cert.id) diff --git a/lemur/migrations/versions/9392b9f9a805_.py b/lemur/migrations/versions/9392b9f9a805_.py new file mode 100644 index 00000000..d6ca734b --- /dev/null +++ b/lemur/migrations/versions/9392b9f9a805_.py @@ -0,0 +1,24 @@ +"""Add last_updated field to Pending Certs +Revision ID: 9392b9f9a805 +Revises: 5ae0ecefb01f +Create Date: 2018-09-17 08:33:37.087488 + +""" + +# revision identifiers, used by Alembic. +revision = '9392b9f9a805' +down_revision = '5ae0ecefb01f' + +from alembic import op +from sqlalchemy_utils import ArrowType +import sqlalchemy as sa + + +def upgrade(): + op.add_column('pending_certs', sa.Column('last_updated', ArrowType, server_default=sa.text('now()'), onupdate=sa.text('now()'), + nullable=False)) + + +def downgrade(): + op.drop_column('pending_certs', 'last_updated') + diff --git a/lemur/pending_certificates/models.py b/lemur/pending_certificates/models.py index bd516f67..a1f61fa1 100644 --- a/lemur/pending_certificates/models.py +++ b/lemur/pending_certificates/models.py @@ -5,19 +5,18 @@ """ from datetime import datetime as dt -from sqlalchemy.orm import relationship from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean -from sqlalchemy_utils.types.arrow import ArrowType +from sqlalchemy.orm import relationship from sqlalchemy_utils import JSONType +from sqlalchemy_utils.types.arrow import ArrowType from lemur.certificates.models import get_or_increase_name from lemur.common import defaults, utils from lemur.database import db -from lemur.utils import Vault - from lemur.models import pending_cert_source_associations, \ pending_cert_destination_associations, pending_cert_notification_associations, \ pending_cert_replacement_associations, pending_cert_role_associations +from lemur.utils import Vault class PendingCertificate(db.Model): @@ -40,6 +39,7 @@ class PendingCertificate(db.Model): dns_provider_id = Column(Integer, ForeignKey('dns_providers.id', ondelete="CASCADE")) status = Column(Text(), nullable=True) + last_updated = Column(ArrowType, PassiveDefault(func.now()), onupdate=func.now(), nullable=False) rotation = Column(Boolean, default=False) user_id = Column(Integer, ForeignKey('users.id')) @@ -47,9 +47,12 @@ class PendingCertificate(db.Model): root_authority_id = Column(Integer, ForeignKey('authorities.id', ondelete="CASCADE")) rotation_policy_id = Column(Integer, ForeignKey('rotation_policies.id')) - notifications = relationship('Notification', secondary=pending_cert_notification_associations, backref='pending_cert', passive_deletes=True) - destinations = relationship('Destination', secondary=pending_cert_destination_associations, backref='pending_cert', passive_deletes=True) - sources = relationship('Source', secondary=pending_cert_source_associations, backref='pending_cert', passive_deletes=True) + notifications = relationship('Notification', secondary=pending_cert_notification_associations, + backref='pending_cert', passive_deletes=True) + destinations = relationship('Destination', secondary=pending_cert_destination_associations, backref='pending_cert', + passive_deletes=True) + sources = relationship('Source', secondary=pending_cert_source_associations, backref='pending_cert', + passive_deletes=True) roles = relationship('Role', secondary=pending_cert_role_associations, backref='pending_cert', passive_deletes=True) replaces = relationship('Certificate', secondary=pending_cert_replacement_associations, @@ -77,7 +80,7 @@ class PendingCertificate(db.Model): # TODO: Fix auto-generated name, it should be renamed on creation self.name = get_or_increase_name( defaults.certificate_name(kwargs['common_name'], kwargs['authority'].name, - dt.now(), dt.now(), False), self.external_id) + dt.now(), dt.now(), False), self.external_id) self.rename = True self.cn = defaults.common_name(utils.parse_csr(self.csr)) diff --git a/lemur/pending_certificates/schemas.py b/lemur/pending_certificates/schemas.py index 35a4c18a..252cd892 100644 --- a/lemur/pending_certificates/schemas.py +++ b/lemur/pending_certificates/schemas.py @@ -1,5 +1,14 @@ from marshmallow import fields, post_load +from lemur.authorities.schemas import AuthorityNestedOutputSchema +from lemur.certificates.schemas import CertificateNestedOutputSchema +from lemur.common.schema import LemurInputSchema, LemurOutputSchema +from lemur.destinations.schemas import DestinationNestedOutputSchema +from lemur.domains.schemas import DomainNestedOutputSchema +from lemur.notifications import service as notification_service +from lemur.notifications.schemas import NotificationNestedOutputSchema +from lemur.policies.schemas import RotationPolicyNestedOutputSchema +from lemur.roles.schemas import RoleNestedOutputSchema from lemur.schemas import ( AssociatedCertificateSchema, AssociatedDestinationSchema, @@ -8,18 +17,7 @@ from lemur.schemas import ( EndpointNestedOutputSchema, ExtensionSchema ) - -from lemur.common.schema import LemurInputSchema, LemurOutputSchema from lemur.users.schemas import UserNestedOutputSchema -from lemur.authorities.schemas import AuthorityNestedOutputSchema -from lemur.certificates.schemas import CertificateNestedOutputSchema -from lemur.destinations.schemas import DestinationNestedOutputSchema -from lemur.domains.schemas import DomainNestedOutputSchema -from lemur.notifications.schemas import NotificationNestedOutputSchema -from lemur.roles.schemas import RoleNestedOutputSchema -from lemur.policies.schemas import RotationPolicyNestedOutputSchema - -from lemur.notifications import service as notification_service class PendingCertificateSchema(LemurInputSchema): @@ -38,6 +36,7 @@ class PendingCertificateOutputSchema(LemurOutputSchema): name = fields.String() number_attempts = fields.Integer() date_created = fields.Date() + last_updated = fields.Date() rotation = fields.Boolean() @@ -88,7 +87,8 @@ class PendingCertificateEditInputSchema(PendingCertificateSchema): """ if data['owner']: notification_name = "DEFAULT_{0}".format(data['owner'].split('@')[0].upper()) - data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, [data['owner']]) + data['notifications'] += notification_service.create_default_expiration_notifications(notification_name, + [data['owner']]) return data diff --git a/requirements-dev.txt b/requirements-dev.txt index 8c6b8b2f..b3c52456 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -12,7 +12,7 @@ chardet==3.0.4 # via requests flake8==3.5.0 identify==1.1.5 # via pre-commit idna==2.7 # via requests -invoke==1.1.1 +invoke==1.2.0 mccabe==0.6.1 # via flake8 nodeenv==1.3.2 pkginfo==1.4.2 # via twine diff --git a/requirements-docs.txt b/requirements-docs.txt index 7e8d2d5e..e00030c6 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -87,7 +87,7 @@ sphinx-rtd-theme==0.4.1 sphinx==1.8.0 sphinxcontrib-httpdomain==1.7.0 sphinxcontrib-websupport==1.1.0 # via sphinx -sqlalchemy-utils==0.33.3 +sqlalchemy-utils==0.33.4 sqlalchemy==1.2.11 tabulate==0.8.2 urllib3==1.23 diff --git a/requirements-tests.txt b/requirements-tests.txt index 1e67cbf3..2ed9ae2f 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -8,9 +8,9 @@ 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.3 # via moto +boto3==1.9.4 # via moto boto==2.49.0 # via moto -botocore==1.12.3 # via boto3, moto, s3transfer +botocore==1.12.4 # via boto3, moto, s3transfer certifi==2018.8.24 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests @@ -23,7 +23,7 @@ docker==3.5.0 # via moto docutils==0.14 # via botocore ecdsa==0.13 # via python-jose factory-boy==2.11.1 -faker==0.9.0 +faker==0.9.1 flask==1.0.2 # via pytest-flask freezegun==0.3.10 future==0.16.0 # via python-jose From 54ba7a053a595624347abac24cf730b09ae82596 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Mon, 17 Sep 2018 10:57:03 -0700 Subject: [PATCH 5/5] up reqs --- requirements-dev.txt | 2 +- requirements.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index b3c52456..eb027066 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -10,7 +10,7 @@ certifi==2018.8.24 # via requests cfgv==1.1.0 # via pre-commit chardet==3.0.4 # via requests flake8==3.5.0 -identify==1.1.5 # via pre-commit +identify==1.1.6 # via pre-commit idna==2.7 # via requests invoke==1.2.0 mccabe==0.6.1 # via flake8 diff --git a/requirements.txt b/requirements.txt index 43ebdb63..9b44bbbe 100644 --- a/requirements.txt +++ b/requirements.txt @@ -51,7 +51,7 @@ lockfile==0.12.2 mako==1.0.7 # via alembic markupsafe==1.0 # via jinja2, mako marshmallow-sqlalchemy==0.14.1 -marshmallow==2.15.4 +marshmallow==2.15.5 mock==2.0.0 # via acme ndg-httpsclient==0.5.1 paramiko==2.4.1