Merge pull request #1553 from castrapel/fix_le_renew

Allow auto-detection of DNS providers / Fix acme renewal flow
This commit is contained in:
Curtis 2018-08-13 15:22:45 -07:00 committed by GitHub
commit e050177c08
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 482 additions and 301 deletions

1
.gitignore vendored
View File

@ -10,6 +10,7 @@
*.db *.db
*.pid *.pid
*.enc *.enc
*.env
MANIFEST MANIFEST
test.conf test.conf
pip-log.txt pip-log.txt

View File

@ -101,7 +101,7 @@ class Certificate(db.Model):
serial = Column(String(128)) serial = Column(String(128))
cn = Column(String(128)) cn = Column(String(128))
deleted = Column(Boolean, index=True) deleted = Column(Boolean, index=True)
dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='cascade'), nullable=True) dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='CASCADE'), nullable=True)
not_before = Column(ArrowType) not_before = Column(ArrowType)
not_after = Column(ArrowType) not_after = Column(ArrowType)

View File

@ -10,6 +10,7 @@ from marshmallow import fields, validate, validates_schema, post_load, pre_load
from marshmallow.exceptions import ValidationError from marshmallow.exceptions import ValidationError
from lemur.authorities.schemas import AuthorityNestedOutputSchema from lemur.authorities.schemas import AuthorityNestedOutputSchema
from lemur.dns_providers.schemas import DnsProvidersNestedOutputSchema
from lemur.common import validators, missing from lemur.common import validators, missing
from lemur.common.fields import ArrowDateTime, Hex from lemur.common.fields import ArrowDateTime, Hex
from lemur.common.schema import LemurInputSchema, LemurOutputSchema from lemur.common.schema import LemurInputSchema, LemurOutputSchema
@ -223,6 +224,7 @@ class CertificateOutputSchema(LemurOutputSchema):
notifications = fields.Nested(NotificationNestedOutputSchema, many=True) notifications = fields.Nested(NotificationNestedOutputSchema, many=True)
replaces = fields.Nested(CertificateNestedOutputSchema, many=True) replaces = fields.Nested(CertificateNestedOutputSchema, many=True)
authority = fields.Nested(AuthorityNestedOutputSchema) authority = fields.Nested(AuthorityNestedOutputSchema)
dns_provider = fields.Nested(DnsProvidersNestedOutputSchema)
roles = fields.Nested(RoleNestedOutputSchema, many=True) roles = fields.Nested(RoleNestedOutputSchema, many=True)
endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[]) endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[])
replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced') replaced_by = fields.Nested(CertificateNestedOutputSchema, many=True, attribute='replaced')

View File

@ -0,0 +1,27 @@
from flask_script import Manager
from lemur.constants import SUCCESS_METRIC_STATUS
from lemur.dns_providers.service import get_all_dns_providers, set_domains
from lemur.extensions import metrics
from lemur.plugins.base import plugins
manager = Manager(usage="Iterates through all DNS providers and sets DNS zones in the database.")
@manager.command
def get_all_zones():
"""
Retrieves all DNS providers from the database. Refreshes the zones associated with each DNS provider
"""
print("[+] Starting dns provider zone lookup and configuration.")
dns_providers = get_all_dns_providers()
acme_plugin = plugins.get("acme-issuer")
for dns_provider in dns_providers:
zones = acme_plugin.get_all_zones(dns_provider)
set_domains(dns_provider, zones)
status = SUCCESS_METRIC_STATUS
metrics.send('get_all_zones', 'counter', 1, metric_tags={'status': status})
print("[+] Done with dns provider zone lookup and configuration.")

View File

@ -1,5 +1,6 @@
from sqlalchemy import Column, Integer, String, text, Text from sqlalchemy import Column, Integer, String, text, Text
from sqlalchemy.dialects.postgresql import JSON from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy.orm import relationship
from sqlalchemy_utils import ArrowType from sqlalchemy_utils import ArrowType
from lemur.database import db from lemur.database import db
@ -22,6 +23,7 @@ class DnsProvider(db.Model):
status = Column(String(length=128), nullable=True) status = Column(String(length=128), nullable=True)
options = Column(JSON, nullable=True) options = Column(JSON, nullable=True)
domains = Column(JSON, nullable=True) domains = Column(JSON, nullable=True)
certificates = relationship("Certificate", backref='dns_provider', foreign_keys='Certificate.dns_provider_id')
def __init__(self, name, description, provider_type, credentials): def __init__(self, name, description, provider_type, credentials):
self.name = name self.name = name

View File

@ -22,6 +22,15 @@ def get(dns_provider_id):
return provider return provider
def get_all_dns_providers():
"""
Retrieves all dns providers within Lemur.
:return:
"""
return DnsProvider.query.all()
def get_friendly(dns_provider_id): def get_friendly(dns_provider_id):
""" """
Retrieves a dns provider by its lemur assigned ID. Retrieves a dns provider by its lemur assigned ID.
@ -96,6 +105,15 @@ def get_types():
return provider_config return provider_config
def set_domains(dns_provider, domains):
"""
Increments pending certificate attempt counter and updates it in the database.
"""
dns_provider.domains = domains
database.update(dns_provider)
return dns_provider
def create(data): def create(data):
provider_name = data.get("name") provider_name = data.get("name")

View File

@ -15,6 +15,7 @@ from flask_script import Manager, Command, Option, prompt_pass
from flask_migrate import Migrate, MigrateCommand, stamp from flask_migrate import Migrate, MigrateCommand, stamp
from flask_script.commands import ShowUrls, Clean, Server from flask_script.commands import ShowUrls, Clean, Server
from lemur.dns_providers.cli import manager as dns_provider_manager
from lemur.sources.cli import manager as source_manager from lemur.sources.cli import manager as source_manager
from lemur.policies.cli import manager as policy_manager from lemur.policies.cli import manager as policy_manager
from lemur.reporting.cli import manager as report_manager from lemur.reporting.cli import manager as report_manager
@ -539,6 +540,7 @@ def main():
manager.add_command("report", report_manager) manager.add_command("report", report_manager)
manager.add_command("policy", policy_manager) manager.add_command("policy", policy_manager)
manager.add_command("pending_certs", pending_certificate_manager) manager.add_command("pending_certs", pending_certificate_manager)
manager.add_command("dns_providers", dns_provider_manager)
manager.run() manager.run()

View File

@ -5,7 +5,7 @@ import dns.exception
import dns.name import dns.name
import dns.query import dns.query
import dns.resolver import dns.resolver
from dyn.tm.errors import DynectGetError from dyn.tm.errors import DynectCreateError
from dyn.tm.session import DynectSession from dyn.tm.session import DynectSession
from dyn.tm.zones import Node, Zone, get_all_zones from dyn.tm.zones import Node, Zone, get_all_zones
from flask import current_app from flask import current_app
@ -49,6 +49,7 @@ def wait_for_dns_change(change_id, account_number=None):
break break
time.sleep(20) time.sleep(20)
if not status: if not status:
# TODO: Delete associated DNS text record here
raise Exception("Unable to query DNS token for fqdn {}.".format(fqdn)) raise Exception("Unable to query DNS token for fqdn {}.".format(fqdn))
return return
@ -70,6 +71,15 @@ def get_zone_name(domain):
return zone_name return zone_name
def get_zones(account_number):
get_dynect_session()
zones = get_all_zones()
zone_list = []
for zone in zones:
zone_list.append(zone.name)
return zone_list
def create_txt_record(domain, token, account_number): def create_txt_record(domain, token, account_number):
get_dynect_session() get_dynect_session()
zone_name = get_zone_name(domain) zone_name = get_zone_name(domain)
@ -77,21 +87,20 @@ def create_txt_record(domain, token, account_number):
node_name = '.'.join(domain.split('.')[:-zone_parts]) node_name = '.'.join(domain.split('.')[:-zone_parts])
fqdn = "{0}.{1}".format(node_name, zone_name) fqdn = "{0}.{1}".format(node_name, zone_name)
zone = Zone(zone_name) zone = Zone(zone_name)
try: try:
# Delete all stale ACME TXT records zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5)
delete_acme_txt_records(domain) zone.publish()
except DynectGetError as e: current_app.logger.debug("TXT record created: {0}, token: {1}".format(fqdn, token))
if ( except DynectCreateError as e:
"No such zone." in e.message or if "Cannot duplicate existing record data" in e.message:
"Host is not in this zone" in e.message or current_app.logger.debug(
"Host not found in this zone" in e.message "Unable to add record. Domain: {}. Token: {}. "
): "Record already exists: {}".format(domain, token, e), exc_info=True
current_app.logger.debug("Unable to delete ACME TXT records. They probably don't exist yet: {}".format(e)) )
else: else:
raise raise
zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5)
zone.publish()
current_app.logger.debug("TXT record created: {0}".format(fqdn))
change_id = (fqdn, token) change_id = (fqdn, token)
return change_id return change_id

View File

@ -33,15 +33,6 @@ from lemur.plugins.bases import IssuerPlugin
from lemur.plugins.lemur_acme import cloudflare, dyn, route53 from lemur.plugins.lemur_acme import cloudflare, dyn, route53
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): class AuthorizationRecord(object):
def __init__(self, host, authz, dns_challenge, change_id): def __init__(self, host, authz, dns_challenge, change_id):
self.host = host self.host = host
@ -50,192 +41,249 @@ class AuthorizationRecord(object):
self.change_id = change_id self.change_id = change_id
def maybe_remove_wildcard(host): class AcmeHandler(object):
return host.replace("*.", "") def __init__(self):
self.dns_providers_for_domain = {}
self.all_dns_providers = dns_provider_service.get_all_dns_providers()
def find_dns_challenge(self, 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
def maybe_add_extension(host, dns_provider_options): def maybe_remove_wildcard(self, host):
if dns_provider_options and dns_provider_options.get("acme_challenge_extension"): return host.replace("*.", "")
host = host + dns_provider_options.get("acme_challenge_extension")
return host
def maybe_add_extension(self, host, dns_provider_options):
if dns_provider_options and dns_provider_options.get("acme_challenge_extension"):
host = host + dns_provider_options.get("acme_challenge_extension")
return host
def start_dns_challenge(acme_client, account_number, host, dns_provider, order, dns_provider_options): def start_dns_challenge(self, acme_client, account_number, host, dns_provider, order, dns_provider_options):
current_app.logger.debug("Starting DNS challenge for {0}".format(host)) current_app.logger.debug("Starting DNS challenge for {0}".format(host))
dns_challenges = find_dns_challenge(order.authorizations) dns_challenges = self.find_dns_challenge(order.authorizations)
change_ids = [] change_ids = []
host_to_validate = maybe_remove_wildcard(host) host_to_validate = self.maybe_remove_wildcard(host)
host_to_validate = maybe_add_extension(host_to_validate, dns_provider_options) host_to_validate = self.maybe_add_extension(host_to_validate, dns_provider_options)
for dns_challenge in find_dns_challenge(order.authorizations): for dns_challenge in self.find_dns_challenge(order.authorizations):
change_id = dns_provider.create_txt_record( change_id = dns_provider.create_txt_record(
dns_challenge.validation_domain_name(host_to_validate),
dns_challenge.validation(acme_client.client.net.key),
account_number
)
change_ids.append(change_id)
return AuthorizationRecord(
host,
order.authorizations,
dns_challenges,
change_ids
)
def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider):
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)
for dns_challenge in authz_record.dns_challenge:
response = dns_challenge.response(acme_client.client.net.key)
verified = response.simple_verify(
dns_challenge.chall,
authz_record.host,
acme_client.client.net.key.public_key()
)
if not verified:
raise ValueError("Failed verification")
time.sleep(5)
acme_client.answer_challenge(dns_challenge, response)
def request_certificate(acme_client, authorizations, csr, order):
for authorization in authorizations:
for authz in authorization.authz:
authorization_resource, _ = acme_client.poll(authz)
deadline = datetime.datetime.now() + datetime.timedelta(seconds=90)
try:
orderr = acme_client.finalize_order(order, deadline)
except AcmeError:
current_app.logger.error("Unable to resolve Acme order: {}".format(order), exc_info=True)
raise
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()
current_app.logger.debug("{0} {1}".format(type(pem_certificate), type(pem_certificate_chain)))
return pem_certificate, pem_certificate_chain
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'))
existing_key = options.get('acme_private_key', current_app.config.get('ACME_PRIVATE_KEY'))
existing_regr = options.get('acme_regr', current_app.config.get('ACME_REGR'))
if existing_key and existing_regr:
# Reuse the same account for each certificate issuance
key = jose.JWK.json_loads(existing_key)
regr = messages.RegistrationResource.json_loads(existing_regr)
current_app.logger.debug("Connecting with directory at {0}".format(directory_url))
net = ClientNetwork(key, account=regr)
client = BackwardsCompatibleClientV2(net, key, directory_url)
return client, {}
else:
# Create an account for each certificate issuance
key = jose.JWKRSA(key=generate_private_key('RSA2048'))
current_app.logger.debug("Connecting with directory at {0}".format(directory_url))
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))
return client, registration
def get_domains(options):
"""
Fetches all domains currently requested
:param options:
:return:
"""
current_app.logger.debug("Fetching domains")
domains = [options['common_name']]
if options.get('extensions'):
for name in options['extensions']['sub_alt_names']['names']:
domains.append(name)
current_app.logger.debug("Got these domains: {0}".format(domains))
return domains
def get_authorizations(acme_client, order, order_info, dns_provider, dns_provider_options):
authorizations = []
for domain in order_info.domains:
authz_record = start_dns_challenge(acme_client, order_info.account_number, domain, dns_provider, order,
dns_provider_options)
authorizations.append(authz_record)
return authorizations
def finalize_authorizations(acme_client, account_number, dns_provider, authorizations, dns_provider_options):
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
host_to_validate = maybe_remove_wildcard(authz_record.host)
host_to_validate = maybe_add_extension(host_to_validate, dns_provider_options)
for dns_challenge in dns_challenges:
dns_provider.delete_txt_record(
authz_record.change_id,
account_number,
dns_challenge.validation_domain_name(host_to_validate), dns_challenge.validation_domain_name(host_to_validate),
dns_challenge.validation(acme_client.client.net.key) dns_challenge.validation(acme_client.client.net.key),
account_number
) )
change_ids.append(change_id)
return authorizations return AuthorizationRecord(
host,
order.authorizations,
dns_challenges,
change_ids
)
def complete_dns_challenge(self, acme_client, authz_record):
current_app.logger.debug("Finalizing DNS challenge for {0}".format(authz_record.authz[0].body.identifier.value))
dns_providers = self.dns_providers_for_domain.get(authz_record.host)
if not dns_providers:
raise Exception("No DNS providers found for domain: {}".format(authz_record.host))
def cleanup_dns_challenges(acme_client, account_number, dns_provider, authorizations, dns_provider_options): for dns_provider in dns_providers:
""" # Grab account number (For Route53)
Best effort attempt to delete DNS challenges that may not have been deleted previously. This is usually called dns_provider_options = json.loads(dns_provider.credentials)
on an exception account_number = dns_provider_options.get("account_id")
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
for change_id in authz_record.change_id:
dns_provider_plugin.wait_for_dns_change(change_id, account_number=account_number)
:param acme_client: for dns_challenge in authz_record.dns_challenge:
:param account_number: response = dns_challenge.response(acme_client.client.net.key)
:param dns_provider:
:param authorizations: verified = response.simple_verify(
:param dns_provider_options: dns_challenge.chall,
:return: authz_record.host,
""" acme_client.client.net.key.public_key()
for authz_record in authorizations:
dns_challenges = authz_record.dns_challenge
host_to_validate = maybe_remove_wildcard(authz_record.host)
host_to_validate = maybe_add_extension(host_to_validate, dns_provider_options)
for dns_challenge in dns_challenges:
try:
dns_provider.delete_txt_record(
authz_record.change_id,
account_number,
dns_challenge.validation_domain_name(host_to_validate),
dns_challenge.validation(acme_client.client.net.key)
) )
except Exception:
# If this fails, it's most likely because the record doesn't exist or we're not authorized to modify it. if not verified:
pass raise ValueError("Failed verification")
time.sleep(5)
acme_client.answer_challenge(dns_challenge, response)
def request_certificate(self, acme_client, authorizations, order):
for authorization in authorizations:
for authz in authorization.authz:
authorization_resource, _ = acme_client.poll(authz)
deadline = datetime.datetime.now() + datetime.timedelta(seconds=90)
try:
orderr = acme_client.finalize_order(order, deadline)
except AcmeError:
current_app.logger.error("Unable to resolve Acme order: {}".format(order), exc_info=True)
raise
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()
current_app.logger.debug("{0} {1}".format(type(pem_certificate), type(pem_certificate_chain)))
return pem_certificate, pem_certificate_chain
def setup_acme_client(self, 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'))
existing_key = options.get('acme_private_key', current_app.config.get('ACME_PRIVATE_KEY'))
existing_regr = options.get('acme_regr', current_app.config.get('ACME_REGR'))
if existing_key and existing_regr:
# Reuse the same account for each certificate issuance
key = jose.JWK.json_loads(existing_key)
regr = messages.RegistrationResource.json_loads(existing_regr)
current_app.logger.debug("Connecting with directory at {0}".format(directory_url))
net = ClientNetwork(key, account=regr)
client = BackwardsCompatibleClientV2(net, key, directory_url)
return client, {}
else:
# Create an account for each certificate issuance
key = jose.JWKRSA(key=generate_private_key('RSA2048'))
current_app.logger.debug("Connecting with directory at {0}".format(directory_url))
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))
return client, registration
def get_domains(self, options):
"""
Fetches all domains currently requested
:param options:
:return:
"""
current_app.logger.debug("Fetching domains")
domains = [options['common_name']]
if options.get('extensions'):
for name in options['extensions']['sub_alt_names']['names']:
domains.append(name)
current_app.logger.debug("Got these domains: {0}".format(domains))
return domains
def get_authorizations(self, acme_client, order, order_info):
authorizations = []
for domain in order_info.domains:
if not self.dns_providers_for_domain.get(domain):
raise Exception("No DNS providers found for domain: {}".format(domain))
for dns_provider in self.dns_providers_for_domain[domain]:
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
authz_record = self.start_dns_challenge(acme_client, account_number, domain,
dns_provider_plugin,
order,
dns_provider.options)
authorizations.append(authz_record)
return authorizations
def autodetect_dns_providers(self, domain):
"""
Get DNS providers associated with a domain when it has not been provided for certificate creation.
:param domain:
:return: dns_providers: List of DNS providers that have the correct zone.
"""
self.dns_providers_for_domain[domain] = []
for dns_provider in self.all_dns_providers:
for name in dns_provider.domains:
if domain.endswith(name):
self.dns_providers_for_domain[domain].append(dns_provider)
return self.dns_providers_for_domain
def finalize_authorizations(self, acme_client, authorizations):
for authz_record in authorizations:
self.complete_dns_challenge(acme_client, authz_record)
for authz_record in authorizations:
dns_challenges = authz_record.dns_challenge
for dns_challenge in dns_challenges:
dns_providers = self.dns_providers_for_domain.get(authz_record.host)
for dns_provider in dns_providers:
# Grab account number (For Route53)
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
host_to_validate = self.maybe_remove_wildcard(authz_record.host)
host_to_validate = self.maybe_add_extension(host_to_validate, dns_provider_options)
dns_provider_plugin.delete_txt_record(
authz_record.change_id,
account_number,
dns_challenge.validation_domain_name(host_to_validate),
dns_challenge.validation(acme_client.client.net.key)
)
return authorizations
def cleanup_dns_challenges(self, acme_client, authorizations):
"""
Best effort attempt to delete DNS challenges that may not have been deleted previously. This is usually called
on an exception
:param acme_client:
:param account_number:
:param dns_provider:
:param authorizations:
:param dns_provider_options:
:return:
"""
for authz_record in authorizations:
dns_providers = self.dns_providers_for_domain.get(authz_record.host)
for dns_provider in dns_providers:
# Grab account number (For Route53)
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
dns_challenges = authz_record.dns_challenge
host_to_validate = self.maybe_remove_wildcard(authz_record.host)
host_to_validate = self.maybe_add_extension(host_to_validate, dns_provider_options)
for dns_challenge in dns_challenges:
try:
dns_provider.delete_txt_record(
authz_record.change_id,
account_number,
dns_challenge.validation_domain_name(host_to_validate),
dns_challenge.validation(acme_client.client.net.key)
)
except Exception as e:
# If this fails, it's most likely because the record doesn't exist (It was already cleaned up)
# or we're not authorized to modify it.
pass
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
class ACMEIssuerPlugin(IssuerPlugin): class ACMEIssuerPlugin(IssuerPlugin):
@ -279,6 +327,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs) super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
self.acme = AcmeHandler()
def get_dns_provider(self, type): def get_dns_provider(self, type):
provider_types = { provider_types = {
@ -291,22 +340,40 @@ class ACMEIssuerPlugin(IssuerPlugin):
raise UnknownProvider("No such DNS provider: {}".format(type)) raise UnknownProvider("No such DNS provider: {}".format(type))
return provider return provider
def get_all_zones(self, dns_provider):
dns_provider_options = json.loads(dns_provider.credentials)
account_number = dns_provider_options.get("account_id")
dns_provider_plugin = self.get_dns_provider(dns_provider.provider_type)
return dns_provider_plugin.get_zones(account_number=account_number)
def get_ordered_certificate(self, pending_cert): def get_ordered_certificate(self, pending_cert):
acme_client, registration = setup_acme_client(pending_cert.authority) acme_client, registration = self.acme.setup_acme_client(pending_cert.authority)
order_info = authorization_service.get(pending_cert.external_id) order_info = authorization_service.get(pending_cert.external_id)
dns_provider = dns_provider_service.get(pending_cert.dns_provider_id) if pending_cert.dns_provider_id:
dns_provider_options = dns_provider.options dns_provider = dns_provider_service.get(pending_cert.dns_provider_id)
dns_provider_type = self.get_dns_provider(dns_provider.provider_type)
for domain in order_info.domains:
# Currently, we only support specifying one DNS provider per certificate, even if that
# certificate has multiple SANs that may belong to different providers.
self.acme.dns_providers_for_domain[domain] = [dns_provider]
else:
for domain in order_info.domains:
self.acme.autodetect_dns_providers(domain)
try: try:
authorizations = get_authorizations( order = acme_client.new_order(pending_cert.csr)
acme_client, order_info.account_number, order_info.domains, dns_provider_type, dns_provider_options) except WildcardUnsupportedError:
raise Exception("The currently selected ACME CA endpoint does"
" not support issuing wildcard certificates.")
try:
authorizations = self.acme.get_authorizations(acme_client, order, order_info)
except ClientError: except ClientError:
current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert.name), exc_info=True) current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert.name), exc_info=True)
return False return False
authorizations = finalize_authorizations( authorizations = self.acme.finalize_authorizations(acme_client, authorizations)
acme_client, order_info.account_number, dns_provider_type, authorizations, dns_provider_options) pem_certificate, pem_certificate_chain = self.acme.request_certificate(
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, pending_cert.csr) acme_client, authorizations, order)
cert = { cert = {
'body': "\n".join(str(pem_certificate).splitlines()), 'body': "\n".join(str(pem_certificate).splitlines()),
'chain': "\n".join(str(pem_certificate_chain).splitlines()), 'chain': "\n".join(str(pem_certificate_chain).splitlines()),
@ -319,28 +386,32 @@ class ACMEIssuerPlugin(IssuerPlugin):
certs = [] certs = []
for pending_cert in pending_certs: for pending_cert in pending_certs:
try: try:
acme_client, registration = setup_acme_client(pending_cert.authority) acme_client, registration = self.acme.setup_acme_client(pending_cert.authority)
order_info = authorization_service.get(pending_cert.external_id) order_info = authorization_service.get(pending_cert.external_id)
dns_provider = dns_provider_service.get(pending_cert.dns_provider_id) if pending_cert.dns_provider_id:
dns_provider_options = dns_provider.options dns_provider = dns_provider_service.get(pending_cert.dns_provider_id)
dns_provider_type = self.get_dns_provider(dns_provider.provider_type)
for domain in order_info.domains:
# Currently, we only support specifying one DNS provider per certificate, even if that
# certificate has multiple SANs that may belong to different providers.
self.acme.dns_providers_for_domain[domain] = [dns_provider]
else:
for domain in order_info.domains:
self.acme.autodetect_dns_providers(domain)
try: try:
order = acme_client.new_order(pending_cert.csr) order = acme_client.new_order(pending_cert.csr)
except WildcardUnsupportedError: except WildcardUnsupportedError:
raise Exception("The currently selected ACME CA endpoint does" raise Exception("The currently selected ACME CA endpoint does"
" not support issuing wildcard certificates.") " not support issuing wildcard certificates.")
authorizations = get_authorizations(acme_client, order, order_info, dns_provider_type, authorizations = self.acme.get_authorizations(acme_client, order, order_info)
dns_provider_options)
pending.append({ pending.append({
"acme_client": acme_client, "acme_client": acme_client,
"account_number": order_info.account_number,
"dns_provider_type": dns_provider_type,
"authorizations": authorizations, "authorizations": authorizations,
"pending_cert": pending_cert, "pending_cert": pending_cert,
"order": order, "order": order,
"dns_provider_options": dns_provider_options,
}) })
except (ClientError, ValueError, Exception) as e: except (ClientError, ValueError, Exception) as e:
current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True) current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True)
@ -352,17 +423,13 @@ class ACMEIssuerPlugin(IssuerPlugin):
for entry in pending: for entry in pending:
try: try:
entry["authorizations"] = finalize_authorizations( entry["authorizations"] = self.acme.finalize_authorizations(
entry["acme_client"], entry["acme_client"],
entry["account_number"],
entry["dns_provider_type"],
entry["authorizations"], entry["authorizations"],
entry["dns_provider_options"],
) )
pem_certificate, pem_certificate_chain = request_certificate( pem_certificate, pem_certificate_chain = self.acme.request_certificate(
entry["acme_client"], entry["acme_client"],
entry["authorizations"], entry["authorizations"],
entry["pending_cert"].csr,
entry["order"] entry["order"]
) )
@ -383,12 +450,9 @@ class ACMEIssuerPlugin(IssuerPlugin):
"last_error": e, "last_error": e,
}) })
# Ensure DNS records get deleted # Ensure DNS records get deleted
cleanup_dns_challenges( self.acme.cleanup_dns_challenges(
entry["acme_client"], entry["acme_client"],
entry["account_number"],
entry["dns_provider_type"],
entry["authorizations"], entry["authorizations"],
entry["dns_provider_options"],
) )
return certs return certs
@ -402,21 +466,25 @@ class ACMEIssuerPlugin(IssuerPlugin):
""" """
authority = issuer_options.get('authority') authority = issuer_options.get('authority')
create_immediately = issuer_options.get('create_immediately', False) create_immediately = issuer_options.get('create_immediately', False)
acme_client, registration = setup_acme_client(authority) acme_client, registration = self.acme.setup_acme_client(authority)
dns_provider = issuer_options.get('dns_provider') dns_provider = issuer_options.get('dns_provider', {})
dns_provider_options = dns_provider.options
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)) if dns_provider:
dns_provider_type = __import__(dns_provider.provider_type, globals(), locals(), [], 1) dns_provider_options = dns_provider.options
account_number = credentials.get("account_id") credentials = json.loads(dns_provider.credentials)
if dns_provider.provider_type == 'route53' and not account_number: current_app.logger.debug("Using DNS provider: {0}".format(dns_provider.provider_type))
error = "Route53 DNS Provider {} does not have an account number configured.".format(dns_provider.name) dns_provider_plugin = __import__(dns_provider.provider_type, globals(), locals(), [], 1)
current_app.logger.error(error) account_number = credentials.get("account_id")
raise InvalidConfiguration(error) if dns_provider.provider_type == 'route53' and not account_number:
domains = get_domains(issuer_options) error = "Route53 DNS Provider {} does not have an account number configured.".format(dns_provider.name)
current_app.logger.error(error)
raise InvalidConfiguration(error)
else:
dns_provider = {}
dns_provider_options = None
account_number = None
domains = self.acme.get_domains(issuer_options)
if not create_immediately: if not create_immediately:
# Create pending authorizations that we'll need to do the creation # Create pending authorizations that we'll need to do the creation
authz_domains = [] authz_domains = []
@ -426,14 +494,16 @@ class ACMEIssuerPlugin(IssuerPlugin):
else: else:
authz_domains.append(d.value) authz_domains.append(d.value)
dns_authorization = authorization_service.create(account_number, authz_domains, dns_provider.provider_type) dns_authorization = authorization_service.create(account_number, authz_domains,
dns_provider.get("provider_type"))
# Return id of the DNS Authorization # Return id of the DNS Authorization
return None, None, dns_authorization.id return None, None, dns_authorization.id
authorizations = get_authorizations(acme_client, account_number, domains, dns_provider_type, authorizations = self.acme.get_authorizations(acme_client, account_number, domains, dns_provider_plugin,
dns_provider_options) dns_provider_options)
finalize_authorizations(acme_client, account_number, dns_provider_type, authorizations, dns_provider_options) self.acme.finalize_authorizations(acme_client, account_number, dns_provider_plugin, authorizations,
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr) dns_provider_options)
pem_certificate, pem_certificate_chain = self.acme.request_certificate(acme_client, authorizations, csr)
# TODO add external ID (if possible) # TODO add external ID (if possible)
return pem_certificate, pem_certificate_chain, None return pem_certificate, pem_certificate_chain, None

View File

@ -31,6 +31,16 @@ def find_zone_id(domain, client=None):
return zones[0][1] return zones[0][1]
@sts_client('route53')
def get_zones(client=None):
paginator = client.get_paginator("list_hosted_zones")
zones = []
for page in paginator.paginate():
for zone in page["HostedZones"]:
zones.append(zone["Name"][:-1]) # We need [:-1] to strip out the trailing dot.
return zones
@sts_client('route53') @sts_client('route53')
def change_txt_record(action, zone_id, domain, value, client=None): def change_txt_record(action, zone_id, domain, value, client=None):
current_txt_records = [] current_txt_records = []
@ -50,7 +60,13 @@ def change_txt_record(action, zone_id, domain, value, client=None):
raise raise
# For some reason TXT records need to be # For some reason TXT records need to be
# manually quoted. # manually quoted.
current_txt_records.append({"Value": '"{}"'.format(value)}) seen = False
for record in current_txt_records:
for k, v in record.items():
if '"{}"'.format(value) == v:
seen = True
if not seen:
current_txt_records.append({"Value": '"{}"'.format(value)})
if action == "DELETE" and len(current_txt_records) > 1: if action == "DELETE" and len(current_txt_records) > 1:
# If we want to delete one record out of many, we'll update the record to not include the deleted value instead. # If we want to delete one record out of many, we'll update the record to not include the deleted value instead.
@ -95,10 +111,17 @@ def create_txt_record(host, value, account_number):
def delete_txt_record(change_ids, account_number, host, value): def delete_txt_record(change_ids, account_number, host, value):
for change_id in change_ids: for change_id in change_ids:
zone_id, _ = change_id zone_id, _ = change_id
change_txt_record( try:
"DELETE", change_txt_record(
zone_id, "DELETE",
host, zone_id,
value, host,
account_number=account_number value,
) account_number=account_number
)
except Exception as e:
if "but it was not found" in e.response.get("Error", {}).get("Message"):
# We tried to delete a record that doesn't exist. We'll ignore this error.
pass
else:
raise

View File

@ -1 +0,0 @@
from lemur.tests.conftest import * # noqa

View File

@ -7,8 +7,16 @@ from lemur.plugins.lemur_acme import plugin
class TestAcme(unittest.TestCase): class TestAcme(unittest.TestCase):
def setUp(self): @patch('lemur.plugins.lemur_acme.plugin.dns_provider_service')
def setUp(self, mock_dns_provider_service):
self.ACMEIssuerPlugin = plugin.ACMEIssuerPlugin() self.ACMEIssuerPlugin = plugin.ACMEIssuerPlugin()
self.acme = plugin.AcmeHandler()
mock_dns_provider = Mock()
mock_dns_provider.name = "cloudflare"
mock_dns_provider.credentials = "{}"
mock_dns_provider.provider_type = "cloudflare"
self.acme.dns_providers_for_domain = {"www.test.com": [mock_dns_provider],
"test.fakedomain.net": [mock_dns_provider]}
@patch('lemur.plugins.lemur_acme.plugin.len', return_value=1) @patch('lemur.plugins.lemur_acme.plugin.len', return_value=1)
def test_find_dns_challenge(self, mock_len): def test_find_dns_challenge(self, mock_len):
@ -22,7 +30,7 @@ class TestAcme(unittest.TestCase):
mock_entry = Mock() mock_entry = Mock()
mock_entry.chall = c mock_entry.chall = c
mock_authz.body.resolved_combinations.append(mock_entry) mock_authz.body.resolved_combinations.append(mock_entry)
result = yield plugin.find_dns_challenge(mock_authz) result = yield self.acme.find_dns_challenge(mock_authz)
self.assertEqual(result, mock_entry) self.assertEqual(result, mock_entry)
def test_authz_record(self): def test_authz_record(self):
@ -32,7 +40,7 @@ class TestAcme(unittest.TestCase):
@patch('acme.client.Client') @patch('acme.client.Client')
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
@patch('lemur.plugins.lemur_acme.plugin.len', return_value=1) @patch('lemur.plugins.lemur_acme.plugin.len', return_value=1)
@patch('lemur.plugins.lemur_acme.plugin.find_dns_challenge') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.find_dns_challenge')
def test_start_dns_challenge(self, mock_find_dns_challenge, mock_len, mock_app, mock_acme): def test_start_dns_challenge(self, mock_find_dns_challenge, mock_len, mock_app, mock_acme):
assert mock_len assert mock_len
mock_order = Mock() mock_order = Mock()
@ -42,7 +50,7 @@ class TestAcme(unittest.TestCase):
mock_entry = MagicMock() mock_entry = MagicMock()
from acme import challenges from acme import challenges
c = challenges.DNS01() c = challenges.DNS01()
mock_entry.chall = c mock_entry.chall = TestAcme.test_complete_dns_challenge_fail
mock_authz.body.resolved_combinations.append(mock_entry) mock_authz.body.resolved_combinations.append(mock_entry)
mock_acme.request_domain_challenges = Mock(return_value=mock_authz) mock_acme.request_domain_challenges = Mock(return_value=mock_authz)
mock_dns_provider = Mock() mock_dns_provider = Mock()
@ -52,19 +60,20 @@ class TestAcme(unittest.TestCase):
iterable = mock_find_dns_challenge.return_value iterable = mock_find_dns_challenge.return_value
iterator = iter(values) iterator = iter(values)
iterable.__iter__.return_value = iterator iterable.__iter__.return_value = iterator
result = plugin.start_dns_challenge(mock_acme, "accountid", "host", mock_dns_provider, mock_order, {}) result = self.acme.start_dns_challenge(mock_acme, "accountid", "host", mock_dns_provider, mock_order, {})
self.assertEqual(type(result), plugin.AuthorizationRecord) self.assertEqual(type(result), plugin.AuthorizationRecord)
@patch('acme.client.Client') @patch('acme.client.Client')
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
def test_complete_dns_challenge_success(self, mock_current_app, mock_acme): @patch('lemur.plugins.lemur_acme.cloudflare.wait_for_dns_change')
def test_complete_dns_challenge_success(self, mock_wait_for_dns_change, mock_current_app, mock_acme):
mock_dns_provider = Mock() mock_dns_provider = Mock()
mock_dns_provider.wait_for_dns_change = Mock(return_value=True) mock_dns_provider.wait_for_dns_change = Mock(return_value=True)
mock_authz = Mock() mock_authz = Mock()
mock_authz.dns_challenge.response = Mock() mock_authz.dns_challenge.response = Mock()
mock_authz.dns_challenge.response.simple_verify = Mock(return_value=True) mock_authz.dns_challenge.response.simple_verify = Mock(return_value=True)
mock_authz.authz = [] mock_authz.authz = []
mock_authz.host = "www.test.com"
mock_authz_record = Mock() mock_authz_record = Mock()
mock_authz_record.body.identifier.value = "test" mock_authz_record.body.identifier.value = "test"
mock_authz.authz.append(mock_authz_record) mock_authz.authz.append(mock_authz_record)
@ -73,11 +82,12 @@ class TestAcme(unittest.TestCase):
mock_authz.dns_challenge = [] mock_authz.dns_challenge = []
dns_challenge = Mock() dns_challenge = Mock()
mock_authz.dns_challenge.append(dns_challenge) mock_authz.dns_challenge.append(dns_challenge)
plugin.complete_dns_challenge(mock_acme, "accountid", mock_authz, mock_dns_provider) self.acme.complete_dns_challenge(mock_acme, mock_authz)
@patch('acme.client.Client') @patch('acme.client.Client')
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
def test_complete_dns_challenge_fail(self, mock_current_app, mock_acme): @patch('lemur.plugins.lemur_acme.cloudflare.wait_for_dns_change')
def test_complete_dns_challenge_fail(self, mock_wait_for_dns_change, mock_current_app, mock_acme):
mock_dns_provider = Mock() mock_dns_provider = Mock()
mock_dns_provider.wait_for_dns_change = Mock(return_value=True) mock_dns_provider.wait_for_dns_change = Mock(return_value=True)
@ -85,6 +95,7 @@ class TestAcme(unittest.TestCase):
mock_authz.dns_challenge.response = Mock() mock_authz.dns_challenge.response = Mock()
mock_authz.dns_challenge.response.simple_verify = Mock(return_value=False) mock_authz.dns_challenge.response.simple_verify = Mock(return_value=False)
mock_authz.authz = [] mock_authz.authz = []
mock_authz.host = "www.test.com"
mock_authz_record = Mock() mock_authz_record = Mock()
mock_authz_record.body.identifier.value = "test" mock_authz_record.body.identifier.value = "test"
mock_authz.authz.append(mock_authz_record) mock_authz.authz.append(mock_authz_record)
@ -95,13 +106,13 @@ class TestAcme(unittest.TestCase):
mock_authz.dns_challenge.append(dns_challenge) mock_authz.dns_challenge.append(dns_challenge)
self.assertRaises( self.assertRaises(
ValueError, ValueError,
plugin.complete_dns_challenge(mock_acme, "accountid", mock_authz, mock_dns_provider) self.acme.complete_dns_challenge(mock_acme, mock_authz)
) )
@patch('acme.client.Client') @patch('acme.client.Client')
@patch('OpenSSL.crypto', return_value="mock_cert") @patch('OpenSSL.crypto', return_value="mock_cert")
@patch('josepy.util.ComparableX509') @patch('josepy.util.ComparableX509')
@patch('lemur.plugins.lemur_acme.plugin.find_dns_challenge') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.find_dns_challenge')
@patch('lemur.plugins.lemur_acme.plugin.current_app') @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): 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 = Mock()
@ -115,13 +126,13 @@ class TestAcme(unittest.TestCase):
mock_acme.fetch_chain = Mock(return_value="mock_chain") mock_acme.fetch_chain = Mock(return_value="mock_chain")
mock_crypto.dump_certificate = Mock(return_value=b'chain') mock_crypto.dump_certificate = Mock(return_value=b'chain')
mock_order = Mock() mock_order = Mock()
plugin.request_certificate(mock_acme, [], "mock_csr", mock_order) self.acme.request_certificate(mock_acme, [], mock_order)
def test_setup_acme_client_fail(self): def test_setup_acme_client_fail(self):
mock_authority = Mock() mock_authority = Mock()
mock_authority.options = [] mock_authority.options = []
with self.assertRaises(Exception): with self.assertRaises(Exception):
plugin.setup_acme_client(mock_authority) self.acme.setup_acme_client(mock_authority)
@patch('lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2') @patch('lemur.plugins.lemur_acme.plugin.BackwardsCompatibleClientV2')
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
@ -135,7 +146,7 @@ class TestAcme(unittest.TestCase):
mock_client.agree_to_tos = Mock(return_value=True) mock_client.agree_to_tos = Mock(return_value=True)
mock_acme.return_value = mock_client mock_acme.return_value = mock_client
mock_current_app.config = {} mock_current_app.config = {}
result_client, result_registration = plugin.setup_acme_client(mock_authority) result_client, result_registration = self.acme.setup_acme_client(mock_authority)
assert result_client assert result_client
assert result_registration assert result_registration
@ -144,7 +155,7 @@ class TestAcme(unittest.TestCase):
options = { options = {
"common_name": "test.netflix.net" "common_name": "test.netflix.net"
} }
result = plugin.get_domains(options) result = self.acme.get_domains(options)
self.assertEqual(result, [options["common_name"]]) self.assertEqual(result, [options["common_name"]])
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
@ -160,10 +171,10 @@ class TestAcme(unittest.TestCase):
} }
} }
} }
result = plugin.get_domains(options) result = self.acme.get_domains(options)
self.assertEqual(result, [options["common_name"], "test2.netflix.net", "test3.netflix.net"]) self.assertEqual(result, [options["common_name"], "test2.netflix.net", "test3.netflix.net"])
@patch('lemur.plugins.lemur_acme.plugin.start_dns_challenge', return_value="test") @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.start_dns_challenge', return_value="test")
def test_get_authorizations(self, mock_start_dns_challenge): def test_get_authorizations(self, mock_start_dns_challenge):
mock_order = Mock() mock_order = Mock()
mock_order.body.identifiers = [] mock_order.body.identifiers = []
@ -172,10 +183,10 @@ class TestAcme(unittest.TestCase):
mock_order_info = Mock() mock_order_info = Mock()
mock_order_info.account_number = 1 mock_order_info.account_number = 1
mock_order_info.domains = ["test.fakedomain.net"] mock_order_info.domains = ["test.fakedomain.net"]
result = plugin.get_authorizations("acme_client", mock_order, mock_order_info, "dns_provider", {}) result = self.acme.get_authorizations("acme_client", mock_order, mock_order_info)
self.assertEqual(result, ["test"]) self.assertEqual(result, ["test"])
@patch('lemur.plugins.lemur_acme.plugin.complete_dns_challenge', return_value="test") @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.complete_dns_challenge', return_value="test")
def test_finalize_authorizations(self, mock_complete_dns_challenge): def test_finalize_authorizations(self, mock_complete_dns_challenge):
mock_authz = [] mock_authz = []
mock_authz_record = MagicMock() mock_authz_record = MagicMock()
@ -188,7 +199,7 @@ class TestAcme(unittest.TestCase):
mock_dns_provider.delete_txt_record = Mock() mock_dns_provider.delete_txt_record = Mock()
mock_acme_client = Mock() mock_acme_client = Mock()
result = plugin.finalize_authorizations(mock_acme_client, "account_number", mock_dns_provider, mock_authz, {}) result = self.acme.finalize_authorizations(mock_acme_client, mock_authz)
self.assertEqual(result, mock_authz) self.assertEqual(result, mock_authz)
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
@ -210,7 +221,9 @@ class TestAcme(unittest.TestCase):
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
@patch('lemur.plugins.lemur_acme.dyn.current_app') @patch('lemur.plugins.lemur_acme.dyn.current_app')
@patch('lemur.plugins.lemur_acme.cloudflare.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): @patch('lemur.plugins.lemur_acme.plugin.dns_provider_service')
def test_get_dns_provider(self, mock_dns_provider_service, mock_current_app_cloudflare, mock_current_app_dyn,
mock_current_app):
provider = plugin.ACMEIssuerPlugin() provider = plugin.ACMEIssuerPlugin()
route53 = provider.get_dns_provider("route53") route53 = provider.get_dns_provider("route53")
assert route53 assert route53
@ -219,13 +232,13 @@ class TestAcme(unittest.TestCase):
dyn = provider.get_dns_provider("dyn") dyn = provider.get_dns_provider("dyn")
assert dyn assert dyn
@patch('lemur.plugins.lemur_acme.plugin.setup_acme_client') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.setup_acme_client')
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
@patch('lemur.plugins.lemur_acme.plugin.authorization_service') @patch('lemur.plugins.lemur_acme.plugin.authorization_service')
@patch('lemur.plugins.lemur_acme.plugin.dns_provider_service') @patch('lemur.plugins.lemur_acme.plugin.dns_provider_service')
@patch('lemur.plugins.lemur_acme.plugin.get_authorizations') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.get_authorizations')
@patch('lemur.plugins.lemur_acme.plugin.finalize_authorizations') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.finalize_authorizations')
@patch('lemur.plugins.lemur_acme.plugin.request_certificate') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.request_certificate')
def test_get_ordered_certificate( def test_get_ordered_certificate(
self, mock_request_certificate, mock_finalize_authorizations, mock_get_authorizations, self, mock_request_certificate, mock_finalize_authorizations, mock_get_authorizations,
mock_dns_provider_service, mock_authorization_service, mock_current_app, mock_acme): mock_dns_provider_service, mock_authorization_service, mock_current_app, mock_acme):
@ -248,13 +261,13 @@ class TestAcme(unittest.TestCase):
} }
) )
@patch('lemur.plugins.lemur_acme.plugin.setup_acme_client') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.setup_acme_client')
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
@patch('lemur.plugins.lemur_acme.plugin.authorization_service') @patch('lemur.plugins.lemur_acme.plugin.authorization_service')
@patch('lemur.plugins.lemur_acme.plugin.dns_provider_service') @patch('lemur.plugins.lemur_acme.plugin.dns_provider_service')
@patch('lemur.plugins.lemur_acme.plugin.get_authorizations') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.get_authorizations')
@patch('lemur.plugins.lemur_acme.plugin.finalize_authorizations') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.finalize_authorizations')
@patch('lemur.plugins.lemur_acme.plugin.request_certificate') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.request_certificate')
def test_get_ordered_certificates( def test_get_ordered_certificates(
self, mock_request_certificate, mock_finalize_authorizations, mock_get_authorizations, self, mock_request_certificate, mock_finalize_authorizations, mock_get_authorizations,
mock_dns_provider_service, mock_authorization_service, mock_current_app, mock_acme): mock_dns_provider_service, mock_authorization_service, mock_current_app, mock_acme):
@ -275,12 +288,12 @@ class TestAcme(unittest.TestCase):
self.assertEqual(result[0]['cert'], {'body': 'pem_certificate', 'chain': 'chain', 'external_id': '1'}) 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'}) 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.AcmeHandler.setup_acme_client')
@patch('lemur.plugins.lemur_acme.plugin.dns_provider_service') @patch('lemur.plugins.lemur_acme.plugin.dns_provider_service')
@patch('lemur.plugins.lemur_acme.plugin.current_app') @patch('lemur.plugins.lemur_acme.plugin.current_app')
@patch('lemur.plugins.lemur_acme.plugin.get_authorizations') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.get_authorizations')
@patch('lemur.plugins.lemur_acme.plugin.finalize_authorizations') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.finalize_authorizations')
@patch('lemur.plugins.lemur_acme.plugin.request_certificate') @patch('lemur.plugins.lemur_acme.plugin.AcmeHandler.request_certificate')
@patch('lemur.plugins.lemur_acme.plugin.authorization_service') @patch('lemur.plugins.lemur_acme.plugin.authorization_service')
def test_create_certificate(self, mock_authorization_service, mock_request_certificate, def test_create_certificate(self, mock_authorization_service, mock_request_certificate,
mock_finalize_authorizations, mock_get_authorizations, mock_finalize_authorizations, mock_get_authorizations,

View File

@ -107,6 +107,17 @@
</ui-select> </ui-select>
</div> </div>
</div> </div>
<div class="form-group" ng-show="certificate.authority.plugin.slug == 'acme-issuer'">
<label class="control-label col-sm-2">
Note:
</label>
<div class="col-sm-10">
The selected authority uses the <a target="_blank" href="https://letsencrypt.org/how-it-works/">ACME protocol</a> and works differently than other authorities.
Your request will initially be created under the "pending certificates" section. Lemur will attempt to create the certificate for you,
and move the final certificate to the "certificates" section. Lemur performs validation by writing a DNS text record. You may choose a specific DNS provider,
or allow Lemur to automatically detect the correct provider for you. Requests may take up to ten minutes.
</div>
</div>
<div class="form-group" ng-show="certificate.authority.plugin.slug == 'acme-issuer'"> <div class="form-group" ng-show="certificate.authority.plugin.slug == 'acme-issuer'">
<label class="control-label col-sm-2"> <label class="control-label col-sm-2">
DNS Provider: DNS Provider:
@ -114,7 +125,7 @@
<div class="col-sm-10"> <div class="col-sm-10">
<select class="form-control" ng-model="certificate.dnsProvider" ng-options="item as item.name for item in dnsProviders.items track by item.id"> <select class="form-control" ng-model="certificate.dnsProvider" ng-options="item as item.name for item in dnsProviders.items track by item.id">
<option value="">- choose an entry. Neded for ACME Providers -</option> <option value="">Automatically select for me</option>
</select> </select>
</div> </div>
</div> </div>

View File

@ -6,7 +6,7 @@
# #
aspy.yaml==1.1.1 # via pre-commit aspy.yaml==1.1.1 # via pre-commit
cached-property==1.4.3 # via pre-commit cached-property==1.4.3 # via pre-commit
certifi==2018.4.16 # via requests certifi==2018.8.13 # via requests
cfgv==1.1.0 # via pre-commit cfgv==1.1.0 # via pre-commit
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
flake8==3.5.0 flake8==3.5.0

View File

@ -15,9 +15,9 @@ asyncpool==1.0
babel==2.6.0 # via sphinx babel==2.6.0 # via sphinx
bcrypt==3.1.4 bcrypt==3.1.4
blinker==1.4 blinker==1.4
boto3==1.7.66 boto3==1.7.75
botocore==1.10.66 botocore==1.10.75
certifi==2018.4.16 certifi==2018.8.13
cffi==1.11.5 cffi==1.11.5
chardet==3.0.4 chardet==3.0.4
click==6.7 click==6.7
@ -50,7 +50,7 @@ lockfile==0.12.2
mako==1.0.7 mako==1.0.7
markupsafe==1.0 markupsafe==1.0
marshmallow-sqlalchemy==0.14.0 marshmallow-sqlalchemy==0.14.0
marshmallow==2.15.3 marshmallow==2.15.4
mock==2.0.0 mock==2.0.0
ndg-httpsclient==0.5.1 ndg-httpsclient==0.5.1
packaging==17.1 # via sphinx packaging==17.1 # via sphinx

View File

@ -8,10 +8,10 @@ asn1crypto==0.24.0 # via cryptography
atomicwrites==1.1.5 # via pytest atomicwrites==1.1.5 # via pytest
attrs==18.1.0 # via pytest attrs==18.1.0 # via pytest
aws-xray-sdk==0.95 # via moto aws-xray-sdk==0.95 # via moto
boto3==1.7.71 # via moto boto3==1.7.75 # via moto
boto==2.49.0 # via moto boto==2.49.0 # via moto
botocore==1.10.71 # via boto3, moto, s3transfer botocore==1.10.75 # via boto3, moto, s3transfer
certifi==2018.4.16 # via requests certifi==2018.8.13 # via requests
cffi==1.11.5 # via cryptography cffi==1.11.5 # via cryptography
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
click==6.7 # via flask click==6.7 # via flask
@ -19,12 +19,14 @@ cookies==2.2.1 # via moto, responses
coverage==4.5.1 coverage==4.5.1
cryptography==2.3 # via moto cryptography==2.3 # via moto
docker-pycreds==0.3.0 # via docker docker-pycreds==0.3.0 # via docker
docker==3.4.1 # via moto docker==3.5.0 # via moto
docutils==0.14 # via botocore docutils==0.14 # via botocore
ecdsa==0.13 # via python-jose
factory-boy==2.11.1 factory-boy==2.11.1
faker==0.8.17 faker==0.9.0
flask==1.0.2 # via pytest-flask flask==1.0.2 # via pytest-flask
freezegun==0.3.10 freezegun==0.3.10
future==0.16.0 # via python-jose
idna==2.7 # via cryptography, requests idna==2.7 # via cryptography, requests
itsdangerous==0.24 # via flask itsdangerous==0.24 # via flask
jinja2==2.10 # via flask, moto jinja2==2.10 # via flask, moto
@ -34,25 +36,27 @@ jsonpickle==0.9.6 # via aws-xray-sdk
markupsafe==1.0 # via jinja2 markupsafe==1.0 # via jinja2
mock==2.0.0 # via moto mock==2.0.0 # via moto
more-itertools==4.3.0 # via pytest more-itertools==4.3.0 # via pytest
moto==1.3.3 moto==1.3.4
nose==1.3.7 nose==1.3.7
pbr==4.2.0 # via mock pbr==4.2.0 # via mock
pluggy==0.7.1 # via pytest pluggy==0.7.1 # via pytest
py==1.5.4 # via pytest py==1.5.4 # via pytest
pyaml==17.12.1 # via moto pyaml==17.12.1 # via moto
pycparser==2.18 # via cffi pycparser==2.18 # via cffi
pycryptodome==3.6.5 # via python-jose
pyflakes==2.0.0 pyflakes==2.0.0
pytest-flask==0.10.0 pytest-flask==0.10.0
pytest-mock==1.10.0 pytest-mock==1.10.0
pytest==3.7.1 pytest==3.7.1
python-dateutil==2.6.1 # via botocore, faker, freezegun, moto python-dateutil==2.7.3 # via botocore, faker, freezegun, moto
python-jose==2.0.2 # via moto
pytz==2018.5 # via moto pytz==2018.5 # via moto
pyyaml==3.13 # via pyaml pyyaml==3.13 # via pyaml
requests-mock==1.5.2 requests-mock==1.5.2
requests==2.19.1 # via aws-xray-sdk, docker, moto, requests-mock, responses requests==2.19.1 # via aws-xray-sdk, docker, moto, requests-mock, responses
responses==0.9.0 # via moto responses==0.9.0 # via moto
s3transfer==0.1.13 # via boto3 s3transfer==0.1.13 # via boto3
six==1.11.0 # via cryptography, docker, docker-pycreds, faker, freezegun, mock, more-itertools, moto, pytest, python-dateutil, requests-mock, responses, websocket-client six==1.11.0 # via cryptography, docker, docker-pycreds, faker, freezegun, mock, more-itertools, moto, pytest, python-dateutil, python-jose, requests-mock, responses, websocket-client
text-unidecode==1.2 # via faker text-unidecode==1.2 # via faker
urllib3==1.23 # via requests urllib3==1.23 # via requests
websocket-client==0.48.0 # via docker websocket-client==0.48.0 # via docker

View File

@ -13,9 +13,9 @@ asn1crypto==0.24.0 # via cryptography
asyncpool==1.0 asyncpool==1.0
bcrypt==3.1.4 # via flask-bcrypt, paramiko bcrypt==3.1.4 # via flask-bcrypt, paramiko
blinker==1.4 # via flask-mail, flask-principal, raven blinker==1.4 # via flask-mail, flask-principal, raven
boto3==1.7.71 boto3==1.7.75
botocore==1.10.71 # via boto3, s3transfer botocore==1.10.75 # via boto3, s3transfer
certifi==2018.4.16 certifi==2018.8.13
cffi==1.11.5 # via bcrypt, cryptography, pynacl cffi==1.11.5 # via bcrypt, cryptography, pynacl
chardet==3.0.4 # via requests chardet==3.0.4 # via requests
click==6.7 # via flask click==6.7 # via flask