dyn support

This commit is contained in:
Curtis Castrapel 2018-05-04 15:00:43 -07:00
parent 3e64dd4653
commit 1be3f8368f
13 changed files with 216 additions and 78 deletions

View File

@ -111,7 +111,7 @@ def create(**kwargs):
cert = upload(**kwargs) cert = upload(**kwargs)
kwargs['authority_certificate'] = cert kwargs['authority_certificate'] = cert
if kwargs.get('plugin', {}).get('plugin_options', []): if kwargs.get('plugin', {}).get('plugin_options', []):
kwargs['options'] = json.dumps(kwargs.get('plugin', {}).get('plugin_options', [])) kwargs['options'] = json.dumps(kwargs['plugin']['plugin_options'])
authority = Authority(**kwargs) authority = Authority(**kwargs)
authority = database.create(authority) authority = database.create(authority)

View File

@ -34,3 +34,7 @@ class AttrNotFound(LemurException):
class InvalidConfiguration(Exception): class InvalidConfiguration(Exception):
pass pass
class InvalidAuthority(Exception):
pass

View File

@ -5,14 +5,12 @@
.. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com> .. moduleauthor:: Curtis Castrapel <ccastrapel@netflix.com>
""" """
from flask_script import Manager from flask_script import Manager
from multiprocessing import Pool
from lemur.pending_certificates import service as pending_certificate_service from lemur.pending_certificates import service as pending_certificate_service
from lemur.plugins.base import plugins from lemur.plugins.base import plugins
from lemur.users import service as user_service from lemur.users import service as user_service
manager = Manager(usage="Handles pending certificate related tasks.") manager = Manager(usage="Handles pending certificate related tasks.")
agents = 20
# Need to call this multiple times and store status of the cert in DB. If it is being worked on by a worker, and which # Need to call this multiple times and store status of the cert in DB. If it is being worked on by a worker, and which
@ -55,31 +53,32 @@ def fetch(ids):
) )
def fetch_all(): @manager.command
def fetch_all_acme():
""" """
Attempt to get full certificates for each pending certificate listed. Attempt to get full certificates for each pending certificate listed for ACME.
Args:
ids: a list of ids of PendingCertificates (passed in by manager options when run as CLI)
`python manager.py pending_certs fetch -i 123 321 all`
""" """
pending_certs = pending_certificate_service.get_pending_certs('all') pending_certs = pending_certificate_service.get_pending_certs('all')
user = user_service.get_by_username('lemur') user = user_service.get_by_username('lemur')
new = 0 new = 0
failed = 0 failed = 0
certs = authority.get_ordered_certificates(pending_certs) authority = plugins.get("acme-issuer")
for cert in certs: resolved_certs = authority.get_ordered_certificates(pending_certs)
authority = plugins.get(cert.authority.plugin_name)
real_cert = authority.get_ordered_certificate(cert) for cert in resolved_certs:
real_cert = cert.get("cert")
# It's necessary to reload the pending cert due to detached instance: http://sqlalche.me/e/bhk3
pending_cert = pending_certificate_service.get(cert.get("pending_cert").id)
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
pending_certificate_service.create_certificate(cert, real_cert, user) pending_certificate_service.create_certificate(pending_cert, real_cert, user)
pending_certificate_service.delete(cert) pending_certificate_service.delete_by_id(pending_cert.id)
# add metrics to metrics extension # add metrics to metrics extension
new += 1 new += 1
else: else:
pending_certificate_service.increment_attempt(cert) pending_certificate_service.increment_attempt(pending_cert)
failed += 1 failed += 1
print( print(
"[+] Certificates: New: {new} Failed: {failed}".format( "[+] Certificates: New: {new} Failed: {failed}".format(

View File

@ -59,6 +59,10 @@ def delete(pending_certificate):
database.delete(pending_certificate) database.delete(pending_certificate)
def delete_by_id(id):
database.delete(get(id))
def get_pending_certs(pending_ids): def get_pending_certs(pending_ids):
""" """
Retrieve a list of pending certs given a list of ids Retrieve a list of pending certs given a list of ids

View File

@ -0,0 +1,80 @@
import dns.exception
import dns.resolver
import time
from dyn.tm.session import DynectSession
from dyn.tm.zones import Node, Zone
from flask import current_app
from tld import get_tld
current_app.logger.debug("Logging in to Dyn API")
dynect_session = DynectSession(
current_app.config.get('ACME_DYN_CUSTOMER_NAME', ''),
current_app.config.get('ACME_DYN_USERNAME', ''),
current_app.config.get('ACME_DYN_PASSWORD', ''),
)
def _has_dns_propagated(name, token):
txt_records = []
try:
dns_resolver = dns.resolver.Resolver()
dns_resolver.nameservers = ['8.8.8.8']
dns_response = dns_resolver.query(name, 'TXT')
for rdata in dns_response:
for txt_record in rdata.strings:
txt_records.append(txt_record.decode("utf-8"))
except dns.exception.DNSException:
return False
for txt_record in txt_records:
if txt_record == token:
return True
return False
def wait_for_dns_change(change_id, account_number=None):
fqdn, token = change_id
while True:
status = _has_dns_propagated(fqdn, token)
current_app.logger.debug("Record status for fqdn: {}: {}".format(fqdn, status))
if status:
break
time.sleep(20)
return
def create_txt_record(domain, token, account_number):
zone_name = get_tld('http://' + domain)
zone_parts = len(zone_name.split('.'))
node_name = '.'.join(domain.split('.')[:-zone_parts])
fqdn = "{0}.{1}".format(node_name, zone_name)
zone = Zone(zone_name)
zone.add_record(node_name, record_type='TXT', txtdata="\"{}\"".format(token), ttl=5)
node = zone.get_node(node_name)
zone.publish()
current_app.logger.debug("TXT record created: {0}".format(fqdn))
change_id = (fqdn, token)
return change_id
def delete_txt_record(change_id, account_number, domain, token):
if not domain:
current_app.logger.debug("delete_txt_record: No domain passed")
return
zone_name = get_tld('http://' + domain)
zone_parts = len(zone_name.split('.'))
node_name = '.'.join(domain.split('.')[:-zone_parts])
fqdn = "{0}.{1}".format(node_name, zone_name)
zone = Zone(zone_name)
node = Node(zone_name, fqdn)
all_txt_records = node.get_all_records_by_type('TXT')
for txt_record in all_txt_records:
if txt_record.txtdata == ("{}".format(token)):
current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn))
txt_record.delete()
zone.publish()

View File

@ -17,8 +17,9 @@ import json
from flask import current_app from flask import current_app
from acme.client import Client from acme.client import Client
from acme import messages from acme import challenges, messages
from acme import challenges from acme.errors import PollError
from botocore.exceptions import ClientError
from lemur.common.utils import generate_private_key from lemur.common.utils import generate_private_key
@ -26,6 +27,7 @@ import OpenSSL.crypto
from lemur.authorizations import service as authorization_service from lemur.authorizations import service as authorization_service
from lemur.dns_providers import service as dns_provider_service from lemur.dns_providers import service as dns_provider_service
from lemur.exceptions import InvalidAuthority, InvalidConfiguration
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
@ -68,6 +70,7 @@ def start_dns_challenge(acme_client, account_number, host, dns_provider):
def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider): def complete_dns_challenge(acme_client, account_number, authz_record, dns_provider):
current_app.logger.debug("Finalizing DNS challenge for {0}".format(authz_record.host))
dns_provider.wait_for_dns_change(authz_record.change_id, account_number=account_number) dns_provider.wait_for_dns_change(authz_record.change_id, account_number=account_number)
response = authz_record.dns_challenge.response(acme_client.key) response = authz_record.dns_challenge.response(acme_client.key)
@ -93,6 +96,8 @@ def request_certificate(acme_client, authorizations, csr):
) )
), ),
authzrs=[authz_record.authz for authz_record in authorizations], authzrs=[authz_record.authz for authz_record in authorizations],
mintime=60,
max_attempts=10,
) )
pem_certificate = OpenSSL.crypto.dump_certificate( pem_certificate = OpenSSL.crypto.dump_certificate(
@ -111,11 +116,11 @@ def request_certificate(acme_client, authorizations, csr):
def setup_acme_client(authority): def setup_acme_client(authority):
if not authority.options: if not authority.options:
raise Exception("Invalid authority. Options not set") raise InvalidAuthority("Invalid authority. Options not set")
options = {} options = {}
for option in json.loads(authority.options): for option in json.loads(authority.options):
options[option.get("name")] = option.get("value") options[option["name"]] = option.get("value")
email = options.get('email', current_app.config.get('ACME_EMAIL')) email = options.get('email', current_app.config.get('ACME_EMAIL'))
tel = options.get('telephone', current_app.config.get('ACME_TEL')) tel = options.get('telephone', current_app.config.get('ACME_TEL'))
directory_url = options.get('acme_url', current_app.config.get('ACME_DIRECTORY_URL')) directory_url = options.get('acme_url', current_app.config.get('ACME_DIRECTORY_URL'))
@ -219,15 +224,29 @@ 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)
def get_dns_provider(self, type):
from lemur.plugins.lemur_acme import cloudflare, dyn, route53
provider_types = {
'cloudflare': cloudflare,
'dyn': dyn,
'route53': route53,
}
return provider_types[type]
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 = 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) dns_provider = dns_provider_service.get(pending_cert.dns_provider_id)
dns_provider_type = __import__(dns_provider.provider_type, globals(), locals(), [], 1) dns_provider_type = self.get_dns_provider(dns_provider.provider_type)
try:
authorizations = get_authorizations( authorizations = get_authorizations(
acme_client, order_info.account_number, order_info.domains, dns_provider_type) acme_client, order_info.account_number, order_info.domains, dns_provider_type)
except ClientError:
current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert.name), exc_info=True)
return False
finalize_authorizations(acme_client, order_info.account_number, dns_provider_type, authorizations) authorizations = 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) pem_certificate, pem_certificate_chain = request_certificate(acme_client, authorizations, pending_cert.csr)
cert = { cert = {
'body': "\n".join(str(pem_certificate).splitlines()), 'body': "\n".join(str(pem_certificate).splitlines()),
@ -238,11 +257,13 @@ class ACMEIssuerPlugin(IssuerPlugin):
def get_ordered_certificates(self, pending_certs): def get_ordered_certificates(self, pending_certs):
pending = [] pending = []
certs = []
for pending_cert in pending_certs: for pending_cert in pending_certs:
acme_client, registration = setup_acme_client(pending_cert.authority) acme_client, registration = 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) dns_provider = dns_provider_service.get(pending_cert.dns_provider_id)
dns_provider_type = __import__(dns_provider.provider_type, globals(), locals(), [], 1) dns_provider_type = self.get_dns_provider(dns_provider.provider_type)
try:
authorizations = get_authorizations( authorizations = get_authorizations(
acme_client, order_info.account_number, order_info.domains, dns_provider_type) acme_client, order_info.account_number, order_info.domains, dns_provider_type)
pending.append({ pending.append({
@ -252,30 +273,42 @@ class ACMEIssuerPlugin(IssuerPlugin):
"authorizations": authorizations, "authorizations": authorizations,
"pending_cert": pending_cert, "pending_cert": pending_cert,
}) })
except (ClientError, ValueError):
certs = [] current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True)
certs.append({
"cert": False,
"pending_cert": pending_cert,
})
for entry in pending: for entry in pending:
finalize_authorizations( entry["authorizations"] = finalize_authorizations(
pending["acme_client"], entry["acme_client"],
pending["account_number"], entry["account_number"],
pending["dns_provider_type"], entry["dns_provider_type"],
pending["authorizations"] entry["authorizations"]
) )
try:
pem_certificate, pem_certificate_chain = request_certificate( pem_certificate, pem_certificate_chain = request_certificate(
pending["acme_client"], entry["acme_client"],
pending["authorizations"], entry["authorizations"],
pending["pending_cert"].csr entry["pending_cert"].csr
) )
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()),
'external_id': str(pending_cert.external_id) 'external_id': str(entry["pending_cert"].external_id)
} }
certs.append({ certs.append({
"cert": cert, "cert": cert,
"pending_cert": pending_cert, "pending_cert": entry["pending_cert"],
})
except PollError:
current_app.logger.error("Unable to resolve pending cert: {}".format(pending_cert), exc_info=True)
certs.append({
"cert": False,
"pending_cert": entry["pending_cert"],
}) })
return certs return certs
@ -292,7 +325,7 @@ class ACMEIssuerPlugin(IssuerPlugin):
acme_client, registration = setup_acme_client(authority) acme_client, registration = setup_acme_client(authority)
dns_provider_d = issuer_options.get('dns_provider') dns_provider_d = issuer_options.get('dns_provider')
if not dns_provider_d: if not dns_provider_d:
raise Exception("DNS Provider setting is required for ACME certificates.") raise InvalidConfiguration("DNS Provider setting is required for ACME certificates.")
dns_provider = dns_provider_service.get(dns_provider_d.get("id")) dns_provider = dns_provider_service.get(dns_provider_d.get("id"))
credentials = json.loads(dns_provider.credentials) credentials = json.loads(dns_provider.credentials)
@ -300,9 +333,9 @@ class ACMEIssuerPlugin(IssuerPlugin):
dns_provider_type = __import__(dns_provider.provider_type, globals(), locals(), [], 1) dns_provider_type = __import__(dns_provider.provider_type, globals(), locals(), [], 1)
account_number = credentials.get("account_id") account_number = credentials.get("account_id")
if dns_provider.provider_type == 'route53' and not 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) error = "Route53 DNS Provider {} does not have an account number configured.".format(dns_provider.name)
current_app.logger.error(error) current_app.logger.error(error)
raise Exception(error) raise InvalidConfiguration(error)
domains = get_domains(issuer_options) domains = 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
@ -333,7 +366,11 @@ class ACMEIssuerPlugin(IssuerPlugin):
:return: :return:
""" """
role = {'username': '', 'password': '', 'name': 'acme'} role = {'username': '', 'password': '', 'name': 'acme'}
plugin_options = options.get('plugin').get('plugin_options') plugin_options = options.get('plugin', {}).get('plugin_options')
if not plugin_options:
error = "Invalid options for lemur_acme plugin: {}".format(options)
current_app.logger.error(error)
raise InvalidConfiguration(error)
# Define static acme_root based off configuration variable by default. However, if user has passed a # Define static acme_root based off configuration variable by default. However, if user has passed a
# certificate, use this certificate as the root. # certificate, use this certificate as the root.
acme_root = current_app.config.get('ACME_ROOT') acme_root = current_app.config.get('ACME_ROOT')

View File

@ -54,7 +54,8 @@ class TestAcme(unittest.TestCase):
self.assertEqual(type(result), plugin.AuthorizationRecord) self.assertEqual(type(result), plugin.AuthorizationRecord)
@patch('acme.client.Client') @patch('acme.client.Client')
def test_complete_dns_challenge_success(self, mock_acme): @patch('lemur.plugins.lemur_acme.plugin.current_app')
def test_complete_dns_challenge_success(self, 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)
@ -65,7 +66,8 @@ class TestAcme(unittest.TestCase):
plugin.complete_dns_challenge(mock_acme, "accountid", mock_authz, mock_dns_provider) plugin.complete_dns_challenge(mock_acme, "accountid", mock_authz, mock_dns_provider)
@patch('acme.client.Client') @patch('acme.client.Client')
def test_complete_dns_challenge_fail(self, mock_acme): @patch('lemur.plugins.lemur_acme.plugin.current_app')
def test_complete_dns_challenge_fail(self, 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)

View File

@ -12,7 +12,7 @@ chardet==3.0.4 # via requests
flake8==3.5.0 flake8==3.5.0
identify==1.0.13 # via pre-commit identify==1.0.13 # via pre-commit
idna==2.6 # via requests idna==2.6 # via requests
invoke==0.22.1 invoke==0.23.0
mccabe==0.6.1 # via flake8 mccabe==0.6.1 # via flake8
nodeenv==1.3.0 nodeenv==1.3.0
pkginfo==1.4.2 # via twine pkginfo==1.4.2 # via twine
@ -23,7 +23,7 @@ pyyaml==3.12 # via aspy.yaml, pre-commit
requests-toolbelt==0.8.0 # via twine requests-toolbelt==0.8.0 # via twine
requests==2.18.4 # via requests-toolbelt, twine requests==2.18.4 # via requests-toolbelt, twine
six==1.11.0 # via cfgv, pre-commit six==1.11.0 # via cfgv, pre-commit
tqdm==4.23.1 # via twine tqdm==4.23.2 # via twine
twine==1.11.0 twine==1.11.0
urllib3==1.22 # via requests urllib3==1.22 # via requests
virtualenv==15.2.0 # via pre-commit virtualenv==15.2.0 # via pre-commit

View File

@ -15,13 +15,15 @@ asyncpool==1.0
babel==2.5.3 # via sphinx babel==2.5.3 # via sphinx
bcrypt==3.1.4 bcrypt==3.1.4
blinker==1.4 blinker==1.4
boto3==1.7.10 boto3==1.7.11
botocore==1.10.10 botocore==1.10.11
certifi==2018.4.16
cffi==1.11.5 cffi==1.11.5
click==6.7 click==6.7
cloudflare==2.1.0 cloudflare==2.1.0
cryptography==2.2.2 cryptography==2.2.2
docutils==0.14 docutils==0.14
dyn==1.8.1
flask-bcrypt==0.7.1 flask-bcrypt==0.7.1
flask-cors==3.0.4 flask-cors==3.0.4
flask-mail==0.9.1 flask-mail==0.9.1
@ -34,11 +36,12 @@ flask==0.12
future==0.16.0 future==0.16.0
gevent==1.2.2 gevent==1.2.2
greenlet==0.4.13 greenlet==0.4.13
gunicorn==19.7.1 gunicorn==19.8.1
idna==2.6 idna==2.6
imagesize==1.0.0 # via sphinx imagesize==1.0.0 # via sphinx
inflection==0.3.1 inflection==0.3.1
itsdangerous==0.24 itsdangerous==0.24
janus==0.3.1
jinja2==2.10 jinja2==2.10
jmespath==0.9.3 jmespath==0.9.3
josepy==1.1.0 josepy==1.1.0
@ -74,11 +77,11 @@ retrying==1.3.3
s3transfer==0.1.13 s3transfer==0.1.13
six==1.11.0 six==1.11.0
snowballstemmer==1.2.1 # via sphinx snowballstemmer==1.2.1 # via sphinx
sphinx-rtd-theme==0.3.0 sphinx-rtd-theme==0.3.1
sphinx==1.7.4 sphinx==1.7.4
sphinxcontrib-httpdomain==1.6.1 sphinxcontrib-httpdomain==1.6.1
sphinxcontrib-websupport==1.0.1 # via sphinx sphinxcontrib-websupport==1.0.1 # via sphinx
sqlalchemy-utils==0.33.2 sqlalchemy-utils==0.33.3
sqlalchemy==1.2.7 sqlalchemy==1.2.7
tabulate==0.8.2 tabulate==0.8.2
werkzeug==0.14.1 werkzeug==0.14.1

View File

@ -7,9 +7,9 @@
asn1crypto==0.24.0 # via cryptography asn1crypto==0.24.0 # via cryptography
attrs==17.4.0 # via pytest attrs==17.4.0 # via pytest
aws-xray-sdk==0.95 # via moto aws-xray-sdk==0.95 # via moto
boto3==1.7.10 # via moto boto3==1.7.11 # via moto
boto==2.48.0 # via moto boto==2.48.0 # via moto
botocore==1.10.10 # via boto3, moto, s3transfer botocore==1.10.11 # via boto3, moto, s3transfer
certifi==2018.4.16 # via requests certifi==2018.4.16 # 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
@ -22,7 +22,7 @@ docker==3.3.0 # via moto
docutils==0.14 # via botocore docutils==0.14 # via botocore
factory-boy==2.10.0 factory-boy==2.10.0
faker==0.8.13 faker==0.8.13
flask==1.0 # via pytest-flask flask==1.0.2 # via pytest-flask
freezegun==0.3.10 freezegun==0.3.10
idna==2.6 # via cryptography, requests idna==2.6 # via cryptography, requests
itsdangerous==0.24 # via flask itsdangerous==0.24 # via flask
@ -42,7 +42,7 @@ pyaml==17.12.1 # via moto
pycparser==2.18 # via cffi pycparser==2.18 # via cffi
pyflakes==1.6.0 pyflakes==1.6.0
pytest-flask==0.10.0 pytest-flask==0.10.0
pytest-mock==1.9.0 pytest-mock==1.10.0
pytest==3.5.1 pytest==3.5.1
python-dateutil==2.6.1 # via botocore, faker, freezegun, moto python-dateutil==2.6.1 # via botocore, faker, freezegun, moto
pytz==2018.4 # via moto pytz==2018.4 # via moto

View File

@ -5,8 +5,11 @@ alembic-autogenerate-enums
arrow arrow
asyncpool asyncpool
boto3 boto3
certifi
CloudFlare CloudFlare
cryptography cryptography
dnspython3
dyn
Flask-Bcrypt==0.7.1 Flask-Bcrypt==0.7.1
Flask-Mail==0.9.1 Flask-Mail==0.9.1
Flask-Migrate==2.1.1 Flask-Migrate==2.1.1
@ -38,4 +41,5 @@ retrying
six six
SQLAlchemy-Utils SQLAlchemy-Utils
tabulate tabulate
tld
xmltodict xmltodict

View File

@ -4,7 +4,7 @@
# #
# pip-compile --no-index --output-file requirements.txt requirements.in # pip-compile --no-index --output-file requirements.txt requirements.in
# #
acme==0.23.0 acme==0.24.0
alembic-autogenerate-enums==0.0.2 alembic-autogenerate-enums==0.0.2
alembic==0.9.9 # via flask-migrate alembic==0.9.9 # via flask-migrate
aniso8601==3.0.0 # via flask-restful aniso8601==3.0.0 # via flask-restful
@ -13,13 +13,17 @@ 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.10 boto3==1.7.11
botocore==1.10.10 # via boto3, s3transfer botocore==1.10.11 # via boto3, s3transfer
certifi==2018.4.16
cffi==1.11.5 # via bcrypt, cryptography, pynacl cffi==1.11.5 # via bcrypt, cryptography, pynacl
click==6.7 # via flask click==6.7 # via flask
cloudflare==2.1.0 cloudflare==2.1.0
cryptography==2.2.2 cryptography==2.2.2
dnspython3==1.15.0
dnspython==1.15.0 # via dnspython3
docutils==0.14 # via botocore docutils==0.14 # via botocore
dyn==1.8.1
flask-bcrypt==0.7.1 flask-bcrypt==0.7.1
flask-cors==3.0.4 flask-cors==3.0.4
flask-mail==0.9.1 flask-mail==0.9.1
@ -32,7 +36,7 @@ flask==0.12
future==0.16.0 future==0.16.0
gevent==1.2.2 gevent==1.2.2
greenlet==0.4.13 # via gevent greenlet==0.4.13 # via gevent
gunicorn==19.7.1 gunicorn==19.8.1
idna==2.6 # via cryptography idna==2.6 # via cryptography
inflection==0.3.1 inflection==0.3.1
itsdangerous==0.24 # via flask itsdangerous==0.24 # via flask
@ -69,8 +73,9 @@ requests[security]==2.11.1
retrying==1.3.3 retrying==1.3.3
s3transfer==0.1.13 # via boto3 s3transfer==0.1.13 # via boto3
six==1.11.0 six==1.11.0
sqlalchemy-utils==0.33.2 sqlalchemy-utils==0.33.3
sqlalchemy==1.2.7 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils sqlalchemy==1.2.7 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils
tabulate==0.8.2 tabulate==0.8.2
tld==0.7.10
werkzeug==0.14.1 # via flask werkzeug==0.14.1 # via flask
xmltodict==0.11.0 xmltodict==0.11.0