commit
8e79fadb0f
18 changed files with 1251 additions and 0 deletions
|
|
@ -5,6 +5,7 @@ import logging
|
|||
__title__ = 'moto'
|
||||
__version__ = '1.0.1'
|
||||
|
||||
from .acm import mock_acm # flake8: noqa
|
||||
from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa
|
||||
from .autoscaling import mock_autoscaling, mock_autoscaling_deprecated # flake8: noqa
|
||||
from .awslambda import mock_lambda, mock_lambda_deprecated # flake8: noqa
|
||||
|
|
|
|||
6
moto/acm/__init__.py
Normal file
6
moto/acm/__init__.py
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
from .models import acm_backends
|
||||
from ..core.models import base_decorator
|
||||
|
||||
acm_backend = acm_backends['us-east-1']
|
||||
mock_acm = base_decorator(acm_backends)
|
||||
395
moto/acm/models.py
Normal file
395
moto/acm/models.py
Normal file
|
|
@ -0,0 +1,395 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
import json
|
||||
import datetime
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.ec2 import ec2_backends
|
||||
|
||||
from .utils import make_arn_for_certificate
|
||||
|
||||
import cryptography.x509
|
||||
import cryptography.hazmat.primitives.asymmetric.rsa
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
|
||||
DEFAULT_ACCOUNT_ID = 123456789012
|
||||
GOOGLE_ROOT_CA = b"""-----BEGIN CERTIFICATE-----
|
||||
MIIEKDCCAxCgAwIBAgIQAQAhJYiw+lmnd+8Fe2Yn3zANBgkqhkiG9w0BAQsFADBC
|
||||
MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMS
|
||||
R2VvVHJ1c3QgR2xvYmFsIENBMB4XDTE3MDUyMjExMzIzN1oXDTE4MTIzMTIzNTk1
|
||||
OVowSTELMAkGA1UEBhMCVVMxEzARBgNVBAoTCkdvb2dsZSBJbmMxJTAjBgNVBAMT
|
||||
HEdvb2dsZSBJbnRlcm5ldCBBdXRob3JpdHkgRzIwggEiMA0GCSqGSIb3DQEBAQUA
|
||||
A4IBDwAwggEKAoIBAQCcKgR3XNhQkToGo4Lg2FBIvIk/8RlwGohGfuCPxfGJziHu
|
||||
Wv5hDbcyRImgdAtTT1WkzoJile7rWV/G4QWAEsRelD+8W0g49FP3JOb7kekVxM/0
|
||||
Uw30SvyfVN59vqBrb4fA0FAfKDADQNoIc1Fsf/86PKc3Bo69SxEE630k3ub5/DFx
|
||||
+5TVYPMuSq9C0svqxGoassxT3RVLix/IGWEfzZ2oPmMrhDVpZYTIGcVGIvhTlb7j
|
||||
gEoQxirsupcgEcc5mRAEoPBhepUljE5SdeK27QjKFPzOImqzTs9GA5eXA37Asd57
|
||||
r0Uzz7o+cbfe9CUlwg01iZ2d+w4ReYkeN8WvjnJpAgMBAAGjggERMIIBDTAfBgNV
|
||||
HSMEGDAWgBTAephojYn7qwVkDBF9qn1luMrMTjAdBgNVHQ4EFgQUSt0GFhu89mi1
|
||||
dvWBtrtiGrpagS8wDgYDVR0PAQH/BAQDAgEGMC4GCCsGAQUFBwEBBCIwIDAeBggr
|
||||
BgEFBQcwAYYSaHR0cDovL2cuc3ltY2QuY29tMBIGA1UdEwEB/wQIMAYBAf8CAQAw
|
||||
NQYDVR0fBC4wLDAqoCigJoYkaHR0cDovL2cuc3ltY2IuY29tL2NybHMvZ3RnbG9i
|
||||
YWwuY3JsMCEGA1UdIAQaMBgwDAYKKwYBBAHWeQIFATAIBgZngQwBAgIwHQYDVR0l
|
||||
BBYwFAYIKwYBBQUHAwEGCCsGAQUFBwMCMA0GCSqGSIb3DQEBCwUAA4IBAQDKSeWs
|
||||
12Rkd1u+cfrP9B4jx5ppY1Rf60zWGSgjZGaOHMeHgGRfBIsmr5jfCnC8vBk97nsz
|
||||
qX+99AXUcLsFJnnqmseYuQcZZTTMPOk/xQH6bwx+23pwXEz+LQDwyr4tjrSogPsB
|
||||
E4jLnD/lu3fKOmc2887VJwJyQ6C9bgLxRwVxPgFZ6RGeGvOED4Cmong1L7bHon8X
|
||||
fOGLVq7uZ4hRJzBgpWJSwzfVO+qFKgE4h6LPcK2kesnE58rF2rwjMvL+GMJ74N87
|
||||
L9TQEOaWTPtEtyFkDbkAlDASJodYmDkFOA/MgkgMCkdm7r+0X8T/cKjhf4t5K7hl
|
||||
MqO5tzHpCvX2HzLc
|
||||
-----END CERTIFICATE-----"""
|
||||
# Added google root CA as AWS returns chain you gave it + root CA (provided or not)
|
||||
# so for now a cheap response is just give any old root CA
|
||||
|
||||
|
||||
def datetime_to_epoch(date):
|
||||
# As only Py3 has datetime.timestamp()
|
||||
return int((date - datetime.datetime(1970, 1, 1)).total_seconds())
|
||||
|
||||
|
||||
class AWSError(Exception):
|
||||
TYPE = None
|
||||
STATUS = 400
|
||||
|
||||
def __init__(self, message):
|
||||
self.message = message
|
||||
|
||||
def response(self):
|
||||
resp = {'__type': self.TYPE, 'message': self.message}
|
||||
return json.dumps(resp), dict(status=self.STATUS)
|
||||
|
||||
|
||||
class AWSValidationException(AWSError):
|
||||
TYPE = 'ValidationException'
|
||||
|
||||
|
||||
class AWSResourceNotFoundException(AWSError):
|
||||
TYPE = 'ResourceNotFoundException'
|
||||
|
||||
|
||||
class CertBundle(BaseModel):
|
||||
def __init__(self, certificate, private_key, chain=None, region='us-east-1', arn=None, cert_type='IMPORTED', cert_status='ISSUED'):
|
||||
self.created_at = datetime.datetime.now()
|
||||
self.cert = certificate
|
||||
self._cert = None
|
||||
self.common_name = None
|
||||
self.key = private_key
|
||||
self._key = None
|
||||
self.chain = chain
|
||||
self.tags = {}
|
||||
self._chain = None
|
||||
self.type = cert_type # Should really be an enum
|
||||
self.status = cert_status # Should really be an enum
|
||||
|
||||
# AWS always returns your chain + root CA
|
||||
if self.chain is None:
|
||||
self.chain = GOOGLE_ROOT_CA
|
||||
else:
|
||||
self.chain += b'\n' + GOOGLE_ROOT_CA
|
||||
|
||||
# Takes care of PEM checking
|
||||
self.validate_pk()
|
||||
self.validate_certificate()
|
||||
if chain is not None:
|
||||
self.validate_chain()
|
||||
|
||||
# TODO check cert is valid, or if self-signed then a chain is provided, otherwise
|
||||
# raise AWSValidationException('Provided certificate is not a valid self signed. Please provide either a valid self-signed certificate or certificate chain.')
|
||||
|
||||
# Used for when one wants to overwrite an arn
|
||||
if arn is None:
|
||||
self.arn = make_arn_for_certificate(DEFAULT_ACCOUNT_ID, region)
|
||||
else:
|
||||
self.arn = arn
|
||||
|
||||
@classmethod
|
||||
def generate_cert(cls, domain_name, sans=None):
|
||||
if sans is None:
|
||||
sans = set()
|
||||
else:
|
||||
sans = set(sans)
|
||||
|
||||
sans.add(domain_name)
|
||||
sans = [cryptography.x509.DNSName(item) for item in sans]
|
||||
|
||||
key = cryptography.hazmat.primitives.asymmetric.rsa.generate_private_key(public_exponent=65537, key_size=2048, backend=default_backend())
|
||||
subject = cryptography.x509.Name([
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.COUNTRY_NAME, u"US"),
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.STATE_OR_PROVINCE_NAME, u"CA"),
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.LOCALITY_NAME, u"San Francisco"),
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.ORGANIZATION_NAME, u"My Company"),
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.COMMON_NAME, domain_name),
|
||||
])
|
||||
issuer = cryptography.x509.Name([ # C = US, O = Amazon, OU = Server CA 1B, CN = Amazon
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.COUNTRY_NAME, u"US"),
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.ORGANIZATION_NAME, u"Amazon"),
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.ORGANIZATIONAL_UNIT_NAME, u"Server CA 1B"),
|
||||
cryptography.x509.NameAttribute(cryptography.x509.NameOID.COMMON_NAME, u"Amazon"),
|
||||
])
|
||||
cert = cryptography.x509.CertificateBuilder().subject_name(
|
||||
subject
|
||||
).issuer_name(
|
||||
issuer
|
||||
).public_key(
|
||||
key.public_key()
|
||||
).serial_number(
|
||||
cryptography.x509.random_serial_number()
|
||||
).not_valid_before(
|
||||
datetime.datetime.utcnow()
|
||||
).not_valid_after(
|
||||
datetime.datetime.utcnow() + datetime.timedelta(days=365)
|
||||
).add_extension(
|
||||
cryptography.x509.SubjectAlternativeName(sans),
|
||||
critical=False,
|
||||
).sign(key, hashes.SHA512(), default_backend())
|
||||
|
||||
cert_armored = cert.public_bytes(serialization.Encoding.PEM)
|
||||
private_key = key.private_bytes(
|
||||
encoding=serialization.Encoding.PEM,
|
||||
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||
encryption_algorithm=serialization.NoEncryption()
|
||||
)
|
||||
|
||||
return cls(cert_armored, private_key, cert_type='AMAZON_ISSUED', cert_status='PENDING_VALIDATION')
|
||||
|
||||
def validate_pk(self):
|
||||
try:
|
||||
self._key = serialization.load_pem_private_key(self.key, password=None, backend=default_backend())
|
||||
|
||||
if self._key.key_size > 2048:
|
||||
AWSValidationException('The private key length is not supported. Only 1024-bit and 2048-bit are allowed.')
|
||||
|
||||
except Exception as err:
|
||||
if isinstance(err, AWSValidationException):
|
||||
raise
|
||||
raise AWSValidationException('The private key is not PEM-encoded or is not valid.')
|
||||
|
||||
def validate_certificate(self):
|
||||
try:
|
||||
self._cert = cryptography.x509.load_pem_x509_certificate(self.cert, default_backend())
|
||||
|
||||
now = datetime.datetime.now()
|
||||
if self._cert.not_valid_after < now:
|
||||
raise AWSValidationException('The certificate has expired, is not valid.')
|
||||
|
||||
if self._cert.not_valid_before > now:
|
||||
raise AWSValidationException('The certificate is not in effect yet, is not valid.')
|
||||
|
||||
# Extracting some common fields for ease of use
|
||||
# Have to search through cert.subject for OIDs
|
||||
self.common_name = self._cert.subject.get_attributes_for_oid(cryptography.x509.OID_COMMON_NAME)[0].value
|
||||
|
||||
except Exception as err:
|
||||
if isinstance(err, AWSValidationException):
|
||||
raise
|
||||
raise AWSValidationException('The certificate is not PEM-encoded or is not valid.')
|
||||
|
||||
def validate_chain(self):
|
||||
try:
|
||||
self._chain = []
|
||||
|
||||
for cert_armored in self.chain.split(b'-\n-'):
|
||||
# Would leave encoded but Py2 does not have raw binary strings
|
||||
cert_armored = cert_armored.decode()
|
||||
|
||||
# Fix missing -'s on split
|
||||
cert_armored = re.sub(r'^----B', '-----B', cert_armored)
|
||||
cert_armored = re.sub(r'E----$', 'E-----', cert_armored)
|
||||
cert = cryptography.x509.load_pem_x509_certificate(cert_armored.encode(), default_backend())
|
||||
self._chain.append(cert)
|
||||
|
||||
now = datetime.datetime.now()
|
||||
if self._cert.not_valid_after < now:
|
||||
raise AWSValidationException('The certificate chain has expired, is not valid.')
|
||||
|
||||
if self._cert.not_valid_before > now:
|
||||
raise AWSValidationException('The certificate chain is not in effect yet, is not valid.')
|
||||
|
||||
except Exception as err:
|
||||
if isinstance(err, AWSValidationException):
|
||||
raise
|
||||
raise AWSValidationException('The certificate is not PEM-encoded or is not valid.')
|
||||
|
||||
def check(self):
|
||||
# Basically, if the certificate is pending, and then checked again after 1 min
|
||||
# It will appear as if its been validated
|
||||
if self.type == 'AMAZON_ISSUED' and self.status == 'PENDING_VALIDATION' and \
|
||||
(datetime.datetime.now() - self.created_at).total_seconds() > 60: # 1min
|
||||
self.status = 'ISSUED'
|
||||
|
||||
def describe(self):
|
||||
# 'RenewalSummary': {}, # Only when cert is amazon issued
|
||||
if self._key.key_size == 1024:
|
||||
key_algo = 'RSA_1024'
|
||||
elif self._key.key_size == 2048:
|
||||
key_algo = 'RSA_2048'
|
||||
else:
|
||||
key_algo = 'EC_prime256v1'
|
||||
|
||||
# Look for SANs
|
||||
san_obj = self._cert.extensions.get_extension_for_oid(cryptography.x509.OID_SUBJECT_ALTERNATIVE_NAME)
|
||||
sans = []
|
||||
if san_obj is not None:
|
||||
sans = [item.value for item in san_obj.value]
|
||||
|
||||
result = {
|
||||
'Certificate': {
|
||||
'CertificateArn': self.arn,
|
||||
'DomainName': self.common_name,
|
||||
'InUseBy': [],
|
||||
'Issuer': self._cert.issuer.get_attributes_for_oid(cryptography.x509.OID_COMMON_NAME)[0].value,
|
||||
'KeyAlgorithm': key_algo,
|
||||
'NotAfter': datetime_to_epoch(self._cert.not_valid_after),
|
||||
'NotBefore': datetime_to_epoch(self._cert.not_valid_before),
|
||||
'Serial': self._cert.serial,
|
||||
'SignatureAlgorithm': self._cert.signature_algorithm_oid._name.upper().replace('ENCRYPTION', ''),
|
||||
'Status': self.status, # One of PENDING_VALIDATION, ISSUED, INACTIVE, EXPIRED, VALIDATION_TIMED_OUT, REVOKED, FAILED.
|
||||
'Subject': 'CN={0}'.format(self.common_name),
|
||||
'SubjectAlternativeNames': sans,
|
||||
'Type': self.type # One of IMPORTED, AMAZON_ISSUED
|
||||
}
|
||||
}
|
||||
|
||||
if self.type == 'IMPORTED':
|
||||
result['Certificate']['ImportedAt'] = datetime_to_epoch(self.created_at)
|
||||
else:
|
||||
result['Certificate']['CreatedAt'] = datetime_to_epoch(self.created_at)
|
||||
result['Certificate']['IssuedAt'] = datetime_to_epoch(self.created_at)
|
||||
|
||||
return result
|
||||
|
||||
def __str__(self):
|
||||
return self.arn
|
||||
|
||||
def __repr__(self):
|
||||
return '<Certificate>'
|
||||
|
||||
|
||||
class AWSCertificateManagerBackend(BaseBackend):
|
||||
def __init__(self, region):
|
||||
super(AWSCertificateManagerBackend, self).__init__()
|
||||
self.region = region
|
||||
self._certificates = {}
|
||||
self._idempotency_tokens = {}
|
||||
|
||||
def reset(self):
|
||||
region = self.region
|
||||
self.__dict__ = {}
|
||||
self.__init__(region)
|
||||
|
||||
@staticmethod
|
||||
def _arn_not_found(arn):
|
||||
msg = 'Certificate with arn {0} not found in account {1}'.format(arn, DEFAULT_ACCOUNT_ID)
|
||||
return AWSResourceNotFoundException(msg)
|
||||
|
||||
def _get_arn_from_idempotency_token(self, token):
|
||||
"""
|
||||
If token doesnt exist, return None, later it will be
|
||||
set with an expiry and arn.
|
||||
|
||||
If token expiry has passed, delete entry and return None
|
||||
|
||||
Else return ARN
|
||||
|
||||
:param token: String token
|
||||
:return: None or ARN
|
||||
"""
|
||||
now = datetime.datetime.now()
|
||||
if token in self._idempotency_tokens:
|
||||
if self._idempotency_tokens[token]['expires'] < now:
|
||||
# Token has expired, new request
|
||||
del self._idempotency_tokens[token]
|
||||
return None
|
||||
else:
|
||||
return self._idempotency_tokens[token]['arn']
|
||||
|
||||
return None
|
||||
|
||||
def _set_idempotency_token_arn(self, token, arn):
|
||||
self._idempotency_tokens[token] = {'arn': arn, 'expires': datetime.datetime.now() + datetime.timedelta(hours=1)}
|
||||
|
||||
def import_cert(self, certificate, private_key, chain=None, arn=None):
|
||||
if arn is not None:
|
||||
if arn not in self._certificates:
|
||||
raise self._arn_not_found(arn)
|
||||
else:
|
||||
# Will reuse provided ARN
|
||||
bundle = CertBundle(certificate, private_key, chain=chain, region=region, arn=arn)
|
||||
else:
|
||||
# Will generate a random ARN
|
||||
bundle = CertBundle(certificate, private_key, chain=chain, region=region)
|
||||
|
||||
self._certificates[bundle.arn] = bundle
|
||||
|
||||
return bundle.arn
|
||||
|
||||
def get_certificates_list(self):
|
||||
"""
|
||||
Get list of certificates
|
||||
|
||||
:return: List of certificates
|
||||
:rtype: list of CertBundle
|
||||
"""
|
||||
for arn in self._certificates.keys():
|
||||
yield self.get_certificate(arn)
|
||||
|
||||
def get_certificate(self, arn):
|
||||
if arn not in self._certificates:
|
||||
raise self._arn_not_found(arn)
|
||||
|
||||
cert_bundle = self._certificates[arn]
|
||||
cert_bundle.check()
|
||||
return cert_bundle
|
||||
|
||||
def delete_certificate(self, arn):
|
||||
if arn not in self._certificates:
|
||||
raise self._arn_not_found(arn)
|
||||
|
||||
del self._certificates[arn]
|
||||
|
||||
def request_certificate(self, domain_name, domain_validation_options, idempotency_token, subject_alt_names):
|
||||
if idempotency_token is not None:
|
||||
arn = self._get_arn_from_idempotency_token(idempotency_token)
|
||||
if arn is not None:
|
||||
return arn
|
||||
|
||||
cert = CertBundle.generate_cert(domain_name, subject_alt_names)
|
||||
if idempotency_token is not None:
|
||||
self._set_idempotency_token_arn(idempotency_token, cert.arn)
|
||||
self._certificates[cert.arn] = cert
|
||||
|
||||
return cert.arn
|
||||
|
||||
def add_tags_to_certificate(self, arn, tags):
|
||||
# get_cert does arn check
|
||||
cert_bundle = self.get_certificate(arn)
|
||||
|
||||
for tag in tags:
|
||||
key = tag['Key']
|
||||
value = tag.get('Value', None)
|
||||
cert_bundle.tags[key] = value
|
||||
|
||||
def remove_tags_from_certificate(self, arn, tags):
|
||||
# get_cert does arn check
|
||||
cert_bundle = self.get_certificate(arn)
|
||||
|
||||
for tag in tags:
|
||||
key = tag['Key']
|
||||
value = tag.get('Value', None)
|
||||
|
||||
try:
|
||||
# If value isnt provided, just delete key
|
||||
if value is None:
|
||||
del cert_bundle.tags[key]
|
||||
# If value is provided, only delete if it matches what already exists
|
||||
elif cert_bundle.tags[key] == value:
|
||||
del cert_bundle.tags[key]
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
||||
acm_backends = {}
|
||||
for region, ec2_backend in ec2_backends.items():
|
||||
acm_backends[region] = AWSCertificateManagerBackend(region)
|
||||
224
moto/acm/responses.py
Normal file
224
moto/acm/responses.py
Normal file
|
|
@ -0,0 +1,224 @@
|
|||
from __future__ import unicode_literals
|
||||
import json
|
||||
import base64
|
||||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import acm_backends, AWSError, AWSValidationException
|
||||
|
||||
|
||||
class AWSCertificateManagerResponse(BaseResponse):
|
||||
|
||||
@property
|
||||
def acm_backend(self):
|
||||
"""
|
||||
ACM Backend
|
||||
|
||||
:return: ACM Backend object
|
||||
:rtype: moto.acm.models.AWSCertificateManagerBackend
|
||||
"""
|
||||
return acm_backends[self.region]
|
||||
|
||||
@property
|
||||
def request_params(self):
|
||||
try:
|
||||
return json.loads(self.body)
|
||||
except ValueError:
|
||||
return {}
|
||||
|
||||
def _get_param(self, param, default=None):
|
||||
return self.request_params.get(param, default)
|
||||
|
||||
def add_tags_to_certificate(self):
|
||||
arn = self._get_param('CertificateArn')
|
||||
tags = self._get_param('Tags')
|
||||
|
||||
if arn is None:
|
||||
msg = 'A required parameter for the specified action is not supplied.'
|
||||
return json.dumps({'__type': 'MissingParameter', 'message': msg}), dict(status=400)
|
||||
|
||||
try:
|
||||
self.acm_backend.add_tags_to_certificate(arn, tags)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
return ''
|
||||
|
||||
def delete_certificate(self):
|
||||
arn = self._get_param('CertificateArn')
|
||||
|
||||
if arn is None:
|
||||
msg = 'A required parameter for the specified action is not supplied.'
|
||||
return json.dumps({'__type': 'MissingParameter', 'message': msg}), dict(status=400)
|
||||
|
||||
try:
|
||||
self.acm_backend.delete_certificate(arn)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
return ''
|
||||
|
||||
def describe_certificate(self):
|
||||
arn = self._get_param('CertificateArn')
|
||||
|
||||
if arn is None:
|
||||
msg = 'A required parameter for the specified action is not supplied.'
|
||||
return json.dumps({'__type': 'MissingParameter', 'message': msg}), dict(status=400)
|
||||
|
||||
try:
|
||||
cert_bundle = self.acm_backend.get_certificate(arn)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
return json.dumps(cert_bundle.describe())
|
||||
|
||||
def get_certificate(self):
|
||||
arn = self._get_param('CertificateArn')
|
||||
|
||||
if arn is None:
|
||||
msg = 'A required parameter for the specified action is not supplied.'
|
||||
return json.dumps({'__type': 'MissingParameter', 'message': msg}), dict(status=400)
|
||||
|
||||
try:
|
||||
cert_bundle = self.acm_backend.get_certificate(arn)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
result = {
|
||||
'Certificate': cert_bundle.cert.decode(),
|
||||
'CertificateChain': cert_bundle.chain.decode()
|
||||
}
|
||||
return json.dumps(result)
|
||||
|
||||
def import_certificate(self):
|
||||
"""
|
||||
Returns errors on:
|
||||
Certificate, PrivateKey or Chain not being properly formatted
|
||||
Arn not existing if its provided
|
||||
PrivateKey size > 2048
|
||||
Certificate expired or is not yet in effect
|
||||
|
||||
Does not return errors on:
|
||||
Checking Certificate is legit, or a selfsigned chain is provided
|
||||
|
||||
:return: str(JSON) for response
|
||||
"""
|
||||
certificate = self._get_param('Certificate')
|
||||
private_key = self._get_param('PrivateKey')
|
||||
chain = self._get_param('CertificateChain') # Optional
|
||||
current_arn = self._get_param('CertificateArn') # Optional
|
||||
|
||||
# Simple parameter decoding. Rather do it here as its a data transport decision not part of the
|
||||
# actual data
|
||||
try:
|
||||
certificate = base64.standard_b64decode(certificate)
|
||||
except:
|
||||
return AWSValidationException('The certificate is not PEM-encoded or is not valid.').response()
|
||||
try:
|
||||
private_key = base64.standard_b64decode(private_key)
|
||||
except:
|
||||
return AWSValidationException('The private key is not PEM-encoded or is not valid.').response()
|
||||
if chain is not None:
|
||||
try:
|
||||
chain = base64.standard_b64decode(chain)
|
||||
except:
|
||||
return AWSValidationException('The certificate chain is not PEM-encoded or is not valid.').response()
|
||||
|
||||
try:
|
||||
arn = self.acm_backend.import_cert(certificate, private_key, chain=chain, arn=current_arn)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
return json.dumps({'CertificateArn': arn})
|
||||
|
||||
def list_certificates(self):
|
||||
certs = []
|
||||
|
||||
for cert_bundle in self.acm_backend.get_certificates_list():
|
||||
certs.append({
|
||||
'CertificateArn': cert_bundle.arn,
|
||||
'DomainName': cert_bundle.common_name
|
||||
})
|
||||
|
||||
result = {'CertificateSummaryList': certs}
|
||||
return json.dumps(result)
|
||||
|
||||
def list_tags_for_certificate(self):
|
||||
arn = self._get_param('CertificateArn')
|
||||
|
||||
if arn is None:
|
||||
msg = 'A required parameter for the specified action is not supplied.'
|
||||
return {'__type': 'MissingParameter', 'message': msg}, dict(status=400)
|
||||
|
||||
try:
|
||||
cert_bundle = self.acm_backend.get_certificate(arn)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
result = {'Tags': []}
|
||||
# Tag "objects" can not contain the Value part
|
||||
for key, value in cert_bundle.tags.items():
|
||||
tag_dict = {'Key': key}
|
||||
if value is not None:
|
||||
tag_dict['Value'] = value
|
||||
result['Tags'].append(tag_dict)
|
||||
|
||||
return json.dumps(result)
|
||||
|
||||
def remove_tags_from_certificate(self):
|
||||
arn = self._get_param('CertificateArn')
|
||||
tags = self._get_param('Tags')
|
||||
|
||||
if arn is None:
|
||||
msg = 'A required parameter for the specified action is not supplied.'
|
||||
return json.dumps({'__type': 'MissingParameter', 'message': msg}), dict(status=400)
|
||||
|
||||
try:
|
||||
self.acm_backend.remove_tags_from_certificate(arn, tags)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
return ''
|
||||
|
||||
def request_certificate(self):
|
||||
domain_name = self._get_param('DomainName')
|
||||
domain_validation_options = self._get_param('DomainValidationOptions') # is ignored atm
|
||||
idempotency_token = self._get_param('IdempotencyToken')
|
||||
subject_alt_names = self._get_param('SubjectAlternativeNames')
|
||||
|
||||
if len(subject_alt_names) > 10:
|
||||
# There is initial AWS limit of 10
|
||||
msg = 'An ACM limit has been exceeded. Need to request SAN limit to be raised'
|
||||
return json.dumps({'__type': 'LimitExceededException', 'message': msg}), dict(status=400)
|
||||
|
||||
try:
|
||||
arn = self.acm_backend.request_certificate(domain_name, domain_validation_options, idempotency_token, subject_alt_names)
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
return json.dumps({'CertificateArn': arn})
|
||||
|
||||
def resend_validation_email(self):
|
||||
arn = self._get_param('CertificateArn')
|
||||
domain = self._get_param('Domain')
|
||||
# ValidationDomain not used yet.
|
||||
# Contains domain which is equal to or a subset of Domain
|
||||
# that AWS will send validation emails to
|
||||
# https://docs.aws.amazon.com/acm/latest/APIReference/API_ResendValidationEmail.html
|
||||
# validation_domain = self._get_param('ValidationDomain')
|
||||
|
||||
if arn is None:
|
||||
msg = 'A required parameter for the specified action is not supplied.'
|
||||
return json.dumps({'__type': 'MissingParameter', 'message': msg}), dict(status=400)
|
||||
|
||||
try:
|
||||
cert_bundle = self.acm_backend.get_certificate(arn)
|
||||
|
||||
if cert_bundle.common_name != domain:
|
||||
msg = 'Parameter Domain does not match certificate domain'
|
||||
_type = 'InvalidDomainValidationOptionsException'
|
||||
return json.dumps({'__type': _type, 'message': msg}), dict(status=400)
|
||||
|
||||
except AWSError as err:
|
||||
return err.response()
|
||||
|
||||
return ''
|
||||
10
moto/acm/urls.py
Normal file
10
moto/acm/urls.py
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
from __future__ import unicode_literals
|
||||
from .responses import AWSCertificateManagerResponse
|
||||
|
||||
url_bases = [
|
||||
"https?://acm.(.+).amazonaws.com",
|
||||
]
|
||||
|
||||
url_paths = {
|
||||
'{0}/$': AWSCertificateManagerResponse.dispatch,
|
||||
}
|
||||
7
moto/acm/utils.py
Normal file
7
moto/acm/utils.py
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
import uuid
|
||||
|
||||
|
||||
def make_arn_for_certificate(account_id, region_name):
|
||||
# Example
|
||||
# arn:aws:acm:eu-west-2:764371465172:certificate/c4b738b8-56fe-4b3a-b841-1c047654780b
|
||||
return "arn:aws:acm:{0}:{1}:certificate/{2}".format(region_name, account_id, uuid.uuid4())
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from moto.acm import acm_backends
|
||||
from moto.apigateway import apigateway_backends
|
||||
from moto.autoscaling import autoscaling_backends
|
||||
from moto.awslambda import lambda_backends
|
||||
|
|
@ -34,6 +35,7 @@ from moto.sts import sts_backends
|
|||
from moto.xray import xray_backends
|
||||
|
||||
BACKENDS = {
|
||||
'acm': acm_backends,
|
||||
'apigateway': apigateway_backends,
|
||||
'autoscaling': autoscaling_backends,
|
||||
'cloudformation': cloudformation_backends,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue