diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 2262a6c8..ea8d23b7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,12 +2,35 @@ Changelog ========= -0.7 - `master` +0.7 - `2018-05-07` ~~~~~~~~~~~~~~ -.. note:: This version is not yet released and is under active development +This release adds LetsEncrypt support with DNS providers Dyn, Route53, and Cloudflare, and expands on the pending certificate functionality. +The linux_dst plugin will also be deprecated and removed. + +The pending_dns_authorizations and dns_providers tables were created. New columns +were added to the certificates and pending_certificates tables, (For the DNS provider ID), and authorities (For options). +Please run a database migration when upgrading. + +The Let's Encrypt flow will run asynchronously. When a certificate is requested through the acme-issuer, a pending certificate +will be created. A cron needs to be defined to run `lemur pending_certs fetch_all_acme`. This command will iterate through all of the pending +certificates, request a DNS challenge token from Let's Encrypt, and set the appropriate _acme-challenge TXT entry. It will +then iterate through and resolve the challenges before requesting a certificate for each pending certificate. If a certificate +is successfully obtained, the pending_certificate will be moved to the certificates table with the appropriate properties. + +Special thanks to all who helped with this release, notably: + +- The folks at Cloudflare +- dmitryzykov +- jchuong +- seils +- titouanc +Upgrading +--------- + +.. note:: This release will need a migration change. Please follow the `documentation `_ to upgrade Lemur. 0.6 - `2018-01-02` ~~~~~~~~~~~~~~~~~~ diff --git a/LICENSE b/LICENSE index ce743570..1df93f8e 100644 --- a/LICENSE +++ b/LICENSE @@ -186,7 +186,7 @@ same "printed page" as the copyright notice for easier identification within third-party archives. - Copyright 2014 Netflix, Inc. + Copyright 2018 Netflix, Inc. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/docs/conf.py b/docs/conf.py index 79630685..d5b1698c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -59,7 +59,7 @@ master_doc = 'index' # General information about the project. project = u'lemur' -copyright = u'2015, Netflix Inc.' +copyright = u'2018, Netflix Inc.' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the diff --git a/lemur/__about__.py b/lemur/__about__.py index ee4e6a6d..d15b7dea 100644 --- a/lemur/__about__.py +++ b/lemur/__about__.py @@ -9,10 +9,10 @@ __title__ = "lemur" __summary__ = ("Certificate management and orchestration service") __uri__ = "https://github.com/Netflix/lemur" -__version__ = "0.7.0dev" +__version__ = "0.7.0" __author__ = "The Lemur developers" __email__ = "security@netflix.com" __license__ = "Apache License, Version 2.0" -__copyright__ = "Copyright 2017 {0}".format(__author__) +__copyright__ = "Copyright 2018 {0}".format(__author__) diff --git a/lemur/__init__.py b/lemur/__init__.py index c3661f4e..1cdb3468 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -1,7 +1,7 @@ """ .. module: lemur :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson @@ -29,6 +29,7 @@ from lemur.endpoints.views import mod as endpoints_bp from lemur.logs.views import mod as logs_bp from lemur.api_keys.views import mod as api_key_bp from lemur.pending_certificates.views import mod as pending_certificates_bp +from lemur.dns_providers.views import mod as dns_providers_bp from lemur.__about__ import ( __author__, __copyright__, __email__, __license__, __summary__, __title__, @@ -57,6 +58,7 @@ LEMUR_BLUEPRINTS = ( logs_bp, api_key_bp, pending_certificates_bp, + dns_providers_bp, ) diff --git a/lemur/api_keys/cli.py b/lemur/api_keys/cli.py index ad7d7a15..2259d774 100644 --- a/lemur/api_keys/cli.py +++ b/lemur/api_keys/cli.py @@ -1,7 +1,7 @@ """ .. module: lemur.api_keys.cli :platform: Unix - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Eric Coan """ diff --git a/lemur/api_keys/models.py b/lemur/api_keys/models.py index c9e4b523..df77edb1 100644 --- a/lemur/api_keys/models.py +++ b/lemur/api_keys/models.py @@ -2,7 +2,7 @@ .. module: lemur.api_keys.models :platform: Unix :synopsis: This module contains all of the models need to create an api key within Lemur. - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Eric Coan """ diff --git a/lemur/api_keys/schemas.py b/lemur/api_keys/schemas.py index 30c41c58..a3c11417 100644 --- a/lemur/api_keys/schemas.py +++ b/lemur/api_keys/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.api_keys.schemas :platform: Unix - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Eric Coan """ diff --git a/lemur/api_keys/service.py b/lemur/api_keys/service.py index 9ebc685b..5ddb8a3a 100644 --- a/lemur/api_keys/service.py +++ b/lemur/api_keys/service.py @@ -1,7 +1,7 @@ """ .. module: lemur.api_keys.service :platform: Unix - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Eric Coan """ diff --git a/lemur/api_keys/views.py b/lemur/api_keys/views.py index 8986afdc..b7af2944 100644 --- a/lemur/api_keys/views.py +++ b/lemur/api_keys/views.py @@ -1,7 +1,7 @@ """ .. module: lemur.api_keys.views :platform: Unix - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Eric Coan diff --git a/lemur/auth/ldap.py b/lemur/auth/ldap.py index e72469a5..398a5830 100644 --- a/lemur/auth/ldap.py +++ b/lemur/auth/ldap.py @@ -1,7 +1,7 @@ """ .. module: lemur.auth.ldap :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Ian Stahnke """ diff --git a/lemur/auth/permissions.py b/lemur/auth/permissions.py index 4fa025f7..e6d14408 100644 --- a/lemur/auth/permissions.py +++ b/lemur/auth/permissions.py @@ -2,7 +2,7 @@ .. module: lemur.auth.permissions :platform: Unix :synopsis: This module defines all the permission used within Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/auth/service.py b/lemur/auth/service.py index 00419c9f..c862aa2e 100644 --- a/lemur/auth/service.py +++ b/lemur/auth/service.py @@ -3,7 +3,7 @@ :platform: Unix :synopsis: This module contains all of the authentication duties for lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/auth/views.py b/lemur/auth/views.py index 5888de8f..347f0393 100644 --- a/lemur/auth/views.py +++ b/lemur/auth/views.py @@ -1,7 +1,7 @@ """ .. module: lemur.auth.views :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/authorities/models.py b/lemur/authorities/models.py index 45744144..6c5f790b 100644 --- a/lemur/authorities/models.py +++ b/lemur/authorities/models.py @@ -2,7 +2,7 @@ .. module: lemur.authorities.models :platform: unix :synopsis: This module contains all of the models need to create an authority within Lemur. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ @@ -42,6 +42,7 @@ class Authority(db.Model): self.description = kwargs.get('description') self.authority_certificate = kwargs['authority_certificate'] self.plugin_name = kwargs['plugin']['slug'] + self.options = kwargs.get('options') @property def plugin(self): diff --git a/lemur/authorities/schemas.py b/lemur/authorities/schemas.py index 0815de73..d1f0adfc 100644 --- a/lemur/authorities/schemas.py +++ b/lemur/authorities/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.authorities.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index 0b475e0b..1d35ad49 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -3,11 +3,14 @@ :platform: Unix :synopsis: This module contains all of the services level functions used to administer authorities in Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ + +import json + from lemur import database from lemur.common.utils import truthiness from lemur.extensions import metrics @@ -107,6 +110,8 @@ def create(**kwargs): cert = upload(**kwargs) kwargs['authority_certificate'] = cert + if kwargs.get('plugin', {}).get('plugin_options', []): + kwargs['options'] = json.dumps(kwargs['plugin']['plugin_options']) authority = Authority(**kwargs) authority = database.create(authority) diff --git a/lemur/authorities/views.py b/lemur/authorities/views.py index 06604c01..6bf7e2a8 100644 --- a/lemur/authorities/views.py +++ b/lemur/authorities/views.py @@ -1,7 +1,7 @@ """ .. module: lemur.authorities.views :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/authorizations/__init__.py b/lemur/authorizations/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/authorizations/models.py b/lemur/authorizations/models.py new file mode 100644 index 00000000..d30de7ed --- /dev/null +++ b/lemur/authorizations/models.py @@ -0,0 +1,34 @@ +""" +.. module: lemur.authorizations.models + :platform: unix + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Netflix Secops +""" +from sqlalchemy import Column, Integer, String +from sqlalchemy_utils import JSONType +from lemur.database import db + +from lemur.plugins.base import plugins + + +class Authorization(db.Model): + __tablename__ = 'pending_dns_authorizations' + id = Column(Integer, primary_key=True, autoincrement=True) + account_number = Column(String(128)) + domains = Column(JSONType) + dns_provider_type = Column(String(128)) + options = Column(JSONType) + + @property + def plugin(self): + return plugins.get(self.plugin_name) + + def __repr__(self): + return "Authorization(id={id})".format(label=self.id) + + def __init__(self, account_number, domains, dns_provider_type, options=None): + self.account_number = account_number + self.domains = domains + self.dns_provider_type = dns_provider_type + self.options = options diff --git a/lemur/authorizations/service.py b/lemur/authorizations/service.py new file mode 100644 index 00000000..6e73c38c --- /dev/null +++ b/lemur/authorizations/service.py @@ -0,0 +1,24 @@ +""" +.. module: lemur.pending_certificates.service + Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved. +.. moduleauthor:: Secops +""" +from lemur import database + +from lemur.authorizations.models import Authorization + + +def get(authorization_id): + """ + Retrieve dns authorization by ID + """ + return database.get(Authorization, authorization_id) + + +def create(account_number, domains, dns_provider_type, options=None): + """ + Creates a new dns authorization. + """ + + authorization = Authorization(account_number, domains, dns_provider_type, options) + return database.create(authorization) diff --git a/lemur/certificates/cli.py b/lemur/certificates/cli.py index 4647d301..f5a84d18 100644 --- a/lemur/certificates/cli.py +++ b/lemur/certificates/cli.py @@ -1,7 +1,7 @@ """ .. module: lemur.certificate.cli :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ @@ -154,6 +154,7 @@ def request_reissue(certificate, commit): except Exception as e: sentry.captureException() + current_app.logger.exception("Error reissuing certificate.", exc_info=True) print( "[!] Failed to reissue certificates. Reason: {}".format( e @@ -245,6 +246,7 @@ def reissue(old_certificate_name, commit): print("[+] Done!") except Exception as e: sentry.captureException() + current_app.logger.exception("Error reissuing certificate.", exc_info=True) print( "[!] Failed to reissue certificates. Reason: {}".format( e diff --git a/lemur/certificates/hooks.py b/lemur/certificates/hooks.py index e352a4d5..16f6c3b0 100644 --- a/lemur/certificates/hooks.py +++ b/lemur/certificates/hooks.py @@ -3,7 +3,7 @@ Debugging hooks for dumping imported or generated CSR and certificate details to .. module: lemur.certificates.hooks :platform: Unix - :copyright: (c) 2016-2017 by Marti Raudsepp, see AUTHORS for more + :copyright: (c) 2018 by Marti Raudsepp, see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Marti Raudsepp diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index a9bb60cc..f88eda0a 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -1,7 +1,7 @@ """ .. module: lemur.certificates.models :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ @@ -102,6 +102,7 @@ class Certificate(db.Model): serial = Column(String(128)) cn = Column(String(128)) deleted = Column(Boolean, index=True) + dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='cascade'), nullable=True) not_before = Column(ArrowType) not_after = Column(ArrowType) @@ -177,6 +178,8 @@ class Certificate(db.Model): self.signing_algorithm = defaults.signing_algorithm(cert) self.bits = defaults.bitstrength(cert) self.external_id = kwargs.get('external_id') + self.authority_id = kwargs.get('authority_id') + self.dns_provider_id = kwargs.get('dns_provider_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 651aa647..99e520c4 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.certificates.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ @@ -9,31 +9,30 @@ from flask import current_app from marshmallow import fields, validate, validates_schema, post_load, pre_load from marshmallow.exceptions import ValidationError +from lemur.authorities.schemas import AuthorityNestedOutputSchema +from lemur.common import validators, missing +from lemur.common.fields import ArrowDateTime, Hex +from lemur.common.schema import LemurInputSchema, LemurOutputSchema +from lemur.constants import CERTIFICATE_KEY_TYPES +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 ( AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, AssociatedNotificationSchema, + AssociatedDnsProviderSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema, EndpointNestedOutputSchema, - AssociatedRotationPolicySchema + AssociatedRotationPolicySchema, ) - -from lemur.authorities.schemas import AuthorityNestedOutputSchema -from lemur.destinations.schemas import DestinationNestedOutputSchema -from lemur.notifications.schemas import NotificationNestedOutputSchema -from lemur.roles.schemas import RoleNestedOutputSchema -from lemur.domains.schemas import DomainNestedOutputSchema from lemur.users.schemas import UserNestedOutputSchema -from lemur.policies.schemas import RotationPolicyNestedOutputSchema - -from lemur.common.schema import LemurInputSchema, LemurOutputSchema -from lemur.common import validators, missing -from lemur.notifications import service as notification_service - -from lemur.common.fields import ArrowDateTime, Hex class CertificateSchema(LemurInputSchema): @@ -70,9 +69,13 @@ class CertificateInputSchema(CertificateCreationSchema): replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) # deprecated roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True) + dns_provider = fields.Nested(AssociatedDnsProviderSchema, missing=None, allow_none=True, required=False) csr = fields.String(validate=validators.csr) - key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048') + + key_type = fields.String( + validate=validate.OneOf(CERTIFICATE_KEY_TYPES), + missing='RSA2048') notify = fields.Boolean(default=True) rotation = fields.Boolean() @@ -183,6 +186,7 @@ class CertificateOutputSchema(LemurOutputSchema): description = fields.String() issuer = fields.String() name = fields.String() + dns_provider_id = fields.Integer(required=False, allow_none=True) rotation = fields.Boolean() diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index ce90b4f0..e7a5afd1 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -1,7 +1,7 @@ """ .. module: lemur.certificate.service :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/certificates/verify.py b/lemur/certificates/verify.py index c976fecc..3d847bdf 100644 --- a/lemur/certificates/verify.py +++ b/lemur/certificates/verify.py @@ -1,7 +1,7 @@ """ .. module: lemur.certificates.verify :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/certificates/views.py b/lemur/certificates/views.py index 59f96522..72cb3b5a 100644 --- a/lemur/certificates/views.py +++ b/lemur/certificates/views.py @@ -1,7 +1,7 @@ """ .. module: lemur.certificates.views :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/common/fields.py b/lemur/common/fields.py index c2b95635..9a0198e9 100644 --- a/lemur/common/fields.py +++ b/lemur/common/fields.py @@ -1,7 +1,7 @@ """ .. module: lemur.common.fields :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/common/health.py b/lemur/common/health.py index c48c1fae..69df3f0c 100644 --- a/lemur/common/health.py +++ b/lemur/common/health.py @@ -1,7 +1,7 @@ """ .. module: lemur.common.health :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/common/managers.py b/lemur/common/managers.py index 234432c0..9f30f216 100644 --- a/lemur/common/managers.py +++ b/lemur/common/managers.py @@ -1,7 +1,7 @@ """ .. module: lemur.common.managers :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/common/schema.py b/lemur/common/schema.py index 1e081f8c..4e2c5306 100644 --- a/lemur/common/schema.py +++ b/lemur/common/schema.py @@ -1,7 +1,7 @@ """ .. module: lemur.common.schema :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson @@ -135,7 +135,6 @@ def unwrap_pagination(data, output_schema): marshaled_data = {'total': len(data)} marshaled_data['items'] = output_schema.dump(data, many=True).data return marshaled_data - return output_schema.dump(data).data diff --git a/lemur/common/utils.py b/lemur/common/utils.py index 02f55340..bcc8a21b 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -1,22 +1,20 @@ """ .. module: lemur.common.utils :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ -import string import random +import string import sqlalchemy -from sqlalchemy import and_, func - from cryptography import x509 from cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa, ec - from flask_restful.reqparse import RequestParser +from sqlalchemy import and_, func from lemur.constants import CERTIFICATE_KEY_TYPES from lemur.exceptions import InvalidConfiguration diff --git a/lemur/constants.py b/lemur/constants.py index 0ee9bc40..060ecfed 100644 --- a/lemur/constants.py +++ b/lemur/constants.py @@ -1,6 +1,6 @@ """ .. module: lemur.constants - :copyright: (c) 2015 by Netflix Inc. + :copyright: (c) 2018 by Netflix Inc. :license: Apache, see LICENSE for more details. """ SAN_NAMING_TEMPLATE = "SAN-{subject}-{issuer}-{not_before}-{not_after}" diff --git a/lemur/database.py b/lemur/database.py index 8efd4b95..ad3899aa 100644 --- a/lemur/database.py +++ b/lemur/database.py @@ -4,7 +4,7 @@ :synopsis: This module contains all of the database related methods needed for lemur to interact with a datastore - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/defaults/schemas.py b/lemur/defaults/schemas.py index c03d6d85..4ff5da61 100644 --- a/lemur/defaults/schemas.py +++ b/lemur/defaults/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.defaults.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/defaults/views.py b/lemur/defaults/views.py index db849011..5a573829 100644 --- a/lemur/defaults/views.py +++ b/lemur/defaults/views.py @@ -1,6 +1,6 @@ """ .. module: lemur.defaults.views - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ from flask import current_app, Blueprint @@ -50,7 +50,8 @@ class LemurDefaults(AuthenticatedResource): "state": "CA", "location": "Los Gatos", "organization": "Netflix", - "organizationalUnit": "Operations" + "organizationalUnit": "Operations", + "dnsProviders": [{"name": "test", ...}, {...}], } :reqheader Authorization: OAuth token to authenticate @@ -67,7 +68,7 @@ class LemurDefaults(AuthenticatedResource): organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'), organizational_unit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'), issuer_plugin=current_app.config.get('LEMUR_DEFAULT_ISSUER_PLUGIN'), - authority=default_authority + authority=default_authority, ) diff --git a/lemur/destinations/models.py b/lemur/destinations/models.py index 3e04eae5..192a5f5d 100644 --- a/lemur/destinations/models.py +++ b/lemur/destinations/models.py @@ -1,7 +1,7 @@ """ .. module: lemur.destinations.models :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/destinations/schemas.py b/lemur/destinations/schemas.py index a7e47954..279889b4 100644 --- a/lemur/destinations/schemas.py +++ b/lemur/destinations/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.destinations.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/destinations/service.py b/lemur/destinations/service.py index 5cccb8a4..ed6fcb0f 100644 --- a/lemur/destinations/service.py +++ b/lemur/destinations/service.py @@ -1,7 +1,7 @@ """ .. module: lemur.destinations.service :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/destinations/views.py b/lemur/destinations/views.py index a0cb1a4e..7084e8e9 100644 --- a/lemur/destinations/views.py +++ b/lemur/destinations/views.py @@ -2,7 +2,7 @@ .. module: lemur.destinations.views :platform: Unix :synopsis: This module contains all of the accounts view code. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/dns_providers/models.py b/lemur/dns_providers/models.py new file mode 100644 index 00000000..0cf41730 --- /dev/null +++ b/lemur/dns_providers/models.py @@ -0,0 +1,37 @@ +from sqlalchemy import Column, Integer, String, text, Text +from sqlalchemy.dialects.postgresql import JSON +from sqlalchemy_utils import ArrowType + +from lemur.database import db +from lemur.plugins.base import plugins +from lemur.utils import Vault + + +class DnsProvider(db.Model): + __tablename__ = 'dns_providers' + id = Column( + Integer(), + primary_key=True, + ) + name = Column(String(length=256), unique=True, nullable=True) + description = Column(Text(), nullable=True) + provider_type = Column(String(length=256), nullable=True) + credentials = Column(Vault, nullable=True) + api_endpoint = Column(String(length=256), nullable=True) + date_created = Column(ArrowType(), server_default=text('now()'), nullable=False) + status = Column(String(length=128), nullable=True) + options = Column(JSON, nullable=True) + domains = Column(JSON, nullable=True) + + def __init__(self, name, description, provider_type, credentials): + self.name = name + self.description = description + self.provider_type = provider_type + self.credentials = credentials + + @property + def plugin(self): + return plugins.get(self.plugin_name) + + def __repr__(self): + return "DnsProvider(name={name})".format(name=self.name) diff --git a/lemur/dns_providers/schemas.py b/lemur/dns_providers/schemas.py new file mode 100644 index 00000000..d871b8ef --- /dev/null +++ b/lemur/dns_providers/schemas.py @@ -0,0 +1,27 @@ +from lemur.common.fields import ArrowDateTime +from lemur.common.schema import LemurInputSchema, LemurOutputSchema + +from marshmallow import fields + + +class DnsProvidersNestedOutputSchema(LemurOutputSchema): + __envelope__ = False + id = fields.Integer() + name = fields.String() + providerType = fields.String() + description = fields.String() + credentials = fields.String() + api_endpoint = fields.String() + date_created = ArrowDateTime() + + +class DnsProvidersNestedInputSchema(LemurInputSchema): + __envelope__ = False + name = fields.String() + description = fields.String() + provider_type = fields.Dict() + + +dns_provider_output_schema = DnsProvidersNestedOutputSchema() + +dns_provider_input_schema = DnsProvidersNestedInputSchema() diff --git a/lemur/dns_providers/service.py b/lemur/dns_providers/service.py new file mode 100644 index 00000000..442b2c31 --- /dev/null +++ b/lemur/dns_providers/service.py @@ -0,0 +1,111 @@ +import json + +from flask import current_app +from lemur import database +from lemur.dns_providers.models import DnsProvider + + +def render(args): + """ + Helper that helps us render the REST Api responses. + :param args: + :return: + """ + query = database.session_query(DnsProvider) + + return database.sort_and_page(query, DnsProvider, args) + + +def get(dns_provider_id): + provider = database.get(DnsProvider, dns_provider_id) + return provider + + +def get_friendly(dns_provider_id): + """ + Retrieves a dns provider by its lemur assigned ID. + + :param dns_provider_id: Lemur assigned ID + :rtype : DnsProvider + :return: + """ + dns_provider = get(dns_provider_id) + dns_provider_friendly = { + "name": dns_provider.name, + "description": dns_provider.description, + "providerType": dns_provider.provider_type, + "options": dns_provider.options, + "credentials": dns_provider.credentials, + } + + if dns_provider.provider_type == "route53": + dns_provider_friendly["account_id"] = json.loads(dns_provider.credentials).get("account_id") + return dns_provider_friendly + + +def delete(dns_provider_id): + """ + Deletes a DNS provider. + + :param dns_provider_id: Lemur assigned ID + """ + database.delete(get(dns_provider_id)) + + +def get_types(): + provider_config = current_app.config.get( + 'ACME_DNS_PROVIDER_TYPES', + {"items": [ + { + 'name': 'route53', + 'requirements': [ + { + 'name': 'account_id', + 'type': 'int', + 'required': True, + 'helpMessage': 'AWS Account number' + }, + ] + }, + { + 'name': 'cloudflare', + 'requirements': [ + { + 'name': 'email', + 'type': 'str', + 'required': True, + 'helpMessage': 'Cloudflare Email' + }, + { + 'name': 'key', + 'type': 'str', + 'required': True, + 'helpMessage': 'Cloudflare Key' + }, + ] + }, + { + 'name': 'dyn', + }, + ]} + ) + if not provider_config: + raise Exception("No DNS Provider configuration specified.") + provider_config["total"] = len(provider_config.get("items")) + return provider_config + + +def create(data): + provider_name = data.get("name") + + credentials = {} + for item in data.get("provider_type", {}).get("requirements", []): + credentials[item["name"]] = item["value"] + dns_provider = DnsProvider( + name=provider_name, + description=data.get("description"), + provider_type=data.get("provider_type").get("name"), + credentials=json.dumps(credentials), + ) + created = database.create(dns_provider) + return created.id diff --git a/lemur/dns_providers/views.py b/lemur/dns_providers/views.py new file mode 100644 index 00000000..b9543034 --- /dev/null +++ b/lemur/dns_providers/views.py @@ -0,0 +1,170 @@ +""" +.. module: lemur.dns)providers.views + :platform: Unix + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Curtis Castrapel +""" +from flask import Blueprint, g +from flask_restful import reqparse, Api + +from lemur.auth.permissions import admin_permission +from lemur.auth.service import AuthenticatedResource +from lemur.common.schema import validate_schema +from lemur.common.utils import paginated_parser +from lemur.dns_providers import service +from lemur.dns_providers.schemas import dns_provider_output_schema, dns_provider_input_schema + +mod = Blueprint('dns_providers', __name__) +api = Api(mod) + + +class DnsProvidersList(AuthenticatedResource): + """ Defines the 'dns_providers' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(DnsProvidersList, self).__init__() + + @validate_schema(None, dns_provider_output_schema) + def get(self): + """ + .. http:get:: /dns_providers + + The current list of DNS Providers + + **Example request**: + + .. sourcecode:: http + + GET /dns_providers 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": "test", + "description": "test", + "provider_type": "dyn", + "status": "active", + }], + "total": 1 + } + + :query sortBy: field to sort on + :query sortDir: asc or desc + :query page: int. default is 1 + :query filter: key value pair format is k;v + :query count: count number. default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + + """ + parser = paginated_parser.copy() + parser.add_argument('dns_provider_id', type=int, location='args') + parser.add_argument('name', type=str, location='args') + parser.add_argument('type', type=str, location='args') + + args = parser.parse_args() + args['user'] = g.user + return service.render(args) + + @validate_schema(dns_provider_input_schema, None) + @admin_permission.require(http_exception=403) + def post(self, data=None): + """ + Creates a DNS Provider + + **Example request**: + { + "providerType": { + "name": "route53", + "requirements": [ + { + "name": "account_id", + "type": "int", + "required": true, + "helpMessage": "AWS Account number", + "value": 12345 + } + ], + "route": "dns_provider_options", + "reqParams": null, + "restangularized": true, + "fromServer": true, + "parentResource": null, + "restangularCollection": false + }, + "name": "provider_name", + "description": "provider_description" + } + + **Example request 2** + { + "providerType": { + "name": "cloudflare", + "requirements": [ + { + "name": "email", + "type": "str", + "required": true, + "helpMessage": "Cloudflare Email", + "value": "test@example.com" + }, + { + "name": "key", + "type": "str", + "required": true, + "helpMessage": "Cloudflare Key", + "value": "secretkey" + } + ], + "route": "dns_provider_options", + "reqParams": null, + "restangularized": true, + "fromServer": true, + "parentResource": null, + "restangularCollection": false + }, + "name": "provider_name", + "description": "provider_description" + } + :return: + """ + return service.create(data) + + +class DnsProviders(AuthenticatedResource): + @validate_schema(None, dns_provider_output_schema) + def get(self, dns_provider_id): + return service.get_friendly(dns_provider_id) + + @admin_permission.require(http_exception=403) + def delete(self, dns_provider_id): + service.delete(dns_provider_id) + return {'result': True} + + +class DnsProviderOptions(AuthenticatedResource): + """ Defines the 'dns_provider_types' endpoint """ + + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(DnsProviderOptions, self).__init__() + + def get(self): + return service.get_types() + + +api.add_resource(DnsProvidersList, '/dns_providers', endpoint='dns_providers') +api.add_resource(DnsProviders, '/dns_providers/', endpoint='dns_provider') +api.add_resource(DnsProviderOptions, '/dns_provider_options', endpoint='dns_provider_options') diff --git a/lemur/domains/models.py b/lemur/domains/models.py index dae55bb7..afd348d6 100644 --- a/lemur/domains/models.py +++ b/lemur/domains/models.py @@ -1,7 +1,7 @@ """ .. module: lemur.domains.models :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/domains/schemas.py b/lemur/domains/schemas.py index 96e41967..6cf7fd31 100644 --- a/lemur/domains/schemas.py +++ b/lemur/domains/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.domains.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/domains/service.py b/lemur/domains/service.py index 60da1062..c9b8f759 100644 --- a/lemur/domains/service.py +++ b/lemur/domains/service.py @@ -1,7 +1,7 @@ """ .. module: lemur.domains.service :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/domains/views.py b/lemur/domains/views.py index dadb091d..db73f5cd 100644 --- a/lemur/domains/views.py +++ b/lemur/domains/views.py @@ -1,7 +1,7 @@ """ .. module: lemur.domains.views :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/endpoints/cli.py b/lemur/endpoints/cli.py index 0f576808..59496930 100644 --- a/lemur/endpoints/cli.py +++ b/lemur/endpoints/cli.py @@ -1,7 +1,7 @@ """ .. module: lemur.endpoints.cli :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/endpoints/models.py b/lemur/endpoints/models.py index 0df2bf31..b5823327 100644 --- a/lemur/endpoints/models.py +++ b/lemur/endpoints/models.py @@ -2,7 +2,7 @@ .. module: lemur.endpoints.models :platform: unix :synopsis: This module contains all of the models need to create an authority within Lemur. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/endpoints/schemas.py b/lemur/endpoints/schemas.py index 57f73642..ec2fd73e 100644 --- a/lemur/endpoints/schemas.py +++ b/lemur/endpoints/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.endpoints.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/endpoints/service.py b/lemur/endpoints/service.py index 55bacde2..d14174df 100644 --- a/lemur/endpoints/service.py +++ b/lemur/endpoints/service.py @@ -3,7 +3,7 @@ :platform: Unix :synopsis: This module contains all of the services level functions used to administer endpoints in Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/endpoints/views.py b/lemur/endpoints/views.py index 02542984..6509f056 100644 --- a/lemur/endpoints/views.py +++ b/lemur/endpoints/views.py @@ -1,7 +1,7 @@ """ .. module: lemur.endpoints.views :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/exceptions.py b/lemur/exceptions.py index fea14e10..d392fe5d 100644 --- a/lemur/exceptions.py +++ b/lemur/exceptions.py @@ -1,6 +1,6 @@ """ .. module: lemur.exceptions - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ from flask import current_app @@ -34,3 +34,11 @@ class AttrNotFound(LemurException): class InvalidConfiguration(Exception): pass + + +class InvalidAuthority(Exception): + pass + + +class UnknownProvider(Exception): + pass diff --git a/lemur/extensions.py b/lemur/extensions.py index 76abcab1..17a8e6e7 100644 --- a/lemur/extensions.py +++ b/lemur/extensions.py @@ -1,6 +1,6 @@ """ .. module: lemur.extensions - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ from flask_sqlalchemy import SQLAlchemy diff --git a/lemur/factory.py b/lemur/factory.py index 97f7f6ca..c2719e9b 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -4,7 +4,7 @@ :synopsis: This module contains all the needed functions to allow the factory app creation. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/logs/models.py b/lemur/logs/models.py index f634ba9b..d4239e59 100644 --- a/lemur/logs/models.py +++ b/lemur/logs/models.py @@ -2,7 +2,7 @@ .. module: lemur.logs.models :platform: unix :synopsis: This module contains all of the models related private key audit log. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/logs/schemas.py b/lemur/logs/schemas.py index db16755a..7d31cb39 100644 --- a/lemur/logs/schemas.py +++ b/lemur/logs/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.logs.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/logs/service.py b/lemur/logs/service.py index 59097cb0..04355938 100644 --- a/lemur/logs/service.py +++ b/lemur/logs/service.py @@ -3,7 +3,7 @@ :platform: Unix :synopsis: This module contains all of the services level functions used to administer logs in Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/logs/views.py b/lemur/logs/views.py index a19f7c4a..1e0bd184 100644 --- a/lemur/logs/views.py +++ b/lemur/logs/views.py @@ -1,7 +1,7 @@ """ .. module: lemur.logs.views :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/metrics.py b/lemur/metrics.py index 64ddeac3..381dc605 100644 --- a/lemur/metrics.py +++ b/lemur/metrics.py @@ -1,6 +1,6 @@ """ .. module: lemur.metrics - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. """ from flask import current_app diff --git a/lemur/migrations/versions/3adfdd6598df_.py b/lemur/migrations/versions/3adfdd6598df_.py new file mode 100644 index 00000000..1f290153 --- /dev/null +++ b/lemur/migrations/versions/3adfdd6598df_.py @@ -0,0 +1,105 @@ +"""Create tables and columns for the acme issuer. + +Revision ID: 3adfdd6598df +Revises: 556ceb3e3c3e +Create Date: 2018-04-10 13:25:47.007556 + +""" + +# revision identifiers, used by Alembic. +revision = '3adfdd6598df' +down_revision = '556ceb3e3c3e' + +import sqlalchemy as sa +from alembic import op +from sqlalchemy.dialects.postgresql import JSON +from sqlalchemy_utils import ArrowType + +from lemur.utils import Vault + + +def upgrade(): + # create provider table + print("Creating dns_providers table") + op.create_table( + 'dns_providers', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('name', sa.String(length=256), nullable=True), + sa.Column('description', sa.String(length=1024), nullable=True), + sa.Column('provider_type', sa.String(length=256), nullable=True), + sa.Column('credentials', Vault(), nullable=True), + sa.Column('api_endpoint', sa.String(length=256), nullable=True), + sa.Column('date_created', ArrowType(), server_default=sa.text('now()'), nullable=False), + sa.Column('status', sa.String(length=128), nullable=True), + sa.Column('options', JSON), + sa.Column('domains', sa.JSON(), nullable=True), + sa.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + print("Adding dns_provider_id column to certificates") + op.add_column('certificates', sa.Column('dns_provider_id', sa.Integer(), nullable=True)) + print("Adding dns_provider_id column to pending_certs") + op.add_column('pending_certs', sa.Column('dns_provider_id', sa.Integer(), nullable=True)) + print("Adding options column to pending_certs") + op.add_column('pending_certs', sa.Column('options', JSON)) + + print("Creating pending_dns_authorizations table") + op.create_table( + 'pending_dns_authorizations', + sa.Column('id', sa.Integer(), primary_key=True, autoincrement=True), + sa.Column('account_number', sa.String(length=128), nullable=True), + sa.Column('domains', JSON, nullable=True), + sa.Column('dns_provider_type', sa.String(length=128), nullable=True), + sa.Column('options', JSON, nullable=True), + ) + + print("Creating certificates_dns_providers_fk foreign key") + op.create_foreign_key('certificates_dns_providers_fk', 'certificates', 'dns_providers', ['dns_provider_id'], ['id'], + ondelete='cascade') + + print("Altering column types in the api_keys table") + op.alter_column('api_keys', 'issued_at', + existing_type=sa.BIGINT(), + nullable=True) + op.alter_column('api_keys', 'revoked', + existing_type=sa.BOOLEAN(), + nullable=True) + op.alter_column('api_keys', 'ttl', + existing_type=sa.BIGINT(), + nullable=True) + op.alter_column('api_keys', 'user_id', + existing_type=sa.INTEGER(), + nullable=True) + + print("Creating dns_providers_id foreign key on pending_certs table") + op.create_foreign_key(None, 'pending_certs', 'dns_providers', ['dns_provider_id'], ['id'], ondelete='CASCADE') + +def downgrade(): + print("Removing dns_providers_id foreign key on pending_certs table") + op.drop_constraint(None, 'pending_certs', type_='foreignkey') + print("Reverting column types in the api_keys table") + op.alter_column('api_keys', 'user_id', + existing_type=sa.INTEGER(), + nullable=False) + op.alter_column('api_keys', 'ttl', + existing_type=sa.BIGINT(), + nullable=False) + op.alter_column('api_keys', 'revoked', + existing_type=sa.BOOLEAN(), + nullable=False) + op.alter_column('api_keys', 'issued_at', + existing_type=sa.BIGINT(), + nullable=False) + print("Reverting certificates_dns_providers_fk foreign key") + op.drop_constraint('certificates_dns_providers_fk', 'certificates', type_='foreignkey') + + print("Dropping pending_dns_authorizations table") + op.drop_table('pending_dns_authorizations') + print("Undoing modifications to pending_certs table") + op.drop_column('pending_certs', 'options') + op.drop_column('pending_certs', 'dns_provider_id') + print("Undoing modifications to certificates table") + op.drop_column('certificates', 'dns_provider_id') + + print("Deleting dns_providers table") + op.drop_table('dns_providers') diff --git a/lemur/models.py b/lemur/models.py index 02c64dbe..69f82360 100644 --- a/lemur/models.py +++ b/lemur/models.py @@ -4,7 +4,7 @@ :synopsis: This module contains all of the associative tables that help define the many to many relationships established in Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ @@ -55,7 +55,7 @@ certificate_replacement_associations = db.Table('certificate_replacement_associa ForeignKey('certificates.id', ondelete='cascade')) ) -Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.replaced_certificate_id, certificate_replacement_associations.c.certificate_id) +Index('certificate_replacement_associations_ix', certificate_replacement_associations.c.replaced_certificate_id, certificate_replacement_associations.c.certificate_id, unique=True) roles_authorities = db.Table('roles_authorities', Column('authority_id', Integer, ForeignKey('authorities.id')), diff --git a/lemur/notifications/cli.py b/lemur/notifications/cli.py index 58f7e86f..e3bf431e 100644 --- a/lemur/notifications/cli.py +++ b/lemur/notifications/cli.py @@ -1,7 +1,7 @@ """ .. module: lemur.notifications.cli :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/notifications/messaging.py b/lemur/notifications/messaging.py index 111c9e2e..4600ac61 100644 --- a/lemur/notifications/messaging.py +++ b/lemur/notifications/messaging.py @@ -2,7 +2,7 @@ .. module: lemur.notifications.messaging :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/notifications/models.py b/lemur/notifications/models.py index 79a3df1c..87646b4c 100644 --- a/lemur/notifications/models.py +++ b/lemur/notifications/models.py @@ -1,7 +1,7 @@ """ .. module: lemur.notifications.models :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/notifications/schemas.py b/lemur/notifications/schemas.py index 3a772d78..b5d4e1e6 100644 --- a/lemur/notifications/schemas.py +++ b/lemur/notifications/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.notifications.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/notifications/service.py b/lemur/notifications/service.py index e26ff47f..466c680b 100644 --- a/lemur/notifications/service.py +++ b/lemur/notifications/service.py @@ -2,7 +2,7 @@ .. module: lemur.notifications.service :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/notifications/views.py b/lemur/notifications/views.py index 30b0f5f8..df75aaa9 100644 --- a/lemur/notifications/views.py +++ b/lemur/notifications/views.py @@ -2,7 +2,7 @@ .. module: lemur.notifications.views :platform: Unix :synopsis: This module contains all of the accounts view code. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/pending_certificates/cli.py b/lemur/pending_certificates/cli.py index ff846fe1..6e12c53b 100644 --- a/lemur/pending_certificates/cli.py +++ b/lemur/pending_certificates/cli.py @@ -2,10 +2,11 @@ .. module: lemur.pending_certificates.cli .. moduleauthor:: James Chuong +.. moduleauthor:: Curtis Castrapel """ - from flask_script import Manager +from lemur.authorities.service import get as get_authority from lemur.pending_certificates import service as pending_certificate_service from lemur.plugins.base import plugins from lemur.users import service as user_service @@ -22,14 +23,14 @@ def fetch(ids): ids: a list of ids of PendingCertificates (passed in by manager options when run as CLI) `python manager.py pending_certs fetch -i 123 321 all` """ - new = 0 - failed = 0 pending_certs = pending_certificate_service.get_pending_certs(ids) user = user_service.get_by_username('lemur') + new = 0 + failed = 0 for cert in pending_certs: authority = plugins.get(cert.authority.plugin_name) - real_cert = authority.get_ordered_certificate(cert.external_id) + real_cert = authority.get_ordered_certificate(cert) if real_cert: # If a real certificate was returned from issuer, then create it in Lemur and delete # the pending certificate @@ -43,6 +44,55 @@ def fetch(ids): print( "[+] Certificates: New: {new} Failed: {failed}".format( new=new, - failed=failed + failed=failed, + ) + ) + + +@manager.command +def fetch_all_acme(): + """ + Attempt to get full certificates for each pending certificate listed with the acme-issuer. This is more efficient + for acme-issued certificates because it will configure all of the DNS challenges prior to resolving any + certificates. + """ + pending_certs = pending_certificate_service.get_pending_certs('all') + user = user_service.get_by_username('lemur') + new = 0 + failed = 0 + wrong_issuer = 0 + acme_certs = [] + + # 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': + acme_certs.append(cert) + else: + wrong_issuer += 1 + + authority = plugins.get("acme-issuer") + resolved_certs = authority.get_ordered_certificates(acme_certs) + + for cert in resolved_certs: + real_cert = cert.get("cert") + # It's necessary to reload the pending cert due to detached instance: http://sqlalche.me/e/bhk3 + pending_cert = pending_certificate_service.get(cert.get("pending_cert").id) + + if real_cert: + # If a real certificate was returned from issuer, then create it in Lemur and delete + # the pending certificate + pending_certificate_service.create_certificate(pending_cert, real_cert, user) + pending_certificate_service.delete_by_id(pending_cert.id) + # add metrics to metrics extension + new += 1 + else: + pending_certificate_service.increment_attempt(pending_cert) + failed += 1 + print( + "[+] Certificates: New: {new} Failed: {failed} Not using ACME: {wrong_issuer}".format( + new=new, + failed=failed, + wrong_issuer=wrong_issuer ) ) diff --git a/lemur/pending_certificates/models.py b/lemur/pending_certificates/models.py index 0e841968..f8ebc7ab 100644 --- a/lemur/pending_certificates/models.py +++ b/lemur/pending_certificates/models.py @@ -1,6 +1,6 @@ """ .. module: lemur.pending_certificates.models - Copyright (c) 2017 and onwards Instart Logic, Inc. All rights reserved. + Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved. .. moduleauthor:: James Chuong """ from datetime import datetime as dt @@ -8,6 +8,7 @@ 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_utils import JSONType import lemur.common.utils from lemur.certificates.models import get_or_increase_name @@ -37,6 +38,7 @@ class PendingCertificate(db.Model): private_key = Column(Vault, nullable=True) date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False) + dns_provider_id = Column(Integer, ForeignKey('dns_providers.id', ondelete="CASCADE")) status = Column(String(128)) @@ -54,6 +56,7 @@ class PendingCertificate(db.Model): secondary=pending_cert_replacement_associations, backref='pending_cert', passive_deletes=True) + options = Column(JSONType) rotation_policy = relationship("RotationPolicy") @@ -93,3 +96,7 @@ class PendingCertificate(db.Model): self.replaces = kwargs.get('replaces', []) self.rotation = kwargs.get('rotation') self.rotation_policy = kwargs.get('rotation_policy') + try: + self.dns_provider_id = kwargs.get('dns_provider').id + except (AttributeError, KeyError, TypeError, Exception): + pass diff --git a/lemur/pending_certificates/service.py b/lemur/pending_certificates/service.py index 9046e0c8..64c03c46 100644 --- a/lemur/pending_certificates/service.py +++ b/lemur/pending_certificates/service.py @@ -1,6 +1,6 @@ """ .. module: lemur.pending_certificates.service - Copyright (c) 2017 and onwards Instart Logic, Inc. All rights reserved. + Copyright (c) 2018 and onwards Netflix, Inc. All rights reserved. .. moduleauthor:: James Chuong """ import arrow @@ -59,6 +59,10 @@ def delete(pending_certificate): database.delete(pending_certificate) +def delete_by_id(id): + database.delete(get(id)) + + def get_pending_certs(pending_ids): """ Retrieve a list of pending certs given a list of ids diff --git a/lemur/plugins/base/__init__.py b/lemur/plugins/base/__init__.py index 107cbcf4..4e9f1e83 100644 --- a/lemur/plugins/base/__init__.py +++ b/lemur/plugins/base/__init__.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.base :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/base/manager.py b/lemur/plugins/base/manager.py index e6c29c4e..a2306445 100644 --- a/lemur/plugins/base/manager.py +++ b/lemur/plugins/base/manager.py @@ -1,6 +1,6 @@ """ .. module: lemur.plugins.base.manager - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson (kglisson@netflix.com) diff --git a/lemur/plugins/base/v1.py b/lemur/plugins/base/v1.py index 36dbaf6e..fb688c73 100644 --- a/lemur/plugins/base/v1.py +++ b/lemur/plugins/base/v1.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.base.v1 :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/bases/destination.py b/lemur/plugins/bases/destination.py index 53c63e86..1e7e4ed2 100644 --- a/lemur/plugins/bases/destination.py +++ b/lemur/plugins/bases/destination.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.bases.destination :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/bases/export.py b/lemur/plugins/bases/export.py index ab493869..1466c1ab 100644 --- a/lemur/plugins/bases/export.py +++ b/lemur/plugins/bases/export.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.bases.export :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/bases/issuer.py b/lemur/plugins/bases/issuer.py index 1cca60d7..5eb0964c 100644 --- a/lemur/plugins/bases/issuer.py +++ b/lemur/plugins/bases/issuer.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.bases.issuer :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson @@ -25,7 +25,7 @@ class IssuerPlugin(Plugin): def revoke_certificate(self, certificate, comments): raise NotImplementedError - def get_ordered_certificate(self, order_id): + def get_ordered_certificate(self, certificate): raise NotImplementedError def cancel_ordered_certificate(self, pending_cert, **kwargs): diff --git a/lemur/plugins/bases/metric.py b/lemur/plugins/bases/metric.py index 586dab41..259af235 100644 --- a/lemur/plugins/bases/metric.py +++ b/lemur/plugins/bases/metric.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.bases.metric :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/bases/notification.py b/lemur/plugins/bases/notification.py index 054bdff5..a7ba4e0d 100644 --- a/lemur/plugins/bases/notification.py +++ b/lemur/plugins/bases/notification.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.bases.notification :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/bases/source.py b/lemur/plugins/bases/source.py index f7fe655b..ff3492fe 100644 --- a/lemur/plugins/bases/source.py +++ b/lemur/plugins/bases/source.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.bases.source :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/lemur_acme/cloudflare.py b/lemur/plugins/lemur_acme/cloudflare.py index 2a665b38..77052242 100644 --- a/lemur/plugins/lemur_acme/cloudflare.py +++ b/lemur/plugins/lemur_acme/cloudflare.py @@ -1,6 +1,6 @@ import time -import CloudFlare +import CloudFlare from flask import current_app diff --git a/lemur/plugins/lemur_acme/dyn.py b/lemur/plugins/lemur_acme/dyn.py new file mode 100644 index 00000000..28832cbc --- /dev/null +++ b/lemur/plugins/lemur_acme/dyn.py @@ -0,0 +1,131 @@ +import time + +import dns +import dns.exception +import dns.name +import dns.query +import dns.resolver +from dyn.tm.errors import DynectCreateError +from dyn.tm.session import DynectSession +from dyn.tm.zones import Node, Zone +from flask import current_app +from tld import get_tld + + +def get_dynect_session(): + dynect_session = DynectSession( + current_app.config.get('ACME_DYN_CUSTOMER_NAME', ''), + current_app.config.get('ACME_DYN_USERNAME', ''), + current_app.config.get('ACME_DYN_PASSWORD', ''), + ) + return dynect_session + + +def _has_dns_propagated(name, token): + txt_records = [] + try: + dns_resolver = dns.resolver.Resolver() + dns_resolver.nameservers = [get_authoritative_nameserver(name)] + dns_response = dns_resolver.query(name, 'TXT') + for rdata in dns_response: + for txt_record in rdata.strings: + txt_records.append(txt_record.decode("utf-8")) + except dns.exception.DNSException: + return False + + for txt_record in txt_records: + if txt_record == token: + return True + + return False + + +def wait_for_dns_change(change_id, account_number=None): + fqdn, token = change_id + while True: + status = _has_dns_propagated(fqdn, token) + current_app.logger.debug("Record status for fqdn: {}: {}".format(fqdn, status)) + if status: + break + time.sleep(20) + return + + +def create_txt_record(domain, token, account_number): + get_dynect_session() + zone_name = get_tld('http://' + domain) + zone_parts = len(zone_name.split('.')) + node_name = '.'.join(domain.split('.')[:-zone_parts]) + fqdn = "{0}.{1}".format(node_name, zone_name) + zone = Zone(zone_name) + try: + zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5) + except DynectCreateError: + delete_txt_record(None, None, domain, token) + zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5) + node = zone.get_node(node_name) + zone.publish() + current_app.logger.debug("TXT record created: {0}".format(fqdn)) + change_id = (fqdn, token) + return change_id + + +def delete_txt_record(change_id, account_number, domain, token): + get_dynect_session() + if not domain: + current_app.logger.debug("delete_txt_record: No domain passed") + return + + zone_name = get_tld('http://' + domain) + zone_parts = len(zone_name.split('.')) + node_name = '.'.join(domain.split('.')[:-zone_parts]) + fqdn = "{0}.{1}".format(node_name, zone_name) + + zone = Zone(zone_name) + node = Node(zone_name, fqdn) + + all_txt_records = node.get_all_records_by_type('TXT') + for txt_record in all_txt_records: + if txt_record.txtdata == ("{}".format(token)): + current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn)) + txt_record.delete() + zone.publish() + + +def get_authoritative_nameserver(domain): + n = dns.name.from_text(domain) + + depth = 2 + default = dns.resolver.get_default_resolver() + nameserver = default.nameservers[0] + + last = False + while not last: + s = n.split(depth) + + last = s[0].to_unicode() == u'@' + sub = s[1] + + query = dns.message.make_query(sub, dns.rdatatype.NS) + response = dns.query.udp(query, nameserver) + + rcode = response.rcode() + if rcode != dns.rcode.NOERROR: + if rcode == dns.rcode.NXDOMAIN: + raise Exception('%s does not exist.' % sub) + else: + raise Exception('Error %s' % dns.rcode.to_text(rcode)) + + if len(response.authority) > 0: + rrset = response.authority[0] + else: + rrset = response.answer[0] + + rr = rrset[0] + if rr.rdtype != dns.rdatatype.SOA: + authority = rr.target + nameserver = default.query(authority).rrset[0].to_text() + + depth += 1 + + return nameserver diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index 5bdb5514..a3f9af00 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -2,38 +2,44 @@ .. module: lemur.plugins.lemur_acme.plugin :platform: Unix :synopsis: This module is responsible for communicating with an ACME CA. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. Snippets from https://raw.githubusercontent.com/alex/letsencrypt-aws/master/letsencrypt-aws.py .. moduleauthor:: Kevin Glisson .. moduleauthor:: Mikhail Khodorovskiy +.. moduleauthor:: Curtis Castrapel """ -import josepy as jose - -from flask import current_app - -from acme.client import Client -from acme import messages -from acme import challenges - -from lemur.common.utils import generate_private_key +import datetime +import json +import time import OpenSSL.crypto +import josepy as jose +from acme import challenges, messages +from acme.client import BackwardsCompatibleClientV2, ClientNetwork +from acme.messages import Error as AcmeError +from acme.errors import PollError, WildcardUnsupportedError +from botocore.exceptions import ClientError +from flask import current_app -from lemur.common.utils import validate_conf -from lemur.plugins.bases import IssuerPlugin +from lemur.authorizations import service as authorization_service +from lemur.common.utils import generate_private_key +from lemur.dns_providers import service as dns_provider_service +from lemur.exceptions import InvalidAuthority, InvalidConfiguration, UnknownProvider from lemur.plugins import lemur_acme as acme +from lemur.plugins.bases import IssuerPlugin +from lemur.plugins.lemur_acme import cloudflare, dyn, route53 -def find_dns_challenge(authz): - for combo in authz.body.resolved_combinations: - if ( - len(combo) == 1 and - isinstance(combo[0].chall, challenges.DNS01) - ): - yield combo[0] +def find_dns_challenge(authorizations): + dns_challenges = [] + for authz in authorizations: + for combo in authz.body.challenges: + if isinstance(combo.chall, challenges.DNS01): + dns_challenges.append(combo) + return dns_challenges class AuthorizationRecord(object): @@ -44,85 +50,90 @@ class AuthorizationRecord(object): self.change_id = change_id -def start_dns_challenge(acme_client, account_number, host, dns_provider): +def maybe_remove_wildcard(host): + return host.replace("*.", "") + + +def start_dns_challenge(acme_client, account_number, host, dns_provider, order): current_app.logger.debug("Starting DNS challenge for {0}".format(host)) - authz = acme_client.request_domain_challenges(host) - [dns_challenge] = find_dns_challenge(authz) + dns_challenges = find_dns_challenge(order.authorizations) + change_ids = [] - change_id = dns_provider.create_txt_record( - dns_challenge.validation_domain_name(host), - dns_challenge.validation(acme_client.key), - account_number - ) + for dns_challenge in find_dns_challenge(order.authorizations): + change_id = dns_provider.create_txt_record( + dns_challenge.validation_domain_name(maybe_remove_wildcard(host)), + dns_challenge.validation(acme_client.client.net.key), + account_number + ) + change_ids.append(change_id) return AuthorizationRecord( host, - authz, - dns_challenge, - change_id, + order.authorizations, + dns_challenges, + change_ids ) def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider): - dns_provider.wait_for_dns_change(authz_record.change_id, account_number=account_number) + current_app.logger.debug("Finalizing DNS challenge for {0}".format(authz_record.authz[0].body.identifier.value)) + for change_id in authz_record.change_id: + dns_provider.wait_for_dns_change(change_id, account_number=account_number) - response = authz_record.dns_challenge.response(acme_client.key) + for dns_challenge in authz_record.dns_challenge: - verified = response.simple_verify( - authz_record.dns_challenge.chall, - authz_record.host, - acme_client.key.public_key() - ) + response = dns_challenge.response(acme_client.client.net.key) - if not verified: - raise ValueError("Failed verification") + verified = response.simple_verify( + dns_challenge.chall, + authz_record.host, + acme_client.client.net.key.public_key() + ) - acme_client.answer_challenge(authz_record.dns_challenge, response) + if not verified: + raise ValueError("Failed verification") + + time.sleep(5) + acme_client.answer_challenge(dns_challenge, response) -def request_certificate(acme_client, authorizations, csr): - cert_response, _ = acme_client.poll_and_request_issuance( - jose.util.ComparableX509( - OpenSSL.crypto.load_certificate_request( - OpenSSL.crypto.FILETYPE_PEM, - csr - ) - ), - authzrs=[authz_record.authz for authz_record in authorizations], - ) +def request_certificate(acme_client, authorizations, csr, order): + for authorization in authorizations: + for authz in authorization.authz: + authorization_resource, _ = acme_client.poll(authz) - pem_certificate = OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_PEM, cert_response.body - ).decode('utf-8') + deadline = datetime.datetime.now() + datetime.timedelta(seconds=90) + orderr = acme_client.finalize_order(order, deadline) + pem_certificate = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, + OpenSSL.crypto.load_certificate(OpenSSL.crypto.FILETYPE_PEM, + orderr.fullchain_pem)).decode() + pem_certificate_chain = orderr.fullchain_pem[len(pem_certificate):].lstrip() - pem_certificate_chain = "\n".join( - OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.decode("utf-8")) - for cert in acme_client.fetch_chain(cert_response) - ).decode('utf-8') - - current_app.logger.debug("{0} {1}".format(type(pem_certificate). type(pem_certificate_chain))) + current_app.logger.debug("{0} {1}".format(type(pem_certificate), type(pem_certificate_chain))) return pem_certificate, pem_certificate_chain -def setup_acme_client(): - email = current_app.config.get('ACME_EMAIL') - tel = current_app.config.get('ACME_TEL') - directory_url = current_app.config.get('ACME_DIRECTORY_URL') - contact = ('mailto:{}'.format(email), 'tel:{}'.format(tel)) +def setup_acme_client(authority): + if not authority.options: + raise InvalidAuthority("Invalid authority. Options not set") + options = {} + + for option in json.loads(authority.options): + options[option["name"]] = option.get("value") + email = options.get('email', current_app.config.get('ACME_EMAIL')) + tel = options.get('telephone', current_app.config.get('ACME_TEL')) + directory_url = options.get('acme_url', current_app.config.get('ACME_DIRECTORY_URL')) key = jose.JWKRSA(key=generate_private_key('RSA2048')) current_app.logger.debug("Connecting with directory at {0}".format(directory_url)) - client = Client(directory_url, key) - - registration = client.register( - messages.NewRegistration.from_data(email=email) - ) + net = ClientNetwork(key, account=None) + client = BackwardsCompatibleClientV2(net, key, directory_url) + registration = client.new_account_and_tos(messages.NewRegistration.from_data(email=email)) current_app.logger.debug("Connected: {0}".format(registration.uri)) - client.agree_to_tos(registration) return client, registration @@ -143,23 +154,25 @@ def get_domains(options): return domains -def get_authorizations(acme_client, account_number, domains, dns_provider): +def get_authorizations(acme_client, order, order_info, dns_provider): authorizations = [] - try: - for domain in domains: - authz_record = start_dns_challenge(acme_client, account_number, domain, dns_provider) - authorizations.append(authz_record) + for domain in order_info.domains: + authz_record = start_dns_challenge(acme_client, order_info.account_number, domain, dns_provider, order) + authorizations.append(authz_record) + return authorizations - for authz_record in authorizations: - complete_dns_challenge(acme_client, account_number, authz_record, dns_provider) - finally: - for authz_record in authorizations: - dns_challenge = authz_record.dns_challenge + +def finalize_authorizations(acme_client, account_number, dns_provider, authorizations): + for authz_record in authorizations: + complete_dns_challenge(acme_client, account_number, authz_record, dns_provider) + for authz_record in authorizations: + dns_challenges = authz_record.dns_challenge + for dns_challenge in dns_challenges: dns_provider.delete_txt_record( authz_record.change_id, account_number, - dns_challenge.validation_domain_name(authz_record.host), - dns_challenge.validation(acme_client.key) + dns_challenge.validation_domain_name(maybe_remove_wildcard(authz_record.host)), + dns_challenge.validation(acme_client.client.net.key) ) return authorizations @@ -171,24 +184,139 @@ class ACMEIssuerPlugin(IssuerPlugin): description = 'Enables the creation of certificates via ACME CAs (including Let\'s Encrypt)' version = acme.VERSION - author = 'Kevin Glisson' + author = 'Netflix' author_url = 'https://github.com/netflix/lemur.git' - def __init__(self, *args, **kwargs): - required_vars = [ - 'ACME_DIRECTORY_URL', - 'ACME_TEL', - 'ACME_EMAIL', - 'ACME_AWS_ACCOUNT_NUMBER', - 'ACME_ROOT' - ] + options = [ + { + 'name': 'acme_url', + 'type': 'str', + 'required': True, + 'validation': '/^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$/', + 'helpMessage': 'Must be a valid web url starting with http[s]://', + }, + { + 'name': 'telephone', + 'type': 'str', + 'default': '', + 'helpMessage': 'Telephone to use' + }, + { + 'name': 'email', + 'type': 'str', + 'default': '', + 'validation': '/^?([-a-zA-Z0-9.`?{}]+@\w+\.\w+)$/', + 'helpMessage': 'Email to use' + }, + { + 'name': 'certificate', + 'type': 'textarea', + 'default': '', + 'validation': '/^-----BEGIN CERTIFICATE-----/', + 'helpMessage': 'Certificate to use' + }, + ] - validate_conf(current_app, required_vars) - self.dns_provider_name = current_app.config.get('ACME_DNS_PROVIDER', 'route53') - current_app.logger.debug("Using DNS provider: {0}".format(self.dns_provider_name)) - self.dns_provider = __import__(self.dns_provider_name, globals(), locals(), [], 1) + def __init__(self, *args, **kwargs): super(ACMEIssuerPlugin, self).__init__(*args, **kwargs) + def get_dns_provider(self, type): + provider_types = { + 'cloudflare': cloudflare, + 'dyn': dyn, + 'route53': route53, + } + provider = provider_types.get(type) + if not provider: + raise UnknownProvider("No such DNS provider: {}".format(type)) + return provider + + def get_ordered_certificate(self, pending_cert): + acme_client, registration = setup_acme_client(pending_cert.authority) + order_info = authorization_service.get(pending_cert.external_id) + dns_provider = dns_provider_service.get(pending_cert.dns_provider_id) + dns_provider_type = self.get_dns_provider(dns_provider.provider_type) + try: + authorizations = get_authorizations( + acme_client, order_info.account_number, order_info.domains, dns_provider_type) + except ClientError: + current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert.name), exc_info=True) + return False + + authorizations = finalize_authorizations( + acme_client, order_info.account_number, dns_provider_type, authorizations) + pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, pending_cert.csr) + cert = { + 'body': "\n".join(str(pem_certificate).splitlines()), + 'chain': "\n".join(str(pem_certificate_chain).splitlines()), + 'external_id': str(pending_cert.external_id) + } + return cert + + def get_ordered_certificates(self, pending_certs): + pending = [] + certs = [] + for pending_cert in pending_certs: + try: + acme_client, registration = setup_acme_client(pending_cert.authority) + order_info = authorization_service.get(pending_cert.external_id) + dns_provider = dns_provider_service.get(pending_cert.dns_provider_id) + dns_provider_type = self.get_dns_provider(dns_provider.provider_type) + try: + order = acme_client.new_order(pending_cert.csr) + except WildcardUnsupportedError: + raise Exception("The currently selected ACME CA endpoint does" + " not support issuing wildcard certificates.") + + authorizations = get_authorizations(acme_client, order, order_info, dns_provider_type) + + pending.append({ + "acme_client": acme_client, + "account_number": order_info.account_number, + "dns_provider_type": dns_provider_type, + "authorizations": authorizations, + "pending_cert": pending_cert, + "order": order, + }) + except (ClientError, ValueError, Exception): + current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True) + certs.append({ + "cert": False, + "pending_cert": pending_cert, + }) + + for entry in pending: + try: + entry["authorizations"] = finalize_authorizations( + entry["acme_client"], + entry["account_number"], + entry["dns_provider_type"], + entry["authorizations"], + ) + pem_certificate, pem_certificate_chain = request_certificate( + entry["acme_client"], + entry["authorizations"], + entry["pending_cert"].csr, + entry["order"] + ) + + cert = { + 'body': "\n".join(str(pem_certificate).splitlines()), + 'chain': "\n".join(str(pem_certificate_chain).splitlines()), + 'external_id': str(entry["pending_cert"].external_id) + } + certs.append({ + "cert": cert, + "pending_cert": entry["pending_cert"], + }) + except (PollError, AcmeError, Exception): + current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True) + certs.append({ + "cert": False, + "pending_cert": entry["pending_cert"], + }) + return certs + def create_certificate(self, csr, issuer_options): """ Creates an ACME certificate. @@ -197,11 +325,37 @@ class ACMEIssuerPlugin(IssuerPlugin): :param issuer_options: :return: :raise Exception: """ - current_app.logger.debug("Requesting a new acme certificate: {0}".format(issuer_options)) - acme_client, registration = setup_acme_client() - account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER') + authority = issuer_options.get('authority') + create_immediately = issuer_options.get('create_immediately', False) + acme_client, registration = setup_acme_client(authority) + dns_provider = issuer_options.get('dns_provider') + if not dns_provider: + raise InvalidConfiguration("DNS Provider setting is required for ACME certificates.") + credentials = json.loads(dns_provider.credentials) + + current_app.logger.debug("Using DNS provider: {0}".format(dns_provider.provider_type)) + dns_provider_type = __import__(dns_provider.provider_type, globals(), locals(), [], 1) + account_number = credentials.get("account_id") + if dns_provider.provider_type == 'route53' and not account_number: + error = "Route53 DNS Provider {} does not have an account number configured.".format(dns_provider.name) + current_app.logger.error(error) + raise InvalidConfiguration(error) domains = get_domains(issuer_options) - authorizations = get_authorizations(acme_client, account_number, domains, self.dns_provider) + if not create_immediately: + # Create pending authorizations that we'll need to do the creation + authz_domains = [] + for d in domains: + if type(d) == str: + authz_domains.append(d) + else: + authz_domains.append(d.value) + + dns_authorization = authorization_service.create(account_number, authz_domains, dns_provider.provider_type) + # Return id of the DNS Authorization + return None, None, dns_authorization.id + + authorizations = get_authorizations(acme_client, account_number, domains, dns_provider_type) + finalize_authorizations(acme_client, account_number, dns_provider_type, authorizations) pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr) # TODO add external ID (if possible) return pem_certificate, pem_certificate_chain, None @@ -216,4 +370,15 @@ class ACMEIssuerPlugin(IssuerPlugin): :return: """ role = {'username': '', 'password': '', 'name': 'acme'} - return current_app.config.get('ACME_ROOT'), "", [role] + plugin_options = options.get('plugin', {}).get('plugin_options') + if not plugin_options: + error = "Invalid options for lemur_acme plugin: {}".format(options) + current_app.logger.error(error) + raise InvalidConfiguration(error) + # Define static acme_root based off configuration variable by default. However, if user has passed a + # certificate, use this certificate as the root. + acme_root = current_app.config.get('ACME_ROOT') + for option in plugin_options: + if option.get('name') == 'certificate': + acme_root = option.get('value') + return acme_root, "", [role] diff --git a/lemur/plugins/lemur_acme/route53.py b/lemur/plugins/lemur_acme/route53.py index 9e5b9688..b55d215b 100644 --- a/lemur/plugins/lemur_acme/route53.py +++ b/lemur/plugins/lemur_acme/route53.py @@ -1,4 +1,5 @@ import time + from lemur.plugins.lemur_aws.sts import sts_client @@ -58,21 +59,23 @@ def change_txt_record(action, zone_id, domain, value, client=None): def create_txt_record(host, value, account_number): zone_id = find_zone_id(host, account_number=account_number) change_id = change_txt_record( - "CREATE", + "UPSERT", zone_id, host, value, account_number=account_number ) + return zone_id, change_id -def delete_txt_record(change_id, account_number, host, value): - zone_id, _ = change_id - change_txt_record( - "DELETE", - zone_id, - host, - value, - account_number=account_number - ) +def delete_txt_record(change_ids, account_number, host, value): + for change_id in change_ids: + zone_id, _ = change_id + change_txt_record( + "DELETE", + zone_id, + host, + value, + account_number=account_number + ) diff --git a/lemur/plugins/lemur_acme/tests/test_acme.py b/lemur/plugins/lemur_acme/tests/test_acme.py index 39d78bbe..c80079af 100644 --- a/lemur/plugins/lemur_acme/tests/test_acme.py +++ b/lemur/plugins/lemur_acme/tests/test_acme.py @@ -1,4 +1,305 @@ +import unittest -def test_get_certificates(app): - from lemur.plugins.base import plugins - p = plugins.get('acme-issuer') +from mock import MagicMock, Mock, patch + +from lemur.plugins.lemur_acme import plugin + + +class TestAcme(unittest.TestCase): + + def setUp(self): + self.ACMEIssuerPlugin = plugin.ACMEIssuerPlugin() + + @patch('lemur.plugins.lemur_acme.plugin.len', return_value=1) + def test_find_dns_challenge(self, mock_len): + assert mock_len + + from acme import challenges + c = challenges.DNS01() + + mock_authz = Mock() + mock_authz.body.resolved_combinations = [] + mock_entry = Mock() + mock_entry.chall = c + mock_authz.body.resolved_combinations.append(mock_entry) + result = yield plugin.find_dns_challenge(mock_authz) + self.assertEqual(result, mock_entry) + + def test_authz_record(self): + a = plugin.AuthorizationRecord("host", "authz", "challenge", "id") + self.assertEqual(type(a), plugin.AuthorizationRecord) + + @patch('acme.client.Client') + @patch('lemur.plugins.lemur_acme.plugin.current_app') + @patch('lemur.plugins.lemur_acme.plugin.len', return_value=1) + @patch('lemur.plugins.lemur_acme.plugin.find_dns_challenge') + def test_start_dns_challenge(self, mock_find_dns_challenge, mock_len, mock_app, mock_acme): + assert mock_len + mock_order = Mock() + mock_app.logger.debug = Mock() + mock_authz = Mock() + mock_authz.body.resolved_combinations = [] + mock_entry = MagicMock() + from acme import challenges + c = challenges.DNS01() + mock_entry.chall = c + mock_authz.body.resolved_combinations.append(mock_entry) + mock_acme.request_domain_challenges = Mock(return_value=mock_authz) + mock_dns_provider = Mock() + mock_dns_provider.create_txt_record = Mock(return_value=1) + + values = [mock_entry] + iterable = mock_find_dns_challenge.return_value + iterator = iter(values) + iterable.__iter__.return_value = iterator + result = plugin.start_dns_challenge(mock_acme, "accountid", "host", mock_dns_provider, mock_order) + self.assertEqual(type(result), plugin.AuthorizationRecord) + + @patch('acme.client.Client') + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_complete_dns_challenge_success(self, mock_current_app, mock_acme): + mock_dns_provider = Mock() + mock_dns_provider.wait_for_dns_change = Mock(return_value=True) + + mock_authz = Mock() + mock_authz.dns_challenge.response = Mock() + mock_authz.dns_challenge.response.simple_verify = Mock(return_value=True) + mock_authz.authz = [] + mock_authz_record = Mock() + mock_authz_record.body.identifier.value = "test" + mock_authz.authz.append(mock_authz_record) + mock_authz.change_id = [] + mock_authz.change_id.append("123") + mock_authz.dns_challenge = [] + dns_challenge = Mock() + mock_authz.dns_challenge.append(dns_challenge) + plugin.complete_dns_challenge(mock_acme, "accountid", mock_authz, mock_dns_provider) + + @patch('acme.client.Client') + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_complete_dns_challenge_fail(self, mock_current_app, mock_acme): + mock_dns_provider = Mock() + mock_dns_provider.wait_for_dns_change = Mock(return_value=True) + + mock_authz = Mock() + mock_authz.dns_challenge.response = Mock() + mock_authz.dns_challenge.response.simple_verify = Mock(return_value=False) + mock_authz.authz = [] + mock_authz_record = Mock() + mock_authz_record.body.identifier.value = "test" + mock_authz.authz.append(mock_authz_record) + mock_authz.change_id = [] + mock_authz.change_id.append("123") + mock_authz.dns_challenge = [] + dns_challenge = Mock() + mock_authz.dns_challenge.append(dns_challenge) + self.assertRaises( + ValueError, + plugin.complete_dns_challenge(mock_acme, "accountid", mock_authz, mock_dns_provider) + ) + + @patch('acme.client.Client') + @patch('OpenSSL.crypto', return_value="mock_cert") + @patch('josepy.util.ComparableX509') + @patch('lemur.plugins.lemur_acme.plugin.find_dns_challenge') + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_request_certificate(self, mock_current_app, mock_find_dns_challenge, mock_jose, mock_crypto, mock_acme): + mock_cert_response = Mock() + mock_cert_response.body = "123" + mock_cert_response_full = [mock_cert_response, True] + mock_acme.poll_and_request_issuance = Mock(return_value=mock_cert_response_full) + mock_authz = [] + mock_authz_record = MagicMock() + mock_authz_record.authz = Mock() + mock_authz.append(mock_authz_record) + mock_acme.fetch_chain = Mock(return_value="mock_chain") + mock_crypto.dump_certificate = Mock(return_value=b'chain') + mock_order = Mock() + plugin.request_certificate(mock_acme, [], "mock_csr", mock_order) + + def test_setup_acme_client_fail(self): + mock_authority = Mock() + mock_authority.options = [] + with self.assertRaises(Exception): + plugin.setup_acme_client(mock_authority) + + @patch('lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2') + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_setup_acme_client_success(self, mock_current_app, mock_acme): + mock_authority = Mock() + mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}]' + mock_client = Mock() + mock_registration = Mock() + mock_registration.uri = "http://test.com" + mock_client.register = mock_registration + mock_client.agree_to_tos = Mock(return_value=True) + mock_acme.return_value = mock_client + result_client, result_registration = plugin.setup_acme_client(mock_authority) + assert result_client + assert result_registration + + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_get_domains_single(self, mock_current_app): + options = { + "common_name": "test.netflix.net" + } + result = plugin.get_domains(options) + self.assertEqual(result, [options["common_name"]]) + + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_get_domains_multiple(self, mock_current_app): + options = { + "common_name": "test.netflix.net", + "extensions": { + "sub_alt_names": { + "names": [ + "test2.netflix.net", + "test3.netflix.net" + ] + } + } + } + result = plugin.get_domains(options) + self.assertEqual(result, [options["common_name"], "test2.netflix.net", "test3.netflix.net"]) + + @patch('lemur.plugins.lemur_acme.plugin.start_dns_challenge', return_value="test") + def test_get_authorizations(self, mock_start_dns_challenge): + mock_order = Mock() + mock_order.body.identifiers = [] + mock_domain = Mock() + mock_order.body.identifiers.append(mock_domain) + mock_order_info = Mock() + mock_order_info.account_number = 1 + mock_order_info.domains = ["test.fakedomain.net"] + result = plugin.get_authorizations("acme_client", mock_order, mock_order_info, "dns_provider") + self.assertEqual(result, ["test"]) + + @patch('lemur.plugins.lemur_acme.plugin.complete_dns_challenge', return_value="test") + def test_finalize_authorizations(self, mock_complete_dns_challenge): + mock_authz = [] + mock_authz_record = MagicMock() + mock_authz_record.authz = Mock() + mock_authz_record.change_id = 1 + mock_authz_record.dns_challenge.validation_domain_name = Mock() + mock_authz_record.dns_challenge.validation = Mock() + mock_authz.append(mock_authz_record) + mock_dns_provider = Mock() + mock_dns_provider.delete_txt_record = Mock() + + mock_acme_client = Mock() + result = plugin.finalize_authorizations(mock_acme_client, "account_number", mock_dns_provider, mock_authz) + self.assertEqual(result, mock_authz) + + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_create_authority(self, mock_current_app): + mock_current_app.config = Mock() + options = { + "plugin": { + "plugin_options": [{ + "name": "certificate", + "value": "123" + }] + } + } + acme_root, b, role = self.ACMEIssuerPlugin.create_authority(options) + self.assertEqual(acme_root, "123") + self.assertEqual(b, "") + self.assertEqual(role, [{'username': '', 'password': '', 'name': 'acme'}]) + + @patch('lemur.plugins.lemur_acme.plugin.current_app') + @patch('lemur.plugins.lemur_acme.dyn.current_app') + @patch('lemur.plugins.lemur_acme.cloudflare.current_app') + def test_get_dns_provider(self, mock_current_app_cloudflare, mock_current_app_dyn, mock_current_app): + provider = plugin.ACMEIssuerPlugin() + route53 = provider.get_dns_provider("route53") + assert route53 + cloudflare = provider.get_dns_provider("cloudflare") + assert cloudflare + dyn = provider.get_dns_provider("dyn") + assert dyn + + @patch('lemur.plugins.lemur_acme.plugin.setup_acme_client') + @patch('lemur.plugins.lemur_acme.plugin.current_app') + @patch('lemur.plugins.lemur_acme.plugin.authorization_service') + @patch('lemur.plugins.lemur_acme.plugin.dns_provider_service') + @patch('lemur.plugins.lemur_acme.plugin.get_authorizations') + @patch('lemur.plugins.lemur_acme.plugin.finalize_authorizations') + @patch('lemur.plugins.lemur_acme.plugin.request_certificate') + def test_get_ordered_certificate( + self, mock_request_certificate, mock_finalize_authorizations, mock_get_authorizations, + mock_dns_provider_service, mock_authorization_service, mock_current_app, mock_acme): + mock_client = Mock() + mock_acme.return_value = (mock_client, "") + mock_request_certificate.return_value = ("pem_certificate", "chain") + + mock_cert = Mock() + mock_cert.external_id = 1 + + provider = plugin.ACMEIssuerPlugin() + provider.get_dns_provider = Mock() + result = provider.get_ordered_certificate(mock_cert) + self.assertEqual( + result, + { + 'body': "pem_certificate", + 'chain': "chain", + 'external_id': "1" + } + ) + + @patch('lemur.plugins.lemur_acme.plugin.setup_acme_client') + @patch('lemur.plugins.lemur_acme.plugin.current_app') + @patch('lemur.plugins.lemur_acme.plugin.authorization_service') + @patch('lemur.plugins.lemur_acme.plugin.dns_provider_service') + @patch('lemur.plugins.lemur_acme.plugin.get_authorizations') + @patch('lemur.plugins.lemur_acme.plugin.finalize_authorizations') + @patch('lemur.plugins.lemur_acme.plugin.request_certificate') + def test_get_ordered_certificates( + self, mock_request_certificate, mock_finalize_authorizations, mock_get_authorizations, + mock_dns_provider_service, mock_authorization_service, mock_current_app, mock_acme): + mock_client = Mock() + mock_acme.return_value = (mock_client, "") + mock_request_certificate.return_value = ("pem_certificate", "chain") + + mock_cert = Mock() + mock_cert.external_id = 1 + + mock_cert2 = Mock() + mock_cert2.external_id = 2 + + provider = plugin.ACMEIssuerPlugin() + provider.get_dns_provider = Mock() + result = provider.get_ordered_certificates([mock_cert, mock_cert2]) + self.assertEqual(len(result), 2) + self.assertEqual(result[0]['cert'], {'body': 'pem_certificate', 'chain': 'chain', 'external_id': '1'}) + self.assertEqual(result[1]['cert'], {'body': 'pem_certificate', 'chain': 'chain', 'external_id': '2'}) + + @patch('lemur.plugins.lemur_acme.plugin.setup_acme_client') + @patch('lemur.plugins.lemur_acme.plugin.dns_provider_service') + @patch('lemur.plugins.lemur_acme.plugin.current_app') + @patch('lemur.plugins.lemur_acme.plugin.get_authorizations') + @patch('lemur.plugins.lemur_acme.plugin.finalize_authorizations') + @patch('lemur.plugins.lemur_acme.plugin.request_certificate') + @patch('lemur.plugins.lemur_acme.plugin.authorization_service') + def test_create_certificate(self, mock_authorization_service, mock_request_certificate, mock_finalize_authorizations, mock_get_authorizations, + mock_current_app, mock_dns_provider_service, mock_acme): + provider = plugin.ACMEIssuerPlugin() + mock_authority = Mock() + + mock_client = Mock() + mock_acme.return_value = (mock_client, "") + + mock_dns_provider = Mock() + mock_dns_provider.credentials = '{"account_id": 1}' + mock_dns_provider.provider_type = "route53" + mock_dns_provider_service.get.return_value = mock_dns_provider + + issuer_options = { + 'authority': mock_authority, + 'dns_provider': mock_dns_provider, + "common_name": "test.netflix.net" + } + csr = "123" + mock_request_certificate.return_value = ("pem_certificate", "chain") + result = provider.create_certificate(csr, issuer_options) + assert result diff --git a/lemur/plugins/lemur_atlas/plugin.py b/lemur/plugins/lemur_atlas/plugin.py index 6b26740f..09d4c9f9 100644 --- a/lemur/plugins/lemur_atlas/plugin.py +++ b/lemur/plugins/lemur_atlas/plugin.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_atlas.plugin :platform: Unix - :copyright: (c) 2016 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/lemur_aws/iam.py b/lemur/plugins/lemur_aws/iam.py index ebe7fa80..53c3719d 100644 --- a/lemur/plugins/lemur_aws/iam.py +++ b/lemur/plugins/lemur_aws/iam.py @@ -2,7 +2,7 @@ .. module: lemur.plugins.lemur_aws.iam :platform: Unix :synopsis: Contains helper functions for interactive with AWS IAM Apis. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/plugins/lemur_aws/plugin.py b/lemur/plugins/lemur_aws/plugin.py index c15e30ca..d959cfdc 100644 --- a/lemur/plugins/lemur_aws/plugin.py +++ b/lemur/plugins/lemur_aws/plugin.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_aws.plugin :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. Terraform example to setup the destination bucket: diff --git a/lemur/plugins/lemur_aws/s3.py b/lemur/plugins/lemur_aws/s3.py index 9e98d3c5..2f8983e5 100644 --- a/lemur/plugins/lemur_aws/s3.py +++ b/lemur/plugins/lemur_aws/s3.py @@ -2,7 +2,7 @@ .. module: lemur.plugins.lemur_aws.s3 :platform: Unix :synopsis: Contains helper functions for interactive with AWS S3 Apis. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/plugins/lemur_aws/sts.py b/lemur/plugins/lemur_aws/sts.py index 0ef1c3f8..001ea2c8 100644 --- a/lemur/plugins/lemur_aws/sts.py +++ b/lemur/plugins/lemur_aws/sts.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_aws.sts :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/plugins/lemur_cfssl/plugin.py b/lemur/plugins/lemur_cfssl/plugin.py index 5f1caf37..530e196d 100644 --- a/lemur/plugins/lemur_cfssl/plugin.py +++ b/lemur/plugins/lemur_cfssl/plugin.py @@ -2,7 +2,7 @@ .. module: lemur.plugins.lemur_cfssl.plugin :platform: Unix :synopsis: This module is responsible for communicating with the CFSSL private CA. - :copyright: (c) 2016 by Thomson Reuters + :copyright: (c) 2018 by Thomson Reuters :license: Apache, see LICENSE for more details. .. moduleauthor:: Charles Hendrie diff --git a/lemur/plugins/lemur_cryptography/plugin.py b/lemur/plugins/lemur_cryptography/plugin.py index 23c691b2..fe9d7bb3 100644 --- a/lemur/plugins/lemur_cryptography/plugin.py +++ b/lemur/plugins/lemur_cryptography/plugin.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_cryptography.plugin :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/lemur_digicert/plugin.py b/lemur/plugins/lemur_digicert/plugin.py index bfe41b4c..619b24e7 100644 --- a/lemur/plugins/lemur_digicert/plugin.py +++ b/lemur/plugins/lemur_digicert/plugin.py @@ -325,8 +325,9 @@ class DigiCertIssuerPlugin(IssuerPlugin): response = self.session.put(create_url, data=json.dumps({'comments': comments})) return handle_response(response) - def get_ordered_certificate(self, order_id): + def get_ordered_certificate(self, pending_cert): """ Retrieve a certificate via order id """ + order_id = pending_cert.external_id base_url = current_app.config.get('DIGICERT_URL') try: certificate_id = get_certificate_id(self.session, base_url, order_id) diff --git a/lemur/plugins/lemur_digicert/tests/test_digicert.py b/lemur/plugins/lemur_digicert/tests/test_digicert.py index 5f14f04b..d8d1519d 100644 --- a/lemur/plugins/lemur_digicert/tests/test_digicert.py +++ b/lemur/plugins/lemur_digicert/tests/test_digicert.py @@ -150,11 +150,7 @@ def test_signature_hash(app): signature_hash('sdfdsf') -def test_issuer_plugin_create_certificate(): - import requests_mock - from lemur.plugins.lemur_digicert.plugin import DigiCertIssuerPlugin - - pem_fixture = """\ +def test_issuer_plugin_create_certificate(certificate_="""\ -----BEGIN CERTIFICATE----- abc -----END CERTIFICATE----- @@ -164,7 +160,11 @@ def -----BEGIN CERTIFICATE----- ghi -----END CERTIFICATE----- -""" +"""): + import requests_mock + from lemur.plugins.lemur_digicert.plugin import DigiCertIssuerPlugin + + pem_fixture = certificate_ subject = DigiCertIssuerPlugin() adapter = requests_mock.Adapter() diff --git a/lemur/plugins/lemur_email/plugin.py b/lemur/plugins/lemur_email/plugin.py index 326fe9e5..18007b99 100644 --- a/lemur/plugins/lemur_email/plugin.py +++ b/lemur/plugins/lemur_email/plugin.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_email.plugin :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/lemur_java/plugin.py b/lemur/plugins/lemur_java/plugin.py index 53ee1618..151794da 100644 --- a/lemur/plugins/lemur_java/plugin.py +++ b/lemur/plugins/lemur_java/plugin.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_java.plugin :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/lemur_kubernetes/plugin.py b/lemur/plugins/lemur_kubernetes/plugin.py index d1d47051..ee466596 100644 --- a/lemur/plugins/lemur_kubernetes/plugin.py +++ b/lemur/plugins/lemur_kubernetes/plugin.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_kubernetes.plugin :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. diff --git a/lemur/plugins/lemur_openssl/plugin.py b/lemur/plugins/lemur_openssl/plugin.py index b013936d..d50b4e43 100644 --- a/lemur/plugins/lemur_openssl/plugin.py +++ b/lemur/plugins/lemur_openssl/plugin.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_openssl.plugin :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/lemur_sftp/plugin.py b/lemur/plugins/lemur_sftp/plugin.py index e782bcf7..d74effc5 100644 --- a/lemur/plugins/lemur_sftp/plugin.py +++ b/lemur/plugins/lemur_sftp/plugin.py @@ -2,7 +2,7 @@ .. module: lemur.plugins.lemur_sftp.plugin :platform: Unix :synopsis: Allow the uploading of certificates to SFTP. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. Allow the uploading of certificates to SFTP. diff --git a/lemur/plugins/lemur_slack/plugin.py b/lemur/plugins/lemur_slack/plugin.py index a8dc52b7..a986aa9a 100644 --- a/lemur/plugins/lemur_slack/plugin.py +++ b/lemur/plugins/lemur_slack/plugin.py @@ -1,7 +1,7 @@ """ .. module: lemur.plugins.lemur_slack.plugin :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Harm Weites diff --git a/lemur/plugins/lemur_verisign/plugin.py b/lemur/plugins/lemur_verisign/plugin.py index 44fa71df..19246eb1 100644 --- a/lemur/plugins/lemur_verisign/plugin.py +++ b/lemur/plugins/lemur_verisign/plugin.py @@ -2,7 +2,7 @@ .. module: lemur.plugins.lemur_verisign.plugin :platform: Unix :synopsis: This module is responsible for communicating with the VeriSign VICE 2.0 API. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/service.py b/lemur/plugins/service.py index 33965963..e91efc2e 100644 --- a/lemur/plugins/service.py +++ b/lemur/plugins/service.py @@ -1,7 +1,7 @@ """ .. module: service :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/plugins/utils.py b/lemur/plugins/utils.py index 2e727a00..a1914dd7 100644 --- a/lemur/plugins/utils.py +++ b/lemur/plugins/utils.py @@ -2,7 +2,7 @@ .. module: lemur.plugins.utils :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/plugins/views.py b/lemur/plugins/views.py index b54ad560..dbdfccab 100644 --- a/lemur/plugins/views.py +++ b/lemur/plugins/views.py @@ -2,7 +2,7 @@ .. module: lemur.plugins.views :platform: Unix :synopsis: This module contains all of the accounts view code. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/policies/cli.py b/lemur/policies/cli.py index 15accd98..725c1583 100644 --- a/lemur/policies/cli.py +++ b/lemur/policies/cli.py @@ -1,7 +1,7 @@ """ .. module: lemur.policies.cli :platform: Unix - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/policies/models.py b/lemur/policies/models.py index 99e33cc0..2329a347 100644 --- a/lemur/policies/models.py +++ b/lemur/policies/models.py @@ -2,7 +2,7 @@ .. module: lemur.policies.models :platform: unix :synopsis: This module contains all of the models need to create a certificate policy within Lemur. - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/policies/schemas.py b/lemur/policies/schemas.py index 190600fe..c20c6156 100644 --- a/lemur/policies/schemas.py +++ b/lemur/policies/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.policies.schemas :platform: unix - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/policies/service.py b/lemur/policies/service.py index c6719a03..10e9053b 100644 --- a/lemur/policies/service.py +++ b/lemur/policies/service.py @@ -1,7 +1,7 @@ """ .. module: lemur.policies.service :platform: Unix - :copyright: (c) 2017 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/reporting/cli.py b/lemur/reporting/cli.py index 8c2fe77a..8f797c33 100644 --- a/lemur/reporting/cli.py +++ b/lemur/reporting/cli.py @@ -1,7 +1,7 @@ """ .. module: lemur.reporting.cli :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/roles/models.py b/lemur/roles/models.py index 21a40be2..85bf1bf1 100644 --- a/lemur/roles/models.py +++ b/lemur/roles/models.py @@ -3,7 +3,7 @@ :platform: unix :synopsis: This module contains all of the models need to create a role within Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/roles/schemas.py b/lemur/roles/schemas.py index bbeb8ef7..319dec5d 100644 --- a/lemur/roles/schemas.py +++ b/lemur/roles/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.roles.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/roles/service.py b/lemur/roles/service.py index 352ebf1f..bbeef1ce 100644 --- a/lemur/roles/service.py +++ b/lemur/roles/service.py @@ -4,7 +4,7 @@ :synopsis: This module contains all of the services level functions used to administer roles in Lemur - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/roles/views.py b/lemur/roles/views.py index d8a328a8..a635fdba 100644 --- a/lemur/roles/views.py +++ b/lemur/roles/views.py @@ -1,7 +1,7 @@ """ .. module: lemur.roles.views :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson diff --git a/lemur/schemas.py b/lemur/schemas.py index 9d1836cd..ffdfe66f 100644 --- a/lemur/schemas.py +++ b/lemur/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson @@ -21,6 +21,7 @@ from lemur.plugins.utils import get_plugin_option from lemur.roles.models import Role from lemur.users.models import User from lemur.authorities.models import Authority +from lemur.dns_providers.models import DnsProvider from lemur.policies.models import RotationPolicy from lemur.certificates.models import Certificate from lemur.destinations.models import Destination @@ -105,6 +106,15 @@ class AssociatedAuthoritySchema(LemurInputSchema): return fetch_objects(Authority, data, many=many) +class AssociatedDnsProviderSchema(LemurInputSchema): + id = fields.Int() + name = fields.String() + + @post_load + def get_object(self, data, many=False): + return fetch_objects(DnsProvider, data, many=many) + + class AssociatedRoleSchema(LemurInputSchema): id = fields.Int() name = fields.String() diff --git a/lemur/sources/cli.py b/lemur/sources/cli.py index 31bdd161..1f2fd9b0 100644 --- a/lemur/sources/cli.py +++ b/lemur/sources/cli.py @@ -1,7 +1,7 @@ """ .. module: lemur.sources.cli :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/sources/models.py b/lemur/sources/models.py index fcf91e48..8cb08eb4 100644 --- a/lemur/sources/models.py +++ b/lemur/sources/models.py @@ -1,7 +1,7 @@ """ .. module: lemur.sources.models :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/sources/schemas.py b/lemur/sources/schemas.py index f5bad719..028fdb32 100644 --- a/lemur/sources/schemas.py +++ b/lemur/sources/schemas.py @@ -1,7 +1,7 @@ """ .. module: lemur.sources.schemas :platform: unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/sources/service.py b/lemur/sources/service.py index 9e6ce289..fbefbba1 100644 --- a/lemur/sources/service.py +++ b/lemur/sources/service.py @@ -1,7 +1,7 @@ """ .. module: lemur.sources.service :platform: Unix - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/sources/views.py b/lemur/sources/views.py index 4ab9ae14..abf68109 100644 --- a/lemur/sources/views.py +++ b/lemur/sources/views.py @@ -2,7 +2,7 @@ .. module: lemur.sources.views :platform: Unix :synopsis: This module contains all of the accounts view code. - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :copyright: (c) 2018 by Netflix Inc., see AUTHORS for more :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ diff --git a/lemur/static/app/angular/app.js b/lemur/static/app/angular/app.js index c19cb37f..b71518b4 100644 --- a/lemur/static/app/angular/app.js +++ b/lemur/static/app/angular/app.js @@ -109,6 +109,15 @@ }; }); + lemur.service('DnsProviders', function (LemurRestangular) { + var DnsProviders = this; + DnsProviders.get = function () { + return LemurRestangular.all('dns_providers').customGET().then(function (dnsProviders) { + return dnsProviders; + }); + }; + }); + lemur.directive('lemurBadRequest', [function () { return { template: '

{{ directiveData.message }}

' + diff --git a/lemur/static/app/angular/authorities/authority/options.tpl.html b/lemur/static/app/angular/authorities/authority/options.tpl.html index 245716cb..dbc4f40a 100644 --- a/lemur/static/app/angular/authorities/authority/options.tpl.html +++ b/lemur/static/app/angular/authorities/authority/options.tpl.html @@ -52,8 +52,58 @@ -
+
+
+ +
+ +
+ + + + + +
+
+ +
+
+ + ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}"> + +
+ + + + + +

{{ item.helpMessage }}

+
+
+ +
+
+ +
+

{{ item.helpMessage }}

+
+
+ +
diff --git a/lemur/static/app/angular/certificates/certificate/certificate.js b/lemur/static/app/angular/certificates/certificate/certificate.js index fdd773fd..abb3a4fa 100644 --- a/lemur/static/app/angular/certificates/certificate/certificate.js +++ b/lemur/static/app/angular/certificates/certificate/certificate.js @@ -134,6 +134,11 @@ angular.module('lemur') $scope.certificate.validityYears = null; }; + CertificateService.getDnsProviders().then(function (providers) { + $scope.dnsProviders = providers; + } + ); + $scope.create = function (certificate) { WizardHandler.wizard().context.loading = true; CertificateService.create(certificate).then( @@ -253,6 +258,11 @@ angular.module('lemur') opened: false }; + CertificateService.getDnsProviders().then(function (providers) { + $scope.dnsProviders = providers; + } + ); + $scope.clearDates = function () { $scope.certificate.validityStart = null; $scope.certificate.validityEnd = null; diff --git a/lemur/static/app/angular/certificates/certificate/options.tpl.html b/lemur/static/app/angular/certificates/certificate/options.tpl.html index fb1d59a1..7e47cf18 100644 --- a/lemur/static/app/angular/certificates/certificate/options.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/options.tpl.html @@ -234,6 +234,9 @@ +
+
+
diff --git a/lemur/static/app/angular/certificates/certificate/tracking.tpl.html b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html index 21277106..fb74d208 100644 --- a/lemur/static/app/angular/certificates/certificate/tracking.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html @@ -107,6 +107,17 @@
+
+ + +
+ +
+