From 532872b3c650832f85d947fcad18f570a953e32e Mon Sep 17 00:00:00 2001 From: Curtis Castrapel Date: Fri, 27 Apr 2018 11:18:41 -0700 Subject: [PATCH] dns_provider ui --- lemur/certificates/models.py | 2 +- lemur/dns_providers/models.py | 19 ++++- lemur/dns_providers/schemas.py | 13 ++- lemur/dns_providers/service.py | 35 +++++++- lemur/dns_providers/views.py | 83 +++++++++++++++++-- lemur/plugins/lemur_acme/plugin.py | 13 +-- lemur/plugins/lemur_acme/tests/test_acme.py | 66 ++++++++++++++- .../dns_provider/dns_provider.js | 24 +----- .../dns_provider/dns_provider.tpl.html | 4 +- .../app/angular/dns_providers/services.js | 11 ++- 10 files changed, 228 insertions(+), 42 deletions(-) diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 63d2ea7c..de338aa2 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -102,7 +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) + dns_provider_id = Column(Integer(), ForeignKey('dns_providers.id', ondelete='cascade'), nullable=True) not_before = Column(ArrowType) not_after = Column(ArrowType) diff --git a/lemur/dns_providers/models.py b/lemur/dns_providers/models.py index 7a1fdd01..42cc992a 100644 --- a/lemur/dns_providers/models.py +++ b/lemur/dns_providers/models.py @@ -3,11 +3,15 @@ from sqlalchemy.dialects.postgresql import JSON from sqlalchemy_utils import ArrowType from lemur.database import db +from lemur.plugins.base import plugins class DnsProviders(db.Model): __tablename__ = 'dns_providers' - id = Column(Integer(), primary_key=True) + 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) @@ -17,3 +21,16 @@ class DnsProviders(db.Model): status = Column(String(length=128), nullable=True) options = Column(JSON, nullable=True) domains = Column(JSON, nullable=True) + + def __init__(self, name, description, provider_type, credentials): + self.name = name + self.description = description + self.provider_type = provider_type + self.credentials = credentials + + @property + def plugin(self): + return plugins.get(self.plugin_name) + + def __repr__(self): + return "DnsProviders(name={name})".format(name=self.name) diff --git a/lemur/dns_providers/schemas.py b/lemur/dns_providers/schemas.py index df2042c6..1f44e0b1 100644 --- a/lemur/dns_providers/schemas.py +++ b/lemur/dns_providers/schemas.py @@ -1,5 +1,5 @@ from lemur.common.fields import ArrowDateTime -from lemur.common.schema import LemurOutputSchema +from lemur.common.schema import LemurInputSchema, LemurOutputSchema from marshmallow import fields @@ -15,4 +15,13 @@ class DnsProvidersNestedOutputSchema(LemurOutputSchema): date_created = ArrowDateTime() -dns_provider_schema = DnsProvidersNestedOutputSchema() +class DnsProvidersNestedInputSchema(LemurInputSchema): + __envelope__ = False + name = fields.String() + description = fields.String() + provider_type = fields.Dict() + + +dns_provider_output_schema = DnsProvidersNestedOutputSchema() + +dns_provider_input_schema = DnsProvidersNestedInputSchema() diff --git a/lemur/dns_providers/service.py b/lemur/dns_providers/service.py index b79967b8..537b50b0 100644 --- a/lemur/dns_providers/service.py +++ b/lemur/dns_providers/service.py @@ -1,3 +1,5 @@ +import json + from flask import current_app from lemur import database from lemur.dns_providers.models import DnsProviders @@ -15,6 +17,10 @@ def render(args): def get(dns_provider_id): + return database.get(DnsProviders, dns_provider_id) + + +def get_friendly(dns_provider_id): """ Retrieves a dns provider by its lemur assigned ID. @@ -22,7 +28,17 @@ def get(dns_provider_id): :rtype : DnsProvider :return: """ - return database.get(DnsProviders, dns_provider_id) + dns_provider = get(dns_provider_id) + dns_provider_friendly = { + "name": dns_provider.name, + "description": dns_provider.description, + "provider_type": dns_provider.provider_type, + "options": dns_provider.options, + } + + if dns_provider.provider_type == "route53": + dns_provider_friendly["account_id"] = json.loads(dns_provider.credentials).get("account_id") + return dns_provider_friendly def delete(dns_provider_id): @@ -38,4 +54,21 @@ def get_types(): provider_config = current_app.config.get('ACME_DNS_PROVIDER_TYPES') if not provider_config: raise Exception("No DNS Provider configuration specified.") + provider_config["total"] = len(provider_config.get("items")) return provider_config + + +def create(data): + provider_name = data.get("name") + + credentials = {} + for item in data.get("provider_type", {}).get("requirements", []): + credentials[item["name"]] = item["value"] + dns_provider = DnsProviders( + name=provider_name, + description=data.get("description"), + provider_type=data.get("provider_type").get("name"), + credentials=json.dumps(credentials), + ) + created = database.create(dns_provider) + return created.id diff --git a/lemur/dns_providers/views.py b/lemur/dns_providers/views.py index 13c71faa..6d5c6759 100644 --- a/lemur/dns_providers/views.py +++ b/lemur/dns_providers/views.py @@ -13,7 +13,7 @@ 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 +from lemur.dns_providers.schemas import dns_provider_output_schema, dns_provider_input_schema mod = Blueprint('dns_providers', __name__) api = Api(mod) @@ -25,7 +25,7 @@ class DnsProvidersList(AuthenticatedResource): self.reqparse = reqparse.RequestParser() super(DnsProvidersList, self).__init__() - @validate_schema(None, dns_provider_schema) + @validate_schema(None, dns_provider_output_schema) def get(self): """ .. http:get:: /dns_providers @@ -70,7 +70,7 @@ class DnsProvidersList(AuthenticatedResource): """ parser = paginated_parser.copy() - parser.add_argument('id', type=int, location='args') + parser.add_argument('dns_provider_id', type=int, location='args') parser.add_argument('name', type=str, location='args') parser.add_argument('type', type=str, location='args') @@ -78,23 +78,92 @@ class DnsProvidersList(AuthenticatedResource): args['user'] = g.user return service.render(args) + @validate_schema(dns_provider_input_schema, None) + @admin_permission.require(http_exception=403) + def post(self, data=None): + """ + Creates a DNS Provider + + **Example request**: + { + "provider_type": { + "name": "route53", + "requirements": [ + { + "name": "account_id", + "type": "int", + "required": true, + "helpMessage": "AWS Account number", + "value": 12345 + } + ], + "route": "dns_provider_options", + "reqParams": null, + "restangularized": true, + "fromServer": true, + "parentResource": null, + "restangularCollection": false + }, + "name": "provider_name", + "description": "provider_description" + } + + **Example request 2** + { + "provider_type": { + "name": "cloudflare", + "requirements": [ + { + "name": "email", + "type": "str", + "required": true, + "helpMessage": "Cloudflare Email", + "value": "test@netflix.com" + }, + { + "name": "key", + "type": "str", + "required": true, + "helpMessage": "Cloudflare Key", + "value": "secretkey" + } + ], + "route": "dns_provider_options", + "reqParams": null, + "restangularized": true, + "fromServer": true, + "parentResource": null, + "restangularCollection": false + }, + "name": "provider_name", + "description": "provider_description" + } + :return: + """ + return service.create(data) + + +class DnsProviders(AuthenticatedResource): + def get(self, dns_provider_id): + return service.get_friendly(dns_provider_id) + @admin_permission.require(http_exception=403) def delete(self, dns_provider_id): service.delete(dns_provider_id) return {'result': True} -class DnsProviderTypes(AuthenticatedResource): +class DnsProviderOptions(AuthenticatedResource): """ Defines the 'dns_provider_types' endpoint """ def __init__(self): self.reqparse = reqparse.RequestParser() - super(DnsProviderTypes, self).__init__() + super(DnsProviderOptions, self).__init__() def get(self): return service.get_types() api.add_resource(DnsProvidersList, '/dns_providers', endpoint='dns_providers') -api.add_resource(DnsProvidersList, '/dns_providers/', endpoint='dns_provider') -api.add_resource(DnsProviderTypes, '/dns_provider_types', endpoint='dns_provider_types') +api.add_resource(DnsProviders, '/dns_providers/', endpoint='dns_provider') +api.add_resource(DnsProviderOptions, '/dns_provider_options', endpoint='dns_provider_options') diff --git a/lemur/plugins/lemur_acme/plugin.py b/lemur/plugins/lemur_acme/plugin.py index a6901c5c..f07106db 100644 --- a/lemur/plugins/lemur_acme/plugin.py +++ b/lemur/plugins/lemur_acme/plugin.py @@ -113,11 +113,12 @@ def setup_acme_client(authority): if not authority.options: raise Exception("Invalid authority. Options not set") options = {} - authority_options = json.loads(authority.options) - options[authority_options.get("name")] = authority_options.get("value") - email = authority_options.get('email', current_app.config.get('ACME_EMAIL')) - tel = authority_options.get('telephone', current_app.config.get('ACME_TEL')) - directory_url = authority_options.get('acme_url', current_app.config.get('ACME_DIRECTORY_URL')) + + for option in json.loads(authority.options): + options[option.get("name")] = option.get("value") + email = options.get('email', current_app.config.get('ACME_EMAIL')) + tel = options.get('telephone', current_app.config.get('ACME_TEL')) + directory_url = options.get('acme_url', current_app.config.get('ACME_DIRECTORY_URL')) key = jose.JWKRSA(key=generate_private_key('RSA2048')) @@ -254,7 +255,7 @@ class ACMEIssuerPlugin(IssuerPlugin): 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") + account_number = credentials.get("account_id") 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) diff --git a/lemur/plugins/lemur_acme/tests/test_acme.py b/lemur/plugins/lemur_acme/tests/test_acme.py index 938d3144..45758a68 100644 --- a/lemur/plugins/lemur_acme/tests/test_acme.py +++ b/lemur/plugins/lemur_acme/tests/test_acme.py @@ -6,6 +6,9 @@ from mock import MagicMock, Mock, patch class TestAcme(unittest.TestCase): + def setUp(self): + self.ACMEIssuerPlugin = plugin.ACMEIssuerPlugin() + @patch('lemur.plugins.lemur_acme.plugin.len', return_value=1) def test_find_dns_challenge(self, mock_len): assert mock_len @@ -103,7 +106,7 @@ class TestAcme(unittest.TestCase): @patch('lemur.plugins.lemur_acme.plugin.current_app') def test_setup_acme_client_success(self, mock_current_app, mock_acme): mock_authority = Mock() - mock_authority.options = '{"o": "mock_name", "v": "mock_value"}' + mock_authority.options = '[{"name": "mock_name", "value": "mock_value"}]' mock_client = Mock() mock_registration = Mock() mock_registration.uri = "http://test.com" @@ -113,3 +116,64 @@ class TestAcme(unittest.TestCase): result_client, result_registration = plugin.setup_acme_client(mock_authority) assert result_client assert result_registration + + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_get_domains_single(self, mock_current_app): + options = { + "common_name": "test.netflix.net" + } + result = plugin.get_domains(options) + self.assertEqual(result, [options["common_name"]]) + + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_get_domains_multiple(self, mock_current_app): + options = { + "common_name": "test.netflix.net", + "extensions": { + "sub_alt_names": { + "names": [ + "test2.netflix.net", + "test3.netflix.net" + ] + } + } + } + result = plugin.get_domains(options) + self.assertEqual(result, [options["common_name"], "test2.netflix.net", "test3.netflix.net"]) + + @patch('lemur.plugins.lemur_acme.plugin.start_dns_challenge', return_value="test") + def test_get_authorizations(self, mock_start_dns_challenge): + result = plugin.get_authorizations("acme_client", "account_number", ["domains"], "dns_provider") + self.assertEqual(result, ["test"]) + + @patch('lemur.plugins.lemur_acme.plugin.complete_dns_challenge', return_value="test") + def test_finalize_authorizations(self, mock_complete_dns_challenge): + mock_authz = [] + mock_authz_record = MagicMock() + mock_authz_record.authz = Mock() + mock_authz_record.change_id = 1 + mock_authz_record.dns_challenge.validation_domain_name = Mock() + mock_authz_record.dns_challenge.validation = Mock() + mock_authz.append(mock_authz_record) + mock_dns_provider = Mock() + mock_dns_provider.delete_txt_record = Mock() + + mock_acme_client = Mock() + result = plugin.finalize_authorizations(mock_acme_client, "account_number", mock_dns_provider, mock_authz) + self.assertEqual(result, mock_authz) + + @patch('lemur.plugins.lemur_acme.plugin.current_app') + def test_create_authority(self, mock_current_app): + mock_current_app.config = Mock() + options = { + "plugin": { + "plugin_options": [{ + "name": "certificate", + "value": "123" + }] + } + } + acme_root, b, role = self.ACMEIssuerPlugin.create_authority(options) + self.assertEqual(acme_root, "123") + self.assertEqual(b, "") + self.assertEqual(role, [{'username': '', 'password': '', 'name': 'acme'}]) diff --git a/lemur/static/app/angular/dns_providers/dns_provider/dns_provider.js b/lemur/static/app/angular/dns_providers/dns_provider/dns_provider.js index 62506101..f7607f84 100644 --- a/lemur/static/app/angular/dns_providers/dns_provider/dns_provider.js +++ b/lemur/static/app/angular/dns_providers/dns_provider/dns_provider.js @@ -5,20 +5,12 @@ angular.module('lemur') .controller('DnsProviderCreateController', function ($scope, $uibModalInstance, PluginService, DnsProviderService, LemurRestangular, toaster) { $scope.dns_provider = LemurRestangular.restangularizeElement(null, {}, 'dns_providers'); - PluginService.getByName('acme-issuer').then(function (acme) { - $scope.acme = acme; - }); - - PluginService.getByType('dns_provider').then(function (plugins) { - $scope.plugins = plugins; - }); - - PluginService.getByType('export').then(function (plugins) { - $scope.exportPlugins = plugins; + DnsProviderService.getDnsProviderOptions().then(function(res) { + $scope.options = res; }); $scope.save = function (dns_provider) { - DnsProviderService.create(dns_provider.then( + DnsProviderService.create(dns_provider).then( function () { toaster.pop({ type: 'success', @@ -35,7 +27,7 @@ angular.module('lemur') directiveData: response.data, timeout: 100000 }); - })); + }); }; $scope.cancel = function () { @@ -48,14 +40,6 @@ angular.module('lemur') DnsProviderApi.get(editId).then(function (dns_provider) { $scope.dns_provider = dns_provider; - - PluginService.getByName('acme-issuer').then(function (acme) { - $scope.acme = acme; - - _.each($scope.acme, function (opts) { - console.log(opts); - }); - }); }); $scope.save = function (dns_provider) { diff --git a/lemur/static/app/angular/dns_providers/dns_provider/dns_provider.tpl.html b/lemur/static/app/angular/dns_providers/dns_provider/dns_provider.tpl.html index faeb46bd..eee8bfd5 100644 --- a/lemur/static/app/angular/dns_providers/dns_provider/dns_provider.tpl.html +++ b/lemur/static/app/angular/dns_providers/dns_provider/dns_provider.tpl.html @@ -31,11 +31,11 @@ Provider Type
-
-
+