diff --git a/Makefile b/Makefile index f740faab..dbfe920b 100644 --- a/Makefile +++ b/Makefile @@ -123,5 +123,4 @@ endif @echo "--> Done installing new dependencies" @echo "" - .PHONY: develop dev-postgres dev-docs setup-git build clean update-submodules test testloop test-cli test-js test-python lint lint-python lint-js coverage publish release diff --git a/lemur/__init__.py b/lemur/__init__.py index c3661f4e..e3156845 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -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/authorities/models.py b/lemur/authorities/models.py index 45744144..9a7521a9 100644 --- a/lemur/authorities/models.py +++ b/lemur/authorities/models.py @@ -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/service.py b/lemur/authorities/service.py index 0b475e0b..8c80757d 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -8,6 +8,9 @@ .. 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.get('plugin', {}).get('plugin_options', [])) authority = Authority(**kwargs) authority = database.create(authority) 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..1c9e587b --- /dev/null +++ b/lemur/authorizations/models.py @@ -0,0 +1,34 @@ +""" +.. module: lemur.authorizations.models + :platform: unix + :copyright: (c) 2015 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 Authorizations(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 "Authorizations(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..d1a8b874 --- /dev/null +++ b/lemur/authorizations/service.py @@ -0,0 +1,24 @@ +""" +.. module: lemur.pending_certificates.service + Copyright (c) 2017 and onwards Instart Logic, Inc. All rights reserved. +.. moduleauthor:: Secops +""" +from lemur import database + +from lemur.authorizations.models import Authorizations + + +def get(authorization_id): + """ + Retrieve dns authorization by ID + """ + return database.get(Authorizations, authorization_id) + + +def create(account_number, domains, dns_provider_type, options=None): + """ + Creates a new dns authorization. + """ + + authorization = Authorizations(account_number, domains, dns_provider_type, options) + return database.create(authorization) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index a9bb60cc..63d2ea7c 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -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(), nullable=True) not_before = Column(ArrowType) not_after = Column(ArrowType) diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index 651aa647..cce4aa33 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -18,7 +18,8 @@ from lemur.schemas import ( ExtensionSchema, AssociatedRoleSchema, EndpointNestedOutputSchema, - AssociatedRotationPolicySchema + AssociatedRotationPolicySchema, + DnsProviderSchema ) from lemur.authorities.schemas import AuthorityNestedOutputSchema @@ -70,6 +71,7 @@ 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(DnsProviderSchema, missing={}, required=False, allow_none=True) csr = fields.String(validate=validators.csr) key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048') diff --git a/lemur/defaults/views.py b/lemur/defaults/views.py index db849011..5fe3cf41 100644 --- a/lemur/defaults/views.py +++ b/lemur/defaults/views.py @@ -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/dns_providers/models.py b/lemur/dns_providers/models.py new file mode 100644 index 00000000..7a1fdd01 --- /dev/null +++ b/lemur/dns_providers/models.py @@ -0,0 +1,19 @@ +from sqlalchemy import Column, Integer, String, text +from sqlalchemy.dialects.postgresql import JSON +from sqlalchemy_utils import ArrowType + +from lemur.database import db + + +class DnsProviders(db.Model): + __tablename__ = 'dns_providers' + id = Column(Integer(), primary_key=True) + name = Column(String(length=256), unique=True, nullable=True) + description = Column(String(length=1024), nullable=True) + provider_type = Column(String(length=256), nullable=True) + credentials = Column(String(length=256), 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) diff --git a/lemur/dns_providers/schemas.py b/lemur/dns_providers/schemas.py new file mode 100644 index 00000000..df2042c6 --- /dev/null +++ b/lemur/dns_providers/schemas.py @@ -0,0 +1,18 @@ +from lemur.common.fields import ArrowDateTime +from lemur.common.schema import LemurOutputSchema + +from marshmallow import fields + + +class DnsProvidersNestedOutputSchema(LemurOutputSchema): + __envelope__ = False + id = fields.Integer() + name = fields.String() + provider_type = fields.String() + description = fields.String() + credentials = fields.String() + api_endpoint = fields.String() + date_created = ArrowDateTime() + + +dns_provider_schema = DnsProvidersNestedOutputSchema() diff --git a/lemur/dns_providers/service.py b/lemur/dns_providers/service.py new file mode 100644 index 00000000..b2eef9e1 --- /dev/null +++ b/lemur/dns_providers/service.py @@ -0,0 +1,33 @@ +from lemur import database +from lemur.dns_providers.models import DnsProviders + + +def render(args): + """ + Helper that helps us render the REST Api responses. + :param args: + :return: + """ + query = database.session_query(DnsProviders) + + return database.sort_and_page(query, DnsProviders, args) + + +def get(dns_provider_id): + """ + Retrieves a dns provider by its lemur assigned ID. + + :param dns_provider_id: Lemur assigned ID + :rtype : DnsProvider + :return: + """ + return database.get(DnsProviders, dns_provider_id) + + +def delete(dns_provider_id): + """ + Deletes a DNS provider. + + :param dns_provider_id: Lemur assigned ID + """ + database.delete(get(dns_provider_id)) diff --git a/lemur/dns_providers/views.py b/lemur/dns_providers/views.py new file mode 100644 index 00000000..0f324bdf --- /dev/null +++ b/lemur/dns_providers/views.py @@ -0,0 +1,87 @@ +""" +.. module: lemur.dns)providers.views + :platform: Unix + :copyright: (c) 2015 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_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_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('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) + + @admin_permission.require(http_exception=403) + def delete(self, dns_provider_id): + service.delete(dns_provider_id) + return {'result': True} + + +api.add_resource(DnsProvidersList, '/dns_providers', endpoint='dns_providers') diff --git a/lemur/migrations/versions/3adfdd6598df_.py b/lemur/migrations/versions/3adfdd6598df_.py new file mode 100644 index 00000000..07918a0f --- /dev/null +++ b/lemur/migrations/versions/3adfdd6598df_.py @@ -0,0 +1,39 @@ +"""Create dns_providers table + +Revision ID: 3adfdd6598df +Revises: 556ceb3e3c3e +Create Date: 2018-04-10 13:25:47.007556 + +""" + +# revision identifiers, used by Alembic. +revision = '3adfdd6598df' +down_revision = '556ceb3e3c3e' + +from alembic import op +import sqlalchemy as sa +from sqlalchemy.dialects.postgresql import JSON + +from sqlalchemy_utils import ArrowType + + +def upgrade(): + # create provider 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', sa.String(length=256), 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.PrimaryKeyConstraint('id'), + sa.UniqueConstraint('name') + ) + + +def downgrade(): + op.drop_table('dns_providers') diff --git a/lemur/migrations/versions/4e78b9e4e1dd_.py b/lemur/migrations/versions/4e78b9e4e1dd_.py new file mode 100644 index 00000000..bde8c5d4 --- /dev/null +++ b/lemur/migrations/versions/4e78b9e4e1dd_.py @@ -0,0 +1,22 @@ +"""Add dns_provider id column to certificates table + +Revision ID: 4e78b9e4e1dd +Revises: 3adfdd6598df +Create Date: 2018-04-10 14:00:30.701669 + +""" + +# revision identifiers, used by Alembic. +revision = '4e78b9e4e1dd' +down_revision = '3adfdd6598df' + +from alembic import op +import sqlalchemy as sa + + +def upgrade(): + op.add_column('certificates', sa.Column('dns_provider_id', sa.Integer(), nullable=True)) + + +def downgrade(): + op.drop_column('certificates', 'dns_provider_id') diff --git a/lemur/pending_certificates/cli.py b/lemur/pending_certificates/cli.py index ff846fe1..ae69bc0d 100644 --- a/lemur/pending_certificates/cli.py +++ b/lemur/pending_certificates/cli.py @@ -29,7 +29,7 @@ def fetch(ids): 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 diff --git a/lemur/pending_certificates/models.py b/lemur/pending_certificates/models.py index 0e841968..b29759ec 100644 --- a/lemur/pending_certificates/models.py +++ b/lemur/pending_certificates/models.py @@ -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(), nullable=True) 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', {}).get("id") + except AttributeError: + self.dns_provider_id = None diff --git a/lemur/plugins/bases/issuer.py b/lemur/plugins/bases/issuer.py index 1cca60d7..dc9ca076 100644 --- a/lemur/plugins/bases/issuer.py +++ b/lemur/plugins/bases/issuer.py @@ -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/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index 5bdb5514..e7e6d8c3 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -9,8 +9,10 @@ .. moduleauthor:: Kevin Glisson .. moduleauthor:: Mikhail Khodorovskiy +.. moduleauthor:: Curtis Castrapel """ import josepy as jose +import json from flask import current_app @@ -22,7 +24,8 @@ from lemur.common.utils import generate_private_key import OpenSSL.crypto -from lemur.common.utils import validate_conf +from lemur.authorizations import service as authorization_service +from lemur.dns_providers import service as dns_provider_service from lemur.plugins.bases import IssuerPlugin from lemur.plugins import lemur_acme as acme @@ -96,19 +99,26 @@ def request_certificate(acme_client, authorizations, csr): OpenSSL.crypto.FILETYPE_PEM, cert_response.body ).decode('utf-8') - 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') + full_chain = [] + for cert in acme_client.fetch_chain(cert_response): + chain = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert) + full_chain.append(chain.decode("utf-8")) + pem_certificate_chain = "\n".join(full_chain) - 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') +def setup_acme_client(authority): + if not authority.options: + raise Exception("Invalid authority. Options not set") + options = {} + for o in json.loads(authority.options): + print(o) + options[o.get("name")] = o.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')) contact = ('mailto:{}'.format(email), 'tel:{}'.format(tel)) key = jose.JWKRSA(key=generate_private_key('RSA2048')) @@ -145,11 +155,14 @@ def get_domains(options): def get_authorizations(acme_client, account_number, domains, 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 domains: + authz_record = start_dns_challenge(acme_client, account_number, domain, dns_provider) + authorizations.append(authz_record) + return authorizations + +def finalize_authorizations(acme_client, account_number, dns_provider, authorizations): + try: for authz_record in authorizations: complete_dns_challenge(acme_client, account_number, authz_record, dns_provider) finally: @@ -171,24 +184,59 @@ 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_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 = __import__(dns_provider.provider_type, globals(), locals(), [], 1) + authorizations = get_authorizations( + acme_client, order_info.account_number, order_info.domains, dns_provider_type) + + 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 create_certificate(self, csr, issuer_options): """ Creates an ACME certificate. @@ -197,11 +245,38 @@ 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_d = issuer_options.get('dns_provider') + if not dns_provider_d: + raise Exception("DNS Provider setting is required for ACME certificates.") + dns_provider = dns_provider_service.get(dns_provider_d.get("id")) + 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_number") + if dns_provider.provider_type == 'route53' and not account_number: + error = "DNS Provider {} does not have an account number configured.".format(dns_provider.name) + current_app.logger.error(error) + raise Exception(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 +291,11 @@ 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') + # 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..a95cfcd1 100644 --- a/lemur/plugins/lemur_acme/route53.py +++ b/lemur/plugins/lemur_acme/route53.py @@ -58,7 +58,7 @@ 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, 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/schemas.py b/lemur/schemas.py index 9d1836cd..09915e70 100644 --- a/lemur/schemas.py +++ b/lemur/schemas.py @@ -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 DnsProviders from lemur.policies.models import RotationPolicy from lemur.certificates.models import Certificate from lemur.destinations.models import Destination @@ -159,6 +160,11 @@ class AssociatedRotationPolicySchema(LemurInputSchema): return fetch_objects(RotationPolicy, data, many=many) +class DnsProviderSchema(LemurInputSchema): + id = fields.Integer() + name = fields.String() + + class PluginInputSchema(LemurInputSchema): plugin_options = fields.List(fields.Dict(), validate=validate_options) slug = fields.String(required=True) 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..1294dd32 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( 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 @@
+
+ + +
+ +
+