WIP: Add support for Acme/LetsEncrypt with DNS Provider integration
This commit is contained in:
parent
0b5f85469c
commit
f61098b874
|
@ -42,6 +42,7 @@ class Authority(db.Model):
|
||||||
self.description = kwargs.get('description')
|
self.description = kwargs.get('description')
|
||||||
self.authority_certificate = kwargs['authority_certificate']
|
self.authority_certificate = kwargs['authority_certificate']
|
||||||
self.plugin_name = kwargs['plugin']['slug']
|
self.plugin_name = kwargs['plugin']['slug']
|
||||||
|
self.options = kwargs.get('options')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def plugin(self):
|
def plugin(self):
|
||||||
|
|
|
@ -8,6 +8,9 @@
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
from lemur import database
|
from lemur import database
|
||||||
from lemur.common.utils import truthiness
|
from lemur.common.utils import truthiness
|
||||||
from lemur.extensions import metrics
|
from lemur.extensions import metrics
|
||||||
|
@ -107,6 +110,8 @@ def create(**kwargs):
|
||||||
|
|
||||||
cert = upload(**kwargs)
|
cert = upload(**kwargs)
|
||||||
kwargs['authority_certificate'] = cert
|
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 = Authority(**kwargs)
|
||||||
authority = database.create(authority)
|
authority = database.create(authority)
|
||||||
|
|
|
@ -102,6 +102,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 = Column(Integer(), nullable=True)
|
||||||
|
|
||||||
not_before = Column(ArrowType)
|
not_before = Column(ArrowType)
|
||||||
not_after = Column(ArrowType)
|
not_after = Column(ArrowType)
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
:license: Apache, see LICENSE for more details.
|
:license: Apache, see LICENSE for more details.
|
||||||
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
|
||||||
"""
|
"""
|
||||||
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
|
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)
|
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'))
|
||||||
|
|
|
@ -11,6 +11,7 @@
|
||||||
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
|
||||||
"""
|
"""
|
||||||
import josepy as jose
|
import josepy as jose
|
||||||
|
import json
|
||||||
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
@ -105,8 +106,9 @@ def request_certificate(acme_client, authorizations, csr):
|
||||||
return pem_certificate, pem_certificate_chain
|
return pem_certificate, pem_certificate_chain
|
||||||
|
|
||||||
|
|
||||||
def setup_acme_client():
|
def setup_acme_client(authority):
|
||||||
email = current_app.config.get('ACME_EMAIL')
|
options = json.loads(authority.get('options', '[]'))
|
||||||
|
email = options.getcurrent_app.config.get('ACME_EMAIL')
|
||||||
tel = current_app.config.get('ACME_TEL')
|
tel = current_app.config.get('ACME_TEL')
|
||||||
directory_url = current_app.config.get('ACME_DIRECTORY_URL')
|
directory_url = current_app.config.get('ACME_DIRECTORY_URL')
|
||||||
contact = ('mailto:{}'.format(email), 'tel:{}'.format(tel))
|
contact = ('mailto:{}'.format(email), 'tel:{}'.format(tel))
|
||||||
|
@ -174,6 +176,36 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||||
author = 'Kevin Glisson'
|
author = 'Kevin Glisson'
|
||||||
author_url = 'https://github.com/netflix/lemur.git'
|
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):
|
def __init__(self, *args, **kwargs):
|
||||||
required_vars = [
|
required_vars = [
|
||||||
'ACME_DIRECTORY_URL',
|
'ACME_DIRECTORY_URL',
|
||||||
|
@ -198,7 +230,8 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||||
:return: :raise Exception:
|
:return: :raise Exception:
|
||||||
"""
|
"""
|
||||||
current_app.logger.debug("Requesting a new acme certificate: {0}".format(issuer_options))
|
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')
|
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER')
|
||||||
domains = get_domains(issuer_options)
|
domains = get_domains(issuer_options)
|
||||||
authorizations = get_authorizations(acme_client, account_number, domains, self.dns_provider)
|
authorizations = get_authorizations(acme_client, account_number, domains, self.dns_provider)
|
||||||
|
@ -216,4 +249,11 @@ class ACMEIssuerPlugin(IssuerPlugin):
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
role = {'username': '', 'password': '', 'name': 'acme'}
|
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]
|
||||||
|
|
|
@ -51,6 +51,7 @@ angular.module('lemur')
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
console.log("HERE2")
|
||||||
|
|
||||||
$scope.getAuthoritiesByName = function (value) {
|
$scope.getAuthoritiesByName = function (value) {
|
||||||
return AuthorityService.findAuthorityByName(value).then(function (authorities) {
|
return AuthorityService.findAuthorityByName(value).then(function (authorities) {
|
||||||
|
|
|
@ -51,8 +51,58 @@
|
||||||
<label class="control-label col-sm-2">
|
<label class="control-label col-sm-2">
|
||||||
Plugin
|
Plugin
|
||||||
</label>
|
</label>
|
||||||
<div class="col-sm-10">
|
<div class="form-group col-sm-10">
|
||||||
<select class="form-control" ng-model="authority.plugin" ng-options="plugin as plugin.title for plugin in plugins" required></select>
|
<select class="form-control" ng-model="authority.plugin" ng-options="plugin as plugin.title for plugin in plugins" required></select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group" ng-repeat="item in authority.plugin.pluginOptions">
|
||||||
|
<ng-form name="subForm" class="form-horizontal" role="form" novalidate>
|
||||||
|
<div ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
{{ item.name | titleCase }}
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="sub" ng-if="item.type == 'int'" type="number" ng-pattern="item.validation?item.validation:'^[0-9]+$'"
|
||||||
|
class="form-control" ng-model="item.value"/>
|
||||||
|
<select name="sub" ng-if="item.type == 'select'" class="form-control" ng-options="i for i in item.available"
|
||||||
|
ng-model="item.value"></select>
|
||||||
|
<input name="sub" ng-if="item.type == 'bool'" class="form-control" type="checkbox" ng-model="item.value">
|
||||||
|
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control" ng-model="item.value"/>
|
||||||
|
<textarea name="sub" ng-if="item.type == 'textarea'" class="form-control" ng-model="item.value"></textarea>
|
||||||
|
<div ng-if="item.type == 'export-plugin'">
|
||||||
|
<form name="exportForm" class="form-horizontal" role="form" novalidate>
|
||||||
|
<select class="form-control" ng-model="item.value"
|
||||||
|
ng-options="plugin.title for plugin in exportPlugins" required></select>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<div class="form-group" ng-repeat="item in item.value.pluginOptions">
|
||||||
|
<ng-form name="subForm" class="form-horizontal" role="form" novalidate>
|
||||||
|
ng-class="{'has-error': subForm.sub.$invalid, 'has-success': !subForm.sub.$invalid&&subForm.sub.$dirty}">
|
||||||
|
<label class="control-label col-sm-2">
|
||||||
|
{{ item.name | titleCase }}
|
||||||
|
</label>
|
||||||
|
<div class="col-sm-10">
|
||||||
|
<input name="sub" ng-if="item.type == 'int'" type="number" ng-pattern="item.validation?item.validation:'^[0-9]+$'"
|
||||||
|
class="form-control" ng-model="item.value"/>
|
||||||
|
<select name="sub" ng-if="item.type == 'select'" class="form-control"
|
||||||
|
ng-options="i for i in item.available" ng-model="item.value"></select>
|
||||||
|
<input name="sub" ng-if="item.type == 'bool'" class="form-control" type="checkbox"
|
||||||
|
ng-model="item.value">
|
||||||
|
<input name="sub" ng-if="item.type == 'str'" type="text" class="form-control"
|
||||||
|
ng-model="item.value" ng-pattern="item.validation"/>
|
||||||
|
<textarea name="sub" ng-if="item.type == 'textarea'" class="form-control"
|
||||||
|
ng-model="item.value" ng-pattern="item.validation"></textarea>
|
||||||
|
<p ng-show="subForm.sub.$invalid && !subForm.sub.$pristine"
|
||||||
|
class="help-block">{{ item.helpMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<p ng-show="subForm.sub.$invalid && !subForm.sub.$pristine" class="help-block">{{ item.helpMessage }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ng-form>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
<form name="trackingForm" novalidate>
|
static/app/angular/certificates/certificate/tracking.tpl.html<form name="trackingForm" novalidate>
|
||||||
<div class="form-horizontal">
|
<div class="form-horizontal">
|
||||||
<div class="form-group"
|
<div class="form-group"
|
||||||
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
|
ng-class="{'has-error': trackingForm.ownerEmail.$invalid, 'has-success': !trackingForm.$invalid&&trackingForm.ownerEmail.$dirty}">
|
||||||
|
|
|
@ -44,20 +44,22 @@ angular.module('lemur')
|
||||||
|
|
||||||
DestinationApi.get(editId).then(function (destination) {
|
DestinationApi.get(editId).then(function (destination) {
|
||||||
$scope.destination = destination;
|
$scope.destination = destination;
|
||||||
|
console.log("HERE1");
|
||||||
PluginService.getByType('destination').then(function (plugins) {
|
PluginService.getByType('destination').then(function (plugins) {
|
||||||
$scope.plugins = plugins;
|
$scope.plugins = plugins;
|
||||||
|
|
||||||
_.each($scope.plugins, function (plugin) {
|
_.each($scope.plugins, function (plugin) {
|
||||||
|
console.log("HERE2");
|
||||||
if (plugin.slug === $scope.destination.plugin.slug) {
|
if (plugin.slug === $scope.destination.plugin.slug) {
|
||||||
plugin.pluginOptions = $scope.destination.plugin.pluginOptions;
|
plugin.pluginOptions = $scope.destination.plugin.pluginOptions;
|
||||||
$scope.destination.plugin = plugin;
|
$scope.destination.plugin = plugin;
|
||||||
_.each($scope.destination.plugin.pluginOptions, function (option) {
|
_.each($scope.destination.plugin.pluginOptions, function (option) {
|
||||||
|
console.log("HERE3");
|
||||||
if (option.type === 'export-plugin') {
|
if (option.type === 'export-plugin') {
|
||||||
PluginService.getByType('export').then(function (plugins) {
|
PluginService.getByType('export').then(function (plugins) {
|
||||||
$scope.exportPlugins = plugins;
|
$scope.exportPlugins = plugins;
|
||||||
|
|
||||||
_.each($scope.exportPlugins, function (plugin) {
|
_.each($scope.exportPlugins, function (plugin) {
|
||||||
|
console.log("HERE4");
|
||||||
if (plugin.slug === option.value.slug) {
|
if (plugin.slug === option.value.slug) {
|
||||||
plugin.pluginOptions = option.value.pluginOptions;
|
plugin.pluginOptions = option.value.pluginOptions;
|
||||||
option.value = plugin;
|
option.value = plugin;
|
||||||
|
|
Loading…
Reference in New Issue