Merge branch 'castrapel-hackday'

This commit is contained in:
Curtis Castrapel 2018-04-24 09:41:26 -07:00
commit 38c33395c8
38 changed files with 902 additions and 58 deletions

@ -123,5 +123,4 @@ endif
@echo "--> Done installing new dependencies" @echo "--> Done installing new dependencies"
@echo "" @echo ""
.PHONY: develop dev-postgres dev-docs setup-git build clean update-submodules test testloop test-cli test-js test-python lint lint-python lint-js coverage publish release .PHONY: develop dev-postgres dev-docs setup-git build clean update-submodules test testloop test-cli test-js test-python lint lint-python lint-js coverage publish release

@ -29,6 +29,7 @@ from lemur.endpoints.views import mod as endpoints_bp
from lemur.logs.views import mod as logs_bp from lemur.logs.views import mod as logs_bp
from lemur.api_keys.views import mod as api_key_bp from lemur.api_keys.views import mod as api_key_bp
from lemur.pending_certificates.views import mod as pending_certificates_bp from lemur.pending_certificates.views import mod as pending_certificates_bp
from lemur.dns_providers.views import mod as dns_providers_bp
from lemur.__about__ import ( from lemur.__about__ import (
__author__, __copyright__, __email__, __license__, __summary__, __title__, __author__, __copyright__, __email__, __license__, __summary__, __title__,
@ -57,6 +58,7 @@ LEMUR_BLUEPRINTS = (
logs_bp, logs_bp,
api_key_bp, api_key_bp,
pending_certificates_bp, pending_certificates_bp,
dns_providers_bp,
) )

@ -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)

@ -0,0 +1,34 @@
"""
.. module: lemur.authorizations.models
:platform: unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Netflix Secops <secops@netflix.com>
"""
from sqlalchemy import Column, Integer, String
from sqlalchemy_utils import JSONType
from lemur.database import db
from lemur.plugins.base import plugins
class Authorizations(db.Model):
__tablename__ = 'pending_dns_authorizations'
id = Column(Integer, primary_key=True, autoincrement=True)
account_number = Column(String(128))
domains = Column(JSONType)
dns_provider_type = Column(String(128))
options = Column(JSONType)
@property
def plugin(self):
return plugins.get(self.plugin_name)
def __repr__(self):
return "Authorizations(id={id})".format(label=self.id)
def __init__(self, account_number, domains, dns_provider_type, options=None):
self.account_number = account_number
self.domains = domains
self.dns_provider_type = dns_provider_type
self.options = options

@ -0,0 +1,24 @@
"""
.. module: lemur.pending_certificates.service
Copyright (c) 2017 and onwards Instart Logic, Inc. All rights reserved.
.. moduleauthor:: Secops <secops@netflix.com>
"""
from lemur import database
from lemur.authorizations.models import Authorizations
def get(authorization_id):
"""
Retrieve dns authorization by ID
"""
return database.get(Authorizations, authorization_id)
def create(account_number, domains, dns_provider_type, options=None):
"""
Creates a new dns authorization.
"""
authorization = Authorizations(account_number, domains, dns_provider_type, options)
return database.create(authorization)

@ -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_id = Column(Integer(), nullable=True)
not_before = Column(ArrowType) not_before = Column(ArrowType)
not_after = Column(ArrowType) not_after = Column(ArrowType)

@ -18,7 +18,8 @@ from lemur.schemas import (
ExtensionSchema, ExtensionSchema,
AssociatedRoleSchema, AssociatedRoleSchema,
EndpointNestedOutputSchema, EndpointNestedOutputSchema,
AssociatedRotationPolicySchema AssociatedRotationPolicySchema,
DnsProviderSchema
) )
from lemur.authorities.schemas import AuthorityNestedOutputSchema from lemur.authorities.schemas import AuthorityNestedOutputSchema
@ -70,6 +71,7 @@ class CertificateInputSchema(CertificateCreationSchema):
replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) replaces = fields.Nested(AssociatedCertificateSchema, missing=[], many=True)
replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) # deprecated replacements = fields.Nested(AssociatedCertificateSchema, missing=[], many=True) # deprecated
roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True) roles = fields.Nested(AssociatedRoleSchema, missing=[], many=True)
dns_provider = fields.Nested(DnsProviderSchema, missing={}, required=False, allow_none=True)
csr = fields.String(validate=validators.csr) csr = fields.String(validate=validators.csr)
key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048') key_type = fields.String(validate=validate.OneOf(['RSA2048', 'RSA4096']), missing='RSA2048')

@ -50,7 +50,8 @@ class LemurDefaults(AuthenticatedResource):
"state": "CA", "state": "CA",
"location": "Los Gatos", "location": "Los Gatos",
"organization": "Netflix", "organization": "Netflix",
"organizationalUnit": "Operations" "organizationalUnit": "Operations",
"dnsProviders": [{"name": "test", ...}, {...}],
} }
:reqheader Authorization: OAuth token to authenticate :reqheader Authorization: OAuth token to authenticate
@ -67,7 +68,7 @@ class LemurDefaults(AuthenticatedResource):
organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'), organization=current_app.config.get('LEMUR_DEFAULT_ORGANIZATION'),
organizational_unit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'), organizational_unit=current_app.config.get('LEMUR_DEFAULT_ORGANIZATIONAL_UNIT'),
issuer_plugin=current_app.config.get('LEMUR_DEFAULT_ISSUER_PLUGIN'), issuer_plugin=current_app.config.get('LEMUR_DEFAULT_ISSUER_PLUGIN'),
authority=default_authority authority=default_authority,
) )

@ -0,0 +1,19 @@
from sqlalchemy import Column, Integer, String, text
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy_utils import ArrowType
from lemur.database import db
class DnsProviders(db.Model):
__tablename__ = 'dns_providers'
id = Column(Integer(), primary_key=True)
name = Column(String(length=256), unique=True, nullable=True)
description = Column(String(length=1024), nullable=True)
provider_type = Column(String(length=256), nullable=True)
credentials = Column(String(length=256), nullable=True)
api_endpoint = Column(String(length=256), nullable=True)
date_created = Column(ArrowType(), server_default=text('now()'), nullable=False)
status = Column(String(length=128), nullable=True)
options = Column(JSON, nullable=True)
domains = Column(JSON, nullable=True)

@ -0,0 +1,18 @@
from lemur.common.fields import ArrowDateTime
from lemur.common.schema import LemurOutputSchema
from marshmallow import fields
class DnsProvidersNestedOutputSchema(LemurOutputSchema):
__envelope__ = False
id = fields.Integer()
name = fields.String()
provider_type = fields.String()
description = fields.String()
credentials = fields.String()
api_endpoint = fields.String()
date_created = ArrowDateTime()
dns_provider_schema = DnsProvidersNestedOutputSchema()

@ -0,0 +1,33 @@
from lemur import database
from lemur.dns_providers.models import DnsProviders
def render(args):
"""
Helper that helps us render the REST Api responses.
:param args:
:return:
"""
query = database.session_query(DnsProviders)
return database.sort_and_page(query, DnsProviders, args)
def get(dns_provider_id):
"""
Retrieves a dns provider by its lemur assigned ID.
:param dns_provider_id: Lemur assigned ID
:rtype : DnsProvider
:return:
"""
return database.get(DnsProviders, dns_provider_id)
def delete(dns_provider_id):
"""
Deletes a DNS provider.
:param dns_provider_id: Lemur assigned ID
"""
database.delete(get(dns_provider_id))

@ -0,0 +1,87 @@
"""
.. module: lemur.dns)providers.views
:platform: Unix
:copyright: (c) 2015 by Netflix Inc., see AUTHORS for more
:license: Apache, see LICENSE for more details.
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
"""
from flask import Blueprint, g
from flask_restful import reqparse, Api
from lemur.auth.permissions import admin_permission
from lemur.auth.service import AuthenticatedResource
from lemur.common.schema import validate_schema
from lemur.common.utils import paginated_parser
from lemur.dns_providers import service
from lemur.dns_providers.schemas import dns_provider_schema
mod = Blueprint('dns_providers', __name__)
api = Api(mod)
class DnsProvidersList(AuthenticatedResource):
""" Defines the 'dns_providers' endpoint """
def __init__(self):
self.reqparse = reqparse.RequestParser()
super(DnsProvidersList, self).__init__()
@validate_schema(None, dns_provider_schema)
def get(self):
"""
.. http:get:: /dns_providers
The current list of DNS Providers
**Example request**:
.. sourcecode:: http
GET /dns_providers HTTP/1.1
Host: example.com
Accept: application/json, text/javascript
**Example response**:
.. sourcecode:: http
HTTP/1.1 200 OK
Vary: Accept
Content-Type: text/javascript
{
"items": [{
"id": 1,
"name": "test",
"description": "test",
"provider_type": "dyn",
"status": "active",
}],
"total": 1
}
:query sortBy: field to sort on
:query sortDir: asc or desc
:query page: int. default is 1
:query filter: key value pair format is k;v
:query count: count number. default is 10
:reqheader Authorization: OAuth token to authenticate
:statuscode 200: no error
:statuscode 403: unauthenticated
"""
parser = paginated_parser.copy()
parser.add_argument('id', type=int, location='args')
parser.add_argument('name', type=str, location='args')
parser.add_argument('type', type=str, location='args')
args = parser.parse_args()
args['user'] = g.user
return service.render(args)
@admin_permission.require(http_exception=403)
def delete(self, dns_provider_id):
service.delete(dns_provider_id)
return {'result': True}
api.add_resource(DnsProvidersList, '/dns_providers', endpoint='dns_providers')

@ -0,0 +1,39 @@
"""Create dns_providers table
Revision ID: 3adfdd6598df
Revises: 556ceb3e3c3e
Create Date: 2018-04-10 13:25:47.007556
"""
# revision identifiers, used by Alembic.
revision = '3adfdd6598df'
down_revision = '556ceb3e3c3e'
from alembic import op
import sqlalchemy as sa
from sqlalchemy.dialects.postgresql import JSON
from sqlalchemy_utils import ArrowType
def upgrade():
# create provider table
op.create_table(
'dns_providers',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=256), nullable=True),
sa.Column('description', sa.String(length=1024), nullable=True),
sa.Column('provider_type', sa.String(length=256), nullable=True),
sa.Column('credentials', sa.String(length=256), nullable=True),
sa.Column('api_endpoint', sa.String(length=256), nullable=True),
sa.Column('date_created', ArrowType(), server_default=sa.text('now()'), nullable=False),
sa.Column('status', sa.String(length=128), nullable=True),
sa.Column('options', JSON),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
def downgrade():
op.drop_table('dns_providers')

@ -0,0 +1,22 @@
"""Add dns_provider id column to certificates table
Revision ID: 4e78b9e4e1dd
Revises: 3adfdd6598df
Create Date: 2018-04-10 14:00:30.701669
"""
# revision identifiers, used by Alembic.
revision = '4e78b9e4e1dd'
down_revision = '3adfdd6598df'
from alembic import op
import sqlalchemy as sa
def upgrade():
op.add_column('certificates', sa.Column('dns_provider_id', sa.Integer(), nullable=True))
def downgrade():
op.drop_column('certificates', 'dns_provider_id')

@ -29,7 +29,7 @@ def fetch(ids):
for cert in pending_certs: for cert in pending_certs:
authority = plugins.get(cert.authority.plugin_name) authority = plugins.get(cert.authority.plugin_name)
real_cert = authority.get_ordered_certificate(cert.external_id) real_cert = authority.get_ordered_certificate(cert)
if real_cert: if real_cert:
# If a real certificate was returned from issuer, then create it in Lemur and delete # If a real certificate was returned from issuer, then create it in Lemur and delete
# the pending certificate # the pending certificate

@ -8,6 +8,7 @@ from datetime import datetime as dt
from sqlalchemy.orm import relationship from sqlalchemy.orm import relationship
from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean from sqlalchemy import Integer, ForeignKey, String, PassiveDefault, func, Column, Text, Boolean
from sqlalchemy_utils.types.arrow import ArrowType from sqlalchemy_utils.types.arrow import ArrowType
from sqlalchemy_utils import JSONType
import lemur.common.utils import lemur.common.utils
from lemur.certificates.models import get_or_increase_name from lemur.certificates.models import get_or_increase_name
@ -37,6 +38,7 @@ class PendingCertificate(db.Model):
private_key = Column(Vault, nullable=True) private_key = Column(Vault, nullable=True)
date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False) date_created = Column(ArrowType, PassiveDefault(func.now()), nullable=False)
dns_provider_id = Column(Integer(), nullable=True)
status = Column(String(128)) status = Column(String(128))
@ -54,6 +56,7 @@ class PendingCertificate(db.Model):
secondary=pending_cert_replacement_associations, secondary=pending_cert_replacement_associations,
backref='pending_cert', backref='pending_cert',
passive_deletes=True) passive_deletes=True)
options = Column(JSONType)
rotation_policy = relationship("RotationPolicy") rotation_policy = relationship("RotationPolicy")
@ -93,3 +96,7 @@ class PendingCertificate(db.Model):
self.replaces = kwargs.get('replaces', []) self.replaces = kwargs.get('replaces', [])
self.rotation = kwargs.get('rotation') self.rotation = kwargs.get('rotation')
self.rotation_policy = kwargs.get('rotation_policy') self.rotation_policy = kwargs.get('rotation_policy')
try:
self.dns_provider_id = kwargs.get('dns_provider', {}).get("id")
except AttributeError:
self.dns_provider_id = None

@ -25,7 +25,7 @@ class IssuerPlugin(Plugin):
def revoke_certificate(self, certificate, comments): def revoke_certificate(self, certificate, comments):
raise NotImplementedError raise NotImplementedError
def get_ordered_certificate(self, order_id): def get_ordered_certificate(self, certificate):
raise NotImplementedError raise NotImplementedError
def cancel_ordered_certificate(self, pending_cert, **kwargs): def cancel_ordered_certificate(self, pending_cert, **kwargs):

@ -9,8 +9,10 @@
.. moduleauthor:: Kevin Glisson <kglisson@netflix.com> .. moduleauthor:: Kevin Glisson <kglisson@netflix.com>
.. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com> .. moduleauthor:: Mikhail Khodorovskiy <mikhail.khodorovskiy@jivesoftware.com>
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
""" """
import josepy as jose import josepy as jose
import json
from flask import current_app from flask import current_app
@ -22,7 +24,8 @@ from lemur.common.utils import generate_private_key
import OpenSSL.crypto import OpenSSL.crypto
from lemur.common.utils import validate_conf from lemur.authorizations import service as authorization_service
from lemur.dns_providers import service as dns_provider_service
from lemur.plugins.bases import IssuerPlugin from lemur.plugins.bases import IssuerPlugin
from lemur.plugins import lemur_acme as acme from lemur.plugins import lemur_acme as acme
@ -96,19 +99,26 @@ def request_certificate(acme_client, authorizations, csr):
OpenSSL.crypto.FILETYPE_PEM, cert_response.body OpenSSL.crypto.FILETYPE_PEM, cert_response.body
).decode('utf-8') ).decode('utf-8')
pem_certificate_chain = "\n".join( full_chain = []
OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert.decode("utf-8")) for cert in acme_client.fetch_chain(cert_response):
for cert in acme_client.fetch_chain(cert_response) chain = OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_PEM, cert)
).decode('utf-8') full_chain.append(chain.decode("utf-8"))
pem_certificate_chain = "\n".join(full_chain)
current_app.logger.debug("{0} {1}".format(type(pem_certificate). type(pem_certificate_chain))) current_app.logger.debug("{0} {1}".format(type(pem_certificate), type(pem_certificate_chain)))
return pem_certificate, pem_certificate_chain return pem_certificate, pem_certificate_chain
def setup_acme_client(): def setup_acme_client(authority):
email = current_app.config.get('ACME_EMAIL') if not authority.options:
tel = current_app.config.get('ACME_TEL') raise Exception("Invalid authority. Options not set")
directory_url = current_app.config.get('ACME_DIRECTORY_URL') options = {}
for o in json.loads(authority.options):
print(o)
options[o.get("name")] = o.get("value")
email = options.get('email', current_app.config.get('ACME_EMAIL'))
tel = options.get('telephone', current_app.config.get('ACME_TEL'))
directory_url = options.get('acme_url', current_app.config.get('ACME_DIRECTORY_URL'))
contact = ('mailto:{}'.format(email), 'tel:{}'.format(tel)) contact = ('mailto:{}'.format(email), 'tel:{}'.format(tel))
key = jose.JWKRSA(key=generate_private_key('RSA2048')) key = jose.JWKRSA(key=generate_private_key('RSA2048'))
@ -145,11 +155,14 @@ def get_domains(options):
def get_authorizations(acme_client, account_number, domains, dns_provider): def get_authorizations(acme_client, account_number, domains, dns_provider):
authorizations = [] authorizations = []
try:
for domain in domains: for domain in domains:
authz_record = start_dns_challenge(acme_client, account_number, domain, dns_provider) authz_record = start_dns_challenge(acme_client, account_number, domain, dns_provider)
authorizations.append(authz_record) authorizations.append(authz_record)
return authorizations
def finalize_authorizations(acme_client, account_number, dns_provider, authorizations):
try:
for authz_record in authorizations: for authz_record in authorizations:
complete_dns_challenge(acme_client, account_number, authz_record, dns_provider) complete_dns_challenge(acme_client, account_number, authz_record, dns_provider)
finally: finally:
@ -171,24 +184,59 @@ class ACMEIssuerPlugin(IssuerPlugin):
description = 'Enables the creation of certificates via ACME CAs (including Let\'s Encrypt)' description = 'Enables the creation of certificates via ACME CAs (including Let\'s Encrypt)'
version = acme.VERSION version = acme.VERSION
author = 'Kevin Glisson' author = 'Netflix'
author_url = 'https://github.com/netflix/lemur.git' author_url = 'https://github.com/netflix/lemur.git'
def __init__(self, *args, **kwargs): options = [
required_vars = [ {
'ACME_DIRECTORY_URL', 'name': 'acme_url',
'ACME_TEL', 'type': 'str',
'ACME_EMAIL', 'required': True,
'ACME_AWS_ACCOUNT_NUMBER', 'validation': '/^http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@.&+]|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+$/',
'ACME_ROOT' 'helpMessage': 'Must be a valid web url starting with http[s]://',
},
{
'name': 'telephone',
'type': 'str',
'default': '',
'helpMessage': 'Telephone to use'
},
{
'name': 'email',
'type': 'str',
'default': '',
'validation': '/^?([-a-zA-Z0-9.`?{}]+@\w+\.\w+)$/',
'helpMessage': 'Email to use'
},
{
'name': 'certificate',
'type': 'textarea',
'default': '',
'validation': '/^-----BEGIN CERTIFICATE-----/',
'helpMessage': 'Certificate to use'
},
] ]
validate_conf(current_app, required_vars) def __init__(self, *args, **kwargs):
self.dns_provider_name = current_app.config.get('ACME_DNS_PROVIDER', 'route53')
current_app.logger.debug("Using DNS provider: {0}".format(self.dns_provider_name))
self.dns_provider = __import__(self.dns_provider_name, globals(), locals(), [], 1)
super(ACMEIssuerPlugin, self).__init__(*args, **kwargs) super(ACMEIssuerPlugin, self).__init__(*args, **kwargs)
def get_ordered_certificate(self, pending_cert):
acme_client, registration = setup_acme_client(pending_cert.authority)
order_info = authorization_service.get(pending_cert.external_id)
dns_provider = dns_provider_service.get(pending_cert.dns_provider_id)
dns_provider_type = __import__(dns_provider.provider_type, globals(), locals(), [], 1)
authorizations = get_authorizations(
acme_client, order_info.account_number, order_info.domains, dns_provider_type)
finalize_authorizations(acme_client, order_info.account_number, dns_provider_type, authorizations)
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, pending_cert.csr)
cert = {
'body': "\n".join(str(pem_certificate).splitlines()),
'chain': "\n".join(str(pem_certificate_chain).splitlines()),
'external_id': str(pending_cert.external_id)
}
return cert
def create_certificate(self, csr, issuer_options): def create_certificate(self, csr, issuer_options):
""" """
Creates an ACME certificate. Creates an ACME certificate.
@ -197,11 +245,38 @@ class ACMEIssuerPlugin(IssuerPlugin):
:param issuer_options: :param issuer_options:
:return: :raise Exception: :return: :raise Exception:
""" """
current_app.logger.debug("Requesting a new acme certificate: {0}".format(issuer_options)) authority = issuer_options.get('authority')
acme_client, registration = setup_acme_client() create_immediately = issuer_options.get('create_immediately', False)
account_number = current_app.config.get('ACME_AWS_ACCOUNT_NUMBER') acme_client, registration = setup_acme_client(authority)
dns_provider_d = issuer_options.get('dns_provider')
if not dns_provider_d:
raise Exception("DNS Provider setting is required for ACME certificates.")
dns_provider = dns_provider_service.get(dns_provider_d.get("id"))
credentials = json.loads(dns_provider.credentials)
current_app.logger.debug("Using DNS provider: {0}".format(dns_provider.provider_type))
dns_provider_type = __import__(dns_provider.provider_type, globals(), locals(), [], 1)
account_number = credentials.get("account_number")
if dns_provider.provider_type == 'route53' and not account_number:
error = "DNS Provider {} does not have an account number configured.".format(dns_provider.name)
current_app.logger.error(error)
raise Exception(error)
domains = get_domains(issuer_options) domains = get_domains(issuer_options)
authorizations = get_authorizations(acme_client, account_number, domains, self.dns_provider) if not create_immediately:
# Create pending authorizations that we'll need to do the creation
authz_domains = []
for d in domains:
if type(d) == str:
authz_domains.append(d)
else:
authz_domains.append(d.value)
dns_authorization = authorization_service.create(account_number, authz_domains, dns_provider.provider_type)
# Return id of the DNS Authorization
return None, None, dns_authorization.id
authorizations = get_authorizations(acme_client, account_number, domains, dns_provider_type)
finalize_authorizations(acme_client, account_number, dns_provider_type, authorizations)
pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, csr) pem_certificate, pem_certificate_chain = 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
@ -216,4 +291,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]

@ -58,7 +58,7 @@ def change_txt_record(action, zone_id, domain, value, client=None):
def create_txt_record(host, value, account_number): def create_txt_record(host, value, account_number):
zone_id = find_zone_id(host, account_number=account_number) zone_id = find_zone_id(host, account_number=account_number)
change_id = change_txt_record( change_id = change_txt_record(
"CREATE", "UPSERT",
zone_id, zone_id,
host, host,
value, value,

@ -325,8 +325,9 @@ class DigiCertIssuerPlugin(IssuerPlugin):
response = self.session.put(create_url, data=json.dumps({'comments': comments})) response = self.session.put(create_url, data=json.dumps({'comments': comments}))
return handle_response(response) return handle_response(response)
def get_ordered_certificate(self, order_id): def get_ordered_certificate(self, pending_cert):
""" Retrieve a certificate via order id """ """ Retrieve a certificate via order id """
order_id = pending_cert.external_id
base_url = current_app.config.get('DIGICERT_URL') base_url = current_app.config.get('DIGICERT_URL')
try: try:
certificate_id = get_certificate_id(self.session, base_url, order_id) certificate_id = get_certificate_id(self.session, base_url, order_id)

@ -150,11 +150,7 @@ def test_signature_hash(app):
signature_hash('sdfdsf') signature_hash('sdfdsf')
def test_issuer_plugin_create_certificate(): def test_issuer_plugin_create_certificate(certificate_="""\
import requests_mock
from lemur.plugins.lemur_digicert.plugin import DigiCertIssuerPlugin
pem_fixture = """\
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
abc abc
-----END CERTIFICATE----- -----END CERTIFICATE-----
@ -164,7 +160,11 @@ def
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
ghi ghi
-----END CERTIFICATE----- -----END CERTIFICATE-----
""" """):
import requests_mock
from lemur.plugins.lemur_digicert.plugin import DigiCertIssuerPlugin
pem_fixture = certificate_
subject = DigiCertIssuerPlugin() subject = DigiCertIssuerPlugin()
adapter = requests_mock.Adapter() adapter = requests_mock.Adapter()

@ -21,6 +21,7 @@ from lemur.plugins.utils import get_plugin_option
from lemur.roles.models import Role from lemur.roles.models import Role
from lemur.users.models import User from lemur.users.models import User
from lemur.authorities.models import Authority from lemur.authorities.models import Authority
from lemur.dns_providers.models import DnsProviders
from lemur.policies.models import RotationPolicy from lemur.policies.models import RotationPolicy
from lemur.certificates.models import Certificate from lemur.certificates.models import Certificate
from lemur.destinations.models import Destination from lemur.destinations.models import Destination
@ -159,6 +160,11 @@ class AssociatedRotationPolicySchema(LemurInputSchema):
return fetch_objects(RotationPolicy, data, many=many) return fetch_objects(RotationPolicy, data, many=many)
class DnsProviderSchema(LemurInputSchema):
id = fields.Integer()
name = fields.String()
class PluginInputSchema(LemurInputSchema): class PluginInputSchema(LemurInputSchema):
plugin_options = fields.List(fields.Dict(), validate=validate_options) plugin_options = fields.List(fields.Dict(), validate=validate_options)
slug = fields.String(required=True) slug = fields.String(required=True)

@ -109,6 +109,15 @@
}; };
}); });
lemur.service('DnsProviders', function (LemurRestangular) {
var DnsProviders = this;
DnsProviders.get = function () {
return LemurRestangular.all('dns_providers').customGET().then(function (dnsProviders) {
return dnsProviders;
});
};
});
lemur.directive('lemurBadRequest', [function () { lemur.directive('lemurBadRequest', [function () {
return { return {
template: '<h4>{{ directiveData.message }}</h4>' + template: '<h4>{{ directiveData.message }}</h4>' +

@ -52,8 +52,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>

@ -134,6 +134,11 @@ angular.module('lemur')
$scope.certificate.validityYears = null; $scope.certificate.validityYears = null;
}; };
CertificateService.getDnsProviders().then(function (providers) {
$scope.dnsProviders = providers;
}
);
$scope.create = function (certificate) { $scope.create = function (certificate) {
WizardHandler.wizard().context.loading = true; WizardHandler.wizard().context.loading = true;
CertificateService.create(certificate).then( CertificateService.create(certificate).then(

@ -234,6 +234,9 @@
</label> </label>
</div> </div>
</div> </div>
<div class="col-sm-10">
</div></div>
</div>
</div> </div>
</div> </div>
<div class="form-group"> <div class="form-group">

@ -106,6 +106,17 @@
</ui-select-choices> </ui-select-choices>
</ui-select> </ui-select>
</div> </div>
</div>
<div class="form-group" ng-show="certificate.authority.plugin.slug == 'acme-issuer'">
<label class="control-label col-sm-2">
DNS Provider:
</label>
<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">
<option value="">- choose an entry. Neded for ACME Providers -</option>
</select>
</div>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="control-label col-sm-2" <label class="control-label col-sm-2"

@ -149,7 +149,7 @@ angular.module('lemur')
}); });
return LemurRestangular.all('certificates'); return LemurRestangular.all('certificates');
}) })
.service('CertificateService', function ($location, CertificateApi, AuthorityService, AuthorityApi, LemurRestangular, DefaultService) { .service('CertificateService', function ($location, CertificateApi, AuthorityService, AuthorityApi, LemurRestangular, DefaultService, DnsProviders) {
var CertificateService = this; var CertificateService = this;
CertificateService.findCertificatesByName = function (filterValue) { CertificateService.findCertificatesByName = function (filterValue) {
return CertificateApi.getList({'filter[name]': filterValue}) return CertificateApi.getList({'filter[name]': filterValue})
@ -246,6 +246,10 @@ angular.module('lemur')
}); });
}; };
CertificateService.getDnsProviders = function () {
return DnsProviders.get();
};
CertificateService.loadPrivateKey = function (certificate) { CertificateService.loadPrivateKey = function (certificate) {
return certificate.customGET('key'); return certificate.customGET('key');
}; };

@ -0,0 +1,98 @@
'use strict';
angular.module('lemur')
.controller('DnsProviderCreateController', function ($scope, $uibModalInstance, PluginService, DnsProviderService, LemurRestangular, toaster) {
$scope.dns_provider = LemurRestangular.restangularizeElement(null, {}, 'dns_providers');
PluginService.getByType('dns_provider').then(function (plugins) {
$scope.plugins = plugins;
});
PluginService.getByType('export').then(function (plugins) {
$scope.exportPlugins = plugins;
});
$scope.save = function (dns_provider) {
DnsProviderService.create(dns_provider.then(
function () {
toaster.pop({
type: 'success',
title: dns_provider.label,
body: 'Successfully Created!'
});
$uibModalInstance.close();
}, function (response) {
toaster.pop({
type: 'error',
title: dns_provider.label,
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: response.data,
timeout: 100000
});
}));
};
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
})
.controller('DnsProviderEditController', function ($scope, $uibModalInstance, DnsProviderService, DnsProviderApi, PluginService, toaster, editId) {
DnsProviderApi.get(editId).then(function (dns_provider) {
$scope.dns_provider = dns_provider;
PluginService.getByType('dns_provider').then(function (plugins) {
$scope.plugins = plugins;
_.each($scope.plugins, function (plugin) {
if (plugin.slug === $scope.dns_provider.plugin.slug) {
plugin.pluginOptions = $scope.dns_provider.plugin.pluginOptions;
$scope.dns_provider.plugin = plugin;
_.each($scope.dns_provider.plugin.pluginOptions, function (option) {
if (option.type === 'export-plugin') {
PluginService.getByType('export').then(function (plugins) {
$scope.exportPlugins = plugins;
_.each($scope.exportPlugins, function (plugin) {
if (plugin.slug === option.value.slug) {
plugin.pluginOptions = option.value.pluginOptions;
option.value = plugin;
}
});
});
}
});
}
});
});
});
$scope.save = function (dns_provider) {
DnsProviderService.update(dns_provider).then(
function () {
toaster.pop({
type: 'success',
title: dns_provider.label,
body: 'Successfully Updated!'
});
$uibModalInstance.close();
}, function (response) {
toaster.pop({
type: 'error',
title: dns_provider.label,
body: 'lemur-bad-request',
bodyOutputType: 'directive',
directiveData: response.data,
timeout: 100000
});
});
};
$scope.cancel = function () {
$uibModalInstance.dismiss('cancel');
};
});

@ -0,0 +1,93 @@
<div class="modal-header">
<button type="button" class="close" ng-click="cancel()" aria-label="Close"><span aria-hidden="true">&times;</span>
</button>
<h3><span ng-show="!dns_provider.fromServer">Create</span><span ng-show="dns_provider.fromServer">Edit</span>
DnsProvider <span class="text-muted"><small>oh the places you will go!</small></span></h3>
</div>
<div class="modal-body">
<form name="createForm" class="form-horizontal" role="form" novalidate>
<div class="form-group"
ng-class="{'has-error': createForm.label.$invalid, 'has-success': !createForm.label.$invalid&&createForm.label.$dirty}">
<label class="control-label col-sm-2">
Label
</label>
<div class="col-sm-10">
<input name="label" ng-model="dns_provider.label" placeholder="Label" class="form-control" required/>
<p ng-show="createForm.label.$invalid && !createForm.label.$pristine" class="help-block">You must enter an
dns_provider label</p>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Description
</label>
<div class="col-sm-10">
<textarea name="comments" ng-model="dns_provider.description" placeholder="Something elegant"
class="form-control"></textarea>
</div>
</div>
<div class="form-group">
<label class="control-label col-sm-2">
Plugin
</label>
<div class="col-sm-10">
<select class="form-control" ng-model="dns_provider.plugin" ng-options="plugin.title for plugin in plugins"
required></select>
</div>
</div>
<div class="form-group" ng-repeat="item in dns_provider.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"/>
<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>
<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" ng-pattern="item.validation"/>
<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>
</form>
</div>
<div class="modal-footer">
<button ng-click="save(dns_provider)" type="submit" ng-disabled="createForm.$invalid" class="btn btn-primary">Save
</button>
<button ng-click="cancel()" class="btn btn-danger">Cancel</button>
</div>

@ -0,0 +1,38 @@
'use strict';
angular.module('lemur')
.service('DnsProviderApi', function (LemurRestangular) {
return LemurRestangular.all('dns_providers');
})
.service('DnsProviderService', function ($location, DnsProviderApi, PluginService, DnsProviders) {
var DnsProviderService = this;
DnsProviderService.findDnsProvidersByName = function (filterValue) {
return DnsProviderApi.getList({'filter[label]': filterValue})
.then(function (dns_providers) {
return dns_providers;
});
};
DnsProviderService.getDnsProviders = function () {
return DnsProviders.get();
};
DnsProviderService.create = function (dns_provider) {
return DnsProviderApi.post(dns_provider);
};
DnsProviderService.get = function () {
return DnsProviderApi.get();
};
DnsProviderService.update = function (dns_provider) {
return dns_provider.put();
};
DnsProviderService.getPlugin = function (dns_provider) {
return PluginService.getByName(dns_provider.pluginName).then(function (plugin) {
dns_provider.plugin = plugin;
});
};
return DnsProviderService;
});

@ -0,0 +1,84 @@
'use strict';
angular.module('lemur')
.config(function config($stateProvider) {
$stateProvider.state('dns_providers', {
url: '/dns_providers',
templateUrl: '/angular/dns_providers/view/view.tpl.html',
controller: 'DnsProvidersViewController'
});
})
.controller('DnsProvidersViewController', function ($scope, $uibModal, DnsProviderApi, DnsProviderService, ngTableParams, toaster) {
$scope.filter = {};
$scope.dnsProvidersTable = new ngTableParams({
page: 1, // show first page
count: 10, // count per page
sorting: {
id: 'desc' // initial sorting
},
filter: $scope.filter
}, {
total: 0, // length of data
getData: function ($defer, params) {
DnsProviderApi.getList(params.url()).then(
function (data) {
params.total(data.total);
$defer.resolve(data);
}
);
}
});
$scope.remove = function (dns_provider) {
dns_provider.remove().then(
function () {
$scope.dnsProvidersTable.reload();
},
function (response) {
toaster.pop({
type: 'error',
title: 'Opps',
body: 'I see what you did there: ' + response.data.message
});
}
);
};
$scope.edit = function (dns_providerId) {
var uibModalInstance = $uibModal.open({
animation: true,
templateUrl: '/angular/dns_providers/dns_provider/dns_provider.tpl.html',
controller: 'DnsProvidersEditController',
size: 'lg',
backdrop: 'static',
resolve: {
editId: function () {
return dns_providerId;
}
}
});
uibModalInstance.result.then(function () {
$scope.dnsProvidersTable.reload();
});
};
$scope.create = function () {
var uibModalInstance = $uibModal.open({
animation: true,
controller: 'DnsProvidersCreateController',
templateUrl: '/angular/dns_providers/dns_provider/dns_provider.tpl.html',
size: 'lg',
backdrop: 'static'
});
uibModalInstance.result.then(function () {
$scope.dnsProvidersTable.reload();
});
};
});

@ -0,0 +1,54 @@
<div class="row">
<div class="col-md-12">
<h2 class="featurette-heading">DNS Providers
<span class="text-muted"><small>the root of all problems</small></span></h2>
<div class="panel panel-default">
<div class="panel-heading">
<div class="btn-group pull-right">
<button ng-click="create()" class="btn btn-primary">Create</button>
</div>
<div class="btn-group">
<button ng-model="showFilter" class="btn btn-default" uib-btn-checkbox
btn-checkbox-true="1"
btn-checkbox-false="0">Filter</button>
</div>
<div class="clearfix"></div>
</div>
<div class="table-responsive">
<table ng-table="dnsProvidersTable" class="table table-striped" show-filter="showFilter" template-pagination="angular/pager.html" >
<tbody>
<tr ng-repeat="dns_provider in $data track by $index">
<td data-title="'Name'" sortable="'name'" filter="{ 'name': 'text' }">
<ul class="list-unstyled">
<li>{{ dns_provider.name }}</li>
<li><span class="text-muted">{{ dns_provider.description }}</span></li>
</ul>
</td>
<td data-title="'Type'" sortable="'type'" filter="{ 'type': 'text' }">
<ul class="list-unstyled">
<li>{{ dns_provider.providerType }}</li>
</ul>
</td>
<td data-title="'Domains'" sortable="'domains'" filter="{ 'domains': 'text' }">
<ul class="list-unstyled">
<li>{{ dns_provider.domains }}</li>
<li><span class="text-muted">{{ dns_provider.description }}</span></li>
</ul>
</td>
<td data-title="''">
<div class="btn-group-vertical pull-right">
<button uib-tooltip="Edit DNS Provider" ng-click="edit(dns_provider.id)" class="btn btn-sm btn-info">
Edit
</button>
<button uib-tooltip="Delete DNS Provider" ng-click="remove(dns_provider)" type="button" class="btn btn-sm btn-danger pull-left">
Remove
</button>
</div>
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>

@ -65,6 +65,7 @@
<li><a ui-sref="domains">Domains</a></li> <li><a ui-sref="domains">Domains</a></li>
<li><a ui-sref="logs">Logs</a></li> <li><a ui-sref="logs">Logs</a></li>
<li><a ui-sref="keys">Api Keys</a></li> <li><a ui-sref="keys">Api Keys</a></li>
<li><a ui-sref="dns_providers">DNS Providers</a></li>
</ul> </ul>
</li> </li>
</ul> </ul>

@ -37,7 +37,8 @@ class TestAsyncIssuerPlugin(IssuerPlugin):
def create_certificate(self, csr, issuer_options): def create_certificate(self, csr, issuer_options):
return "", "", 12345 return "", "", 12345
def get_ordered_certificate(self, order_id): def get_ordered_certificate(self, pending_cert):
order_id = pending_cert.external_id
return INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR, 54321 return INTERNAL_VALID_LONG_STR, INTERNAL_VALID_SAN_STR, 54321
@staticmethod @staticmethod

@ -63,7 +63,7 @@ def test_get_certificate_primitives(certificate):
with freeze_time(datetime.date(year=2016, month=10, day=30)): with freeze_time(datetime.date(year=2016, month=10, day=30)):
primitives = get_certificate_primitives(certificate) primitives = get_certificate_primitives(certificate)
assert len(primitives) == 23 assert len(primitives) == 24
def test_certificate_edit_schema(session): def test_certificate_edit_schema(session):
@ -152,7 +152,8 @@ def test_certificate_input_schema(client, authority):
'authority': {'id': authority.id}, 'authority': {'id': authority.id},
'description': 'testtestest', 'description': 'testtestest',
'validityEnd': arrow.get(2016, 11, 9).isoformat(), 'validityEnd': arrow.get(2016, 11, 9).isoformat(),
'validityStart': arrow.get(2015, 11, 9).isoformat() 'validityStart': arrow.get(2015, 11, 9).isoformat(),
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -165,7 +166,7 @@ def test_certificate_input_schema(client, authority):
assert data['country'] == 'US' assert data['country'] == 'US'
assert data['location'] == 'Los Gatos' assert data['location'] == 'Los Gatos'
assert len(data.keys()) == 18 assert len(data.keys()) == 19
def test_certificate_input_with_extensions(client, authority): def test_certificate_input_with_extensions(client, authority):
@ -192,7 +193,8 @@ def test_certificate_input_with_extensions(client, authority):
{'nameType': 'DNSName', 'value': 'test.example.com'} {'nameType': 'DNSName', 'value': 'test.example.com'}
] ]
} }
} },
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -206,7 +208,8 @@ def test_certificate_out_of_range_date(client, authority):
'owner': 'jim@example.com', 'owner': 'jim@example.com',
'authority': {'id': authority.id}, 'authority': {'id': authority.id},
'description': 'testtestest', 'description': 'testtestest',
'validityYears': 100 'validityYears': 100,
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -230,7 +233,8 @@ def test_certificate_valid_years(client, authority):
'owner': 'jim@example.com', 'owner': 'jim@example.com',
'authority': {'id': authority.id}, 'authority': {'id': authority.id},
'description': 'testtestest', 'description': 'testtestest',
'validityYears': 1 'validityYears': 1,
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -245,7 +249,8 @@ def test_certificate_valid_dates(client, authority):
'authority': {'id': authority.id}, 'authority': {'id': authority.id},
'description': 'testtestest', 'description': 'testtestest',
'validityStart': '2020-01-01T00:00:00', 'validityStart': '2020-01-01T00:00:00',
'validityEnd': '2020-01-01T00:00:01' 'validityEnd': '2020-01-01T00:00:01',
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -262,6 +267,7 @@ def test_certificate_cn_admin(client, authority, logged_in_admin):
'description': 'testtestest', 'description': 'testtestest',
'validityStart': '2020-01-01T00:00:00', 'validityStart': '2020-01-01T00:00:00',
'validityEnd': '2020-01-01T00:00:01', 'validityEnd': '2020-01-01T00:00:01',
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -285,7 +291,8 @@ def test_certificate_allowed_names(client, authority, session, logged_in_user):
{'nameType': 'IPAddress', 'value': '127.0.0.1'}, {'nameType': 'IPAddress', 'value': '127.0.0.1'},
] ]
} }
} },
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -306,6 +313,7 @@ def test_certificate_incative_authority(client, authority, session, logged_in_us
'description': 'testtestest', 'description': 'testtestest',
'validityStart': '2020-01-01T00:00:00', 'validityStart': '2020-01-01T00:00:00',
'validityEnd': '2020-01-01T00:00:01', 'validityEnd': '2020-01-01T00:00:01',
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -329,7 +337,8 @@ def test_certificate_disallowed_names(client, authority, session, logged_in_user
{'nameType': 'DNSName', 'value': 'evilhacker.org'}, {'nameType': 'DNSName', 'value': 'evilhacker.org'},
] ]
} }
} },
'dns_provider': None,
} }
data, errors = CertificateInputSchema().load(input_data) data, errors = CertificateInputSchema().load(input_data)
@ -348,6 +357,7 @@ def test_certificate_sensitive_name(client, authority, session, logged_in_user):
'description': 'testtestest', 'description': 'testtestest',
'validityStart': '2020-01-01T00:00:00', 'validityStart': '2020-01-01T00:00:00',
'validityEnd': '2020-01-01T00:00:01', 'validityEnd': '2020-01-01T00:00:01',
'dns_provider': None,
} }
session.add(Domain(name='sensitive.example.com', sensitive=True)) session.add(Domain(name='sensitive.example.com', sensitive=True))