Merge branch 'master' into mark-alias-target
This commit is contained in:
commit
414ff930ed
43 changed files with 26142 additions and 7134 deletions
|
|
@ -3,7 +3,7 @@ import logging
|
|||
# logging.getLogger('boto').setLevel(logging.CRITICAL)
|
||||
|
||||
__title__ = 'moto'
|
||||
__version__ = '1.3.8'
|
||||
__version__ = '1.3.9'
|
||||
|
||||
from .acm import mock_acm # flake8: noqa
|
||||
from .apigateway import mock_apigateway, mock_apigateway_deprecated # flake8: noqa
|
||||
|
|
|
|||
|
|
@ -246,7 +246,8 @@ def resource_name_property_from_type(resource_type):
|
|||
|
||||
|
||||
def generate_resource_name(resource_type, stack_name, logical_id):
|
||||
if resource_type == "AWS::ElasticLoadBalancingV2::TargetGroup":
|
||||
if resource_type in ["AWS::ElasticLoadBalancingV2::TargetGroup",
|
||||
"AWS::ElasticLoadBalancingV2::LoadBalancer"]:
|
||||
# Target group names need to be less than 32 characters, so when cloudformation creates a name for you
|
||||
# it makes sure to stay under that limit
|
||||
name_prefix = '{0}-{1}'.format(stack_name, logical_id)
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ import six
|
|||
import random
|
||||
import yaml
|
||||
import os
|
||||
import string
|
||||
|
||||
from cfnlint import decode, core
|
||||
|
||||
|
|
@ -29,7 +30,7 @@ def generate_stackset_arn(stackset_id, region_name):
|
|||
|
||||
def random_suffix():
|
||||
size = 12
|
||||
chars = list(range(10)) + ['A-Z']
|
||||
chars = list(range(10)) + list(string.ascii_uppercase)
|
||||
return ''.join(six.text_type(random.choice(chars)) for x in range(size))
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ GET_METRIC_STATISTICS_TEMPLATE = """<GetMetricStatisticsResponse xmlns="http://m
|
|||
<Label>{{ label }}</Label>
|
||||
<Datapoints>
|
||||
{% for datapoint in datapoints %}
|
||||
<Datapoint>
|
||||
<member>
|
||||
{% if datapoint.sum is not none %}
|
||||
<Sum>{{ datapoint.sum }}</Sum>
|
||||
{% endif %}
|
||||
|
|
@ -302,7 +302,7 @@ GET_METRIC_STATISTICS_TEMPLATE = """<GetMetricStatisticsResponse xmlns="http://m
|
|||
|
||||
<Timestamp>{{ datapoint.timestamp }}</Timestamp>
|
||||
<Unit>{{ datapoint.unit }}</Unit>
|
||||
</Datapoint>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Datapoints>
|
||||
</GetMetricStatisticsResult>
|
||||
|
|
|
|||
|
|
@ -724,7 +724,7 @@ class Table(BaseModel):
|
|||
if idx_col_set.issubset(set(hash_set.attrs)):
|
||||
yield hash_set
|
||||
|
||||
def scan(self, filters, limit, exclusive_start_key, filter_expression=None, index_name=None):
|
||||
def scan(self, filters, limit, exclusive_start_key, filter_expression=None, index_name=None, projection_expression=None):
|
||||
results = []
|
||||
scanned_count = 0
|
||||
all_indexes = self.all_indexes()
|
||||
|
|
@ -763,6 +763,14 @@ class Table(BaseModel):
|
|||
if passes_all_conditions:
|
||||
results.append(item)
|
||||
|
||||
if projection_expression:
|
||||
expressions = [x.strip() for x in projection_expression.split(',')]
|
||||
results = copy.deepcopy(results)
|
||||
for result in results:
|
||||
for attr in list(result.attrs):
|
||||
if attr not in expressions:
|
||||
result.attrs.pop(attr)
|
||||
|
||||
results, last_evaluated_key = self._trim_results(results, limit,
|
||||
exclusive_start_key, index_name)
|
||||
return results, scanned_count, last_evaluated_key
|
||||
|
|
@ -962,7 +970,7 @@ class DynamoDBBackend(BaseBackend):
|
|||
return table.query(hash_key, range_comparison, range_values, limit,
|
||||
exclusive_start_key, scan_index_forward, projection_expression, index_name, filter_expression, **filter_kwargs)
|
||||
|
||||
def scan(self, table_name, filters, limit, exclusive_start_key, filter_expression, expr_names, expr_values, index_name):
|
||||
def scan(self, table_name, filters, limit, exclusive_start_key, filter_expression, expr_names, expr_values, index_name, projection_expression):
|
||||
table = self.tables.get(table_name)
|
||||
if not table:
|
||||
return None, None, None
|
||||
|
|
@ -977,7 +985,9 @@ class DynamoDBBackend(BaseBackend):
|
|||
else:
|
||||
filter_expression = Op(None, None) # Will always eval to true
|
||||
|
||||
return table.scan(scan_filters, limit, exclusive_start_key, filter_expression, index_name)
|
||||
projection_expression = ','.join([expr_names.get(attr, attr) for attr in projection_expression.replace(' ', '').split(',')])
|
||||
|
||||
return table.scan(scan_filters, limit, exclusive_start_key, filter_expression, index_name, projection_expression)
|
||||
|
||||
def update_item(self, table_name, key, update_expression, attribute_updates, expression_attribute_names,
|
||||
expression_attribute_values, expected=None):
|
||||
|
|
|
|||
|
|
@ -166,7 +166,7 @@ class DynamoHandler(BaseResponse):
|
|||
when BillingMode is PAY_PER_REQUEST')
|
||||
throughput = None
|
||||
else: # Provisioned (default billing mode)
|
||||
throughput = body["ProvisionedThroughput"]
|
||||
throughput = body.get("ProvisionedThroughput")
|
||||
# getting the schema
|
||||
key_schema = body['KeySchema']
|
||||
# getting attribute definition
|
||||
|
|
@ -558,7 +558,7 @@ class DynamoHandler(BaseResponse):
|
|||
filter_expression = self.body.get('FilterExpression')
|
||||
expression_attribute_values = self.body.get('ExpressionAttributeValues', {})
|
||||
expression_attribute_names = self.body.get('ExpressionAttributeNames', {})
|
||||
|
||||
projection_expression = self.body.get('ProjectionExpression', '')
|
||||
exclusive_start_key = self.body.get('ExclusiveStartKey')
|
||||
limit = self.body.get("Limit")
|
||||
index_name = self.body.get('IndexName')
|
||||
|
|
@ -570,7 +570,8 @@ class DynamoHandler(BaseResponse):
|
|||
filter_expression,
|
||||
expression_attribute_names,
|
||||
expression_attribute_values,
|
||||
index_name)
|
||||
index_name,
|
||||
projection_expression)
|
||||
except InvalidIndexNameError as err:
|
||||
er = 'com.amazonaws.dynamodb.v20111205#ValidationException'
|
||||
return self.error(er, str(err))
|
||||
|
|
|
|||
|
|
@ -332,6 +332,15 @@ class InvalidParameterValueErrorTagNull(EC2ClientError):
|
|||
"Tag value cannot be null. Use empty string instead.")
|
||||
|
||||
|
||||
class InvalidParameterValueErrorUnknownAttribute(EC2ClientError):
|
||||
|
||||
def __init__(self, parameter_value):
|
||||
super(InvalidParameterValueErrorUnknownAttribute, self).__init__(
|
||||
"InvalidParameterValue",
|
||||
"Value ({0}) for parameter attribute is invalid. Unknown attribute."
|
||||
.format(parameter_value))
|
||||
|
||||
|
||||
class InvalidInternetGatewayIdError(EC2ClientError):
|
||||
|
||||
def __init__(self, internet_gateway_id):
|
||||
|
|
|
|||
|
|
@ -54,6 +54,7 @@ from .exceptions import (
|
|||
InvalidNetworkInterfaceIdError,
|
||||
InvalidParameterValueError,
|
||||
InvalidParameterValueErrorTagNull,
|
||||
InvalidParameterValueErrorUnknownAttribute,
|
||||
InvalidPermissionNotFoundError,
|
||||
InvalidPermissionDuplicateError,
|
||||
InvalidRouteTableIdError,
|
||||
|
|
@ -383,6 +384,10 @@ class NetworkInterfaceBackend(object):
|
|||
|
||||
|
||||
class Instance(TaggedEC2Resource, BotoInstance):
|
||||
VALID_ATTRIBUTES = {'instanceType', 'kernel', 'ramdisk', 'userData', 'disableApiTermination',
|
||||
'instanceInitiatedShutdownBehavior', 'rootDeviceName', 'blockDeviceMapping',
|
||||
'productCodes', 'sourceDestCheck', 'groupSet', 'ebsOptimized', 'sriovNetSupport'}
|
||||
|
||||
def __init__(self, ec2_backend, image_id, user_data, security_groups, **kwargs):
|
||||
super(Instance, self).__init__()
|
||||
self.ec2_backend = ec2_backend
|
||||
|
|
@ -405,6 +410,8 @@ class Instance(TaggedEC2Resource, BotoInstance):
|
|||
self.launch_time = utc_date_and_time()
|
||||
self.ami_launch_index = kwargs.get("ami_launch_index", 0)
|
||||
self.disable_api_termination = kwargs.get("disable_api_termination", False)
|
||||
self.instance_initiated_shutdown_behavior = kwargs.get("instance_initiated_shutdown_behavior", "stop")
|
||||
self.sriov_net_support = "simple"
|
||||
self._spot_fleet_id = kwargs.get("spot_fleet_id", None)
|
||||
associate_public_ip = kwargs.get("associate_public_ip", False)
|
||||
if in_ec2_classic:
|
||||
|
|
@ -788,14 +795,22 @@ class InstanceBackend(object):
|
|||
setattr(instance, key, value)
|
||||
return instance
|
||||
|
||||
def modify_instance_security_groups(self, instance_id, new_group_list):
|
||||
def modify_instance_security_groups(self, instance_id, new_group_id_list):
|
||||
instance = self.get_instance(instance_id)
|
||||
new_group_list = []
|
||||
for new_group_id in new_group_id_list:
|
||||
new_group_list.append(self.get_security_group_from_id(new_group_id))
|
||||
setattr(instance, 'security_groups', new_group_list)
|
||||
return instance
|
||||
|
||||
def describe_instance_attribute(self, instance_id, key):
|
||||
if key == 'group_set':
|
||||
def describe_instance_attribute(self, instance_id, attribute):
|
||||
if attribute not in Instance.VALID_ATTRIBUTES:
|
||||
raise InvalidParameterValueErrorUnknownAttribute(attribute)
|
||||
|
||||
if attribute == 'groupSet':
|
||||
key = 'security_groups'
|
||||
else:
|
||||
key = camelcase_to_underscores(attribute)
|
||||
instance = self.get_instance(instance_id)
|
||||
value = getattr(instance, key)
|
||||
return instance, value
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ class InstanceResponse(BaseResponse):
|
|||
associate_public_ip = self._get_param('AssociatePublicIpAddress')
|
||||
key_name = self._get_param('KeyName')
|
||||
ebs_optimized = self._get_param('EbsOptimized')
|
||||
instance_initiated_shutdown_behavior = self._get_param("InstanceInitiatedShutdownBehavior")
|
||||
tags = self._parse_tag_specification("TagSpecification")
|
||||
region_name = self.region
|
||||
|
||||
|
|
@ -55,7 +56,7 @@ class InstanceResponse(BaseResponse):
|
|||
instance_type=instance_type, placement=placement, region_name=region_name, subnet_id=subnet_id,
|
||||
owner_id=owner_id, key_name=key_name, security_group_ids=security_group_ids,
|
||||
nics=nics, private_ip=private_ip, associate_public_ip=associate_public_ip,
|
||||
tags=tags, ebs_optimized=ebs_optimized)
|
||||
tags=tags, ebs_optimized=ebs_optimized, instance_initiated_shutdown_behavior=instance_initiated_shutdown_behavior)
|
||||
|
||||
template = self.response_template(EC2_RUN_INSTANCES)
|
||||
return template.render(reservation=new_reservation)
|
||||
|
|
@ -113,12 +114,11 @@ class InstanceResponse(BaseResponse):
|
|||
# TODO this and modify below should raise IncorrectInstanceState if
|
||||
# instance not in stopped state
|
||||
attribute = self._get_param('Attribute')
|
||||
key = camelcase_to_underscores(attribute)
|
||||
instance_id = self._get_param('InstanceId')
|
||||
instance, value = self.ec2_backend.describe_instance_attribute(
|
||||
instance_id, key)
|
||||
instance_id, attribute)
|
||||
|
||||
if key == "group_set":
|
||||
if attribute == "groupSet":
|
||||
template = self.response_template(
|
||||
EC2_DESCRIBE_INSTANCE_GROUPSET_ATTRIBUTE)
|
||||
else:
|
||||
|
|
@ -597,7 +597,9 @@ EC2_DESCRIBE_INSTANCE_ATTRIBUTE = """<DescribeInstanceAttributeResponse xmlns="h
|
|||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<instanceId>{{ instance.id }}</instanceId>
|
||||
<{{ attribute }}>
|
||||
{% if value is not none %}
|
||||
<value>{{ value }}</value>
|
||||
{% endif %}
|
||||
</{{ attribute }}>
|
||||
</DescribeInstanceAttributeResponse>"""
|
||||
|
||||
|
|
@ -605,9 +607,9 @@ EC2_DESCRIBE_INSTANCE_GROUPSET_ATTRIBUTE = """<DescribeInstanceAttributeResponse
|
|||
<requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
|
||||
<instanceId>{{ instance.id }}</instanceId>
|
||||
<{{ attribute }}>
|
||||
{% for sg_id in value %}
|
||||
{% for sg in value %}
|
||||
<item>
|
||||
<groupId>{{ sg_id }}</groupId>
|
||||
<groupId>{{ sg.id }}</groupId>
|
||||
</item>
|
||||
{% endfor %}
|
||||
</{{ attribute }}>
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ from __future__ import unicode_literals
|
|||
import hashlib
|
||||
import re
|
||||
from copy import copy
|
||||
from datetime import datetime
|
||||
from random import random
|
||||
|
||||
from botocore.exceptions import ParamValidationError
|
||||
|
|
@ -106,7 +107,7 @@ class Image(BaseObject):
|
|||
self.repository = repository
|
||||
self.registry_id = registry_id
|
||||
self.image_digest = digest
|
||||
self.image_pushed_at = None
|
||||
self.image_pushed_at = str(datetime.utcnow().isoformat())
|
||||
|
||||
def _create_digest(self):
|
||||
image_contents = 'docker_image{0}'.format(int(random() * 10 ** 6))
|
||||
|
|
@ -158,7 +159,7 @@ class Image(BaseObject):
|
|||
response_object['repositoryName'] = self.repository
|
||||
response_object['registryId'] = self.registry_id
|
||||
response_object['imageSizeInBytes'] = self.image_size_in_bytes
|
||||
response_object['imagePushedAt'] = '2017-05-09'
|
||||
response_object['imagePushedAt'] = self.image_pushed_at
|
||||
return {k: v for k, v in response_object.items() if v is not None and v != []}
|
||||
|
||||
@property
|
||||
|
|
@ -402,7 +403,10 @@ class ECRBackend(BaseBackend):
|
|||
image_found = True
|
||||
repository.images[num].image_tag = image_id["imageTag"]
|
||||
response["imageIds"].append(image.response_batch_delete_image)
|
||||
repository.images[num].remove_tag(image_id["imageTag"])
|
||||
if len(image.image_tags) > 1:
|
||||
repository.images[num].remove_tag(image_id["imageTag"])
|
||||
else:
|
||||
repository.images.remove(image)
|
||||
|
||||
if not image_found:
|
||||
failure_response = {
|
||||
|
|
|
|||
|
|
@ -138,6 +138,12 @@ class FakeTable(BaseModel):
|
|||
raise PartitionAlreadyExistsException()
|
||||
self.partitions[key] = partition
|
||||
|
||||
def delete_partition(self, values):
|
||||
try:
|
||||
del self.partitions[str(values)]
|
||||
except KeyError:
|
||||
raise PartitionNotFoundException()
|
||||
|
||||
|
||||
class FakePartition(BaseModel):
|
||||
def __init__(self, database_name, table_name, partiton_input):
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@ import json
|
|||
|
||||
from moto.core.responses import BaseResponse
|
||||
from .models import glue_backend
|
||||
from .exceptions import (
|
||||
PartitionAlreadyExistsException,
|
||||
PartitionNotFoundException,
|
||||
TableNotFoundException
|
||||
)
|
||||
|
||||
|
||||
class GlueResponse(BaseResponse):
|
||||
|
|
@ -90,6 +95,28 @@ class GlueResponse(BaseResponse):
|
|||
resp = self.glue_backend.delete_table(database_name, table_name)
|
||||
return json.dumps(resp)
|
||||
|
||||
def batch_delete_table(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
|
||||
errors = []
|
||||
for table_name in self.parameters.get('TablesToDelete'):
|
||||
try:
|
||||
self.glue_backend.delete_table(database_name, table_name)
|
||||
except TableNotFoundException:
|
||||
errors.append({
|
||||
"TableName": table_name,
|
||||
"ErrorDetail": {
|
||||
"ErrorCode": "EntityNotFoundException",
|
||||
"ErrorMessage": "Table not found"
|
||||
}
|
||||
})
|
||||
|
||||
out = {}
|
||||
if errors:
|
||||
out["Errors"] = errors
|
||||
|
||||
return json.dumps(out)
|
||||
|
||||
def get_partitions(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_name = self.parameters.get('TableName')
|
||||
|
|
@ -124,6 +151,30 @@ class GlueResponse(BaseResponse):
|
|||
|
||||
return ""
|
||||
|
||||
def batch_create_partition(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_name = self.parameters.get('TableName')
|
||||
table = self.glue_backend.get_table(database_name, table_name)
|
||||
|
||||
errors_output = []
|
||||
for part_input in self.parameters.get('PartitionInputList'):
|
||||
try:
|
||||
table.create_partition(part_input)
|
||||
except PartitionAlreadyExistsException:
|
||||
errors_output.append({
|
||||
'PartitionValues': part_input['Values'],
|
||||
'ErrorDetail': {
|
||||
'ErrorCode': 'AlreadyExistsException',
|
||||
'ErrorMessage': 'Partition already exists.'
|
||||
}
|
||||
})
|
||||
|
||||
out = {}
|
||||
if errors_output:
|
||||
out["Errors"] = errors_output
|
||||
|
||||
return json.dumps(out)
|
||||
|
||||
def update_partition(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_name = self.parameters.get('TableName')
|
||||
|
|
@ -134,3 +185,38 @@ class GlueResponse(BaseResponse):
|
|||
table.update_partition(part_to_update, part_input)
|
||||
|
||||
return ""
|
||||
|
||||
def delete_partition(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_name = self.parameters.get('TableName')
|
||||
part_to_delete = self.parameters.get('PartitionValues')
|
||||
|
||||
table = self.glue_backend.get_table(database_name, table_name)
|
||||
table.delete_partition(part_to_delete)
|
||||
|
||||
return ""
|
||||
|
||||
def batch_delete_partition(self):
|
||||
database_name = self.parameters.get('DatabaseName')
|
||||
table_name = self.parameters.get('TableName')
|
||||
table = self.glue_backend.get_table(database_name, table_name)
|
||||
|
||||
errors_output = []
|
||||
for part_input in self.parameters.get('PartitionsToDelete'):
|
||||
values = part_input.get('Values')
|
||||
try:
|
||||
table.delete_partition(values)
|
||||
except PartitionNotFoundException:
|
||||
errors_output.append({
|
||||
'PartitionValues': values,
|
||||
'ErrorDetail': {
|
||||
'ErrorCode': 'EntityNotFoundException',
|
||||
'ErrorMessage': 'Partition not found',
|
||||
}
|
||||
})
|
||||
|
||||
out = {}
|
||||
if errors_output:
|
||||
out['Errors'] = errors_output
|
||||
|
||||
return json.dumps(out)
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -26,6 +26,14 @@ class IAMReportNotPresentException(RESTError):
|
|||
"ReportNotPresent", message)
|
||||
|
||||
|
||||
class IAMLimitExceededException(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message):
|
||||
super(IAMLimitExceededException, self).__init__(
|
||||
"LimitExceeded", message)
|
||||
|
||||
|
||||
class MalformedCertificate(RESTError):
|
||||
code = 400
|
||||
|
||||
|
|
@ -34,6 +42,14 @@ class MalformedCertificate(RESTError):
|
|||
'MalformedCertificate', 'Certificate {cert} is malformed'.format(cert=cert))
|
||||
|
||||
|
||||
class MalformedPolicyDocument(RESTError):
|
||||
code = 400
|
||||
|
||||
def __init__(self, message=""):
|
||||
super(MalformedPolicyDocument, self).__init__(
|
||||
'MalformedPolicyDocument', message)
|
||||
|
||||
|
||||
class DuplicateTags(RESTError):
|
||||
code = 400
|
||||
|
||||
|
|
|
|||
|
|
@ -8,14 +8,14 @@ import re
|
|||
from cryptography import x509
|
||||
from cryptography.hazmat.backends import default_backend
|
||||
|
||||
import pytz
|
||||
from moto.core.exceptions import RESTError
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.utils import iso_8601_datetime_without_milliseconds
|
||||
from moto.core.utils import iso_8601_datetime_without_milliseconds, iso_8601_datetime_with_milliseconds
|
||||
from moto.iam.policy_validation import IAMPolicyDocumentValidator
|
||||
|
||||
from .aws_managed_policies import aws_managed_policies_data
|
||||
from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, MalformedCertificate, \
|
||||
DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig
|
||||
from .exceptions import IAMNotFoundException, IAMConflictException, IAMReportNotPresentException, IAMLimitExceededException, \
|
||||
MalformedCertificate, DuplicateTags, TagKeyTooBig, InvalidTagCharacters, TooManyTags, TagValueTooBig
|
||||
from .utils import random_access_key, random_alphanumeric, random_resource_id, random_policy_id
|
||||
|
||||
ACCOUNT_ID = 123456789012
|
||||
|
|
@ -28,11 +28,15 @@ class MFADevice(object):
|
|||
serial_number,
|
||||
authentication_code_1,
|
||||
authentication_code_2):
|
||||
self.enable_date = datetime.now(pytz.utc)
|
||||
self.enable_date = datetime.utcnow()
|
||||
self.serial_number = serial_number
|
||||
self.authentication_code_1 = authentication_code_1
|
||||
self.authentication_code_2 = authentication_code_2
|
||||
|
||||
@property
|
||||
def enabled_iso_8601(self):
|
||||
return iso_8601_datetime_without_milliseconds(self.enable_date)
|
||||
|
||||
|
||||
class Policy(BaseModel):
|
||||
is_attachable = False
|
||||
|
|
@ -42,7 +46,9 @@ class Policy(BaseModel):
|
|||
default_version_id=None,
|
||||
description=None,
|
||||
document=None,
|
||||
path=None):
|
||||
path=None,
|
||||
create_date=None,
|
||||
update_date=None):
|
||||
self.name = name
|
||||
|
||||
self.attachment_count = 0
|
||||
|
|
@ -56,10 +62,25 @@ class Policy(BaseModel):
|
|||
else:
|
||||
self.default_version_id = 'v1'
|
||||
self.next_version_num = 2
|
||||
self.versions = [PolicyVersion(self.arn, document, True)]
|
||||
self.versions = [PolicyVersion(self.arn, document, True, self.default_version_id, update_date)]
|
||||
|
||||
self.create_datetime = datetime.now(pytz.utc)
|
||||
self.update_datetime = datetime.now(pytz.utc)
|
||||
self.create_date = create_date if create_date is not None else datetime.utcnow()
|
||||
self.update_date = update_date if update_date is not None else datetime.utcnow()
|
||||
|
||||
def update_default_version(self, new_default_version_id):
|
||||
for version in self.versions:
|
||||
if version.version_id == self.default_version_id:
|
||||
version.is_default = False
|
||||
break
|
||||
self.default_version_id = new_default_version_id
|
||||
|
||||
@property
|
||||
def created_iso_8601(self):
|
||||
return iso_8601_datetime_with_milliseconds(self.create_date)
|
||||
|
||||
@property
|
||||
def updated_iso_8601(self):
|
||||
return iso_8601_datetime_with_milliseconds(self.update_date)
|
||||
|
||||
|
||||
class SAMLProvider(BaseModel):
|
||||
|
|
@ -77,13 +98,19 @@ class PolicyVersion(object):
|
|||
def __init__(self,
|
||||
policy_arn,
|
||||
document,
|
||||
is_default=False):
|
||||
is_default=False,
|
||||
version_id='v1',
|
||||
create_date=None):
|
||||
self.policy_arn = policy_arn
|
||||
self.document = document or {}
|
||||
self.is_default = is_default
|
||||
self.version_id = 'v1'
|
||||
self.version_id = version_id
|
||||
|
||||
self.create_datetime = datetime.now(pytz.utc)
|
||||
self.create_date = create_date if create_date is not None else datetime.utcnow()
|
||||
|
||||
@property
|
||||
def created_iso_8601(self):
|
||||
return iso_8601_datetime_with_milliseconds(self.create_date)
|
||||
|
||||
|
||||
class ManagedPolicy(Policy):
|
||||
|
|
@ -112,7 +139,9 @@ class AWSManagedPolicy(ManagedPolicy):
|
|||
return cls(name,
|
||||
default_version_id=data.get('DefaultVersionId'),
|
||||
path=data.get('Path'),
|
||||
document=data.get('Document'))
|
||||
document=json.dumps(data.get('Document')),
|
||||
create_date=datetime.strptime(data.get('CreateDate'), "%Y-%m-%dT%H:%M:%S+00:00"),
|
||||
update_date=datetime.strptime(data.get('UpdateDate'), "%Y-%m-%dT%H:%M:%S+00:00"))
|
||||
|
||||
@property
|
||||
def arn(self):
|
||||
|
|
@ -139,11 +168,15 @@ class Role(BaseModel):
|
|||
self.path = path or '/'
|
||||
self.policies = {}
|
||||
self.managed_policies = {}
|
||||
self.create_date = datetime.now(pytz.utc)
|
||||
self.create_date = datetime.utcnow()
|
||||
self.tags = {}
|
||||
self.description = ""
|
||||
self.permissions_boundary = permissions_boundary
|
||||
|
||||
@property
|
||||
def created_iso_8601(self):
|
||||
return iso_8601_datetime_with_milliseconds(self.create_date)
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
properties = cloudformation_json['Properties']
|
||||
|
|
@ -198,7 +231,11 @@ class InstanceProfile(BaseModel):
|
|||
self.name = name
|
||||
self.path = path or '/'
|
||||
self.roles = roles if roles else []
|
||||
self.create_date = datetime.now(pytz.utc)
|
||||
self.create_date = datetime.utcnow()
|
||||
|
||||
@property
|
||||
def created_iso_8601(self):
|
||||
return iso_8601_datetime_with_milliseconds(self.create_date)
|
||||
|
||||
@classmethod
|
||||
def create_from_cloudformation_json(cls, resource_name, cloudformation_json, region_name):
|
||||
|
|
@ -250,25 +287,31 @@ class SigningCertificate(BaseModel):
|
|||
self.id = id
|
||||
self.user_name = user_name
|
||||
self.body = body
|
||||
self.upload_date = datetime.strftime(datetime.utcnow(), "%Y-%m-%d-%H-%M-%S")
|
||||
self.upload_date = datetime.utcnow()
|
||||
self.status = 'Active'
|
||||
|
||||
@property
|
||||
def uploaded_iso_8601(self):
|
||||
return iso_8601_datetime_without_milliseconds(self.upload_date)
|
||||
|
||||
|
||||
class AccessKey(BaseModel):
|
||||
|
||||
def __init__(self, user_name):
|
||||
self.user_name = user_name
|
||||
self.access_key_id = random_access_key()
|
||||
self.secret_access_key = random_alphanumeric(32)
|
||||
self.access_key_id = "AKIA" + random_access_key()
|
||||
self.secret_access_key = random_alphanumeric(40)
|
||||
self.status = 'Active'
|
||||
self.create_date = datetime.strftime(
|
||||
datetime.utcnow(),
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
self.last_used = datetime.strftime(
|
||||
datetime.utcnow(),
|
||||
"%Y-%m-%dT%H:%M:%SZ"
|
||||
)
|
||||
self.create_date = datetime.utcnow()
|
||||
self.last_used = datetime.utcnow()
|
||||
|
||||
@property
|
||||
def created_iso_8601(self):
|
||||
return iso_8601_datetime_without_milliseconds(self.create_date)
|
||||
|
||||
@property
|
||||
def last_used_iso_8601(self):
|
||||
return iso_8601_datetime_without_milliseconds(self.last_used)
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
|
|
@ -283,15 +326,16 @@ class Group(BaseModel):
|
|||
self.name = name
|
||||
self.id = random_resource_id()
|
||||
self.path = path
|
||||
self.created = datetime.strftime(
|
||||
datetime.utcnow(),
|
||||
"%Y-%m-%d-%H-%M-%S"
|
||||
)
|
||||
self.create_date = datetime.utcnow()
|
||||
|
||||
self.users = []
|
||||
self.managed_policies = {}
|
||||
self.policies = {}
|
||||
|
||||
@property
|
||||
def created_iso_8601(self):
|
||||
return iso_8601_datetime_with_milliseconds(self.create_date)
|
||||
|
||||
def get_cfn_attribute(self, attribute_name):
|
||||
from moto.cloudformation.exceptions import UnformattedGetAttTemplateException
|
||||
if attribute_name == 'Arn':
|
||||
|
|
@ -306,10 +350,6 @@ class Group(BaseModel):
|
|||
else:
|
||||
return "arn:aws:iam::{0}:group/{1}/{2}".format(ACCOUNT_ID, self.path, self.name)
|
||||
|
||||
@property
|
||||
def create_date(self):
|
||||
return self.created
|
||||
|
||||
def get_policy(self, policy_name):
|
||||
try:
|
||||
policy_json = self.policies[policy_name]
|
||||
|
|
@ -335,7 +375,7 @@ class User(BaseModel):
|
|||
self.name = name
|
||||
self.id = random_resource_id()
|
||||
self.path = path if path else "/"
|
||||
self.created = datetime.utcnow()
|
||||
self.create_date = datetime.utcnow()
|
||||
self.mfa_devices = {}
|
||||
self.policies = {}
|
||||
self.managed_policies = {}
|
||||
|
|
@ -350,7 +390,7 @@ class User(BaseModel):
|
|||
|
||||
@property
|
||||
def created_iso_8601(self):
|
||||
return iso_8601_datetime_without_milliseconds(self.created)
|
||||
return iso_8601_datetime_with_milliseconds(self.create_date)
|
||||
|
||||
def get_policy(self, policy_name):
|
||||
policy_json = None
|
||||
|
|
@ -421,7 +461,7 @@ class User(BaseModel):
|
|||
|
||||
def to_csv(self):
|
||||
date_format = '%Y-%m-%dT%H:%M:%S+00:00'
|
||||
date_created = self.created
|
||||
date_created = self.create_date
|
||||
# aagrawal,arn:aws:iam::509284790694:user/aagrawal,2014-09-01T22:28:48+00:00,true,2014-11-12T23:36:49+00:00,2014-09-03T18:59:00+00:00,N/A,false,true,2014-09-01T22:28:48+00:00,false,N/A,false,N/A,false,N/A
|
||||
if not self.password:
|
||||
password_enabled = 'false'
|
||||
|
|
@ -478,7 +518,7 @@ class IAMBackend(BaseBackend):
|
|||
super(IAMBackend, self).__init__()
|
||||
|
||||
def _init_managed_policies(self):
|
||||
return dict((p.name, p) for p in aws_managed_policies)
|
||||
return dict((p.arn, p) for p in aws_managed_policies)
|
||||
|
||||
def attach_role_policy(self, policy_arn, role_name):
|
||||
arns = dict((p.arn, p) for p in self.managed_policies.values())
|
||||
|
|
@ -536,6 +576,9 @@ class IAMBackend(BaseBackend):
|
|||
policy.detach_from(self.get_user(user_name))
|
||||
|
||||
def create_policy(self, description, path, policy_document, policy_name):
|
||||
iam_policy_document_validator = IAMPolicyDocumentValidator(policy_document)
|
||||
iam_policy_document_validator.validate()
|
||||
|
||||
policy = ManagedPolicy(
|
||||
policy_name,
|
||||
description=description,
|
||||
|
|
@ -628,6 +671,9 @@ class IAMBackend(BaseBackend):
|
|||
|
||||
def put_role_policy(self, role_name, policy_name, policy_json):
|
||||
role = self.get_role(role_name)
|
||||
|
||||
iam_policy_document_validator = IAMPolicyDocumentValidator(policy_json)
|
||||
iam_policy_document_validator.validate()
|
||||
role.put_policy(policy_name, policy_json)
|
||||
|
||||
def delete_role_policy(self, role_name, policy_name):
|
||||
|
|
@ -639,6 +685,7 @@ class IAMBackend(BaseBackend):
|
|||
for p, d in role.policies.items():
|
||||
if p == policy_name:
|
||||
return p, d
|
||||
raise IAMNotFoundException("Policy Document {0} not attached to role {1}".format(policy_name, role_name))
|
||||
|
||||
def list_role_policies(self, role_name):
|
||||
role = self.get_role(role_name)
|
||||
|
|
@ -725,15 +772,21 @@ class IAMBackend(BaseBackend):
|
|||
role.tags.pop(ref_key, None)
|
||||
|
||||
def create_policy_version(self, policy_arn, policy_document, set_as_default):
|
||||
iam_policy_document_validator = IAMPolicyDocumentValidator(policy_document)
|
||||
iam_policy_document_validator.validate()
|
||||
|
||||
policy = self.get_policy(policy_arn)
|
||||
if not policy:
|
||||
raise IAMNotFoundException("Policy not found")
|
||||
if len(policy.versions) >= 5:
|
||||
raise IAMLimitExceededException("A managed policy can have up to 5 versions. Before you create a new version, you must delete an existing version.")
|
||||
set_as_default = (set_as_default == "true") # convert it to python bool
|
||||
version = PolicyVersion(policy_arn, policy_document, set_as_default)
|
||||
policy.versions.append(version)
|
||||
version.version_id = 'v{0}'.format(policy.next_version_num)
|
||||
policy.next_version_num += 1
|
||||
if set_as_default:
|
||||
policy.default_version_id = version.version_id
|
||||
policy.update_default_version(version.version_id)
|
||||
return version
|
||||
|
||||
def get_policy_version(self, policy_arn, version_id):
|
||||
|
|
@ -756,8 +809,8 @@ class IAMBackend(BaseBackend):
|
|||
if not policy:
|
||||
raise IAMNotFoundException("Policy not found")
|
||||
if version_id == policy.default_version_id:
|
||||
raise IAMConflictException(
|
||||
"Cannot delete the default version of a policy")
|
||||
raise IAMConflictException(code="DeleteConflict",
|
||||
message="Cannot delete the default version of a policy.")
|
||||
for i, v in enumerate(policy.versions):
|
||||
if v.version_id == version_id:
|
||||
del policy.versions[i]
|
||||
|
|
@ -869,6 +922,9 @@ class IAMBackend(BaseBackend):
|
|||
|
||||
def put_group_policy(self, group_name, policy_name, policy_json):
|
||||
group = self.get_group(group_name)
|
||||
|
||||
iam_policy_document_validator = IAMPolicyDocumentValidator(policy_json)
|
||||
iam_policy_document_validator.validate()
|
||||
group.put_policy(policy_name, policy_json)
|
||||
|
||||
def list_group_policies(self, group_name, marker=None, max_items=None):
|
||||
|
|
@ -1029,6 +1085,9 @@ class IAMBackend(BaseBackend):
|
|||
|
||||
def put_user_policy(self, user_name, policy_name, policy_json):
|
||||
user = self.get_user(user_name)
|
||||
|
||||
iam_policy_document_validator = IAMPolicyDocumentValidator(policy_json)
|
||||
iam_policy_document_validator.validate()
|
||||
user.put_policy(policy_name, policy_json)
|
||||
|
||||
def delete_user_policy(self, user_name, policy_name):
|
||||
|
|
@ -1050,7 +1109,7 @@ class IAMBackend(BaseBackend):
|
|||
if key.access_key_id == access_key_id:
|
||||
return {
|
||||
'user_name': key.user_name,
|
||||
'last_used': key.last_used
|
||||
'last_used': key.last_used_iso_8601,
|
||||
}
|
||||
else:
|
||||
raise IAMNotFoundException(
|
||||
|
|
|
|||
450
moto/iam/policy_validation.py
Normal file
450
moto/iam/policy_validation.py
Normal file
|
|
@ -0,0 +1,450 @@
|
|||
import json
|
||||
import re
|
||||
|
||||
from six import string_types
|
||||
|
||||
from moto.iam.exceptions import MalformedPolicyDocument
|
||||
|
||||
|
||||
VALID_TOP_ELEMENTS = [
|
||||
"Version",
|
||||
"Id",
|
||||
"Statement",
|
||||
"Conditions"
|
||||
]
|
||||
|
||||
VALID_VERSIONS = [
|
||||
"2008-10-17",
|
||||
"2012-10-17"
|
||||
]
|
||||
|
||||
VALID_STATEMENT_ELEMENTS = [
|
||||
"Sid",
|
||||
"Action",
|
||||
"NotAction",
|
||||
"Resource",
|
||||
"NotResource",
|
||||
"Effect",
|
||||
"Condition"
|
||||
]
|
||||
|
||||
VALID_EFFECTS = [
|
||||
"Allow",
|
||||
"Deny"
|
||||
]
|
||||
|
||||
VALID_CONDITIONS = [
|
||||
"StringEquals",
|
||||
"StringNotEquals",
|
||||
"StringEqualsIgnoreCase",
|
||||
"StringNotEqualsIgnoreCase",
|
||||
"StringLike",
|
||||
"StringNotLike",
|
||||
"NumericEquals",
|
||||
"NumericNotEquals",
|
||||
"NumericLessThan",
|
||||
"NumericLessThanEquals",
|
||||
"NumericGreaterThan",
|
||||
"NumericGreaterThanEquals",
|
||||
"DateEquals",
|
||||
"DateNotEquals",
|
||||
"DateLessThan",
|
||||
"DateLessThanEquals",
|
||||
"DateGreaterThan",
|
||||
"DateGreaterThanEquals",
|
||||
"Bool",
|
||||
"BinaryEquals",
|
||||
"IpAddress",
|
||||
"NotIpAddress",
|
||||
"ArnEquals",
|
||||
"ArnLike",
|
||||
"ArnNotEquals",
|
||||
"ArnNotLike",
|
||||
"Null"
|
||||
]
|
||||
|
||||
VALID_CONDITION_PREFIXES = [
|
||||
"ForAnyValue:",
|
||||
"ForAllValues:"
|
||||
]
|
||||
|
||||
VALID_CONDITION_POSTFIXES = [
|
||||
"IfExists"
|
||||
]
|
||||
|
||||
SERVICE_TYPE_REGION_INFORMATION_ERROR_ASSOCIATIONS = {
|
||||
"iam": 'IAM resource {resource} cannot contain region information.',
|
||||
"s3": 'Resource {resource} can not contain region information.'
|
||||
}
|
||||
|
||||
VALID_RESOURCE_PATH_STARTING_VALUES = {
|
||||
"iam": {
|
||||
"values": ["user/", "federated-user/", "role/", "group/", "instance-profile/", "mfa/", "server-certificate/",
|
||||
"policy/", "sms-mfa/", "saml-provider/", "oidc-provider/", "report/", "access-report/"],
|
||||
"error_message": 'IAM resource path must either be "*" or start with {values}.'
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class IAMPolicyDocumentValidator:
|
||||
|
||||
def __init__(self, policy_document):
|
||||
self._policy_document = policy_document
|
||||
self._policy_json = {}
|
||||
self._statements = []
|
||||
self._resource_error = "" # the first resource error found that does not generate a legacy parsing error
|
||||
|
||||
def validate(self):
|
||||
try:
|
||||
self._validate_syntax()
|
||||
except Exception:
|
||||
raise MalformedPolicyDocument("Syntax errors in policy.")
|
||||
try:
|
||||
self._validate_version()
|
||||
except Exception:
|
||||
raise MalformedPolicyDocument("Policy document must be version 2012-10-17 or greater.")
|
||||
try:
|
||||
self._perform_first_legacy_parsing()
|
||||
self._validate_resources_for_formats()
|
||||
self._validate_not_resources_for_formats()
|
||||
except Exception:
|
||||
raise MalformedPolicyDocument("The policy failed legacy parsing")
|
||||
try:
|
||||
self._validate_sid_uniqueness()
|
||||
except Exception:
|
||||
raise MalformedPolicyDocument("Statement IDs (SID) in a single policy must be unique.")
|
||||
try:
|
||||
self._validate_action_like_exist()
|
||||
except Exception:
|
||||
raise MalformedPolicyDocument("Policy statement must contain actions.")
|
||||
try:
|
||||
self._validate_resource_exist()
|
||||
except Exception:
|
||||
raise MalformedPolicyDocument("Policy statement must contain resources.")
|
||||
|
||||
if self._resource_error != "":
|
||||
raise MalformedPolicyDocument(self._resource_error)
|
||||
|
||||
self._validate_actions_for_prefixes()
|
||||
self._validate_not_actions_for_prefixes()
|
||||
|
||||
def _validate_syntax(self):
|
||||
self._policy_json = json.loads(self._policy_document)
|
||||
assert isinstance(self._policy_json, dict)
|
||||
self._validate_top_elements()
|
||||
self._validate_version_syntax()
|
||||
self._validate_id_syntax()
|
||||
self._validate_statements_syntax()
|
||||
|
||||
def _validate_top_elements(self):
|
||||
top_elements = self._policy_json.keys()
|
||||
for element in top_elements:
|
||||
assert element in VALID_TOP_ELEMENTS
|
||||
|
||||
def _validate_version_syntax(self):
|
||||
if "Version" in self._policy_json:
|
||||
assert self._policy_json["Version"] in VALID_VERSIONS
|
||||
|
||||
def _validate_version(self):
|
||||
assert self._policy_json["Version"] == "2012-10-17"
|
||||
|
||||
def _validate_sid_uniqueness(self):
|
||||
sids = []
|
||||
for statement in self._statements:
|
||||
if "Sid" in statement:
|
||||
assert statement["Sid"] not in sids
|
||||
sids.append(statement["Sid"])
|
||||
|
||||
def _validate_statements_syntax(self):
|
||||
assert "Statement" in self._policy_json
|
||||
assert isinstance(self._policy_json["Statement"], (dict, list))
|
||||
|
||||
if isinstance(self._policy_json["Statement"], dict):
|
||||
self._statements.append(self._policy_json["Statement"])
|
||||
else:
|
||||
self._statements += self._policy_json["Statement"]
|
||||
|
||||
assert self._statements
|
||||
for statement in self._statements:
|
||||
self._validate_statement_syntax(statement)
|
||||
|
||||
@staticmethod
|
||||
def _validate_statement_syntax(statement):
|
||||
assert isinstance(statement, dict)
|
||||
for statement_element in statement.keys():
|
||||
assert statement_element in VALID_STATEMENT_ELEMENTS
|
||||
|
||||
assert ("Resource" not in statement or "NotResource" not in statement)
|
||||
assert ("Action" not in statement or "NotAction" not in statement)
|
||||
|
||||
IAMPolicyDocumentValidator._validate_effect_syntax(statement)
|
||||
IAMPolicyDocumentValidator._validate_action_syntax(statement)
|
||||
IAMPolicyDocumentValidator._validate_not_action_syntax(statement)
|
||||
IAMPolicyDocumentValidator._validate_resource_syntax(statement)
|
||||
IAMPolicyDocumentValidator._validate_not_resource_syntax(statement)
|
||||
IAMPolicyDocumentValidator._validate_condition_syntax(statement)
|
||||
IAMPolicyDocumentValidator._validate_sid_syntax(statement)
|
||||
|
||||
@staticmethod
|
||||
def _validate_effect_syntax(statement):
|
||||
assert "Effect" in statement
|
||||
assert isinstance(statement["Effect"], string_types)
|
||||
assert statement["Effect"].lower() in [allowed_effect.lower() for allowed_effect in VALID_EFFECTS]
|
||||
|
||||
@staticmethod
|
||||
def _validate_action_syntax(statement):
|
||||
IAMPolicyDocumentValidator._validate_string_or_list_of_strings_syntax(statement, "Action")
|
||||
|
||||
@staticmethod
|
||||
def _validate_not_action_syntax(statement):
|
||||
IAMPolicyDocumentValidator._validate_string_or_list_of_strings_syntax(statement, "NotAction")
|
||||
|
||||
@staticmethod
|
||||
def _validate_resource_syntax(statement):
|
||||
IAMPolicyDocumentValidator._validate_string_or_list_of_strings_syntax(statement, "Resource")
|
||||
|
||||
@staticmethod
|
||||
def _validate_not_resource_syntax(statement):
|
||||
IAMPolicyDocumentValidator._validate_string_or_list_of_strings_syntax(statement, "NotResource")
|
||||
|
||||
@staticmethod
|
||||
def _validate_string_or_list_of_strings_syntax(statement, key):
|
||||
if key in statement:
|
||||
assert isinstance(statement[key], (string_types, list))
|
||||
if isinstance(statement[key], list):
|
||||
for resource in statement[key]:
|
||||
assert isinstance(resource, string_types)
|
||||
|
||||
@staticmethod
|
||||
def _validate_condition_syntax(statement):
|
||||
if "Condition" in statement:
|
||||
assert isinstance(statement["Condition"], dict)
|
||||
for condition_key, condition_value in statement["Condition"].items():
|
||||
assert isinstance(condition_value, dict)
|
||||
for condition_element_key, condition_element_value in condition_value.items():
|
||||
assert isinstance(condition_element_value, (list, string_types))
|
||||
|
||||
if IAMPolicyDocumentValidator._strip_condition_key(condition_key) not in VALID_CONDITIONS:
|
||||
assert not condition_value # empty dict
|
||||
|
||||
@staticmethod
|
||||
def _strip_condition_key(condition_key):
|
||||
for valid_prefix in VALID_CONDITION_PREFIXES:
|
||||
if condition_key.startswith(valid_prefix):
|
||||
condition_key = condition_key[len(valid_prefix):]
|
||||
break # strip only the first match
|
||||
|
||||
for valid_postfix in VALID_CONDITION_POSTFIXES:
|
||||
if condition_key.endswith(valid_postfix):
|
||||
condition_key = condition_key[:-len(valid_postfix)]
|
||||
break # strip only the first match
|
||||
|
||||
return condition_key
|
||||
|
||||
@staticmethod
|
||||
def _validate_sid_syntax(statement):
|
||||
if "Sid" in statement:
|
||||
assert isinstance(statement["Sid"], string_types)
|
||||
|
||||
def _validate_id_syntax(self):
|
||||
if "Id" in self._policy_json:
|
||||
assert isinstance(self._policy_json["Id"], string_types)
|
||||
|
||||
def _validate_resource_exist(self):
|
||||
for statement in self._statements:
|
||||
assert ("Resource" in statement or "NotResource" in statement)
|
||||
if "Resource" in statement and isinstance(statement["Resource"], list):
|
||||
assert statement["Resource"]
|
||||
elif "NotResource" in statement and isinstance(statement["NotResource"], list):
|
||||
assert statement["NotResource"]
|
||||
|
||||
def _validate_action_like_exist(self):
|
||||
for statement in self._statements:
|
||||
assert ("Action" in statement or "NotAction" in statement)
|
||||
if "Action" in statement and isinstance(statement["Action"], list):
|
||||
assert statement["Action"]
|
||||
elif "NotAction" in statement and isinstance(statement["NotAction"], list):
|
||||
assert statement["NotAction"]
|
||||
|
||||
def _validate_actions_for_prefixes(self):
|
||||
self._validate_action_like_for_prefixes("Action")
|
||||
|
||||
def _validate_not_actions_for_prefixes(self):
|
||||
self._validate_action_like_for_prefixes("NotAction")
|
||||
|
||||
def _validate_action_like_for_prefixes(self, key):
|
||||
for statement in self._statements:
|
||||
if key in statement:
|
||||
if isinstance(statement[key], string_types):
|
||||
self._validate_action_prefix(statement[key])
|
||||
else:
|
||||
for action in statement[key]:
|
||||
self._validate_action_prefix(action)
|
||||
|
||||
@staticmethod
|
||||
def _validate_action_prefix(action):
|
||||
action_parts = action.split(":")
|
||||
if len(action_parts) == 1 and action_parts[0] != "*":
|
||||
raise MalformedPolicyDocument("Actions/Conditions must be prefaced by a vendor, e.g., iam, sdb, ec2, etc.")
|
||||
elif len(action_parts) > 2:
|
||||
raise MalformedPolicyDocument("Actions/Condition can contain only one colon.")
|
||||
|
||||
vendor_pattern = re.compile(r'[^a-zA-Z0-9\-.]')
|
||||
if action_parts[0] != "*" and vendor_pattern.search(action_parts[0]):
|
||||
raise MalformedPolicyDocument("Vendor {vendor} is not valid".format(vendor=action_parts[0]))
|
||||
|
||||
def _validate_resources_for_formats(self):
|
||||
self._validate_resource_like_for_formats("Resource")
|
||||
|
||||
def _validate_not_resources_for_formats(self):
|
||||
self._validate_resource_like_for_formats("NotResource")
|
||||
|
||||
def _validate_resource_like_for_formats(self, key):
|
||||
for statement in self._statements:
|
||||
if key in statement:
|
||||
if isinstance(statement[key], string_types):
|
||||
self._validate_resource_format(statement[key])
|
||||
else:
|
||||
for resource in sorted(statement[key], reverse=True):
|
||||
self._validate_resource_format(resource)
|
||||
if self._resource_error == "":
|
||||
IAMPolicyDocumentValidator._legacy_parse_resource_like(statement, key)
|
||||
|
||||
def _validate_resource_format(self, resource):
|
||||
if resource != "*":
|
||||
resource_partitions = resource.partition(":")
|
||||
|
||||
if resource_partitions[1] == "":
|
||||
self._resource_error = 'Resource {resource} must be in ARN format or "*".'.format(resource=resource)
|
||||
return
|
||||
|
||||
resource_partitions = resource_partitions[2].partition(":")
|
||||
if resource_partitions[0] != "aws":
|
||||
remaining_resource_parts = resource_partitions[2].split(":")
|
||||
|
||||
arn1 = remaining_resource_parts[0] if remaining_resource_parts[0] != "" or len(remaining_resource_parts) > 1 else "*"
|
||||
arn2 = remaining_resource_parts[1] if len(remaining_resource_parts) > 1 else "*"
|
||||
arn3 = remaining_resource_parts[2] if len(remaining_resource_parts) > 2 else "*"
|
||||
arn4 = ":".join(remaining_resource_parts[3:]) if len(remaining_resource_parts) > 3 else "*"
|
||||
self._resource_error = 'Partition "{partition}" is not valid for resource "arn:{partition}:{arn1}:{arn2}:{arn3}:{arn4}".'.format(
|
||||
partition=resource_partitions[0],
|
||||
arn1=arn1,
|
||||
arn2=arn2,
|
||||
arn3=arn3,
|
||||
arn4=arn4
|
||||
)
|
||||
return
|
||||
|
||||
if resource_partitions[1] != ":":
|
||||
self._resource_error = "Resource vendor must be fully qualified and cannot contain regexes."
|
||||
return
|
||||
|
||||
resource_partitions = resource_partitions[2].partition(":")
|
||||
|
||||
service = resource_partitions[0]
|
||||
|
||||
if service in SERVICE_TYPE_REGION_INFORMATION_ERROR_ASSOCIATIONS.keys() and not resource_partitions[2].startswith(":"):
|
||||
self._resource_error = SERVICE_TYPE_REGION_INFORMATION_ERROR_ASSOCIATIONS[service].format(resource=resource)
|
||||
return
|
||||
|
||||
resource_partitions = resource_partitions[2].partition(":")
|
||||
resource_partitions = resource_partitions[2].partition(":")
|
||||
|
||||
if service in VALID_RESOURCE_PATH_STARTING_VALUES.keys():
|
||||
valid_start = False
|
||||
for valid_starting_value in VALID_RESOURCE_PATH_STARTING_VALUES[service]["values"]:
|
||||
if resource_partitions[2].startswith(valid_starting_value):
|
||||
valid_start = True
|
||||
break
|
||||
if not valid_start:
|
||||
self._resource_error = VALID_RESOURCE_PATH_STARTING_VALUES[service]["error_message"].format(
|
||||
values=", ".join(VALID_RESOURCE_PATH_STARTING_VALUES[service]["values"])
|
||||
)
|
||||
|
||||
def _perform_first_legacy_parsing(self):
|
||||
"""This method excludes legacy parsing resources, since that have to be done later."""
|
||||
for statement in self._statements:
|
||||
self._legacy_parse_statement(statement)
|
||||
|
||||
@staticmethod
|
||||
def _legacy_parse_statement(statement):
|
||||
assert statement["Effect"] in VALID_EFFECTS # case-sensitive matching
|
||||
if "Condition" in statement:
|
||||
for condition_key, condition_value in statement["Condition"].items():
|
||||
IAMPolicyDocumentValidator._legacy_parse_condition(condition_key, condition_value)
|
||||
|
||||
@staticmethod
|
||||
def _legacy_parse_resource_like(statement, key):
|
||||
if isinstance(statement[key], string_types):
|
||||
if statement[key] != "*":
|
||||
assert statement[key].count(":") >= 5 or "::" not in statement[key]
|
||||
assert statement[key].split(":")[2] != ""
|
||||
else: # list
|
||||
for resource in statement[key]:
|
||||
if resource != "*":
|
||||
assert resource.count(":") >= 5 or "::" not in resource
|
||||
assert resource[2] != ""
|
||||
|
||||
@staticmethod
|
||||
def _legacy_parse_condition(condition_key, condition_value):
|
||||
stripped_condition_key = IAMPolicyDocumentValidator._strip_condition_key(condition_key)
|
||||
|
||||
if stripped_condition_key.startswith("Date"):
|
||||
for condition_element_key, condition_element_value in condition_value.items():
|
||||
if isinstance(condition_element_value, string_types):
|
||||
IAMPolicyDocumentValidator._legacy_parse_date_condition_value(condition_element_value)
|
||||
else: # it has to be a list
|
||||
for date_condition_value in condition_element_value:
|
||||
IAMPolicyDocumentValidator._legacy_parse_date_condition_value(date_condition_value)
|
||||
|
||||
@staticmethod
|
||||
def _legacy_parse_date_condition_value(date_condition_value):
|
||||
if "t" in date_condition_value.lower() or "-" in date_condition_value:
|
||||
IAMPolicyDocumentValidator._validate_iso_8601_datetime(date_condition_value.lower())
|
||||
else: # timestamp
|
||||
assert 0 <= int(date_condition_value) <= 9223372036854775807
|
||||
|
||||
@staticmethod
|
||||
def _validate_iso_8601_datetime(datetime):
|
||||
datetime_parts = datetime.partition("t")
|
||||
negative_year = datetime_parts[0].startswith("-")
|
||||
date_parts = datetime_parts[0][1:].split("-") if negative_year else datetime_parts[0].split("-")
|
||||
year = "-" + date_parts[0] if negative_year else date_parts[0]
|
||||
assert -292275054 <= int(year) <= 292278993
|
||||
if len(date_parts) > 1:
|
||||
month = date_parts[1]
|
||||
assert 1 <= int(month) <= 12
|
||||
if len(date_parts) > 2:
|
||||
day = date_parts[2]
|
||||
assert 1 <= int(day) <= 31
|
||||
assert len(date_parts) < 4
|
||||
|
||||
time_parts = datetime_parts[2].split(":")
|
||||
if time_parts[0] != "":
|
||||
hours = time_parts[0]
|
||||
assert 0 <= int(hours) <= 23
|
||||
if len(time_parts) > 1:
|
||||
minutes = time_parts[1]
|
||||
assert 0 <= int(minutes) <= 59
|
||||
if len(time_parts) > 2:
|
||||
if "z" in time_parts[2]:
|
||||
seconds_with_decimal_fraction = time_parts[2].partition("z")[0]
|
||||
assert time_parts[2].partition("z")[2] == ""
|
||||
elif "+" in time_parts[2]:
|
||||
seconds_with_decimal_fraction = time_parts[2].partition("+")[0]
|
||||
time_zone_data = time_parts[2].partition("+")[2].partition(":")
|
||||
time_zone_hours = time_zone_data[0]
|
||||
assert len(time_zone_hours) == 2
|
||||
assert 0 <= int(time_zone_hours) <= 23
|
||||
if time_zone_data[1] == ":":
|
||||
time_zone_minutes = time_zone_data[2]
|
||||
assert len(time_zone_minutes) == 2
|
||||
assert 0 <= int(time_zone_minutes) <= 59
|
||||
else:
|
||||
seconds_with_decimal_fraction = time_parts[2]
|
||||
seconds_with_decimal_fraction_partition = seconds_with_decimal_fraction.partition(".")
|
||||
seconds = seconds_with_decimal_fraction_partition[0]
|
||||
assert 0 <= int(seconds) <= 59
|
||||
if seconds_with_decimal_fraction_partition[1] == ".":
|
||||
decimal_seconds = seconds_with_decimal_fraction_partition[2]
|
||||
assert 0 <= int(decimal_seconds) <= 999999999
|
||||
|
|
@ -818,12 +818,12 @@ CREATE_POLICY_TEMPLATE = """<CreatePolicyResponse>
|
|||
<Policy>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>{{ policy.attachment_count }}</AttachmentCount>
|
||||
<CreateDate>{{ policy.create_datetime.isoformat() }}</CreateDate>
|
||||
<CreateDate>{{ policy.created_iso_8601 }}</CreateDate>
|
||||
<DefaultVersionId>{{ policy.default_version_id }}</DefaultVersionId>
|
||||
<Path>{{ policy.path }}</Path>
|
||||
<PolicyId>{{ policy.id }}</PolicyId>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<UpdateDate>{{ policy.update_datetime.isoformat() }}</UpdateDate>
|
||||
<UpdateDate>{{ policy.updated_iso_8601 }}</UpdateDate>
|
||||
</Policy>
|
||||
</CreatePolicyResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -841,8 +841,8 @@ GET_POLICY_TEMPLATE = """<GetPolicyResponse>
|
|||
<Path>{{ policy.path }}</Path>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>{{ policy.attachment_count }}</AttachmentCount>
|
||||
<CreateDate>{{ policy.create_datetime.isoformat() }}</CreateDate>
|
||||
<UpdateDate>{{ policy.update_datetime.isoformat() }}</UpdateDate>
|
||||
<CreateDate>{{ policy.created_iso_8601 }}</CreateDate>
|
||||
<UpdateDate>{{ policy.updated_iso_8601 }}</UpdateDate>
|
||||
</Policy>
|
||||
</GetPolicyResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -929,12 +929,12 @@ LIST_POLICIES_TEMPLATE = """<ListPoliciesResponse>
|
|||
<member>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>{{ policy.attachment_count }}</AttachmentCount>
|
||||
<CreateDate>{{ policy.create_datetime.isoformat() }}</CreateDate>
|
||||
<CreateDate>{{ policy.created_iso_8601 }}</CreateDate>
|
||||
<DefaultVersionId>{{ policy.default_version_id }}</DefaultVersionId>
|
||||
<Path>{{ policy.path }}</Path>
|
||||
<PolicyId>{{ policy.id }}</PolicyId>
|
||||
<PolicyName>{{ policy.name }}</PolicyName>
|
||||
<UpdateDate>{{ policy.update_datetime.isoformat() }}</UpdateDate>
|
||||
<UpdateDate>{{ policy.updated_iso_8601 }}</UpdateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Policies>
|
||||
|
|
@ -958,7 +958,7 @@ CREATE_INSTANCE_PROFILE_TEMPLATE = """<CreateInstanceProfileResponse xmlns="http
|
|||
<InstanceProfileName>{{ profile.name }}</InstanceProfileName>
|
||||
<Path>{{ profile.path }}</Path>
|
||||
<Arn>{{ profile.arn }}</Arn>
|
||||
<CreateDate>{{ profile.create_date }}</CreateDate>
|
||||
<CreateDate>{{ profile.created_iso_8601 }}</CreateDate>
|
||||
</InstanceProfile>
|
||||
</CreateInstanceProfileResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -977,7 +977,7 @@ GET_INSTANCE_PROFILE_TEMPLATE = """<GetInstanceProfileResponse xmlns="https://ia
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
|
|
@ -985,7 +985,7 @@ GET_INSTANCE_PROFILE_TEMPLATE = """<GetInstanceProfileResponse xmlns="https://ia
|
|||
<InstanceProfileName>{{ profile.name }}</InstanceProfileName>
|
||||
<Path>{{ profile.path }}</Path>
|
||||
<Arn>{{ profile.arn }}</Arn>
|
||||
<CreateDate>{{ profile.create_date }}</CreateDate>
|
||||
<CreateDate>{{ profile.created_iso_8601 }}</CreateDate>
|
||||
</InstanceProfile>
|
||||
</GetInstanceProfileResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -1000,7 +1000,7 @@ CREATE_ROLE_TEMPLATE = """<CreateRoleResponse xmlns="https://iam.amazonaws.com/d
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
{% if role.permissions_boundary %}
|
||||
<PermissionsBoundary>
|
||||
|
|
@ -1041,7 +1041,7 @@ UPDATE_ROLE_DESCRIPTION_TEMPLATE = """<UpdateRoleDescriptionResponse xmlns="http
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date.isoformat() }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
{% if role.tags %}
|
||||
<Tags>
|
||||
|
|
@ -1067,7 +1067,7 @@ GET_ROLE_TEMPLATE = """<GetRoleResponse xmlns="https://iam.amazonaws.com/doc/201
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
{% if role.tags %}
|
||||
<Tags>
|
||||
|
|
@ -1108,7 +1108,7 @@ LIST_ROLES_TEMPLATE = """<ListRolesResponse xmlns="https://iam.amazonaws.com/doc
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
{% if role.permissions_boundary %}
|
||||
<PermissionsBoundary>
|
||||
|
|
@ -1144,8 +1144,8 @@ CREATE_POLICY_VERSION_TEMPLATE = """<CreatePolicyVersionResponse xmlns="https://
|
|||
<PolicyVersion>
|
||||
<Document>{{ policy_version.document }}</Document>
|
||||
<VersionId>{{ policy_version.version_id }}</VersionId>
|
||||
<IsDefaultVersion>{{ policy_version.is_default }}</IsDefaultVersion>
|
||||
<CreateDate>{{ policy_version.create_datetime }}</CreateDate>
|
||||
<IsDefaultVersion>{{ policy_version.is_default | lower }}</IsDefaultVersion>
|
||||
<CreateDate>{{ policy_version.created_iso_8601 }}</CreateDate>
|
||||
</PolicyVersion>
|
||||
</CreatePolicyVersionResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -1158,8 +1158,8 @@ GET_POLICY_VERSION_TEMPLATE = """<GetPolicyVersionResponse xmlns="https://iam.am
|
|||
<PolicyVersion>
|
||||
<Document>{{ policy_version.document }}</Document>
|
||||
<VersionId>{{ policy_version.version_id }}</VersionId>
|
||||
<IsDefaultVersion>{{ policy_version.is_default }}</IsDefaultVersion>
|
||||
<CreateDate>{{ policy_version.create_datetime }}</CreateDate>
|
||||
<IsDefaultVersion>{{ policy_version.is_default | lower }}</IsDefaultVersion>
|
||||
<CreateDate>{{ policy_version.created_iso_8601 }}</CreateDate>
|
||||
</PolicyVersion>
|
||||
</GetPolicyVersionResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -1175,8 +1175,8 @@ LIST_POLICY_VERSIONS_TEMPLATE = """<ListPolicyVersionsResponse xmlns="https://ia
|
|||
<member>
|
||||
<Document>{{ policy_version.document }}</Document>
|
||||
<VersionId>{{ policy_version.version_id }}</VersionId>
|
||||
<IsDefaultVersion>{{ policy_version.is_default }}</IsDefaultVersion>
|
||||
<CreateDate>{{ policy_version.create_datetime }}</CreateDate>
|
||||
<IsDefaultVersion>{{ policy_version.is_default | lower }}</IsDefaultVersion>
|
||||
<CreateDate>{{ policy_version.created_iso_8601 }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Versions>
|
||||
|
|
@ -1200,7 +1200,7 @@ LIST_INSTANCE_PROFILES_TEMPLATE = """<ListInstanceProfilesResponse xmlns="https:
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
|
|
@ -1208,7 +1208,7 @@ LIST_INSTANCE_PROFILES_TEMPLATE = """<ListInstanceProfilesResponse xmlns="https:
|
|||
<InstanceProfileName>{{ instance.name }}</InstanceProfileName>
|
||||
<Path>{{ instance.path }}</Path>
|
||||
<Arn>{{ instance.arn }}</Arn>
|
||||
<CreateDate>{{ instance.create_date }}</CreateDate>
|
||||
<CreateDate>{{ instance.created_iso_8601 }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</InstanceProfiles>
|
||||
|
|
@ -1287,7 +1287,7 @@ CREATE_GROUP_TEMPLATE = """<CreateGroupResponse>
|
|||
<GroupName>{{ group.name }}</GroupName>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>{{ group.create_date }}</CreateDate>
|
||||
<CreateDate>{{ group.created_iso_8601 }}</CreateDate>
|
||||
</Group>
|
||||
</CreateGroupResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -1302,7 +1302,7 @@ GET_GROUP_TEMPLATE = """<GetGroupResponse>
|
|||
<GroupName>{{ group.name }}</GroupName>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>{{ group.create_date }}</CreateDate>
|
||||
<CreateDate>{{ group.created_iso_8601 }}</CreateDate>
|
||||
</Group>
|
||||
<Users>
|
||||
{% for user in group.users %}
|
||||
|
|
@ -1349,6 +1349,7 @@ LIST_GROUPS_FOR_USER_TEMPLATE = """<ListGroupsForUserResponse>
|
|||
<GroupName>{{ group.name }}</GroupName>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>{{ group.created_iso_8601 }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Groups>
|
||||
|
|
@ -1493,6 +1494,7 @@ CREATE_ACCESS_KEY_TEMPLATE = """<CreateAccessKeyResponse>
|
|||
<AccessKeyId>{{ key.access_key_id }}</AccessKeyId>
|
||||
<Status>{{ key.status }}</Status>
|
||||
<SecretAccessKey>{{ key.secret_access_key }}</SecretAccessKey>
|
||||
<CreateDate>{{ key.created_iso_8601 }}</CreateDate>
|
||||
</AccessKey>
|
||||
</CreateAccessKeyResult>
|
||||
<ResponseMetadata>
|
||||
|
|
@ -1509,7 +1511,7 @@ LIST_ACCESS_KEYS_TEMPLATE = """<ListAccessKeysResponse>
|
|||
<UserName>{{ user_name }}</UserName>
|
||||
<AccessKeyId>{{ key.access_key_id }}</AccessKeyId>
|
||||
<Status>{{ key.status }}</Status>
|
||||
<CreateDate>{{ key.create_date }}</CreateDate>
|
||||
<CreateDate>{{ key.created_iso_8601 }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</AccessKeyMetadata>
|
||||
|
|
@ -1577,7 +1579,7 @@ LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE = """<ListInstanceProfilesForRoleRespon
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
|
|
@ -1585,7 +1587,7 @@ LIST_INSTANCE_PROFILES_FOR_ROLE_TEMPLATE = """<ListInstanceProfilesForRoleRespon
|
|||
<InstanceProfileName>{{ profile.name }}</InstanceProfileName>
|
||||
<Path>{{ profile.path }}</Path>
|
||||
<Arn>{{ profile.arn }}</Arn>
|
||||
<CreateDate>{{ profile.create_date }}</CreateDate>
|
||||
<CreateDate>{{ profile.created_iso_8601 }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</InstanceProfiles>
|
||||
|
|
@ -1651,6 +1653,7 @@ LIST_GROUPS_FOR_USER_TEMPLATE = """<ListGroupsForUserResponse>
|
|||
<GroupName>{{ group.name }}</GroupName>
|
||||
<GroupId>{{ group.id }}</GroupId>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>{{ group.created_iso_8601 }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Groups>
|
||||
|
|
@ -1704,7 +1707,7 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<GroupName>{{ group.name }}</GroupName>
|
||||
<Path>{{ group.path }}</Path>
|
||||
<Arn>{{ group.arn }}</Arn>
|
||||
<CreateDate>{{ group.create_date }}</CreateDate>
|
||||
<CreateDate>{{ group.created_iso_8601 }}</CreateDate>
|
||||
<GroupPolicyList>
|
||||
{% for policy in group.policies %}
|
||||
<member>
|
||||
|
|
@ -1754,7 +1757,7 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
|
|
@ -1762,7 +1765,7 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<InstanceProfileName>{{ profile.name }}</InstanceProfileName>
|
||||
<Path>{{ profile.path }}</Path>
|
||||
<Arn>{{ profile.arn }}</Arn>
|
||||
<CreateDate>{{ profile.create_date }}</CreateDate>
|
||||
<CreateDate>{{ profile.created_iso_8601 }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</InstanceProfileList>
|
||||
|
|
@ -1770,7 +1773,7 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
<Arn>{{ role.arn }}</Arn>
|
||||
<RoleName>{{ role.name }}</RoleName>
|
||||
<AssumeRolePolicyDocument>{{ role.assume_role_policy_document }}</AssumeRolePolicyDocument>
|
||||
<CreateDate>{{ role.create_date }}</CreateDate>
|
||||
<CreateDate>{{ role.created_iso_8601 }}</CreateDate>
|
||||
<RoleId>{{ role.id }}</RoleId>
|
||||
</member>
|
||||
{% endfor %}
|
||||
|
|
@ -1786,17 +1789,17 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """<GetAccountAuthorizationDetailsR
|
|||
{% for policy_version in policy.versions %}
|
||||
<member>
|
||||
<Document>{{ policy_version.document }}</Document>
|
||||
<IsDefaultVersion>{{ policy_version.is_default }}</IsDefaultVersion>
|
||||
<IsDefaultVersion>{{ policy_version.is_default | lower }}</IsDefaultVersion>
|
||||
<VersionId>{{ policy_version.version_id }}</VersionId>
|
||||
<CreateDate>{{ policy_version.create_datetime }}</CreateDate>
|
||||
<CreateDate>{{ policy_version.created_iso_8601 }}</CreateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</PolicyVersionList>
|
||||
<Arn>{{ policy.arn }}</Arn>
|
||||
<AttachmentCount>1</AttachmentCount>
|
||||
<CreateDate>{{ policy.create_datetime }}</CreateDate>
|
||||
<CreateDate>{{ policy.created_iso_8601 }}</CreateDate>
|
||||
<IsAttachable>true</IsAttachable>
|
||||
<UpdateDate>{{ policy.update_datetime }}</UpdateDate>
|
||||
<UpdateDate>{{ policy.updated_iso_8601 }}</UpdateDate>
|
||||
</member>
|
||||
{% endfor %}
|
||||
</Policies>
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ import six
|
|||
def random_alphanumeric(length):
|
||||
return ''.join(six.text_type(
|
||||
random.choice(
|
||||
string.ascii_letters + string.digits
|
||||
string.ascii_letters + string.digits + "+" + "/"
|
||||
)) for _ in range(length)
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -123,17 +123,12 @@ class Stream(BaseModel):
|
|||
self.tags = {}
|
||||
self.status = "ACTIVE"
|
||||
|
||||
if six.PY3:
|
||||
izip_longest = itertools.zip_longest
|
||||
else:
|
||||
izip_longest = itertools.izip_longest
|
||||
step = 2**128 // shard_count
|
||||
hash_ranges = itertools.chain(map(lambda i: (i, i * step, (i + 1) * step),
|
||||
range(shard_count - 1)),
|
||||
[(shard_count - 1, (shard_count - 1) * step, 2**128)])
|
||||
for index, start, end in hash_ranges:
|
||||
|
||||
for index, start, end in izip_longest(range(shard_count),
|
||||
range(0, 2**128, 2 **
|
||||
128 // shard_count),
|
||||
range(2**128 // shard_count, 2 **
|
||||
128, 2**128 // shard_count),
|
||||
fillvalue=2**128):
|
||||
shard = Shard(index, start, end)
|
||||
self.shards[shard.shard_id] = shard
|
||||
|
||||
|
|
|
|||
|
|
@ -268,10 +268,26 @@ class fakesock(object):
|
|||
_sent_data = []
|
||||
|
||||
def __init__(self, family=socket.AF_INET, type=socket.SOCK_STREAM,
|
||||
protocol=0):
|
||||
self.truesock = (old_socket(family, type, protocol)
|
||||
if httpretty.allow_net_connect
|
||||
else None)
|
||||
proto=0, fileno=None, _sock=None):
|
||||
"""
|
||||
Matches both the Python 2 API:
|
||||
def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, _sock=None):
|
||||
https://github.com/python/cpython/blob/2.7/Lib/socket.py
|
||||
|
||||
and the Python 3 API:
|
||||
def __init__(self, family=-1, type=-1, proto=-1, fileno=None):
|
||||
https://github.com/python/cpython/blob/3.5/Lib/socket.py
|
||||
"""
|
||||
if httpretty.allow_net_connect:
|
||||
if PY3:
|
||||
self.truesock = old_socket(family, type, proto, fileno)
|
||||
else:
|
||||
# If Python 2, if parameters are passed as arguments, instead of kwargs,
|
||||
# the 4th argument `_sock` will be interpreted as the `fileno`.
|
||||
# Check if _sock is none, and if so, pass fileno.
|
||||
self.truesock = old_socket(family, type, proto, fileno or _sock)
|
||||
else:
|
||||
self.truesock = None
|
||||
self._closed = True
|
||||
self.fd = FakeSockFile()
|
||||
self.fd.socket = self
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ class RecordSet(BaseModel):
|
|||
properties["HostedZoneId"])
|
||||
|
||||
try:
|
||||
hosted_zone.delete_rrset_by_name(resource_name)
|
||||
hosted_zone.delete_rrset({'Name': resource_name})
|
||||
except KeyError:
|
||||
pass
|
||||
|
||||
|
|
@ -171,7 +171,13 @@ class RecordSet(BaseModel):
|
|||
self.hosted_zone_name)
|
||||
if not hosted_zone:
|
||||
hosted_zone = route53_backend.get_hosted_zone(self.hosted_zone_id)
|
||||
hosted_zone.delete_rrset_by_name(self.name)
|
||||
hosted_zone.delete_rrset({'Name': self.name, 'Type': self.type_})
|
||||
|
||||
|
||||
def reverse_domain_name(domain_name):
|
||||
if domain_name.endswith('.'): # normalize without trailing dot
|
||||
domain_name = domain_name[:-1]
|
||||
return '.'.join(reversed(domain_name.split('.')))
|
||||
|
||||
|
||||
class FakeZone(BaseModel):
|
||||
|
|
@ -199,9 +205,13 @@ class FakeZone(BaseModel):
|
|||
self.rrsets.append(new_rrset)
|
||||
return new_rrset
|
||||
|
||||
def delete_rrset_by_name(self, name):
|
||||
def delete_rrset(self, rrset):
|
||||
self.rrsets = [
|
||||
record_set for record_set in self.rrsets if record_set.name != name]
|
||||
record_set
|
||||
for record_set in self.rrsets
|
||||
if record_set.name != rrset['Name'] or
|
||||
(rrset.get('Type') is not None and record_set.type_ != rrset['Type'])
|
||||
]
|
||||
|
||||
def delete_rrset_by_id(self, set_identifier):
|
||||
self.rrsets = [
|
||||
|
|
@ -209,12 +219,15 @@ class FakeZone(BaseModel):
|
|||
|
||||
def get_record_sets(self, start_type, start_name):
|
||||
record_sets = list(self.rrsets) # Copy the list
|
||||
if start_name:
|
||||
record_sets = [
|
||||
record_set
|
||||
for record_set in record_sets
|
||||
if reverse_domain_name(record_set.name) >= reverse_domain_name(start_name)
|
||||
]
|
||||
if start_type:
|
||||
record_sets = [
|
||||
record_set for record_set in record_sets if record_set.type_ >= start_type]
|
||||
if start_name:
|
||||
record_sets = [
|
||||
record_set for record_set in record_sets if record_set.name >= start_name]
|
||||
|
||||
return record_sets
|
||||
|
||||
|
|
|
|||
|
|
@ -144,7 +144,7 @@ class Route53(BaseResponse):
|
|||
the_zone.delete_rrset_by_id(
|
||||
record_set["SetIdentifier"])
|
||||
else:
|
||||
the_zone.delete_rrset_by_name(record_set["Name"])
|
||||
the_zone.delete_rrset(record_set)
|
||||
|
||||
return 200, headers, CHANGE_RRSET_RESPONSE
|
||||
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ from boto3 import Session
|
|||
|
||||
from moto.compat import OrderedDict
|
||||
from moto.core import BaseBackend, BaseModel
|
||||
from moto.core.utils import iso_8601_datetime_with_milliseconds
|
||||
from moto.core.utils import iso_8601_datetime_with_milliseconds, camelcase_to_underscores
|
||||
from moto.sqs import sqs_backends
|
||||
from moto.awslambda import lambda_backends
|
||||
|
||||
|
|
@ -243,11 +243,14 @@ class SNSBackend(BaseBackend):
|
|||
def update_sms_attributes(self, attrs):
|
||||
self.sms_attributes.update(attrs)
|
||||
|
||||
def create_topic(self, name):
|
||||
def create_topic(self, name, attributes=None):
|
||||
fails_constraints = not re.match(r'^[a-zA-Z0-9_-]{1,256}$', name)
|
||||
if fails_constraints:
|
||||
raise InvalidParameterValue("Topic names must be made up of only uppercase and lowercase ASCII letters, numbers, underscores, and hyphens, and must be between 1 and 256 characters long.")
|
||||
candidate_topic = Topic(name, self)
|
||||
if attributes:
|
||||
for attribute in attributes:
|
||||
setattr(candidate_topic, camelcase_to_underscores(attribute), attributes[attribute])
|
||||
if candidate_topic.arn in self.topics:
|
||||
return self.topics[candidate_topic.arn]
|
||||
else:
|
||||
|
|
|
|||
|
|
@ -75,7 +75,8 @@ class SNSResponse(BaseResponse):
|
|||
|
||||
def create_topic(self):
|
||||
name = self._get_param('Name')
|
||||
topic = self.backend.create_topic(name)
|
||||
attributes = self._get_attributes()
|
||||
topic = self.backend.create_topic(name, attributes)
|
||||
|
||||
if self.request_json:
|
||||
return json.dumps({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue