diff --git a/Makefile b/Makefile index d3d3ecc2..92181461 100644 --- a/Makefile +++ b/Makefile @@ -110,10 +110,10 @@ ifndef VIRTUAL_ENV endif @echo "--> Updating Python requirements" pip install --upgrade pip-tools - pip-compile --output-file requirements-docs.txt requirements-docs.in -U - pip-compile --output-file requirements-dev.txt requirements-dev.in -U - pip-compile --output-file requirements-tests.txt requirements-tests.in -U - pip-compile --output-file requirements.txt requirements.in -U + pip-compile --output-file requirements-docs.txt requirements-docs.in -U --no-index + pip-compile --output-file requirements-dev.txt requirements-dev.in -U --no-index + pip-compile --output-file requirements-tests.txt requirements-tests.in -U --no-index + pip-compile --output-file requirements.txt requirements.in -U --no-index @echo "--> Done updating Python requirements" @echo "--> Installing new dependencies" pip install -e . diff --git a/docs/administration.rst b/docs/administration.rst index 3c0ca457..1d50f280 100644 --- a/docs/administration.rst +++ b/docs/administration.rst @@ -65,6 +65,36 @@ Basic Configuration SQLALCHEMY_DATABASE_URI = 'postgresql://:@:5432/lemur' +.. data:: SQLALCHEMY_POOL_SIZE +:noindex: + + The default connection pool size is 5 for sqlalchemy managed connections. Depending on the number of Lemur instances, + please specify per instance connection pool size. Below is an example to set connection pool size to 10. + + :: + + SQLALCHEMY_POOL_SIZE = 10 + + + .. warning:: +This is an optional setting but important to review and set for optimal database connection usage and for overall database performance. + +.. data:: SQLALCHEMY_MAX_OVERFLOW +:noindex: + + This setting allows to create connections in addition to specified number of connections in pool size. By default, sqlalchemy + allows 10 connections to create in addition to the pool size. This is also an optional setting. If `SQLALCHEMY_POOL_SIZE` and + `SQLALCHEMY_MAX_OVERFLOW` are not speficied then each Lemur instance may create maximum of 15 connections. + + :: + + SQLALCHECK_MAX_OVERFLOW = 0 + + + .. note:: +Specifying the `SQLALCHEMY_MAX_OVERFLOW` to 0 will enforce limit to not create connections above specified pool size. + + .. data:: LEMUR_ALLOW_WEEKEND_EXPIRATION :noindex: diff --git a/docs/developer/index.rst b/docs/developer/index.rst index 8b6da224..4c46566a 100644 --- a/docs/developer/index.rst +++ b/docs/developer/index.rst @@ -48,7 +48,7 @@ of Lemur. You'll want to make sure you have a few things on your local system fi * pip * virtualenv (ideally virtualenvwrapper) * node.js (for npm and building css/javascript) -* (Optional) PostgreSQL ++* `PostgreSQL `_ Once you've got all that, the rest is simple: @@ -77,6 +77,7 @@ Create a default Lemur configuration just as if this were a production instance: :: + lemur create_config lemur init You'll likely want to make some changes to the default configuration (we recommend developing against Postgres, for example). Once done, migrate your database using the following command: diff --git a/lemur/__init__.py b/lemur/__init__.py index 6f29733c..c3661f4e 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -8,7 +8,8 @@ """ -from __future__ import absolute_import, division, print_function +import time +from flask import g, request from lemur import factory from lemur.extensions import metrics @@ -73,17 +74,6 @@ def configure_hook(app): """ from flask import jsonify from werkzeug.exceptions import HTTPException - from lemur.decorators import crossdomain - if app.config.get('CORS'): - @app.after_request - @crossdomain(origin=u"http://localhost:3000", methods=['PUT', 'HEAD', 'GET', 'POST', 'OPTIONS', 'DELETE']) - def after(response): - return response - - @app.after_request - def log_status(response): - metrics.send('status_code_{}'.format(response.status_code), 'counter', 1) - return response @app.errorhandler(Exception) def handle_error(e): @@ -93,3 +83,29 @@ def configure_hook(app): app.logger.exception(e) return jsonify(error=str(e)), code + + @app.before_request + def before_request(): + g.request_start_time = time.time() + + @app.after_request + def after_request(response): + # Return early if we don't have the start time + if not hasattr(g, 'request_start_time'): + return response + + # Get elapsed time in milliseconds + elapsed = time.time() - g.request_start_time + elapsed = int(round(1000 * elapsed)) + + # Collect request/response tags + tags = { + 'endpoint': request.endpoint, + 'request_method': request.method.lower(), + 'status_code': response.status_code + } + + # Record our response time metric + metrics.send('response_time', 'TIMER', elapsed, metric_tags=tags) + metrics.send('status_code_{}'.format(response.status_code), 'counter', 1) + return response diff --git a/lemur/common/utils.py b/lemur/common/utils.py index 4db31a4e..02f55340 100644 --- a/lemur/common/utils.py +++ b/lemur/common/utils.py @@ -14,10 +14,11 @@ from sqlalchemy import and_, func from cryptography import x509 from cryptography.hazmat.backends import default_backend -from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.hazmat.primitives.asymmetric import rsa, ec from flask_restful.reqparse import RequestParser +from lemur.constants import CERTIFICATE_KEY_TYPES from lemur.exceptions import InvalidConfiguration paginated_parser = RequestParser() @@ -78,17 +79,43 @@ def generate_private_key(key_type): """ Generates a new private key based on key_type. - Valid key types: RSA2048, RSA4096 + Valid key types: RSA2048, RSA4096', 'ECCPRIME192V1', 'ECCPRIME256V1', 'ECCSECP192R1', + 'ECCSECP224R1', 'ECCSECP256R1', 'ECCSECP384R1', 'ECCSECP521R1', 'ECCSECP256K1', + 'ECCSECT163K1', 'ECCSECT233K1', 'ECCSECT283K1', 'ECCSECT409K1', 'ECCSECT571K1', + 'ECCSECT163R2', 'ECCSECT233R1', 'ECCSECT283R1', 'ECCSECT409R1', 'ECCSECT571R2' :param key_type: :return: """ - valid_key_types = ['RSA2048', 'RSA4096'] - if key_type not in valid_key_types: + _CURVE_TYPES = { + "ECCPRIME192V1": ec.SECP192R1(), + "ECCPRIME256V1": ec.SECP256R1(), + + "ECCSECP192R1": ec.SECP192R1(), + "ECCSECP224R1": ec.SECP224R1(), + "ECCSECP256R1": ec.SECP256R1(), + "ECCSECP384R1": ec.SECP384R1(), + "ECCSECP521R1": ec.SECP521R1(), + "ECCSECP256K1": ec.SECP256K1(), + + "ECCSECT163K1": ec.SECT163K1(), + "ECCSECT233K1": ec.SECT233K1(), + "ECCSECT283K1": ec.SECT283K1(), + "ECCSECT409K1": ec.SECT409K1(), + "ECCSECT571K1": ec.SECT571K1(), + + "ECCSECT163R2": ec.SECT163R2(), + "ECCSECT233R1": ec.SECT233R1(), + "ECCSECT283R1": ec.SECT283R1(), + "ECCSECT409R1": ec.SECT409R1(), + "ECCSECT571R2": ec.SECT571R1(), + } + + if key_type not in CERTIFICATE_KEY_TYPES: raise Exception("Invalid key type: {key_type}. Supported key types: {choices}".format( key_type=key_type, - choices=",".join(valid_key_types) + choices=",".join(CERTIFICATE_KEY_TYPES) )) if 'RSA' in key_type: @@ -98,6 +125,11 @@ def generate_private_key(key_type): key_size=key_size, backend=default_backend() ) + elif 'ECC' in key_type: + return ec.generate_private_key( + curve=_CURVE_TYPES[key_type], + backend=default_backend() + ) def is_weekend(date): diff --git a/lemur/constants.py b/lemur/constants.py index 04824c12..0ee9bc40 100644 --- a/lemur/constants.py +++ b/lemur/constants.py @@ -9,3 +9,26 @@ NONSTANDARD_NAMING_TEMPLATE = "{issuer}-{not_before}-{not_after}" SUCCESS_METRIC_STATUS = 'success' FAILURE_METRIC_STATUS = 'failure' + +CERTIFICATE_KEY_TYPES = [ + 'RSA2048', + 'RSA4096', + 'ECCPRIME192V1', + 'ECCPRIME256V1', + 'ECCSECP192R1', + 'ECCSECP224R1', + 'ECCSECP256R1', + 'ECCSECP384R1', + 'ECCSECP521R1', + 'ECCSECP256K1', + 'ECCSECT163K1', + 'ECCSECT233K1', + 'ECCSECT283K1', + 'ECCSECT409K1', + 'ECCSECT571K1', + 'ECCSECT163R2', + 'ECCSECT233R1', + 'ECCSECT283R1', + 'ECCSECT409R1', + 'ECCSECT571R2' +] diff --git a/lemur/decorators.py b/lemur/decorators.py deleted file mode 100644 index 7b93ec70..00000000 --- a/lemur/decorators.py +++ /dev/null @@ -1,56 +0,0 @@ -""" -.. module: lemur.decorators - :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more - :license: Apache, see LICENSE for more details. -""" -from builtins import str - -from datetime import timedelta -from flask import make_response, request, current_app - -from functools import update_wrapper - - -# this is only used for dev -def crossdomain(origin=None, methods=None, headers=None, - max_age=21600, attach_to_all=True, - automatic_options=True): # pragma: no cover - if methods is not None: - methods = ', '.join(sorted(x.upper() for x in methods)) - - if headers is not None and not isinstance(headers, str): - headers = ', '.join(x.upper() for x in headers) - - if not isinstance(origin, str): - origin = ', '.join(origin) - - if isinstance(max_age, timedelta): - max_age = max_age.total_seconds() - - def get_methods(): - if methods is not None: - return methods - - options_resp = current_app.make_default_options_response() - return options_resp.headers['allow'] - - def decorator(f): - def wrapped_function(*args, **kwargs): - if automatic_options and request.method == 'OPTIONS': - resp = current_app.make_default_options_response() - else: - resp = make_response(f(*args, **kwargs)) - if not attach_to_all and request.method != 'OPTIONS': - return resp - - h = resp.headers - h['Access-Control-Allow-Origin'] = origin - h['Access-Control-Allow-Methods'] = get_methods() - h['Access-Control-Max-Age'] = str(max_age) - h['Access-Control-Allow-Headers'] = "Origin, X-Requested-With, Content-Type, Accept, Authorization " - h['Access-Control-Allow-Credentials'] = 'true' - return resp - - f.provide_automatic_options = False - return update_wrapper(wrapped_function, f) - return decorator diff --git a/lemur/extensions.py b/lemur/extensions.py index b5a18569..76abcab1 100644 --- a/lemur/extensions.py +++ b/lemur/extensions.py @@ -26,3 +26,6 @@ sentry = Sentry() from blinker import Namespace signals = Namespace() + +from flask_cors import CORS +cors = CORS() diff --git a/lemur/factory.py b/lemur/factory.py index 107fb70f..93c18e71 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -21,7 +21,7 @@ from flask import Flask from lemur.certificates.hooks import activate_debug_dump from lemur.common.health import mod as health -from lemur.extensions import db, migrate, principal, smtp_mail, metrics, sentry +from lemur.extensions import db, migrate, principal, smtp_mail, metrics, sentry, cors DEFAULT_BLUEPRINTS = ( @@ -124,6 +124,8 @@ def configure_extensions(app): smtp_mail.init_app(app) metrics.init_app(app) sentry.init_app(app) + app.config['CORS_HEADERS'] = 'Content-Type' + cors.init_app(app, resources=r'/api/*', headers='Content-Type', origin='*', supports_credentials=True) def configure_blueprints(app, blueprints): diff --git a/lemur/plugins/lemur_digicert/plugin.py b/lemur/plugins/lemur_digicert/plugin.py index 0e93f487..bfe41b4c 100644 --- a/lemur/plugins/lemur_digicert/plugin.py +++ b/lemur/plugins/lemur_digicert/plugin.py @@ -157,7 +157,7 @@ def map_cis_fields(options, csr): "csr": csr, "signature_hash": signature_hash(options.get('signing_algorithm')), "validity": { - "valid_to": options['validity_end'].format('YYYY-MM-DDTHH:MM:SS' + Z) + "valid_to": options['validity_end'].format('YYYY-MM-DDTHH:MM') + 'Z' }, "organization": { "name": options['organization'], @@ -491,6 +491,11 @@ class DigiCertCISIssuerPlugin(IssuerPlugin): self.session.headers.pop('Accept') end_entity = pem.parse(certificate_pem)[0] + + if 'ECC' in issuer_options['key_type']: + return "\n".join(str(end_entity).splitlines()), current_app.config.get('DIGICERT_ECC_CIS_INTERMEDIATE'), data['id'] + + # By default return RSA return "\n".join(str(end_entity).splitlines()), current_app.config.get('DIGICERT_CIS_INTERMEDIATE'), data['id'] def revoke_certificate(self, certificate, comments): diff --git a/lemur/plugins/lemur_digicert/tests/test_digicert.py b/lemur/plugins/lemur_digicert/tests/test_digicert.py index 7b19f318..5f14f04b 100644 --- a/lemur/plugins/lemur_digicert/tests/test_digicert.py +++ b/lemur/plugins/lemur_digicert/tests/test_digicert.py @@ -103,7 +103,7 @@ def test_map_cis_fields(app): 'signature_hash': 'sha256', 'organization': {'name': 'Example, Inc.', 'units': ['Example Org']}, 'validity': { - 'valid_to': arrow.get(2017, 5, 7).format('YYYY-MM-DDTHH:MM:SS' + Z) + 'valid_to': arrow.get(2017, 5, 7).format('YYYY-MM-DDTHH:MM') + 'Z' }, 'profile_name': None } @@ -132,7 +132,7 @@ def test_map_cis_fields(app): 'signature_hash': 'sha256', 'organization': {'name': 'Example, Inc.', 'units': ['Example Org']}, 'validity': { - 'valid_to': arrow.get(2018, 11, 3).format('YYYY-MM-DDTHH:MM:SS' + Z) + 'valid_to': arrow.get(2018, 11, 3).format('YYYY-MM-DDTHH:MM') + 'Z' }, 'profile_name': None } diff --git a/lemur/plugins/lemur_statsd/docs/requirements.txt b/lemur/plugins/lemur_statsd/docs/requirements.txt new file mode 100644 index 00000000..73a2e5b6 --- /dev/null +++ b/lemur/plugins/lemur_statsd/docs/requirements.txt @@ -0,0 +1 @@ +datadog==0.14.0 diff --git a/lemur/plugins/lemur_statsd/lemur_statsd/__init__.py b/lemur/plugins/lemur_statsd/lemur_statsd/__init__.py new file mode 100644 index 00000000..3a751848 --- /dev/null +++ b/lemur/plugins/lemur_statsd/lemur_statsd/__init__.py @@ -0,0 +1,4 @@ +try: + VERSION = __import__('pkg_resources').get_distribution(__name__).version +except Exception as e: + VERSION = 'Unknown' diff --git a/lemur/plugins/lemur_statsd/lemur_statsd/plugin.py b/lemur/plugins/lemur_statsd/lemur_statsd/plugin.py new file mode 100644 index 00000000..a6a87c66 --- /dev/null +++ b/lemur/plugins/lemur_statsd/lemur_statsd/plugin.py @@ -0,0 +1,45 @@ +import lemur_statsd as plug + +from flask import current_app +from lemur.plugins.bases.metric import MetricPlugin +from datadog import DogStatsd + + +class StatsdMetricPlugin(MetricPlugin): + title = 'Statsd' + slug = 'statsd-metrics' + description = 'Adds support for sending metrics to Statsd' + version = plug.VERSION + + def __init__(self): + host = current_app.config.get('STATSD_HOST') + port = current_app.config.get('STATSD_PORT') + prefix = current_app.config.get('STATSD_PREFIX') + + self.statsd = DogStatsd(host=host, port=port, namespace=prefix) + + def submit(self, metric_name, metric_type, metric_value, metric_tags=None, options=None): + valid_types = ['COUNTER', 'GAUGE', 'TIMER'] + tags = [] + + if metric_type.upper() not in valid_types: + raise Exception( + "Invalid Metric Type for Statsd, '{metric}' choose from: {options}".format( + metric=metric_type, options=','.join(valid_types) + ) + ) + + if metric_tags: + if not isinstance(metric_tags, dict): + raise Exception("Invalid Metric Tags for Statsd: Tags must be in dict format") + else: + tags = map(lambda e: "{0}:{1}".format(*e), metric_tags.items()) + + if metric_type.upper() == 'COUNTER': + self.statsd.increment(metric_name, metric_value, tags) + elif metric_type.upper() == 'GAUGE': + self.statsd.gauge(metric_name, metric_value, tags) + elif metric_type.upper() == 'TIMER': + self.statsd.timing(metric_name, metric_value, tags) + + return diff --git a/lemur/plugins/lemur_statsd/setup.py b/lemur/plugins/lemur_statsd/setup.py new file mode 100644 index 00000000..6c4c2dd6 --- /dev/null +++ b/lemur/plugins/lemur_statsd/setup.py @@ -0,0 +1,24 @@ +"""Basic package information""" +from __future__ import absolute_import +from setuptools import setup, find_packages + +install_requires = [ + 'lemur', + 'datadog' +] + +setup( + name='lemur_statsd', + version='1.0.0', + author='Cloudflare Security Engineering', + author_email='', + include_package_data=True, + packages=find_packages(), + zip_safe=False, + install_requires=install_requires, + entry_points={ + 'lemur.plugins': [ + 'statsd = lemur_statsd.plugin:StatsdMetricPlugin', + ] + } +) diff --git a/lemur/static/app/angular/authorities/authority/options.tpl.html b/lemur/static/app/angular/authorities/authority/options.tpl.html index 57fc29e6..245716cb 100644 --- a/lemur/static/app/angular/authorities/authority/options.tpl.html +++ b/lemur/static/app/angular/authorities/authority/options.tpl.html @@ -20,7 +20,8 @@ Key Type
- +
diff --git a/lemur/static/app/angular/certificates/certificate/options.tpl.html b/lemur/static/app/angular/certificates/certificate/options.tpl.html index a52ee387..fb1d59a1 100644 --- a/lemur/static/app/angular/certificates/certificate/options.tpl.html +++ b/lemur/static/app/angular/certificates/certificate/options.tpl.html @@ -32,7 +32,10 @@
diff --git a/lemur/tests/test_utils.py b/lemur/tests/test_utils.py index cc03fcd5..62d021a4 100644 --- a/lemur/tests/test_utils.py +++ b/lemur/tests/test_utils.py @@ -6,9 +6,27 @@ def test_generate_private_key(): assert generate_private_key('RSA2048') assert generate_private_key('RSA4096') + assert generate_private_key('ECCPRIME192V1') + assert generate_private_key('ECCPRIME256V1') + assert generate_private_key('ECCSECP192R1') + assert generate_private_key('ECCSECP224R1') + assert generate_private_key('ECCSECP256R1') + assert generate_private_key('ECCSECP384R1') + assert generate_private_key('ECCSECP521R1') + assert generate_private_key('ECCSECP256K1') + assert generate_private_key('ECCSECT163K1') + assert generate_private_key('ECCSECT233K1') + assert generate_private_key('ECCSECT283K1') + assert generate_private_key('ECCSECT409K1') + assert generate_private_key('ECCSECT571K1') + assert generate_private_key('ECCSECT163R2') + assert generate_private_key('ECCSECT233R1') + assert generate_private_key('ECCSECT283R1') + assert generate_private_key('ECCSECT409R1') + assert generate_private_key('ECCSECT571R2') with pytest.raises(Exception): - generate_private_key('ECC') + generate_private_key('LEMUR') def test_get_authority_key(): diff --git a/requirements-dev.txt b/requirements-dev.txt index 1b11eb2b..f94fa610 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -2,15 +2,15 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements-dev.txt requirements-dev.in +# pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in # -aspy.yaml==1.0.0 # via pre-commit +aspy.yaml==1.1.0 # via pre-commit cached-property==1.4.2 # via pre-commit certifi==2018.1.18 # via requests cfgv==1.0.0 # via pre-commit chardet==3.0.4 # via requests flake8==3.5.0 -identify==1.0.9 # via pre-commit +identify==1.0.11 # via pre-commit idna==2.6 # via requests invoke==0.22.1 mccabe==0.6.1 # via flake8 @@ -23,7 +23,7 @@ pyyaml==3.12 # via aspy.yaml, pre-commit requests-toolbelt==0.8.0 # via twine requests==2.18.4 # via requests-toolbelt, twine six==1.11.0 # via cfgv, pre-commit -tqdm==4.21.0 # via twine +tqdm==4.22.0 # via twine twine==1.11.0 urllib3==1.22 # via requests virtualenv==15.2.0 # via pre-commit diff --git a/requirements-docs.txt b/requirements-docs.txt index 35fce72b..a8b93db2 100644 --- a/requirements-docs.txt +++ b/requirements-docs.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements-docs.txt requirements-docs.in +# pip-compile --no-index --output-file requirements-docs.txt requirements-docs.in # alabaster==0.7.10 # via sphinx babel==2.5.3 # via sphinx @@ -16,7 +16,7 @@ markupsafe==1.0 # via jinja2 packaging==17.1 # via sphinx pygments==2.2.0 # via sphinx pyparsing==2.2.0 # via packaging -pytz==2018.3 # via babel +pytz==2018.4 # via babel requests==2.18.4 # via sphinx six==1.11.0 # via packaging, sphinx, sphinxcontrib-httpdomain snowballstemmer==1.2.1 # via sphinx diff --git a/requirements-tests.txt b/requirements-tests.txt index 983aeb0e..4113ad2a 100644 --- a/requirements-tests.txt +++ b/requirements-tests.txt @@ -2,14 +2,14 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements-tests.txt requirements-tests.in +# pip-compile --no-index --output-file requirements-tests.txt requirements-tests.in # asn1crypto==0.24.0 # via cryptography attrs==17.4.0 # via pytest aws-xray-sdk==0.95 # via moto -boto3==1.7.2 # via moto +boto3==1.7.4 # via moto boto==2.48.0 # via moto -botocore==1.10.2 # via boto3, moto, s3transfer +botocore==1.10.4 # via boto3, moto, s3transfer certifi==2018.1.18 # via requests cffi==1.11.5 # via cryptography chardet==3.0.4 # via requests @@ -21,7 +21,7 @@ docker-pycreds==0.2.2 # via docker docker==3.2.1 # via moto docutils==0.14 # via botocore factory-boy==2.10.0 -faker==0.8.12 +faker==0.8.13 flask==0.12.2 # via pytest-flask freezegun==0.3.10 idna==2.6 # via cryptography, requests @@ -35,17 +35,17 @@ mock==2.0.0 # via moto more-itertools==4.1.0 # via pytest moto==1.3.1 nose==1.3.7 -pbr==4.0.1 # via mock +pbr==4.0.2 # via mock pluggy==0.6.0 # via pytest py==1.5.3 # via pytest pyaml==17.12.1 # via moto pycparser==2.18 # via cffi pyflakes==1.6.0 pytest-flask==0.10.0 -pytest-mock==1.8.0 +pytest-mock==1.9.0 pytest==3.5.0 python-dateutil==2.6.1 # via botocore, faker, freezegun, moto -pytz==2018.3 # via moto +pytz==2018.4 # via moto pyyaml==3.12 # via pyaml requests-mock==1.4.0 requests==2.18.4 # via aws-xray-sdk, docker, moto, requests-mock diff --git a/requirements.in b/requirements.in index 4dad196d..90a48170 100644 --- a/requirements.in +++ b/requirements.in @@ -11,6 +11,7 @@ Flask-RESTful==0.3.6 Flask-Script==2.0.6 Flask-SQLAlchemy Flask==0.12 +Flask-Cors future gunicorn inflection diff --git a/requirements.txt b/requirements.txt index dafb2bdf..5e8ef031 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ # This file is autogenerated by pip-compile # To update, run: # -# pip-compile --output-file requirements.txt requirements.in +# pip-compile --no-index --output-file requirements.txt requirements.in # acme==0.23.0 alembic-autogenerate-enums==0.0.2 @@ -12,13 +12,14 @@ arrow==0.12.1 asn1crypto==0.24.0 # via cryptography bcrypt==3.1.4 # via flask-bcrypt, paramiko blinker==1.4 # via flask-mail, flask-principal, raven -boto3==1.7.2 -botocore==1.10.2 # via boto3, s3transfer +boto3==1.7.4 +botocore==1.10.4 # via boto3, s3transfer cffi==1.11.5 # via bcrypt, cryptography, pynacl click==6.7 # via flask cryptography==2.2.2 docutils==0.14 # via botocore flask-bcrypt==0.7.1 +flask-cors==3.0.3 flask-mail==0.9.1 flask-migrate==2.1.1 flask-principal==0.4.0 @@ -42,7 +43,7 @@ marshmallow==2.15.0 mock==2.0.0 # via acme ndg-httpsclient==0.4.4 paramiko==2.4.1 -pbr==4.0.1 # via mock +pbr==4.0.2 # via mock pem==17.1.0 psycopg2==2.7.4 pyasn1-modules==0.2.1 # via python-ldap @@ -55,7 +56,7 @@ pyrfc3339==1.0 # via acme python-dateutil==2.6.1 # via alembic, arrow, botocore python-editor==1.0.3 # via alembic python-ldap==3.0.0 -pytz==2018.3 # via acme, flask-restful, pyrfc3339 +pytz==2018.4 # via acme, flask-restful, pyrfc3339 raven[flask]==6.6.0 requests[security]==2.11.1 retrying==1.3.3