From f61098b87412bc4693b4916f1cd4f577750da404 Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Tue, 10 Apr 2018 14:28:53 -0700 Subject: [PATCH] WIP: Add support for Acme/LetsEncrypt with DNS Provider integration --- lemur/authorities/models.py | 1 + lemur/authorities/service.py | 5 ++ lemur/certificates/models.py | 1 + lemur/models.py | 17 +++++- lemur/plugins/lemur_acme/plugin.py | 48 +++++++++++++++-- .../authorities/authority/authority.js | 1 + .../authorities/authority/options.tpl.html | 52 ++++++++++++++++++- .../certificate/tracking.tpl.html | 2 +- .../destinations/destination/destination.js | 6 ++- 9 files changed, 124 insertions(+), 9 deletions(-) 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/certificates/models.py b/lemur/certificates/models.py index a9bb60cc..d8354d94 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 = Column(Integer(), nullable=True) not_before = Column(ArrowType) not_after = Column(ArrowType) diff --git a/lemur/models.py b/lemur/models.py index 02c64dbe..3a9cf121 100644 --- a/lemur/models.py +++ b/lemur/models.py @@ -8,7 +8,9 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ -from sqlalchemy import Column, Integer, ForeignKey, Index, UniqueConstraint +from sqlalchemy import Column, Integer, ForeignKey, Index, PrimaryKeyConstraint, String, text, UniqueConstraint +from sqlalchemy.dialects.postgresql import JSON +from sqlalchemy_utils import ArrowType from lemur.database import db @@ -130,3 +132,16 @@ pending_cert_role_associations = db.Table('pending_cert_role_associations', ) Index('pending_cert_role_associations_ix', pending_cert_role_associations.c.pending_cert_id, pending_cert_role_associations.c.role_id) + +dns_providers = db.Table('dns_providers', + Column('id', Integer(), nullable=False), + Column('name', String(length=256), nullable=True), + Column('description', String(length=1024), nullable=True), + Column('provider_type', String(length=256), nullable=True), + Column('credentials', String(length=256), nullable=True), + Column('api_endpoint', String(length=256), nullable=True), + Column('date_created', ArrowType(), server_default=text('now()'), nullable=False), + Column('status', String(length=128), nullable=True), + Column('options', JSON), + PrimaryKeyConstraint('id'), + UniqueConstraint('name')) diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index 5bdb5514..e5fd1730 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -11,6 +11,7 @@ .. moduleauthor:: Mikhail Khodorovskiy """ import josepy as jose +import json from flask import current_app @@ -105,8 +106,9 @@ def request_certificate(acme_client, authorizations, csr): return pem_certificate, pem_certificate_chain -def setup_acme_client(): - email = current_app.config.get('ACME_EMAIL') +def setup_acme_client(authority): + options = json.loads(authority.get('options', '[]')) + email = options.getcurrent_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)) @@ -174,6 +176,36 @@ class ACMEIssuerPlugin(IssuerPlugin): author = 'Kevin Glisson' author_url = 'https://github.com/netflix/lemur.git' + 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' + }, + ] + def __init__(self, *args, **kwargs): required_vars = [ 'ACME_DIRECTORY_URL', @@ -198,7 +230,8 @@ class ACMEIssuerPlugin(IssuerPlugin): :return: :raise Exception: """ current_app.logger.debug("Requesting a new acme certificate: {0}".format(issuer_options)) - acme_client, registration = setup_acme_client() + acme_client, registration = setup_acme_client(issuer_options.get(issuer_options.get('authority'))) + # Deal with account number per certificate account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER') domains = get_domains(issuer_options) authorizations = get_authorizations(acme_client, account_number, domains, self.dns_provider) @@ -216,4 +249,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/static/app/angular/authorities/authority/authority.js b/lemur/static/app/angular/authorities/authority/authority.js index 9863bf4d..223d8fc6 100644 --- a/lemur/static/app/angular/authorities/authority/authority.js +++ b/lemur/static/app/angular/authorities/authority/authority.js @@ -51,6 +51,7 @@ angular.module('lemur') } }); }); + console.log("HERE2") $scope.getAuthoritiesByName = function (value) { return AuthorityService.findAuthorityByName(value).then(function (authorities) { diff --git a/lemur/static/app/angular/authorities/authority/options.tpl.html b/lemur/static/app/angular/authorities/authority/options.tpl.html index 57fc29e6..be4b27cc 100644 --- a/lemur/static/app/angular/authorities/authority/options.tpl.html +++ b/lemur/static/app/angular/authorities/authority/options.tpl.html @@ -51,8 +51,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/tracking.tpl.html b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html index 21277106..fa8425ad 100644 --- a/lemur/static/app/angular/certificates/certificate/tracking.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/tracking.tpl.html @@ -1,4 +1,4 @@ -
+static/app/angular/certificates/certificate/tracking.tpl.html
diff --git a/lemur/static/app/angular/destinations/destination/destination.js b/lemur/static/app/angular/destinations/destination/destination.js index 21f624c8..03533949 100644 --- a/lemur/static/app/angular/destinations/destination/destination.js +++ b/lemur/static/app/angular/destinations/destination/destination.js @@ -44,20 +44,22 @@ angular.module('lemur') DestinationApi.get(editId).then(function (destination) { $scope.destination = destination; - + console.log("HERE1"); PluginService.getByType('destination').then(function (plugins) { $scope.plugins = plugins; - _.each($scope.plugins, function (plugin) { + console.log("HERE2"); if (plugin.slug === $scope.destination.plugin.slug) { plugin.pluginOptions = $scope.destination.plugin.pluginOptions; $scope.destination.plugin = plugin; _.each($scope.destination.plugin.pluginOptions, function (option) { + console.log("HERE3"); if (option.type === 'export-plugin') { PluginService.getByType('export').then(function (plugins) { $scope.exportPlugins = plugins; _.each($scope.exportPlugins, function (plugin) { + console.log("HERE4"); if (plugin.slug === option.value.slug) { plugin.pluginOptions = option.value.pluginOptions; option.value = plugin;