Merge branch 'master' into ADCS-plugin
This commit is contained in:
commit
c77ccdf46e
|
@ -15,6 +15,7 @@ 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
|
||||||
from lemur.authorities.models import Authority
|
from lemur.authorities.models import Authority
|
||||||
|
from lemur.certificates.models import Certificate
|
||||||
from lemur.roles import service as role_service
|
from lemur.roles import service as role_service
|
||||||
|
|
||||||
from lemur.certificates.service import upload
|
from lemur.certificates.service import upload
|
||||||
|
@ -179,7 +180,12 @@ def render(args):
|
||||||
if 'active' in filt:
|
if 'active' in filt:
|
||||||
query = query.filter(Authority.active == truthiness(terms[1]))
|
query = query.filter(Authority.active == truthiness(terms[1]))
|
||||||
elif 'cn' in filt:
|
elif 'cn' in filt:
|
||||||
query = query.join(Authority.active == truthiness(terms[1]))
|
term = '%{0}%'.format(terms[1])
|
||||||
|
sub_query = database.session_query(Certificate.root_authority_id) \
|
||||||
|
.filter(Certificate.cn.ilike(term)) \
|
||||||
|
.subquery()
|
||||||
|
|
||||||
|
query = query.filter(Authority.id.in_(sub_query))
|
||||||
else:
|
else:
|
||||||
query = database.filter(query, Authority, terms)
|
query = database.filter(query, Authority, terms)
|
||||||
|
|
||||||
|
|
|
@ -19,7 +19,7 @@ from sqlalchemy.sql.expression import case, extract
|
||||||
from sqlalchemy_utils.types.arrow import ArrowType
|
from sqlalchemy_utils.types.arrow import ArrowType
|
||||||
from werkzeug.utils import cached_property
|
from werkzeug.utils import cached_property
|
||||||
|
|
||||||
from lemur.common import defaults, utils
|
from lemur.common import defaults, utils, validators
|
||||||
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
from lemur.constants import SUCCESS_METRIC_STATUS, FAILURE_METRIC_STATUS
|
||||||
from lemur.database import db
|
from lemur.database import db
|
||||||
from lemur.domains.models import Domain
|
from lemur.domains.models import Domain
|
||||||
|
@ -186,6 +186,18 @@ class Certificate(db.Model):
|
||||||
for domain in defaults.domains(cert):
|
for domain in defaults.domains(cert):
|
||||||
self.domains.append(Domain(name=domain))
|
self.domains.append(Domain(name=domain))
|
||||||
|
|
||||||
|
# Check integrity before saving anything into the database.
|
||||||
|
# For user-facing API calls, validation should also be done in schema validators.
|
||||||
|
self.check_integrity()
|
||||||
|
|
||||||
|
def check_integrity(self):
|
||||||
|
"""
|
||||||
|
Integrity checks: Does the cert have a matching private key?
|
||||||
|
"""
|
||||||
|
if self.private_key:
|
||||||
|
validators.verify_private_key_match(utils.parse_private_key(self.private_key), self.parsed_cert,
|
||||||
|
error_class=AssertionError)
|
||||||
|
|
||||||
@cached_property
|
@cached_property
|
||||||
def parsed_cert(self):
|
def parsed_cert(self):
|
||||||
assert self.body, "Certificate body not set"
|
assert self.body, "Certificate body not set"
|
||||||
|
|
|
@ -10,7 +10,7 @@ from marshmallow import fields, validate, validates_schema, post_load, pre_load
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
from lemur.authorities.schemas import AuthorityNestedOutputSchema
|
||||||
from lemur.common import validators, missing
|
from lemur.common import missing, utils, validators
|
||||||
from lemur.common.fields import ArrowDateTime, Hex
|
from lemur.common.fields import ArrowDateTime, Hex
|
||||||
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
from lemur.common.schema import LemurInputSchema, LemurOutputSchema
|
||||||
from lemur.constants import CERTIFICATE_KEY_TYPES
|
from lemur.constants import CERTIFICATE_KEY_TYPES
|
||||||
|
@ -242,8 +242,8 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
|
||||||
authority = fields.Nested(AssociatedAuthoritySchema, required=False)
|
authority = fields.Nested(AssociatedAuthoritySchema, required=False)
|
||||||
notify = fields.Boolean(missing=True)
|
notify = fields.Boolean(missing=True)
|
||||||
external_id = fields.String(missing=None, allow_none=True)
|
external_id = fields.String(missing=None, allow_none=True)
|
||||||
private_key = fields.String(validate=validators.private_key)
|
private_key = fields.String()
|
||||||
body = fields.String(required=True, validate=validators.public_certificate)
|
body = fields.String(required=True)
|
||||||
chain = fields.String(validate=validators.public_certificate, missing=None,
|
chain = fields.String(validate=validators.public_certificate, missing=None,
|
||||||
allow_none=True) # TODO this could be multiple certificates
|
allow_none=True) # TODO this could be multiple certificates
|
||||||
|
|
||||||
|
@ -258,6 +258,26 @@ class CertificateUploadInputSchema(CertificateCreationSchema):
|
||||||
if not data.get('private_key'):
|
if not data.get('private_key'):
|
||||||
raise ValidationError('Destinations require private key.')
|
raise ValidationError('Destinations require private key.')
|
||||||
|
|
||||||
|
@validates_schema
|
||||||
|
def validate_cert_private_key(self, data):
|
||||||
|
cert = None
|
||||||
|
key = None
|
||||||
|
if data.get('body'):
|
||||||
|
try:
|
||||||
|
cert = utils.parse_certificate(data['body'])
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError("Public certificate presented is not valid.", field_names=['body'])
|
||||||
|
|
||||||
|
if data.get('private_key'):
|
||||||
|
try:
|
||||||
|
key = utils.parse_private_key(data['private_key'])
|
||||||
|
except ValueError:
|
||||||
|
raise ValidationError("Private key presented is not valid.", field_names=['private_key'])
|
||||||
|
|
||||||
|
if cert and key:
|
||||||
|
# Throws ValidationError
|
||||||
|
validators.verify_private_key_match(key, cert)
|
||||||
|
|
||||||
|
|
||||||
class CertificateExportInputSchema(LemurInputSchema):
|
class CertificateExportInputSchema(LemurInputSchema):
|
||||||
plugin = fields.Nested(PluginInputSchema)
|
plugin = fields.Nested(PluginInputSchema)
|
||||||
|
|
|
@ -20,7 +20,6 @@ from lemur.common.utils import generate_private_key, truthiness
|
||||||
from lemur.destinations.models import Destination
|
from lemur.destinations.models import Destination
|
||||||
from lemur.domains.models import Domain
|
from lemur.domains.models import Domain
|
||||||
from lemur.extensions import metrics, sentry, signals
|
from lemur.extensions import metrics, sentry, signals
|
||||||
from lemur.models import certificate_associations
|
|
||||||
from lemur.notifications.models import Notification
|
from lemur.notifications.models import Notification
|
||||||
from lemur.pending_certificates.models import PendingCertificate
|
from lemur.pending_certificates.models import PendingCertificate
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
|
@ -307,7 +306,7 @@ def render(args):
|
||||||
|
|
||||||
if filt:
|
if filt:
|
||||||
terms = filt.split(';')
|
terms = filt.split(';')
|
||||||
term = '%{0}%'.format(terms[1])
|
term = '{0}%'.format(terms[1])
|
||||||
# Exact matches for quotes. Only applies to name, issuer, and cn
|
# Exact matches for quotes. Only applies to name, issuer, and cn
|
||||||
if terms[1].startswith('"') and terms[1].endswith('"'):
|
if terms[1].startswith('"') and terms[1].endswith('"'):
|
||||||
term = terms[1][1:-1]
|
term = terms[1][1:-1]
|
||||||
|
@ -341,13 +340,13 @@ def render(args):
|
||||||
elif 'id' in terms:
|
elif 'id' in terms:
|
||||||
query = query.filter(Certificate.id == cast(terms[1], Integer))
|
query = query.filter(Certificate.id == cast(terms[1], Integer))
|
||||||
elif 'name' in terms:
|
elif 'name' in terms:
|
||||||
query = query.outerjoin(certificate_associations).outerjoin(Domain).filter(
|
query = query.filter(
|
||||||
or_(
|
or_(
|
||||||
Certificate.name.ilike(term),
|
Certificate.name.ilike(term),
|
||||||
Domain.name.ilike(term),
|
Certificate.domains.any(Domain.name.ilike(term)),
|
||||||
Certificate.cn.ilike(term),
|
Certificate.cn.ilike(term),
|
||||||
)
|
)
|
||||||
).group_by(Certificate.id)
|
)
|
||||||
else:
|
else:
|
||||||
query = database.filter(query, Certificate, terms)
|
query = database.filter(query, Certificate, terms)
|
||||||
|
|
||||||
|
|
|
@ -12,7 +12,9 @@ import string
|
||||||
import sqlalchemy
|
import sqlalchemy
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
|
from cryptography.hazmat.primitives import hashes
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
||||||
|
from cryptography.hazmat.primitives.serialization import load_pem_private_key
|
||||||
from flask_restful.reqparse import RequestParser
|
from flask_restful.reqparse import RequestParser
|
||||||
from sqlalchemy import and_, func
|
from sqlalchemy import and_, func
|
||||||
|
|
||||||
|
@ -52,6 +54,20 @@ def parse_certificate(body):
|
||||||
return x509.load_pem_x509_certificate(body, default_backend())
|
return x509.load_pem_x509_certificate(body, default_backend())
|
||||||
|
|
||||||
|
|
||||||
|
def parse_private_key(private_key):
|
||||||
|
"""
|
||||||
|
Parses a PEM-format private key (RSA, DSA, ECDSA or any other supported algorithm).
|
||||||
|
|
||||||
|
Raises ValueError for an invalid string.
|
||||||
|
|
||||||
|
:param private_key: String containing PEM private key
|
||||||
|
"""
|
||||||
|
if isinstance(private_key, str):
|
||||||
|
private_key = private_key.encode('utf8')
|
||||||
|
|
||||||
|
return load_pem_private_key(private_key, password=None, backend=default_backend())
|
||||||
|
|
||||||
|
|
||||||
def parse_csr(csr):
|
def parse_csr(csr):
|
||||||
"""
|
"""
|
||||||
Helper function that parses a CSR.
|
Helper function that parses a CSR.
|
||||||
|
@ -211,3 +227,13 @@ def truthiness(s):
|
||||||
"""If input string resembles something truthy then return True, else False."""
|
"""If input string resembles something truthy then return True, else False."""
|
||||||
|
|
||||||
return s.lower() in ('true', 'yes', 'on', 't', '1')
|
return s.lower() in ('true', 'yes', 'on', 't', '1')
|
||||||
|
|
||||||
|
|
||||||
|
def find_matching_certificates_by_hash(cert, matching_certs):
|
||||||
|
"""Given a Cryptography-formatted certificate cert, and Lemur-formatted certificates (matching_certs),
|
||||||
|
determine if any of the certificate hashes match and return the matches."""
|
||||||
|
matching = []
|
||||||
|
for c in matching_certs:
|
||||||
|
if parse_certificate(c.body).fingerprint(hashes.SHA256()) == cert.fingerprint(hashes.SHA256()):
|
||||||
|
matching.append(c)
|
||||||
|
return matching
|
||||||
|
|
|
@ -2,14 +2,12 @@ import re
|
||||||
|
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.backends import default_backend
|
from cryptography.hazmat.backends import default_backend
|
||||||
from cryptography.hazmat.primitives import serialization
|
|
||||||
from cryptography.x509 import NameOID
|
from cryptography.x509 import NameOID
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
from lemur.auth.permissions import SensitiveDomainPermission
|
from lemur.auth.permissions import SensitiveDomainPermission
|
||||||
from lemur.common.utils import parse_certificate, is_weekend
|
from lemur.common.utils import parse_certificate, is_weekend
|
||||||
from lemur.domains import service as domain_service
|
|
||||||
|
|
||||||
|
|
||||||
def public_certificate(body):
|
def public_certificate(body):
|
||||||
|
@ -26,22 +24,6 @@ def public_certificate(body):
|
||||||
raise ValidationError('Public certificate presented is not valid.')
|
raise ValidationError('Public certificate presented is not valid.')
|
||||||
|
|
||||||
|
|
||||||
def private_key(key):
|
|
||||||
"""
|
|
||||||
User to validate that a given string is a RSA private key
|
|
||||||
|
|
||||||
:param key:
|
|
||||||
:return: :raise ValueError:
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
if isinstance(key, bytes):
|
|
||||||
serialization.load_pem_private_key(key, None, backend=default_backend())
|
|
||||||
else:
|
|
||||||
serialization.load_pem_private_key(key.encode('utf-8'), None, backend=default_backend())
|
|
||||||
except Exception:
|
|
||||||
raise ValidationError('Private key presented is not valid.')
|
|
||||||
|
|
||||||
|
|
||||||
def common_name(value):
|
def common_name(value):
|
||||||
"""If the common name could be a domain name, apply domain validation rules."""
|
"""If the common name could be a domain name, apply domain validation rules."""
|
||||||
# Common name could be a domain name, or a human-readable name of the subject (often used in CA names or client
|
# Common name could be a domain name, or a human-readable name of the subject (often used in CA names or client
|
||||||
|
@ -66,6 +48,9 @@ def sensitive_domain(domain):
|
||||||
raise ValidationError('Domain {0} does not match whitelisted domain patterns. '
|
raise ValidationError('Domain {0} does not match whitelisted domain patterns. '
|
||||||
'Contact an administrator to issue the certificate.'.format(domain))
|
'Contact an administrator to issue the certificate.'.format(domain))
|
||||||
|
|
||||||
|
# Avoid circular import.
|
||||||
|
from lemur.domains import service as domain_service
|
||||||
|
|
||||||
if any(d.sensitive for d in domain_service.get_by_name(domain)):
|
if any(d.sensitive for d in domain_service.get_by_name(domain)):
|
||||||
raise ValidationError('Domain {0} has been marked as sensitive. '
|
raise ValidationError('Domain {0} has been marked as sensitive. '
|
||||||
'Contact an administrator to issue the certificate.'.format(domain))
|
'Contact an administrator to issue the certificate.'.format(domain))
|
||||||
|
@ -141,3 +126,15 @@ def dates(data):
|
||||||
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
|
raise ValidationError('Validity end must not be after {0}'.format(data['authority'].authority_certificate.not_after))
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def verify_private_key_match(key, cert, error_class=ValidationError):
|
||||||
|
"""
|
||||||
|
Checks that the supplied private key matches the certificate.
|
||||||
|
|
||||||
|
:param cert: Parsed certificate
|
||||||
|
:param key: Parsed private key
|
||||||
|
:param error_class: Exception class to raise on error
|
||||||
|
"""
|
||||||
|
if key.public_key().public_numbers() != cert.public_key().public_numbers():
|
||||||
|
raise error_class("Private key does not match certificate.")
|
||||||
|
|
|
@ -5,7 +5,7 @@ import dns.exception
|
||||||
import dns.name
|
import dns.name
|
||||||
import dns.query
|
import dns.query
|
||||||
import dns.resolver
|
import dns.resolver
|
||||||
from dyn.tm.errors import DynectCreateError
|
from dyn.tm.errors import DynectCreateError, DynectGetError
|
||||||
from dyn.tm.session import DynectSession
|
from dyn.tm.session import DynectSession
|
||||||
from dyn.tm.zones import Node, Zone, get_all_zones
|
from dyn.tm.zones import Node, Zone, get_all_zones
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
@ -119,7 +119,11 @@ def delete_txt_record(change_id, account_number, domain, token):
|
||||||
zone = Zone(zone_name)
|
zone = Zone(zone_name)
|
||||||
node = Node(zone_name, fqdn)
|
node = Node(zone_name, fqdn)
|
||||||
|
|
||||||
|
try:
|
||||||
all_txt_records = node.get_all_records_by_type('TXT')
|
all_txt_records = node.get_all_records_by_type('TXT')
|
||||||
|
except DynectGetError:
|
||||||
|
# No Text Records remain or host is not in the zone anymore because all records have been deleted.
|
||||||
|
return
|
||||||
for txt_record in all_txt_records:
|
for txt_record in all_txt_records:
|
||||||
if txt_record.txtdata == ("{}".format(token)):
|
if txt_record.txtdata == ("{}".format(token)):
|
||||||
current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn))
|
current_app.logger.debug("Deleting TXT record name: {0}".format(fqdn))
|
||||||
|
|
|
@ -95,7 +95,7 @@ def get_all_elbs_v2(**kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elbv2')
|
@sts_client('elbv2')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def get_listener_arn_from_endpoint(endpoint_name, endpoint_port, **kwargs):
|
def get_listener_arn_from_endpoint(endpoint_name, endpoint_port, **kwargs):
|
||||||
"""
|
"""
|
||||||
Get a listener ARN from an endpoint.
|
Get a listener ARN from an endpoint.
|
||||||
|
@ -113,7 +113,7 @@ def get_listener_arn_from_endpoint(endpoint_name, endpoint_port, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elb')
|
@sts_client('elb')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def get_elbs(**kwargs):
|
def get_elbs(**kwargs):
|
||||||
"""
|
"""
|
||||||
Fetches one page elb objects for a given account and region.
|
Fetches one page elb objects for a given account and region.
|
||||||
|
@ -123,7 +123,7 @@ def get_elbs(**kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elbv2')
|
@sts_client('elbv2')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def get_elbs_v2(**kwargs):
|
def get_elbs_v2(**kwargs):
|
||||||
"""
|
"""
|
||||||
Fetches one page of elb objects for a given account and region.
|
Fetches one page of elb objects for a given account and region.
|
||||||
|
@ -136,7 +136,7 @@ def get_elbs_v2(**kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elbv2')
|
@sts_client('elbv2')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def describe_listeners_v2(**kwargs):
|
def describe_listeners_v2(**kwargs):
|
||||||
"""
|
"""
|
||||||
Fetches one page of listener objects for a given elb arn.
|
Fetches one page of listener objects for a given elb arn.
|
||||||
|
@ -149,7 +149,7 @@ def describe_listeners_v2(**kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elb')
|
@sts_client('elb')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
|
def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
|
||||||
"""
|
"""
|
||||||
Fetching all policies currently associated with an ELB.
|
Fetching all policies currently associated with an ELB.
|
||||||
|
@ -161,7 +161,7 @@ def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elbv2')
|
@sts_client('elbv2')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def describe_ssl_policies_v2(policy_names, **kwargs):
|
def describe_ssl_policies_v2(policy_names, **kwargs):
|
||||||
"""
|
"""
|
||||||
Fetching all policies currently associated with an ELB.
|
Fetching all policies currently associated with an ELB.
|
||||||
|
@ -173,7 +173,7 @@ def describe_ssl_policies_v2(policy_names, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elb')
|
@sts_client('elb')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def describe_load_balancer_types(policies, **kwargs):
|
def describe_load_balancer_types(policies, **kwargs):
|
||||||
"""
|
"""
|
||||||
Describe the policies with policy details.
|
Describe the policies with policy details.
|
||||||
|
@ -185,7 +185,7 @@ def describe_load_balancer_types(policies, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elb')
|
@sts_client('elb')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def attach_certificate(name, port, certificate_id, **kwargs):
|
def attach_certificate(name, port, certificate_id, **kwargs):
|
||||||
"""
|
"""
|
||||||
Attaches a certificate to a listener, throws exception
|
Attaches a certificate to a listener, throws exception
|
||||||
|
@ -205,7 +205,7 @@ def attach_certificate(name, port, certificate_id, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('elbv2')
|
@sts_client('elbv2')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=1000)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def attach_certificate_v2(listener_arn, port, certificates, **kwargs):
|
def attach_certificate_v2(listener_arn, port, certificates, **kwargs):
|
||||||
"""
|
"""
|
||||||
Attaches a certificate to a listener, throws exception
|
Attaches a certificate to a listener, throws exception
|
||||||
|
|
|
@ -52,7 +52,7 @@ def create_arn_from_cert(account_number, region, certificate_name):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('iam')
|
@sts_client('iam')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
|
def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
|
||||||
"""
|
"""
|
||||||
Upload a certificate to AWS
|
Upload a certificate to AWS
|
||||||
|
@ -95,7 +95,7 @@ def upload_cert(name, body, private_key, path, cert_chain=None, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('iam')
|
@sts_client('iam')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def delete_cert(cert_name, **kwargs):
|
def delete_cert(cert_name, **kwargs):
|
||||||
"""
|
"""
|
||||||
Delete a certificate from AWS
|
Delete a certificate from AWS
|
||||||
|
@ -112,7 +112,7 @@ def delete_cert(cert_name, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('iam')
|
@sts_client('iam')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def get_certificate(name, **kwargs):
|
def get_certificate(name, **kwargs):
|
||||||
"""
|
"""
|
||||||
Retrieves an SSL certificate.
|
Retrieves an SSL certificate.
|
||||||
|
@ -126,7 +126,7 @@ def get_certificate(name, **kwargs):
|
||||||
|
|
||||||
|
|
||||||
@sts_client('iam')
|
@sts_client('iam')
|
||||||
@retry(retry_on_exception=retry_throttled, stop_max_attempt_number=7, wait_exponential_multiplier=100)
|
@retry(retry_on_exception=retry_throttled, wait_fixed=2000)
|
||||||
def get_certificates(**kwargs):
|
def get_certificates(**kwargs):
|
||||||
"""
|
"""
|
||||||
Fetches one page of certificate objects for a given account.
|
Fetches one page of certificate objects for a given account.
|
||||||
|
|
|
@ -9,14 +9,22 @@ from functools import wraps
|
||||||
|
|
||||||
import boto3
|
import boto3
|
||||||
|
|
||||||
|
from botocore.config import Config
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
|
||||||
|
config = Config(
|
||||||
|
retries=dict(
|
||||||
|
max_attempts=20
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def sts_client(service, service_type='client'):
|
def sts_client(service, service_type='client'):
|
||||||
def decorator(f):
|
def decorator(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def decorated_function(*args, **kwargs):
|
def decorated_function(*args, **kwargs):
|
||||||
sts = boto3.client('sts')
|
sts = boto3.client('sts', config=config)
|
||||||
arn = 'arn:aws:iam::{0}:role/{1}'.format(
|
arn = 'arn:aws:iam::{0}:role/{1}'.format(
|
||||||
kwargs.pop('account_number'),
|
kwargs.pop('account_number'),
|
||||||
current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur')
|
current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur')
|
||||||
|
@ -31,7 +39,8 @@ def sts_client(service, service_type='client'):
|
||||||
region_name=kwargs.pop('region', 'us-east-1'),
|
region_name=kwargs.pop('region', 'us-east-1'),
|
||||||
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
||||||
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
||||||
aws_session_token=role['Credentials']['SessionToken']
|
aws_session_token=role['Credentials']['SessionToken'],
|
||||||
|
config=config
|
||||||
)
|
)
|
||||||
kwargs['client'] = client
|
kwargs['client'] = client
|
||||||
elif service_type == 'resource':
|
elif service_type == 'resource':
|
||||||
|
@ -40,7 +49,8 @@ def sts_client(service, service_type='client'):
|
||||||
region_name=kwargs.pop('region', 'us-east-1'),
|
region_name=kwargs.pop('region', 'us-east-1'),
|
||||||
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
aws_access_key_id=role['Credentials']['AccessKeyId'],
|
||||||
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
aws_secret_access_key=role['Credentials']['SecretAccessKey'],
|
||||||
aws_session_token=role['Credentials']['SessionToken']
|
aws_session_token=role['Credentials']['SessionToken'],
|
||||||
|
config=config
|
||||||
)
|
)
|
||||||
kwargs['resource'] = resource
|
kwargs['resource'] = resource
|
||||||
return f(*args, **kwargs)
|
return f(*args, **kwargs)
|
||||||
|
|
|
@ -17,7 +17,7 @@ from lemur.endpoints import service as endpoint_service
|
||||||
from lemur.destinations import service as destination_service
|
from lemur.destinations import service as destination_service
|
||||||
|
|
||||||
from lemur.certificates.schemas import CertificateUploadInputSchema
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
from lemur.common.utils import parse_certificate
|
from lemur.common.utils import find_matching_certificates_by_hash, parse_certificate
|
||||||
from lemur.common.defaults import serial
|
from lemur.common.defaults import serial
|
||||||
|
|
||||||
from lemur.plugins.base import plugins
|
from lemur.plugins.base import plugins
|
||||||
|
@ -126,7 +126,8 @@ def sync_certificates(source, user):
|
||||||
|
|
||||||
if not exists:
|
if not exists:
|
||||||
cert = parse_certificate(certificate['body'])
|
cert = parse_certificate(certificate['body'])
|
||||||
exists = certificate_service.get_by_serial(serial(cert))
|
matching_serials = certificate_service.get_by_serial(serial(cert))
|
||||||
|
exists = find_matching_certificates_by_hash(cert, matching_serials)
|
||||||
|
|
||||||
if not certificate.get('owner'):
|
if not certificate.get('owner'):
|
||||||
certificate['owner'] = user.email
|
certificate['owner'] = user.email
|
||||||
|
|
|
@ -15,7 +15,7 @@ from lemur.tests.vectors import SAN_CERT_KEY, INTERMEDIATE_KEY
|
||||||
|
|
||||||
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
|
from .factories import ApiKeyFactory, AuthorityFactory, NotificationFactory, DestinationFactory, \
|
||||||
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \
|
CertificateFactory, UserFactory, RoleFactory, SourceFactory, EndpointFactory, \
|
||||||
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory
|
RotationPolicyFactory, PendingCertificateFactory, AsyncAuthorityFactory, CryptoAuthorityFactory
|
||||||
|
|
||||||
|
|
||||||
def pytest_runtest_setup(item):
|
def pytest_runtest_setup(item):
|
||||||
|
@ -91,6 +91,13 @@ def authority(session):
|
||||||
return a
|
return a
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def crypto_authority(session):
|
||||||
|
a = CryptoAuthorityFactory()
|
||||||
|
session.commit()
|
||||||
|
return a
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def async_authority(session):
|
def async_authority(session):
|
||||||
a = AsyncAuthorityFactory()
|
a = AsyncAuthorityFactory()
|
||||||
|
|
|
@ -168,6 +168,11 @@ class AsyncAuthorityFactory(AuthorityFactory):
|
||||||
authority_certificate = SubFactory(CertificateFactory)
|
authority_certificate = SubFactory(CertificateFactory)
|
||||||
|
|
||||||
|
|
||||||
|
class CryptoAuthorityFactory(AuthorityFactory):
|
||||||
|
"""Authority factory based on 'cryptography' plugin."""
|
||||||
|
plugin = {'slug': 'cryptography-issuer'}
|
||||||
|
|
||||||
|
|
||||||
class DestinationFactory(BaseFactory):
|
class DestinationFactory(BaseFactory):
|
||||||
"""Destination factory."""
|
"""Destination factory."""
|
||||||
plugin_name = 'test-destination'
|
plugin_name = 'test-destination'
|
||||||
|
|
|
@ -18,7 +18,7 @@ from lemur.domains.models import Domain
|
||||||
|
|
||||||
|
|
||||||
from lemur.tests.vectors import VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN, CSR_STR, \
|
from lemur.tests.vectors import VALID_ADMIN_API_TOKEN, VALID_ADMIN_HEADER_TOKEN, VALID_USER_HEADER_TOKEN, CSR_STR, \
|
||||||
INTERMEDIATE_CERT_STR, SAN_CERT_STR, SAN_CERT_KEY
|
INTERMEDIATE_CERT_STR, SAN_CERT_STR, SAN_CERT_KEY, ROOTCA_KEY, ROOTCA_CERT_STR
|
||||||
|
|
||||||
|
|
||||||
def test_get_or_increase_name(session, certificate):
|
def test_get_or_increase_name(session, certificate):
|
||||||
|
@ -365,6 +365,85 @@ def test_certificate_sensitive_name(client, authority, session, logged_in_user):
|
||||||
assert errors['common_name'][0].startswith("Domain sensitive.example.com has been marked as sensitive")
|
assert errors['common_name'][0].startswith("Domain sensitive.example.com has been marked as sensitive")
|
||||||
|
|
||||||
|
|
||||||
|
def test_certificate_upload_schema_ok(client):
|
||||||
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
|
data = {
|
||||||
|
'name': 'Jane',
|
||||||
|
'owner': 'pwner@example.com',
|
||||||
|
'body': SAN_CERT_STR,
|
||||||
|
'privateKey': SAN_CERT_KEY,
|
||||||
|
'chain': INTERMEDIATE_CERT_STR,
|
||||||
|
'external_id': '1234',
|
||||||
|
}
|
||||||
|
data, errors = CertificateUploadInputSchema().load(data)
|
||||||
|
assert not errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_certificate_upload_schema_minimal(client):
|
||||||
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
|
data = {
|
||||||
|
'owner': 'pwner@example.com',
|
||||||
|
'body': SAN_CERT_STR,
|
||||||
|
}
|
||||||
|
data, errors = CertificateUploadInputSchema().load(data)
|
||||||
|
assert not errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_certificate_upload_schema_long_chain(client):
|
||||||
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
|
data = {
|
||||||
|
'owner': 'pwner@example.com',
|
||||||
|
'body': SAN_CERT_STR,
|
||||||
|
'chain': INTERMEDIATE_CERT_STR + '\n' + ROOTCA_CERT_STR
|
||||||
|
}
|
||||||
|
data, errors = CertificateUploadInputSchema().load(data)
|
||||||
|
assert not errors
|
||||||
|
|
||||||
|
|
||||||
|
def test_certificate_upload_schema_invalid_body(client):
|
||||||
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
|
data = {
|
||||||
|
'owner': 'pwner@example.com',
|
||||||
|
'body': 'Hereby I certify that this is a valid body',
|
||||||
|
}
|
||||||
|
data, errors = CertificateUploadInputSchema().load(data)
|
||||||
|
assert errors == {'body': ['Public certificate presented is not valid.']}
|
||||||
|
|
||||||
|
|
||||||
|
def test_certificate_upload_schema_invalid_pkey(client):
|
||||||
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
|
data = {
|
||||||
|
'owner': 'pwner@example.com',
|
||||||
|
'body': SAN_CERT_STR,
|
||||||
|
'privateKey': 'Look at me Im a private key!!111',
|
||||||
|
}
|
||||||
|
data, errors = CertificateUploadInputSchema().load(data)
|
||||||
|
assert errors == {'private_key': ['Private key presented is not valid.']}
|
||||||
|
|
||||||
|
|
||||||
|
def test_certificate_upload_schema_invalid_chain(client):
|
||||||
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
|
data = {
|
||||||
|
'body': SAN_CERT_STR,
|
||||||
|
'chain': 'CHAINSAW',
|
||||||
|
'owner': 'pwner@example.com',
|
||||||
|
}
|
||||||
|
data, errors = CertificateUploadInputSchema().load(data)
|
||||||
|
assert errors == {'chain': ['Public certificate presented is not valid.']}
|
||||||
|
|
||||||
|
|
||||||
|
def test_certificate_upload_schema_wrong_pkey(client):
|
||||||
|
from lemur.certificates.schemas import CertificateUploadInputSchema
|
||||||
|
data = {
|
||||||
|
'body': SAN_CERT_STR,
|
||||||
|
'privateKey': ROOTCA_KEY,
|
||||||
|
'chain': INTERMEDIATE_CERT_STR,
|
||||||
|
'owner': 'pwner@example.com',
|
||||||
|
}
|
||||||
|
data, errors = CertificateUploadInputSchema().load(data)
|
||||||
|
assert errors == {'_schema': ['Private key does not match certificate.']}
|
||||||
|
|
||||||
|
|
||||||
def test_create_basic_csr(client):
|
def test_create_basic_csr(client):
|
||||||
csr_config = dict(
|
csr_config = dict(
|
||||||
common_name='example.com',
|
common_name='example.com',
|
||||||
|
@ -462,8 +541,11 @@ def test_create_certificate(issuer_plugin, authority, user):
|
||||||
assert cert.name == 'ACustomName1'
|
assert cert.name == 'ACustomName1'
|
||||||
|
|
||||||
|
|
||||||
def test_reissue_certificate(issuer_plugin, authority, certificate):
|
def test_reissue_certificate(issuer_plugin, crypto_authority, certificate, logged_in_user):
|
||||||
from lemur.certificates.service import reissue_certificate
|
from lemur.certificates.service import reissue_certificate
|
||||||
|
|
||||||
|
# test-authority would return a mismatching private key, so use 'cryptography-issuer' plugin instead.
|
||||||
|
certificate.authority = crypto_authority
|
||||||
new_cert = reissue_certificate(certificate)
|
new_cert = reissue_certificate(certificate)
|
||||||
assert new_cert
|
assert new_cert
|
||||||
|
|
||||||
|
@ -487,7 +569,7 @@ def test_import(user):
|
||||||
assert str(cert.not_after) == '2047-12-31T22:00:00+00:00'
|
assert str(cert.not_after) == '2047-12-31T22:00:00+00:00'
|
||||||
assert str(cert.not_before) == '2017-12-31T22:00:00+00:00'
|
assert str(cert.not_before) == '2017-12-31T22:00:00+00:00'
|
||||||
assert cert.issuer == 'LemurTrustUnittestsClass1CA2018'
|
assert cert.issuer == 'LemurTrustUnittestsClass1CA2018'
|
||||||
assert cert.name == 'SAN-san.example.org-LemurTrustUnittestsClass1CA2018-20171231-20471231-AFF2DB4F8D2D4D8E80FA382AE27C2333-2'
|
assert cert.name.startswith('SAN-san.example.org-LemurTrustUnittestsClass1CA2018-20171231-20471231')
|
||||||
|
|
||||||
cert = import_certificate(body=SAN_CERT_STR, chain=INTERMEDIATE_CERT_STR, private_key=SAN_CERT_KEY, owner='joe@example.com', name='ACustomName2', creator=user['user'])
|
cert = import_certificate(body=SAN_CERT_STR, chain=INTERMEDIATE_CERT_STR, private_key=SAN_CERT_KEY, owner='joe@example.com', name='ACustomName2', creator=user['user'])
|
||||||
assert cert.name == 'ACustomName2'
|
assert cert.name == 'ACustomName2'
|
||||||
|
|
|
@ -1,16 +1,28 @@
|
||||||
import pytest
|
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from .vectors import SAN_CERT_KEY
|
|
||||||
|
import pytest
|
||||||
from marshmallow.exceptions import ValidationError
|
from marshmallow.exceptions import ValidationError
|
||||||
|
|
||||||
|
from lemur.common.utils import parse_private_key
|
||||||
|
from lemur.common.validators import verify_private_key_match
|
||||||
|
from lemur.tests.vectors import INTERMEDIATE_CERT, SAN_CERT, SAN_CERT_KEY
|
||||||
|
|
||||||
|
|
||||||
def test_private_key(session):
|
def test_private_key(session):
|
||||||
from lemur.common.validators import private_key
|
parse_private_key(SAN_CERT_KEY)
|
||||||
|
|
||||||
private_key(SAN_CERT_KEY)
|
with pytest.raises(ValueError):
|
||||||
|
parse_private_key('invalid_private_key')
|
||||||
|
|
||||||
|
|
||||||
|
def test_validate_private_key(session):
|
||||||
|
key = parse_private_key(SAN_CERT_KEY)
|
||||||
|
|
||||||
|
verify_private_key_match(key, SAN_CERT)
|
||||||
|
|
||||||
with pytest.raises(ValidationError):
|
with pytest.raises(ValidationError):
|
||||||
private_key('invalid_private_key')
|
# Wrong key for certificate
|
||||||
|
verify_private_key_match(key, INTERMEDIATE_CERT)
|
||||||
|
|
||||||
|
|
||||||
def test_sub_alt_type(session):
|
def test_sub_alt_type(session):
|
||||||
|
|
|
@ -5,8 +5,7 @@
|
||||||
# pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in
|
# pip-compile --no-index --output-file requirements-dev.txt requirements-dev.in
|
||||||
#
|
#
|
||||||
aspy.yaml==1.1.1 # via pre-commit
|
aspy.yaml==1.1.1 # via pre-commit
|
||||||
bleach==3.0.2 # via readme-renderer
|
bleach==3.1.0 # via readme-renderer
|
||||||
cached-property==1.5.1 # via pre-commit
|
|
||||||
certifi==2018.11.29 # via requests
|
certifi==2018.11.29 # via requests
|
||||||
cfgv==1.4.0 # via pre-commit
|
cfgv==1.4.0 # via pre-commit
|
||||||
chardet==3.0.4 # via requests
|
chardet==3.0.4 # via requests
|
||||||
|
@ -15,12 +14,11 @@ flake8==3.5.0
|
||||||
identify==1.1.8 # via pre-commit
|
identify==1.1.8 # via pre-commit
|
||||||
idna==2.8 # via requests
|
idna==2.8 # via requests
|
||||||
importlib-metadata==0.8 # via pre-commit
|
importlib-metadata==0.8 # via pre-commit
|
||||||
importlib-resources==1.0.2 # via pre-commit
|
|
||||||
invoke==1.2.0
|
invoke==1.2.0
|
||||||
mccabe==0.6.1 # via flake8
|
mccabe==0.6.1 # via flake8
|
||||||
nodeenv==1.3.3
|
nodeenv==1.3.3
|
||||||
pkginfo==1.5.0 # via twine
|
pkginfo==1.5.0.1 # via twine
|
||||||
pre-commit==1.13.0
|
pre-commit==1.14.2
|
||||||
pycodestyle==2.3.1 # via flake8
|
pycodestyle==2.3.1 # via flake8
|
||||||
pyflakes==1.6.0 # via flake8
|
pyflakes==1.6.0 # via flake8
|
||||||
pygments==2.3.1 # via readme-renderer
|
pygments==2.3.1 # via readme-renderer
|
||||||
|
@ -30,7 +28,7 @@ requests-toolbelt==0.8.0 # via twine
|
||||||
requests==2.21.0 # via requests-toolbelt, twine
|
requests==2.21.0 # via requests-toolbelt, twine
|
||||||
six==1.12.0 # via bleach, cfgv, pre-commit, readme-renderer
|
six==1.12.0 # via bleach, cfgv, pre-commit, readme-renderer
|
||||||
toml==0.10.0 # via pre-commit
|
toml==0.10.0 # via pre-commit
|
||||||
tqdm==4.29.0 # via twine
|
tqdm==4.29.1 # via twine
|
||||||
twine==1.12.1
|
twine==1.12.1
|
||||||
urllib3==1.24.1 # via requests
|
urllib3==1.24.1 # via requests
|
||||||
virtualenv==16.2.0 # via pre-commit
|
virtualenv==16.2.0 # via pre-commit
|
||||||
|
|
|
@ -7,18 +7,18 @@
|
||||||
acme==0.30.0
|
acme==0.30.0
|
||||||
alabaster==0.7.12 # via sphinx
|
alabaster==0.7.12 # via sphinx
|
||||||
alembic-autogenerate-enums==0.0.2
|
alembic-autogenerate-enums==0.0.2
|
||||||
alembic==1.0.5
|
alembic==1.0.6
|
||||||
amqp==2.3.2
|
amqp==2.4.0
|
||||||
aniso8601==4.0.1
|
aniso8601==4.1.0
|
||||||
arrow==0.13.0
|
arrow==0.13.0
|
||||||
asn1crypto==0.24.0
|
asn1crypto==0.24.0
|
||||||
asyncpool==1.0
|
asyncpool==1.0
|
||||||
babel==2.6.0 # via sphinx
|
babel==2.6.0 # via sphinx
|
||||||
bcrypt==3.1.5
|
bcrypt==3.1.6
|
||||||
billiard==3.5.0.5
|
billiard==3.5.0.5
|
||||||
blinker==1.4
|
blinker==1.4
|
||||||
boto3==1.9.75
|
boto3==1.9.80
|
||||||
botocore==1.12.75
|
botocore==1.12.80
|
||||||
celery[redis]==4.2.1
|
celery[redis]==4.2.1
|
||||||
certifi==2018.11.29
|
certifi==2018.11.29
|
||||||
cffi==1.11.5
|
cffi==1.11.5
|
||||||
|
@ -54,7 +54,7 @@ lockfile==0.12.2
|
||||||
mako==1.0.7
|
mako==1.0.7
|
||||||
markupsafe==1.1.0
|
markupsafe==1.1.0
|
||||||
marshmallow-sqlalchemy==0.15.0
|
marshmallow-sqlalchemy==0.15.0
|
||||||
marshmallow==2.17.0
|
marshmallow==2.18.0
|
||||||
mock==2.0.0
|
mock==2.0.0
|
||||||
ndg-httpsclient==0.5.1
|
ndg-httpsclient==0.5.1
|
||||||
packaging==18.0 # via sphinx
|
packaging==18.0 # via sphinx
|
||||||
|
@ -69,7 +69,7 @@ pygments==2.3.1 # via sphinx
|
||||||
pyjwt==1.7.1
|
pyjwt==1.7.1
|
||||||
pynacl==1.3.0
|
pynacl==1.3.0
|
||||||
pyopenssl==18.0.0
|
pyopenssl==18.0.0
|
||||||
pyparsing==2.3.0 # via packaging
|
pyparsing==2.3.1 # via packaging
|
||||||
pyrfc3339==1.1
|
pyrfc3339==1.1
|
||||||
python-dateutil==2.7.5
|
python-dateutil==2.7.5
|
||||||
python-editor==1.0.3
|
python-editor==1.0.3
|
||||||
|
@ -87,8 +87,8 @@ sphinx-rtd-theme==0.4.2
|
||||||
sphinx==1.8.3
|
sphinx==1.8.3
|
||||||
sphinxcontrib-httpdomain==1.7.0
|
sphinxcontrib-httpdomain==1.7.0
|
||||||
sphinxcontrib-websupport==1.1.0 # via sphinx
|
sphinxcontrib-websupport==1.1.0 # via sphinx
|
||||||
sqlalchemy-utils==0.33.10
|
sqlalchemy-utils==0.33.11
|
||||||
sqlalchemy==1.2.15
|
sqlalchemy==1.2.16
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
urllib3==1.24.1
|
urllib3==1.24.1
|
||||||
vine==1.2.0
|
vine==1.2.0
|
||||||
|
|
|
@ -8,9 +8,9 @@ asn1crypto==0.24.0 # via cryptography
|
||||||
atomicwrites==1.2.1 # via pytest
|
atomicwrites==1.2.1 # via pytest
|
||||||
attrs==18.2.0 # via pytest
|
attrs==18.2.0 # via pytest
|
||||||
aws-xray-sdk==0.95 # via moto
|
aws-xray-sdk==0.95 # via moto
|
||||||
boto3==1.9.75 # via moto
|
boto3==1.9.80 # via moto
|
||||||
boto==2.49.0 # via moto
|
boto==2.49.0 # via moto
|
||||||
botocore==1.12.75 # via boto3, moto, s3transfer
|
botocore==1.12.80 # via boto3, moto, s3transfer
|
||||||
certifi==2018.11.29 # via requests
|
certifi==2018.11.29 # 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
|
||||||
|
@ -18,7 +18,7 @@ click==7.0 # via flask
|
||||||
coverage==4.5.2
|
coverage==4.5.2
|
||||||
cryptography==2.4.2 # via moto
|
cryptography==2.4.2 # via moto
|
||||||
docker-pycreds==0.4.0 # via docker
|
docker-pycreds==0.4.0 # via docker
|
||||||
docker==3.6.0 # via moto
|
docker==3.7.0 # via moto
|
||||||
docutils==0.14 # via botocore
|
docutils==0.14 # via botocore
|
||||||
ecdsa==0.13 # via python-jose
|
ecdsa==0.13 # via python-jose
|
||||||
factory-boy==2.11.1
|
factory-boy==2.11.1
|
||||||
|
@ -38,7 +38,7 @@ more-itertools==5.0.0 # via pytest
|
||||||
moto==1.3.7
|
moto==1.3.7
|
||||||
nose==1.3.7
|
nose==1.3.7
|
||||||
pbr==5.1.1 # via mock
|
pbr==5.1.1 # via mock
|
||||||
pluggy==0.8.0 # via pytest
|
pluggy==0.8.1 # via pytest
|
||||||
py==1.7.0 # via pytest
|
py==1.7.0 # via pytest
|
||||||
pyaml==18.11.0 # via moto
|
pyaml==18.11.0 # via moto
|
||||||
pycparser==2.19 # via cffi
|
pycparser==2.19 # via cffi
|
||||||
|
@ -46,7 +46,7 @@ pycryptodome==3.7.2 # via python-jose
|
||||||
pyflakes==2.0.0
|
pyflakes==2.0.0
|
||||||
pytest-flask==0.14.0
|
pytest-flask==0.14.0
|
||||||
pytest-mock==1.10.0
|
pytest-mock==1.10.0
|
||||||
pytest==4.1.0
|
pytest==4.1.1
|
||||||
python-dateutil==2.7.5 # via botocore, faker, freezegun, moto
|
python-dateutil==2.7.5 # via botocore, faker, freezegun, moto
|
||||||
python-jose==2.0.2 # via moto
|
python-jose==2.0.2 # via moto
|
||||||
pytz==2018.9 # via moto
|
pytz==2018.9 # via moto
|
||||||
|
@ -60,5 +60,5 @@ text-unidecode==1.2 # via faker
|
||||||
urllib3==1.24.1 # via botocore, requests
|
urllib3==1.24.1 # via botocore, requests
|
||||||
websocket-client==0.54.0 # via docker
|
websocket-client==0.54.0 # via docker
|
||||||
werkzeug==0.14.1 # via flask, moto, pytest-flask
|
werkzeug==0.14.1 # via flask, moto, pytest-flask
|
||||||
wrapt==1.10.11 # via aws-xray-sdk
|
wrapt==1.11.0 # via aws-xray-sdk
|
||||||
xmltodict==0.11.0 # via moto
|
xmltodict==0.11.0 # via moto
|
||||||
|
|
|
@ -6,17 +6,17 @@
|
||||||
#
|
#
|
||||||
acme==0.30.0
|
acme==0.30.0
|
||||||
alembic-autogenerate-enums==0.0.2
|
alembic-autogenerate-enums==0.0.2
|
||||||
alembic==1.0.5 # via flask-migrate
|
alembic==1.0.6 # via flask-migrate
|
||||||
amqp==2.3.2 # via kombu
|
amqp==2.4.0 # via kombu
|
||||||
aniso8601==4.0.1 # via flask-restful
|
aniso8601==4.1.0 # via flask-restful
|
||||||
arrow==0.13.0
|
arrow==0.13.0
|
||||||
asn1crypto==0.24.0 # via cryptography
|
asn1crypto==0.24.0 # via cryptography
|
||||||
asyncpool==1.0
|
asyncpool==1.0
|
||||||
bcrypt==3.1.5 # via flask-bcrypt, paramiko
|
bcrypt==3.1.6 # via flask-bcrypt, paramiko
|
||||||
billiard==3.5.0.5 # via celery
|
billiard==3.5.0.5 # via celery
|
||||||
blinker==1.4 # via flask-mail, flask-principal, raven
|
blinker==1.4 # via flask-mail, flask-principal, raven
|
||||||
boto3==1.9.75
|
boto3==1.9.80
|
||||||
botocore==1.12.75
|
botocore==1.12.80
|
||||||
celery[redis]==4.2.1
|
celery[redis]==4.2.1
|
||||||
certifi==2018.11.29
|
certifi==2018.11.29
|
||||||
certsrv==2.1.0
|
certsrv==2.1.0
|
||||||
|
@ -52,7 +52,7 @@ lockfile==0.12.2
|
||||||
mako==1.0.7 # via alembic
|
mako==1.0.7 # via alembic
|
||||||
markupsafe==1.1.0 # via jinja2, mako
|
markupsafe==1.1.0 # via jinja2, mako
|
||||||
marshmallow-sqlalchemy==0.15.0
|
marshmallow-sqlalchemy==0.15.0
|
||||||
marshmallow==2.17.0
|
marshmallow==2.18.0
|
||||||
mock==2.0.0 # via acme
|
mock==2.0.0 # via acme
|
||||||
ndg-httpsclient==0.5.1
|
ndg-httpsclient==0.5.1
|
||||||
paramiko==2.4.2
|
paramiko==2.4.2
|
||||||
|
@ -78,8 +78,8 @@ requests[security]==2.21.0
|
||||||
retrying==1.3.3
|
retrying==1.3.3
|
||||||
s3transfer==0.1.13 # via boto3
|
s3transfer==0.1.13 # via boto3
|
||||||
six==1.12.0
|
six==1.12.0
|
||||||
sqlalchemy-utils==0.33.10
|
sqlalchemy-utils==0.33.11
|
||||||
sqlalchemy==1.2.15 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils
|
sqlalchemy==1.2.16 # via alembic, flask-sqlalchemy, marshmallow-sqlalchemy, sqlalchemy-utils
|
||||||
tabulate==0.8.2
|
tabulate==0.8.2
|
||||||
urllib3==1.24.1 # via botocore, requests
|
urllib3==1.24.1 # via botocore, requests
|
||||||
vine==1.2.0 # via amqp
|
vine==1.2.0 # via amqp
|
||||||
|
|
Loading…
Reference in New Issue