diff --git a/IMPLEMENTATION_COVERAGE.md b/IMPLEMENTATION_COVERAGE.md
index 98e426e3c..32e3140c1 100644
--- a/IMPLEMENTATION_COVERAGE.md
+++ b/IMPLEMENTATION_COVERAGE.md
@@ -477,12 +477,12 @@
- [X] create_stack
- [X] create_stack_instances
- [X] create_stack_set
-- [ ] delete_change_set
+- [X] delete_change_set
- [X] delete_stack
- [X] delete_stack_instances
- [X] delete_stack_set
- [ ] describe_account_limits
-- [ ] describe_change_set
+- [X] describe_change_set
- [ ] describe_stack_events
- [X] describe_stack_instance
- [ ] describe_stack_resource
@@ -495,7 +495,7 @@
- [ ] get_stack_policy
- [ ] get_template
- [ ] get_template_summary
-- [ ] list_change_sets
+- [X] list_change_sets
- [X] list_exports
- [ ] list_imports
- [X] list_stack_instances
@@ -2208,7 +2208,7 @@
- [ ] describe_event_types
- [ ] describe_events
-## iam - 48% implemented
+## iam - 62% implemented
- [ ] add_client_id_to_open_id_connect_provider
- [X] add_role_to_instance_profile
- [X] add_user_to_group
@@ -2247,7 +2247,7 @@
- [X] delete_server_certificate
- [ ] delete_service_linked_role
- [ ] delete_service_specific_credential
-- [ ] delete_signing_certificate
+- [X] delete_signing_certificate
- [ ] delete_ssh_public_key
- [X] delete_user
- [X] delete_user_policy
@@ -2279,7 +2279,7 @@
- [ ] get_ssh_public_key
- [X] get_user
- [X] get_user_policy
-- [ ] list_access_keys
+- [X] list_access_keys
- [X] list_account_aliases
- [X] list_attached_group_policies
- [X] list_attached_role_policies
@@ -2287,19 +2287,21 @@
- [ ] list_entities_for_policy
- [X] list_group_policies
- [X] list_groups
-- [ ] list_groups_for_user
-- [ ] list_instance_profiles
-- [ ] list_instance_profiles_for_role
+- [X] list_groups_for_user
+- [X] list_instance_profiles
+- [X] list_instance_profiles_for_role
- [X] list_mfa_devices
- [ ] list_open_id_connect_providers
- [X] list_policies
- [X] list_policy_versions
- [X] list_role_policies
-- [ ] list_roles
+- [X] list_roles
+- [X] list_role_tags
+- [ ] list_user_tags
- [X] list_saml_providers
-- [ ] list_server_certificates
+- [X] list_server_certificates
- [ ] list_service_specific_credentials
-- [ ] list_signing_certificates
+- [X] list_signing_certificates
- [ ] list_ssh_public_keys
- [X] list_user_policies
- [X] list_users
@@ -2315,6 +2317,10 @@
- [ ] set_default_policy_version
- [ ] simulate_custom_policy
- [ ] simulate_principal_policy
+- [X] tag_role
+- [ ] tag_user
+- [X] untag_role
+- [ ] untag_user
- [X] update_access_key
- [ ] update_account_password_policy
- [ ] update_assume_role_policy
@@ -2326,11 +2332,11 @@
- [X] update_saml_provider
- [ ] update_server_certificate
- [ ] update_service_specific_credential
-- [ ] update_signing_certificate
+- [X] update_signing_certificate
- [ ] update_ssh_public_key
- [ ] update_user
-- [ ] upload_server_certificate
-- [ ] upload_signing_certificate
+- [X] upload_server_certificate
+- [X] upload_signing_certificate
- [ ] upload_ssh_public_key
## importexport - 0% implemented
@@ -3542,7 +3548,7 @@
- [ ] get_bucket_inventory_configuration
- [ ] get_bucket_lifecycle
- [ ] get_bucket_lifecycle_configuration
-- [ ] get_bucket_location
+- [X] get_bucket_location
- [ ] get_bucket_logging
- [ ] get_bucket_metrics_configuration
- [ ] get_bucket_notification
diff --git a/moto/cloudformation/models.py b/moto/cloudformation/models.py
index 11b97aa34..67a2ba6d7 100644
--- a/moto/cloudformation/models.py
+++ b/moto/cloudformation/models.py
@@ -271,6 +271,49 @@ class FakeStack(BaseModel):
self.status = "DELETE_COMPLETE"
+class FakeChange(BaseModel):
+
+ def __init__(self, action, logical_resource_id, resource_type):
+ self.action = action
+ self.logical_resource_id = logical_resource_id
+ self.resource_type = resource_type
+
+
+class FakeChangeSet(FakeStack):
+
+ def __init__(self, stack_id, stack_name, stack_template, change_set_id, change_set_name, template, parameters, region_name, notification_arns=None, tags=None, role_arn=None, cross_stack_resources=None):
+ super(FakeChangeSet, self).__init__(
+ stack_id,
+ stack_name,
+ stack_template,
+ parameters,
+ region_name,
+ notification_arns=notification_arns,
+ tags=tags,
+ role_arn=role_arn,
+ cross_stack_resources=cross_stack_resources,
+ create_change_set=True,
+ )
+ self.stack_name = stack_name
+ self.change_set_id = change_set_id
+ self.change_set_name = change_set_name
+ self.changes = self.diff(template=template, parameters=parameters)
+
+ def diff(self, template, parameters=None):
+ self.template = template
+ self._parse_template()
+ changes = []
+ resources_by_action = self.resource_map.diff(self.template_dict, parameters)
+ for action, resources in resources_by_action.items():
+ for resource_name, resource in resources.items():
+ changes.append(FakeChange(
+ action=action,
+ logical_resource_id=resource_name,
+ resource_type=resource['ResourceType'],
+ ))
+ return changes
+
+
class FakeEvent(BaseModel):
def __init__(self, stack_id, stack_name, logical_resource_id, physical_resource_id, resource_type, resource_status, resource_status_reason=None, resource_properties=None):
@@ -377,24 +420,62 @@ class CloudFormationBackend(BaseBackend):
return new_stack
def create_change_set(self, stack_name, change_set_name, template, parameters, region_name, change_set_type, notification_arns=None, tags=None, role_arn=None):
+ stack_id = None
+ stack_template = None
if change_set_type == 'UPDATE':
stacks = self.stacks.values()
stack = None
for s in stacks:
if s.name == stack_name:
stack = s
+ stack_id = stack.stack_id
+ stack_template = stack.template
if stack is None:
raise ValidationError(stack_name)
-
else:
- stack = self.create_stack(stack_name, template, parameters,
- region_name, notification_arns, tags,
- role_arn, create_change_set=True)
+ stack_id = generate_stack_id(stack_name)
+ stack_template = template
+
change_set_id = generate_changeset_id(change_set_name, region_name)
- self.stacks[change_set_name] = {'Id': change_set_id,
- 'StackId': stack.stack_id}
- self.change_sets[change_set_id] = stack
- return change_set_id, stack.stack_id
+ new_change_set = FakeChangeSet(
+ stack_id=stack_id,
+ stack_name=stack_name,
+ stack_template=stack_template,
+ change_set_id=change_set_id,
+ change_set_name=change_set_name,
+ template=template,
+ parameters=parameters,
+ region_name=region_name,
+ notification_arns=notification_arns,
+ tags=tags,
+ role_arn=role_arn,
+ cross_stack_resources=self.exports
+ )
+ self.change_sets[change_set_id] = new_change_set
+ self.stacks[stack_id] = new_change_set
+ return change_set_id, stack_id
+
+ def delete_change_set(self, change_set_name, stack_name=None):
+ if change_set_name in self.change_sets:
+ # This means arn was passed in
+ del self.change_sets[change_set_name]
+ else:
+ for cs in self.change_sets:
+ if self.change_sets[cs].change_set_name == change_set_name:
+ del self.change_sets[cs]
+
+ def describe_change_set(self, change_set_name, stack_name=None):
+ change_set = None
+ if change_set_name in self.change_sets:
+ # This means arn was passed in
+ change_set = self.change_sets[change_set_name]
+ else:
+ for cs in self.change_sets:
+ if self.change_sets[cs].change_set_name == change_set_name:
+ change_set = self.change_sets[cs]
+ if change_set is None:
+ raise ValidationError(change_set_name)
+ return change_set
def execute_change_set(self, change_set_name, stack_name=None):
stack = None
@@ -403,7 +484,7 @@ class CloudFormationBackend(BaseBackend):
stack = self.change_sets[change_set_name]
else:
for cs in self.change_sets:
- if self.change_sets[cs].name == change_set_name:
+ if self.change_sets[cs].change_set_name == change_set_name:
stack = self.change_sets[cs]
if stack is None:
raise ValidationError(stack_name)
@@ -429,6 +510,9 @@ class CloudFormationBackend(BaseBackend):
else:
return list(stacks)
+ def list_change_sets(self):
+ return self.change_sets.values()
+
def list_stacks(self):
return [
v for v in self.stacks.values()
diff --git a/moto/cloudformation/parsing.py b/moto/cloudformation/parsing.py
index 35b05d101..0be68944b 100644
--- a/moto/cloudformation/parsing.py
+++ b/moto/cloudformation/parsing.py
@@ -465,36 +465,70 @@ class ResourceMap(collections.Mapping):
ec2_models.ec2_backends[self._region_name].create_tags(
[self[resource].physical_resource_id], self.tags)
- def update(self, template, parameters=None):
+ def diff(self, template, parameters=None):
if parameters:
self.input_parameters = parameters
self.load_mapping()
self.load_parameters()
self.load_conditions()
+ old_template = self._resource_json_map
+ new_template = template['Resources']
+
+ resource_names_by_action = {
+ 'Add': set(new_template) - set(old_template),
+ 'Modify': set(name for name in new_template if name in old_template and new_template[
+ name] != old_template[name]),
+ 'Remove': set(old_template) - set(new_template)
+ }
+ resources_by_action = {
+ 'Add': {},
+ 'Modify': {},
+ 'Remove': {},
+ }
+
+ for resource_name in resource_names_by_action['Add']:
+ resources_by_action['Add'][resource_name] = {
+ 'LogicalResourceId': resource_name,
+ 'ResourceType': new_template[resource_name]['Type']
+ }
+
+ for resource_name in resource_names_by_action['Modify']:
+ resources_by_action['Modify'][resource_name] = {
+ 'LogicalResourceId': resource_name,
+ 'ResourceType': new_template[resource_name]['Type']
+ }
+
+ for resource_name in resource_names_by_action['Remove']:
+ resources_by_action['Remove'][resource_name] = {
+ 'LogicalResourceId': resource_name,
+ 'ResourceType': old_template[resource_name]['Type']
+ }
+
+ return resources_by_action
+
+ def update(self, template, parameters=None):
+ resources_by_action = self.diff(template, parameters)
+
old_template = self._resource_json_map
new_template = template['Resources']
self._resource_json_map = new_template
- new_resource_names = set(new_template) - set(old_template)
- for resource_name in new_resource_names:
+ for resource_name, resource in resources_by_action['Add'].items():
resource_json = new_template[resource_name]
new_resource = parse_and_create_resource(
resource_name, resource_json, self, self._region_name)
self._parsed_resources[resource_name] = new_resource
- removed_resource_names = set(old_template) - set(new_template)
- for resource_name in removed_resource_names:
+ for resource_name, resource in resources_by_action['Remove'].items():
resource_json = old_template[resource_name]
parse_and_delete_resource(
resource_name, resource_json, self, self._region_name)
self._parsed_resources.pop(resource_name)
- resources_to_update = set(name for name in new_template if name in old_template and new_template[
- name] != old_template[name])
tries = 1
- while resources_to_update and tries < 5:
- for resource_name in resources_to_update.copy():
+ while resources_by_action['Modify'] and tries < 5:
+ for resource_name, resource in resources_by_action['Modify'].copy().items():
resource_json = new_template[resource_name]
try:
changed_resource = parse_and_update_resource(
@@ -505,7 +539,7 @@ class ResourceMap(collections.Mapping):
last_exception = e
else:
self._parsed_resources[resource_name] = changed_resource
- resources_to_update.remove(resource_name)
+ del resources_by_action['Modify'][resource_name]
tries += 1
if tries == 5:
raise last_exception
diff --git a/moto/cloudformation/responses.py b/moto/cloudformation/responses.py
index 2c7f6b91a..d1ef5ba8a 100644
--- a/moto/cloudformation/responses.py
+++ b/moto/cloudformation/responses.py
@@ -120,6 +120,31 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(CREATE_CHANGE_SET_RESPONSE_TEMPLATE)
return template.render(stack_id=stack_id, change_set_id=change_set_id)
+ def delete_change_set(self):
+ stack_name = self._get_param('StackName')
+ change_set_name = self._get_param('ChangeSetName')
+
+ self.cloudformation_backend.delete_change_set(change_set_name=change_set_name, stack_name=stack_name)
+ if self.request_json:
+ return json.dumps({
+ 'DeleteChangeSetResponse': {
+ 'DeleteChangeSetResult': {},
+ }
+ })
+ else:
+ template = self.response_template(DELETE_CHANGE_SET_RESPONSE_TEMPLATE)
+ return template.render()
+
+ def describe_change_set(self):
+ stack_name = self._get_param('StackName')
+ change_set_name = self._get_param('ChangeSetName')
+ change_set = self.cloudformation_backend.describe_change_set(
+ change_set_name=change_set_name,
+ stack_name=stack_name,
+ )
+ template = self.response_template(DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE)
+ return template.render(change_set=change_set)
+
@amzn_request_id
def execute_change_set(self):
stack_name = self._get_param('StackName')
@@ -187,6 +212,11 @@ class CloudFormationResponse(BaseResponse):
template = self.response_template(DESCRIBE_STACK_EVENTS_RESPONSE)
return template.render(stack=stack)
+ def list_change_sets(self):
+ change_sets = self.cloudformation_backend.list_change_sets()
+ template = self.response_template(LIST_CHANGE_SETS_RESPONSE)
+ return template.render(change_sets=change_sets)
+
def list_stacks(self):
stacks = self.cloudformation_backend.list_stacks()
template = self.response_template(LIST_STACKS_RESPONSE)
@@ -523,6 +553,66 @@ CREATE_CHANGE_SET_RESPONSE_TEMPLATE = """
"""
+DELETE_CHANGE_SET_RESPONSE_TEMPLATE = """
+
+
+
+ 3d3200a1-810e-3023-6cc3-example
+
+
+"""
+
+DESCRIBE_CHANGE_SET_RESPONSE_TEMPLATE = """
+
+ {{ change_set.change_set_id }}
+ {{ change_set.change_set_name }}
+ {{ change_set.stack_id }}
+ {{ change_set.stack_name }}
+ {{ change_set.description }}
+
+ {% for param_name, param_value in change_set.stack_parameters.items() %}
+
+ {{ param_name }}
+ {{ param_value }}
+
+ {% endfor %}
+
+ 2011-05-23T15:47:44Z
+ {{ change_set.execution_status }}
+ {{ change_set.status }}
+ {{ change_set.status_reason }}
+ {% if change_set.notification_arns %}
+
+ {% for notification_arn in change_set.notification_arns %}
+ {{ notification_arn }}
+ {% endfor %}
+
+ {% else %}
+
+ {% endif %}
+ {% if change_set.role_arn %}
+ {{ change_set.role_arn }}
+ {% endif %}
+ {% if change_set.changes %}
+
+ {% for change in change_set.changes %}
+
+ Resource
+
+ {{ change.action }}
+ {{ change.logical_resource_id }}
+ {{ change.resource_type }}
+
+
+ {% endfor %}
+
+ {% endif %}
+ {% if next_token %}
+ {{ next_token }}
+ {% endif %}
+
+"""
+
EXECUTE_CHANGE_SET_RESPONSE_TEMPLATE = """
@@ -648,6 +738,27 @@ DESCRIBE_STACK_EVENTS_RESPONSE = """
+
+
+ {% for change_set in change_sets %}
+
+ {{ change_set.stack_id }}
+ {{ change_set.stack_name }}
+ {{ change_set.change_set_id }}
+ {{ change_set.change_set_name }}
+ {{ change_set.execution_status }}
+ {{ change_set.status }}
+ {{ change_set.status_reason }}
+ 2011-05-23T15:47:44Z
+ {{ change_set.description }}
+
+ {% endfor %}
+
+
+"""
+
+
LIST_STACKS_RESPONSE = """
diff --git a/moto/dynamodb2/models.py b/moto/dynamodb2/models.py
index 8187ceaf9..677bbfb07 100644
--- a/moto/dynamodb2/models.py
+++ b/moto/dynamodb2/models.py
@@ -66,6 +66,8 @@ class DynamoType(object):
return int(self.value)
except ValueError:
return float(self.value)
+ elif self.is_set():
+ return set(self.value)
else:
return self.value
@@ -509,15 +511,12 @@ class Table(BaseModel):
elif 'Value' in val and DynamoType(val['Value']).value != current_attr[key].value:
raise ValueError("The conditional request failed")
elif 'ComparisonOperator' in val:
- comparison_func = get_comparison_func(
- val['ComparisonOperator'])
dynamo_types = [
DynamoType(ele) for ele in
val.get("AttributeValueList", [])
]
- for t in dynamo_types:
- if not comparison_func(current_attr[key].value, t.value):
- raise ValueError('The conditional request failed')
+ if not current_attr[key].compare(val['ComparisonOperator'], dynamo_types):
+ raise ValueError('The conditional request failed')
if range_value:
self.items[hash_value][range_value] = item
else:
@@ -946,15 +945,12 @@ class DynamoDBBackend(BaseBackend):
elif 'Value' in val and DynamoType(val['Value']).value != item_attr[key].value:
raise ValueError("The conditional request failed")
elif 'ComparisonOperator' in val:
- comparison_func = get_comparison_func(
- val['ComparisonOperator'])
dynamo_types = [
DynamoType(ele) for ele in
val.get("AttributeValueList", [])
]
- for t in dynamo_types:
- if not comparison_func(item_attr[key].value, t.value):
- raise ValueError('The conditional request failed')
+ if not item_attr[key].compare(val['ComparisonOperator'], dynamo_types):
+ raise ValueError('The conditional request failed')
# Update does not fail on new items, so create one
if item is None:
diff --git a/moto/ec2/models.py b/moto/ec2/models.py
old mode 100755
new mode 100644
index a450943b7..2953d41e3
--- a/moto/ec2/models.py
+++ b/moto/ec2/models.py
@@ -2464,7 +2464,7 @@ class SubnetBackend(object):
default_for_az, map_public_ip_on_launch)
# AWS associates a new subnet with the default Network ACL
- self.associate_default_network_acl_with_subnet(subnet_id)
+ self.associate_default_network_acl_with_subnet(subnet_id, vpc_id)
self.subnets[availability_zone][subnet_id] = subnet
return subnet
@@ -2879,7 +2879,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
def __init__(self, ec2_backend, spot_request_id, price, image_id, type,
valid_from, valid_until, launch_group, availability_zone_group,
key_name, security_groups, user_data, instance_type, placement,
- kernel_id, ramdisk_id, monitoring_enabled, subnet_id, spot_fleet_id,
+ kernel_id, ramdisk_id, monitoring_enabled, subnet_id, tags, spot_fleet_id,
**kwargs):
super(SpotInstanceRequest, self).__init__(**kwargs)
ls = LaunchSpecification()
@@ -2903,6 +2903,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
ls.monitored = monitoring_enabled
ls.subnet_id = subnet_id
self.spot_fleet_id = spot_fleet_id
+ self.tags = tags
if security_groups:
for group_name in security_groups:
@@ -2936,6 +2937,7 @@ class SpotInstanceRequest(BotoSpotRequest, TaggedEC2Resource):
security_group_names=[],
security_group_ids=self.launch_specification.groups,
spot_fleet_id=self.spot_fleet_id,
+ tags=self.tags,
)
instance = reservation.instances[0]
return instance
@@ -2951,15 +2953,16 @@ class SpotRequestBackend(object):
valid_until, launch_group, availability_zone_group,
key_name, security_groups, user_data,
instance_type, placement, kernel_id, ramdisk_id,
- monitoring_enabled, subnet_id, spot_fleet_id=None):
+ monitoring_enabled, subnet_id, tags=None, spot_fleet_id=None):
requests = []
+ tags = tags or {}
for _ in range(count):
spot_request_id = random_spot_request_id()
request = SpotInstanceRequest(self,
spot_request_id, price, image_id, type, valid_from, valid_until,
launch_group, availability_zone_group, key_name, security_groups,
user_data, instance_type, placement, kernel_id, ramdisk_id,
- monitoring_enabled, subnet_id, spot_fleet_id)
+ monitoring_enabled, subnet_id, tags, spot_fleet_id)
self.spot_instance_requests[spot_request_id] = request
requests.append(request)
return requests
@@ -2979,8 +2982,8 @@ class SpotRequestBackend(object):
class SpotFleetLaunchSpec(object):
def __init__(self, ebs_optimized, group_set, iam_instance_profile, image_id,
- instance_type, key_name, monitoring, spot_price, subnet_id, user_data,
- weighted_capacity):
+ instance_type, key_name, monitoring, spot_price, subnet_id, tag_specifications,
+ user_data, weighted_capacity):
self.ebs_optimized = ebs_optimized
self.group_set = group_set
self.iam_instance_profile = iam_instance_profile
@@ -2990,6 +2993,7 @@ class SpotFleetLaunchSpec(object):
self.monitoring = monitoring
self.spot_price = spot_price
self.subnet_id = subnet_id
+ self.tag_specifications = tag_specifications
self.user_data = user_data
self.weighted_capacity = float(weighted_capacity)
@@ -3020,6 +3024,7 @@ class SpotFleetRequest(TaggedEC2Resource):
monitoring=spec.get('monitoring._enabled'),
spot_price=spec.get('spot_price', self.spot_price),
subnet_id=spec['subnet_id'],
+ tag_specifications=self._parse_tag_specifications(spec),
user_data=spec.get('user_data'),
weighted_capacity=spec['weighted_capacity'],
)
@@ -3102,6 +3107,7 @@ class SpotFleetRequest(TaggedEC2Resource):
monitoring_enabled=launch_spec.monitoring,
subnet_id=launch_spec.subnet_id,
spot_fleet_id=self.id,
+ tags=launch_spec.tag_specifications,
)
self.spot_requests.extend(requests)
self.fulfilled_capacity += added_weight
@@ -3124,6 +3130,25 @@ class SpotFleetRequest(TaggedEC2Resource):
self.spot_requests = [req for req in self.spot_requests if req.instance.id not in instance_ids]
self.ec2_backend.terminate_instances(instance_ids)
+ def _parse_tag_specifications(self, spec):
+ try:
+ tag_spec_num = max([int(key.split('.')[1]) for key in spec if key.startswith("tag_specification_set")])
+ except ValueError: # no tag specifications
+ return {}
+
+ tag_specifications = {}
+ for si in range(1, tag_spec_num + 1):
+ resource_type = spec["tag_specification_set.{si}._resource_type".format(si=si)]
+
+ tags = [key for key in spec if key.startswith("tag_specification_set.{si}._tag".format(si=si))]
+ tag_num = max([int(key.split('.')[3]) for key in tags])
+ tag_specifications[resource_type] = dict((
+ spec["tag_specification_set.{si}._tag.{ti}._key".format(si=si, ti=ti)],
+ spec["tag_specification_set.{si}._tag.{ti}._value".format(si=si, ti=ti)],
+ ) for ti in range(1, tag_num + 1))
+
+ return tag_specifications
+
class SpotFleetBackend(object):
def __init__(self):
@@ -3560,8 +3585,22 @@ class NetworkAclBackend(object):
self.get_vpc(vpc_id)
network_acl = NetworkAcl(self, network_acl_id, vpc_id, default)
self.network_acls[network_acl_id] = network_acl
+ if default:
+ self.add_default_entries(network_acl_id)
return network_acl
+ def add_default_entries(self, network_acl_id):
+ default_acl_entries = [
+ {'rule_number': 100, 'rule_action': 'allow', 'egress': 'true'},
+ {'rule_number': 32767, 'rule_action': 'deny', 'egress': 'true'},
+ {'rule_number': 100, 'rule_action': 'allow', 'egress': 'false'},
+ {'rule_number': 32767, 'rule_action': 'deny', 'egress': 'false'}
+ ]
+ for entry in default_acl_entries:
+ self.create_network_acl_entry(network_acl_id=network_acl_id, rule_number=entry['rule_number'], protocol='-1',
+ rule_action=entry['rule_action'], egress=entry['egress'], cidr_block='0.0.0.0/0',
+ icmp_code=None, icmp_type=None, port_range_from=None, port_range_to=None)
+
def get_all_network_acls(self, network_acl_ids=None, filters=None):
network_acls = self.network_acls.values()
@@ -3636,9 +3675,9 @@ class NetworkAclBackend(object):
new_acl.associations[new_assoc_id] = association
return association
- def associate_default_network_acl_with_subnet(self, subnet_id):
+ def associate_default_network_acl_with_subnet(self, subnet_id, vpc_id):
association_id = random_network_acl_subnet_association_id()
- acl = next(acl for acl in self.network_acls.values() if acl.default)
+ acl = next(acl for acl in self.network_acls.values() if acl.default and acl.vpc_id == vpc_id)
acl.associations[association_id] = NetworkAclAssociation(self, association_id,
subnet_id, acl.id)
diff --git a/moto/ec2/responses/spot_fleets.py b/moto/ec2/responses/spot_fleets.py
index 0366af9d6..bb9aeb4ca 100644
--- a/moto/ec2/responses/spot_fleets.py
+++ b/moto/ec2/responses/spot_fleets.py
@@ -107,6 +107,21 @@ DESCRIBE_SPOT_FLEET_TEMPLATE = """ 128:
+ raise TagKeyTooBig(tag_key, param=exception_param)
+
+ # Validate that the tag key fits the proper Regex:
+ # [\w\s_.:/=+\-@]+ SHOULD be the same as the Java regex on the AWS documentation: [\p{L}\p{Z}\p{N}_.:/=+\-@]+
+ match = re.findall(r'[\w\s_.:/=+\-@]+', tag_key)
+ # Kudos if you can come up with a better way of doing a global search :)
+ if not len(match) or len(match[0]) < len(tag_key):
+ raise InvalidTagCharacters(tag_key, param=exception_param)
+
+ def _check_tag_duplicate(self, all_tags, tag_key):
+ """Validates that a tag key is not a duplicate
+
+ :param all_tags: Dict to check if there is a duplicate tag.
+ :param tag_key: The tag key to check against.
+ :return:
+ """
+ if tag_key in all_tags:
+ raise DuplicateTags()
+
+ def list_role_tags(self, role_name, marker, max_items=100):
+ role = self.get_role(role_name)
+
+ max_items = int(max_items)
+ tag_index = sorted(role.tags)
+ start_idx = int(marker) if marker else 0
+
+ tag_index = tag_index[start_idx:start_idx + max_items]
+
+ if len(role.tags) <= (start_idx + max_items):
+ marker = None
+ else:
+ marker = str(start_idx + max_items)
+
+ # Make the tag list of dict's:
+ tags = [role.tags[tag] for tag in tag_index]
+
+ return tags, marker
+
+ def tag_role(self, role_name, tags):
+ if len(tags) > 50:
+ raise TooManyTags(tags)
+
+ role = self.get_role(role_name)
+
+ tag_keys = {}
+ for tag in tags:
+ # Need to index by the lowercase tag key since the keys are case insensitive, but their case is retained.
+ ref_key = tag['Key'].lower()
+ self._check_tag_duplicate(tag_keys, ref_key)
+ self._validate_tag_key(tag['Key'])
+ if len(tag['Value']) > 256:
+ raise TagValueTooBig(tag['Value'])
+
+ tag_keys[ref_key] = tag
+
+ role.tags.update(tag_keys)
+
+ def untag_role(self, role_name, tag_keys):
+ if len(tag_keys) > 50:
+ raise TooManyTags(tag_keys, param='tagKeys')
+
+ role = self.get_role(role_name)
+
+ for key in tag_keys:
+ ref_key = key.lower()
+ self._validate_tag_key(key, exception_param='tagKeys')
+
+ role.tags.pop(ref_key, None)
+
def create_policy_version(self, policy_arn, policy_document, set_as_default):
policy = self.get_policy(policy_arn)
if not policy:
diff --git a/moto/iam/responses.py b/moto/iam/responses.py
index d0e749d57..e3cc4b90b 100644
--- a/moto/iam/responses.py
+++ b/moto/iam/responses.py
@@ -554,7 +554,8 @@ class IamResponse(BaseResponse):
policies=account_details['managed_policies'],
users=account_details['users'],
groups=account_details['groups'],
- roles=account_details['roles']
+ roles=account_details['roles'],
+ get_groups_for_user=iam_backend.get_groups_for_user
)
def create_saml_provider(self):
@@ -625,6 +626,34 @@ class IamResponse(BaseResponse):
template = self.response_template(LIST_SIGNING_CERTIFICATES_TEMPLATE)
return template.render(user_name=user_name, certificates=certs)
+ def list_role_tags(self):
+ role_name = self._get_param('RoleName')
+ marker = self._get_param('Marker')
+ max_items = self._get_param('MaxItems', 100)
+
+ tags, marker = iam_backend.list_role_tags(role_name, marker, max_items)
+
+ template = self.response_template(LIST_ROLE_TAG_TEMPLATE)
+ return template.render(tags=tags, marker=marker)
+
+ def tag_role(self):
+ role_name = self._get_param('RoleName')
+ tags = self._get_multi_param('Tags.member')
+
+ iam_backend.tag_role(role_name, tags)
+
+ template = self.response_template(TAG_ROLE_TEMPLATE)
+ return template.render()
+
+ def untag_role(self):
+ role_name = self._get_param('RoleName')
+ tag_keys = self._get_multi_param('TagKeys.member')
+
+ iam_backend.untag_role(role_name, tag_keys)
+
+ template = self.response_template(UNTAG_ROLE_TEMPLATE)
+ return template.render()
+
ATTACH_ROLE_POLICY_TEMPLATE = """
@@ -878,6 +907,16 @@ GET_ROLE_TEMPLATE = """
{% for user in users %}
-
-
+
+ {% for group in get_groups_for_user(user.name) %}
+ {{ group.name }}
+ {% endfor %}
+
+
+ {% for policy in user.managed_policies %}
+
+ {{ user.managed_policies[policy].name }}
+ {{ policy }}
+
+ {% endfor %}
+
{{ user.id }}
{{ user.path }}
{{ user.name }}
@@ -1476,33 +1526,55 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """
{{ group.id }}
- {% for policy in group.managed_policies %}
-
- {{ policy.name }}
- {{ policy.arn }}
-
+ {% for policy_arn in group.managed_policies %}
+
+ {{ group.managed_policies[policy_arn].name }}
+ {{ policy_arn }}
+
{% endfor %}
{{ group.name }}
{{ group.path }}
{{ group.arn }}
{{ group.create_date }}
-
+
+ {% for policy in group.policies %}
+
+ {{ policy }}
+ {{ group.get_policy(policy) }}
+
+ {% endfor %}
+
{% endfor %}
{% for role in roles %}
-
-
- {% for policy in role.managed_policies %}
+
+ {% for inline_policy in role.policies %}
- {{ policy.name }}
- {{ policy.arn }}
+ {{ inline_policy }}
+ {{ role.policies[inline_policy] }}
+
+ {% endfor %}
+
+
+ {% for policy_arn in role.managed_policies %}
+
+ {{ role.managed_policies[policy_arn].name }}
+ {{ policy_arn }}
{% endfor %}
+
+ {% for tag in role.get_tags() %}
+
+ {{ tag['Key'] }}
+ {{ tag['Value'] }}
+
+ {% endfor %}
+
{% for profile in instance_profiles %}
@@ -1543,19 +1615,14 @@ GET_ACCOUNT_AUTHORIZATION_DETAILS_TEMPLATE = """{{ policy.id }}
{{ policy.path }}
+ {% for policy_version in policy.versions %}
-
- {"Version":"2012-10-17","Statement":{"Effect":"Allow",
- "Action":["iam:CreatePolicy","iam:CreatePolicyVersion",
- "iam:DeletePolicy","iam:DeletePolicyVersion","iam:GetPolicy",
- "iam:GetPolicyVersion","iam:ListPolicies",
- "iam:ListPolicyVersions","iam:SetDefaultPolicyVersion"],
- "Resource":"*"}}
-
- true
- v1
- 2012-05-09T16:27:11Z
+ {{ policy_version.document }}
+ {{ policy_version.is_default }}
+ {{ policy_version.version_id }}
+ {{ policy_version.create_datetime }}
+ {% endfor %}
{{ policy.arn }}
1
@@ -1671,3 +1738,38 @@ LIST_SIGNING_CERTIFICATES_TEMPLATE = """
7a62c49f-347e-4fc4-9331-6e8eEXAMPLE
"""
+
+
+TAG_ROLE_TEMPLATE = """
+
+ EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE
+
+"""
+
+
+LIST_ROLE_TAG_TEMPLATE = """
+
+ {{ 'true' if marker else 'false' }}
+ {% if marker %}
+ {{ marker }}
+ {% endif %}
+
+ {% for tag in tags %}
+
+ {{ tag['Key'] }}
+ {{ tag['Value'] }}
+
+ {% endfor %}
+
+
+
+ EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE
+
+"""
+
+
+UNTAG_ROLE_TEMPLATE = """
+
+ EXAMPLE8-90ab-cdef-fedc-ba987EXAMPLE
+
+"""
diff --git a/setup.py b/setup.py
index 74683836e..8ac06c4dd 100755
--- a/setup.py
+++ b/setup.py
@@ -21,8 +21,8 @@ def read(*parts):
install_requires = [
"Jinja2>=2.7.3",
"boto>=2.36.0",
- "boto3>=1.6.16",
- "botocore>=1.12.13",
+ "boto3>=1.9.86",
+ "botocore>=1.12.86",
"cryptography>=2.3.0",
"requests>=2.5",
"xmltodict",
diff --git a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
index eb7f6bcc5..d05bc1b53 100644
--- a/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
+++ b/tests/test_cloudformation/test_cloudformation_stack_crud_boto3.py
@@ -816,6 +816,32 @@ def test_create_change_set_from_s3_url():
assert 'arn:aws:cloudformation:us-east-1:123456789:stack/NewStack' in response['StackId']
+@mock_cloudformation
+def test_describe_change_set():
+ cf_conn = boto3.client('cloudformation', region_name='us-east-1')
+ cf_conn.create_change_set(
+ StackName='NewStack',
+ TemplateBody=dummy_template_json,
+ ChangeSetName='NewChangeSet',
+ ChangeSetType='CREATE',
+ )
+
+ stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet")
+ stack['ChangeSetName'].should.equal('NewChangeSet')
+ stack['StackName'].should.equal('NewStack')
+
+ cf_conn.create_change_set(
+ StackName='NewStack',
+ TemplateBody=dummy_update_template_json,
+ ChangeSetName='NewChangeSet2',
+ ChangeSetType='UPDATE',
+ )
+ stack = cf_conn.describe_change_set(ChangeSetName="NewChangeSet2")
+ stack['ChangeSetName'].should.equal('NewChangeSet2')
+ stack['StackName'].should.equal('NewStack')
+ stack['Changes'].should.have.length_of(2)
+
+
@mock_cloudformation
def test_execute_change_set_w_arn():
cf_conn = boto3.client('cloudformation', region_name='us-east-1')
@@ -837,7 +863,7 @@ def test_execute_change_set_w_name():
ChangeSetName='NewChangeSet',
ChangeSetType='CREATE',
)
- cf_conn.execute_change_set(ChangeSetName='NewStack', StackName='NewStack')
+ cf_conn.execute_change_set(ChangeSetName='NewChangeSet', StackName='NewStack')
@mock_cloudformation
@@ -906,6 +932,20 @@ def test_describe_stack_by_stack_id():
stack_by_id['StackName'].should.equal("test_stack")
+@mock_cloudformation
+def test_list_change_sets():
+ cf_conn = boto3.client('cloudformation', region_name='us-east-1')
+ cf_conn.create_change_set(
+ StackName='NewStack2',
+ TemplateBody=dummy_template_json,
+ ChangeSetName='NewChangeSet2',
+ ChangeSetType='CREATE',
+ )
+ change_set = cf_conn.list_change_sets(StackName='NewStack2')['Summaries'][0]
+ change_set['StackName'].should.equal('NewStack2')
+ change_set['ChangeSetName'].should.equal('NewChangeSet2')
+
+
@mock_cloudformation
def test_list_stacks():
cf = boto3.resource('cloudformation', region_name='us-east-1')
@@ -938,6 +978,22 @@ def test_delete_stack_from_resource():
list(cf.stacks.all()).should.have.length_of(0)
+@mock_cloudformation
+@mock_ec2
+def test_delete_change_set():
+ cf_conn = boto3.client('cloudformation', region_name='us-east-1')
+ cf_conn.create_change_set(
+ StackName='NewStack',
+ TemplateBody=dummy_template_json,
+ ChangeSetName='NewChangeSet',
+ ChangeSetType='CREATE',
+ )
+
+ cf_conn.list_change_sets(StackName='NewStack')['Summaries'].should.have.length_of(1)
+ cf_conn.delete_change_set(ChangeSetName='NewChangeSet', StackName='NewStack')
+ cf_conn.list_change_sets(StackName='NewStack')['Summaries'].should.have.length_of(0)
+
+
@mock_cloudformation
@mock_ec2
def test_delete_stack_by_name():
diff --git a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py
index 15e5284b7..874804db0 100644
--- a/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py
+++ b/tests/test_dynamodb2/test_dynamodb_table_without_range_key.py
@@ -750,6 +750,47 @@ def test_boto3_update_item_conditions_pass_because_expect_exists_by_compare_to_n
returned_item = table.get_item(Key={'username': 'johndoe'})
assert dict(returned_item)['Item']['foo'].should.equal("baz")
+
+@mock_dynamodb2
+def test_boto3_update_settype_item_with_conditions():
+ class OrderedSet(set):
+ """A set with predictable iteration order"""
+ def __init__(self, values):
+ super(OrderedSet, self).__init__(values)
+ self.__ordered_values = values
+
+ def __iter__(self):
+ return iter(self.__ordered_values)
+
+ table = _create_user_table()
+ table.put_item(Item={'username': 'johndoe'})
+ table.update_item(
+ Key={'username': 'johndoe'},
+ UpdateExpression='SET foo=:new_value',
+ ExpressionAttributeValues={
+ ':new_value': OrderedSet(['hello', 'world']),
+ },
+ )
+
+ table.update_item(
+ Key={'username': 'johndoe'},
+ UpdateExpression='SET foo=:new_value',
+ ExpressionAttributeValues={
+ ':new_value': set(['baz']),
+ },
+ Expected={
+ 'foo': {
+ 'ComparisonOperator': 'EQ',
+ 'AttributeValueList': [
+ OrderedSet(['world', 'hello']), # Opposite order to original
+ ],
+ }
+ },
+ )
+ returned_item = table.get_item(Key={'username': 'johndoe'})
+ assert dict(returned_item)['Item']['foo'].should.equal(set(['baz']))
+
+
@mock_dynamodb2
def test_boto3_put_item_conditions_pass():
table = _create_user_table()
diff --git a/tests/test_ec2/test_network_acls.py b/tests/test_ec2/test_network_acls.py
index fd2ec105e..9c92c949e 100644
--- a/tests/test_ec2/test_network_acls.py
+++ b/tests/test_ec2/test_network_acls.py
@@ -1,8 +1,9 @@
from __future__ import unicode_literals
import boto
+import boto3
import sure # noqa
-from moto import mock_ec2_deprecated
+from moto import mock_ec2_deprecated, mock_ec2
@mock_ec2_deprecated
@@ -173,3 +174,43 @@ def test_network_acl_tagging():
if na.id == network_acl.id)
test_network_acl.tags.should.have.length_of(1)
test_network_acl.tags["a key"].should.equal("some value")
+
+
+@mock_ec2
+def test_new_subnet_in_new_vpc_associates_with_default_network_acl():
+ ec2 = boto3.resource('ec2', region_name='us-west-1')
+ new_vpc = ec2.create_vpc(CidrBlock='10.0.0.0/16')
+ new_vpc.reload()
+
+ subnet = ec2.create_subnet(VpcId=new_vpc.id, CidrBlock='10.0.0.0/24')
+ subnet.reload()
+
+ new_vpcs_default_network_acl = next(iter(new_vpc.network_acls.all()), None)
+ new_vpcs_default_network_acl.reload()
+ new_vpcs_default_network_acl.vpc_id.should.equal(new_vpc.id)
+ new_vpcs_default_network_acl.associations.should.have.length_of(1)
+ new_vpcs_default_network_acl.associations[0]['SubnetId'].should.equal(subnet.id)
+
+
+@mock_ec2
+def test_default_network_acl_default_entries():
+ ec2 = boto3.resource('ec2', region_name='us-west-1')
+ default_network_acl = next(iter(ec2.network_acls.all()), None)
+ default_network_acl.is_default.should.be.ok
+
+ default_network_acl.entries.should.have.length_of(4)
+ unique_entries = []
+ for entry in default_network_acl.entries:
+ entry['CidrBlock'].should.equal('0.0.0.0/0')
+ entry['Protocol'].should.equal('-1')
+ entry['RuleNumber'].should.be.within([100, 32767])
+ entry['RuleAction'].should.be.within(['allow', 'deny'])
+ assert type(entry['Egress']) is bool
+ if entry['RuleAction'] == 'allow':
+ entry['RuleNumber'].should.be.equal(100)
+ else:
+ entry['RuleNumber'].should.be.equal(32767)
+ if entry not in unique_entries:
+ unique_entries.append(entry)
+
+ unique_entries.should.have.length_of(4)
diff --git a/tests/test_ec2/test_spot_fleet.py b/tests/test_ec2/test_spot_fleet.py
index a2bd1d061..190f3b1f1 100644
--- a/tests/test_ec2/test_spot_fleet.py
+++ b/tests/test_ec2/test_spot_fleet.py
@@ -54,7 +54,7 @@ def spot_config(subnet_id, allocation_strategy="lowestPrice"):
},
'EbsOptimized': False,
'WeightedCapacity': 2.0,
- 'SpotPrice': '0.13'
+ 'SpotPrice': '0.13',
}, {
'ImageId': 'ami-123',
'KeyName': 'my-key',
@@ -148,6 +148,48 @@ def test_create_diversified_spot_fleet():
instances[0]['InstanceId'].should.contain("i-")
+@mock_ec2
+def test_create_spot_fleet_request_with_tag_spec():
+ conn = boto3.client("ec2", region_name='us-west-2')
+ subnet_id = get_subnet_id(conn)
+
+ tag_spec = [
+ {
+ 'ResourceType': 'instance',
+ 'Tags': [
+ {
+ 'Key': 'tag-1',
+ 'Value': 'foo',
+ },
+ {
+ 'Key': 'tag-2',
+ 'Value': 'bar',
+ },
+ ]
+ },
+ ]
+ config = spot_config(subnet_id)
+ config['LaunchSpecifications'][0]['TagSpecifications'] = tag_spec
+ spot_fleet_res = conn.request_spot_fleet(
+ SpotFleetRequestConfig=config
+ )
+ spot_fleet_id = spot_fleet_res['SpotFleetRequestId']
+ spot_fleet_requests = conn.describe_spot_fleet_requests(
+ SpotFleetRequestIds=[spot_fleet_id])['SpotFleetRequestConfigs']
+ spot_fleet_config = spot_fleet_requests[0]['SpotFleetRequestConfig']
+ spot_fleet_config['LaunchSpecifications'][0]['TagSpecifications'][0][
+ 'ResourceType'].should.equal('instance')
+ for tag in tag_spec[0]['Tags']:
+ spot_fleet_config['LaunchSpecifications'][0]['TagSpecifications'][0]['Tags'].should.contain(tag)
+
+ instance_res = conn.describe_spot_fleet_instances(
+ SpotFleetRequestId=spot_fleet_id)
+ instances = conn.describe_instances(InstanceIds=[i['InstanceId'] for i in instance_res['ActiveInstances']])
+ for instance in instances['Reservations'][0]['Instances']:
+ for tag in tag_spec[0]['Tags']:
+ instance['Tags'].should.contain(tag)
+
+
@mock_ec2
def test_cancel_spot_fleet_request():
conn = boto3.client("ec2", region_name='us-west-2')
diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py
index 2b5f16d77..d5f1bb4f9 100644
--- a/tests/test_iam/test_iam.py
+++ b/tests/test_iam/test_iam.py
@@ -306,6 +306,7 @@ def test_create_policy_versions():
PolicyDocument='{"some":"policy"}')
version.get('PolicyVersion').get('Document').should.equal({'some': 'policy'})
+
@mock_iam
def test_get_policy():
conn = boto3.client('iam', region_name='us-east-1')
@@ -579,6 +580,7 @@ def test_get_credential_report():
'get_credential_report_result']['content'].encode('ascii')).decode('ascii')
report.should.match(r'.*my-user.*')
+
@mock_iam
def test_boto3_get_credential_report():
conn = boto3.client('iam', region_name='us-east-1')
@@ -757,6 +759,17 @@ def test_get_access_key_last_used():
@mock_iam
def test_get_account_authorization_details():
import json
+ test_policy = json.dumps({
+ "Version": "2012-10-17",
+ "Statement": [
+ {
+ "Action": "s3:ListBucket",
+ "Resource": "*",
+ "Effect": "Allow",
+ }
+ ]
+ })
+
conn = boto3.client('iam', region_name='us-east-1')
conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="some policy", Path="/my-path/")
conn.create_user(Path='/', UserName='testUser')
@@ -764,21 +777,34 @@ def test_get_account_authorization_details():
conn.create_policy(
PolicyName='testPolicy',
Path='/',
- PolicyDocument=json.dumps({
- "Version": "2012-10-17",
- "Statement": [
- {
- "Action": "s3:ListBucket",
- "Resource": "*",
- "Effect": "Allow",
- }
- ]
- }),
+ PolicyDocument=test_policy,
Description='Test Policy'
)
+ # Attach things to the user and group:
+ conn.put_user_policy(UserName='testUser', PolicyName='testPolicy', PolicyDocument=test_policy)
+ conn.put_group_policy(GroupName='testGroup', PolicyName='testPolicy', PolicyDocument=test_policy)
+
+ conn.attach_user_policy(UserName='testUser', PolicyArn='arn:aws:iam::123456789012:policy/testPolicy')
+ conn.attach_group_policy(GroupName='testGroup', PolicyArn='arn:aws:iam::123456789012:policy/testPolicy')
+
+ conn.add_user_to_group(UserName='testUser', GroupName='testGroup')
+
+ # Add things to the role:
conn.create_instance_profile(InstanceProfileName='ipn')
conn.add_role_to_instance_profile(InstanceProfileName='ipn', RoleName='my-role')
+ conn.tag_role(RoleName='my-role', Tags=[
+ {
+ 'Key': 'somekey',
+ 'Value': 'somevalue'
+ },
+ {
+ 'Key': 'someotherkey',
+ 'Value': 'someothervalue'
+ }
+ ])
+ conn.put_role_policy(RoleName='my-role', PolicyName='test-policy', PolicyDocument=test_policy)
+ conn.attach_role_policy(RoleName='my-role', PolicyArn='arn:aws:iam::123456789012:policy/testPolicy')
result = conn.get_account_authorization_details(Filter=['Role'])
assert len(result['RoleDetailList']) == 1
@@ -786,24 +812,41 @@ def test_get_account_authorization_details():
assert len(result['GroupDetailList']) == 0
assert len(result['Policies']) == 0
assert len(result['RoleDetailList'][0]['InstanceProfileList']) == 1
+ assert len(result['RoleDetailList'][0]['Tags']) == 2
+ assert len(result['RoleDetailList'][0]['RolePolicyList']) == 1
+ assert len(result['RoleDetailList'][0]['AttachedManagedPolicies']) == 1
+ assert result['RoleDetailList'][0]['AttachedManagedPolicies'][0]['PolicyName'] == 'testPolicy'
+ assert result['RoleDetailList'][0]['AttachedManagedPolicies'][0]['PolicyArn'] == \
+ 'arn:aws:iam::123456789012:policy/testPolicy'
result = conn.get_account_authorization_details(Filter=['User'])
assert len(result['RoleDetailList']) == 0
assert len(result['UserDetailList']) == 1
+ assert len(result['UserDetailList'][0]['GroupList']) == 1
+ assert len(result['UserDetailList'][0]['AttachedManagedPolicies']) == 1
assert len(result['GroupDetailList']) == 0
assert len(result['Policies']) == 0
+ assert result['UserDetailList'][0]['AttachedManagedPolicies'][0]['PolicyName'] == 'testPolicy'
+ assert result['UserDetailList'][0]['AttachedManagedPolicies'][0]['PolicyArn'] == \
+ 'arn:aws:iam::123456789012:policy/testPolicy'
result = conn.get_account_authorization_details(Filter=['Group'])
assert len(result['RoleDetailList']) == 0
assert len(result['UserDetailList']) == 0
assert len(result['GroupDetailList']) == 1
+ assert len(result['GroupDetailList'][0]['GroupPolicyList']) == 1
+ assert len(result['GroupDetailList'][0]['AttachedManagedPolicies']) == 1
assert len(result['Policies']) == 0
+ assert result['GroupDetailList'][0]['AttachedManagedPolicies'][0]['PolicyName'] == 'testPolicy'
+ assert result['GroupDetailList'][0]['AttachedManagedPolicies'][0]['PolicyArn'] == \
+ 'arn:aws:iam::123456789012:policy/testPolicy'
result = conn.get_account_authorization_details(Filter=['LocalManagedPolicy'])
assert len(result['RoleDetailList']) == 0
assert len(result['UserDetailList']) == 0
assert len(result['GroupDetailList']) == 0
assert len(result['Policies']) == 1
+ assert len(result['Policies'][0]['PolicyVersionList']) == 1
# Check for greater than 1 since this should always be greater than one but might change.
# See iam/aws_managed_policies.py
@@ -872,6 +915,7 @@ def test_signing_certs():
with assert_raises(ClientError):
client.delete_signing_certificate(UserName='notauser', CertificateId=cert_id)
+
@mock_iam()
def test_create_saml_provider():
conn = boto3.client('iam', region_name='us-east-1')
@@ -881,6 +925,7 @@ def test_create_saml_provider():
)
response['SAMLProviderArn'].should.equal("arn:aws:iam::123456789012:saml-provider/TestSAMLProvider")
+
@mock_iam()
def test_get_saml_provider():
conn = boto3.client('iam', region_name='us-east-1')
@@ -893,6 +938,7 @@ def test_get_saml_provider():
)
response['SAMLMetadataDocument'].should.equal('a' * 1024)
+
@mock_iam()
def test_list_saml_providers():
conn = boto3.client('iam', region_name='us-east-1')
@@ -903,6 +949,7 @@ def test_list_saml_providers():
response = conn.list_saml_providers()
response['SAMLProviderList'][0]['Arn'].should.equal("arn:aws:iam::123456789012:saml-provider/TestSAMLProvider")
+
@mock_iam()
def test_delete_saml_provider():
conn = boto3.client('iam', region_name='us-east-1')
@@ -929,3 +976,178 @@ def test_delete_saml_provider():
# Verify that it's not in the list:
resp = conn.list_signing_certificates(UserName='testing')
assert not resp['Certificates']
+
+
+@mock_iam()
+def test_tag_role():
+ """Tests both the tag_role and get_role_tags capability"""
+ conn = boto3.client('iam', region_name='us-east-1')
+ conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="{}")
+
+ # Get without tags:
+ role = conn.get_role(RoleName='my-role')['Role']
+ assert not role.get('Tags')
+
+ # With proper tag values:
+ conn.tag_role(RoleName='my-role', Tags=[
+ {
+ 'Key': 'somekey',
+ 'Value': 'somevalue'
+ },
+ {
+ 'Key': 'someotherkey',
+ 'Value': 'someothervalue'
+ }
+ ])
+
+ # Get role:
+ role = conn.get_role(RoleName='my-role')['Role']
+ assert len(role['Tags']) == 2
+ assert role['Tags'][0]['Key'] == 'somekey'
+ assert role['Tags'][0]['Value'] == 'somevalue'
+ assert role['Tags'][1]['Key'] == 'someotherkey'
+ assert role['Tags'][1]['Value'] == 'someothervalue'
+
+ # Same -- but for list_role_tags:
+ tags = conn.list_role_tags(RoleName='my-role')
+ assert len(tags['Tags']) == 2
+ assert role['Tags'][0]['Key'] == 'somekey'
+ assert role['Tags'][0]['Value'] == 'somevalue'
+ assert role['Tags'][1]['Key'] == 'someotherkey'
+ assert role['Tags'][1]['Value'] == 'someothervalue'
+ assert not tags['IsTruncated']
+ assert not tags.get('Marker')
+
+ # Test pagination:
+ tags = conn.list_role_tags(RoleName='my-role', MaxItems=1)
+ assert len(tags['Tags']) == 1
+ assert tags['IsTruncated']
+ assert tags['Tags'][0]['Key'] == 'somekey'
+ assert tags['Tags'][0]['Value'] == 'somevalue'
+ assert tags['Marker'] == '1'
+
+ tags = conn.list_role_tags(RoleName='my-role', Marker=tags['Marker'])
+ assert len(tags['Tags']) == 1
+ assert tags['Tags'][0]['Key'] == 'someotherkey'
+ assert tags['Tags'][0]['Value'] == 'someothervalue'
+ assert not tags['IsTruncated']
+ assert not tags.get('Marker')
+
+ # Test updating an existing tag:
+ conn.tag_role(RoleName='my-role', Tags=[
+ {
+ 'Key': 'somekey',
+ 'Value': 'somenewvalue'
+ }
+ ])
+ tags = conn.list_role_tags(RoleName='my-role')
+ assert len(tags['Tags']) == 2
+ assert tags['Tags'][0]['Key'] == 'somekey'
+ assert tags['Tags'][0]['Value'] == 'somenewvalue'
+
+ # Empty is good:
+ conn.tag_role(RoleName='my-role', Tags=[
+ {
+ 'Key': 'somekey',
+ 'Value': ''
+ }
+ ])
+ tags = conn.list_role_tags(RoleName='my-role')
+ assert len(tags['Tags']) == 2
+ assert tags['Tags'][0]['Key'] == 'somekey'
+ assert tags['Tags'][0]['Value'] == ''
+
+ # Test creating tags with invalid values:
+ # With more than 50 tags:
+ with assert_raises(ClientError) as ce:
+ too_many_tags = list(map(lambda x: {'Key': str(x), 'Value': str(x)}, range(0, 51)))
+ conn.tag_role(RoleName='my-role', Tags=too_many_tags)
+ assert 'failed to satisfy constraint: Member must have length less than or equal to 50.' \
+ in ce.exception.response['Error']['Message']
+
+ # With a duplicate tag:
+ with assert_raises(ClientError) as ce:
+ conn.tag_role(RoleName='my-role', Tags=[{'Key': '0', 'Value': ''}, {'Key': '0', 'Value': ''}])
+ assert 'Duplicate tag keys found. Please note that Tag keys are case insensitive.' \
+ in ce.exception.response['Error']['Message']
+
+ # Duplicate tag with different casing:
+ with assert_raises(ClientError) as ce:
+ conn.tag_role(RoleName='my-role', Tags=[{'Key': 'a', 'Value': ''}, {'Key': 'A', 'Value': ''}])
+ assert 'Duplicate tag keys found. Please note that Tag keys are case insensitive.' \
+ in ce.exception.response['Error']['Message']
+
+ # With a really big key:
+ with assert_raises(ClientError) as ce:
+ conn.tag_role(RoleName='my-role', Tags=[{'Key': '0' * 129, 'Value': ''}])
+ assert 'Member must have length less than or equal to 128.' in ce.exception.response['Error']['Message']
+
+ # With a really big value:
+ with assert_raises(ClientError) as ce:
+ conn.tag_role(RoleName='my-role', Tags=[{'Key': '0', 'Value': '0' * 257}])
+ assert 'Member must have length less than or equal to 256.' in ce.exception.response['Error']['Message']
+
+ # With an invalid character:
+ with assert_raises(ClientError) as ce:
+ conn.tag_role(RoleName='my-role', Tags=[{'Key': 'NOWAY!', 'Value': ''}])
+ assert 'Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]+' \
+ in ce.exception.response['Error']['Message']
+
+ # With a role that doesn't exist:
+ with assert_raises(ClientError):
+ conn.tag_role(RoleName='notarole', Tags=[{'Key': 'some', 'Value': 'value'}])
+
+
+@mock_iam
+def test_untag_role():
+ conn = boto3.client('iam', region_name='us-east-1')
+ conn.create_role(RoleName="my-role", AssumeRolePolicyDocument="{}")
+
+ # With proper tag values:
+ conn.tag_role(RoleName='my-role', Tags=[
+ {
+ 'Key': 'somekey',
+ 'Value': 'somevalue'
+ },
+ {
+ 'Key': 'someotherkey',
+ 'Value': 'someothervalue'
+ }
+ ])
+
+ # Remove them:
+ conn.untag_role(RoleName='my-role', TagKeys=['somekey'])
+ tags = conn.list_role_tags(RoleName='my-role')
+ assert len(tags['Tags']) == 1
+ assert tags['Tags'][0]['Key'] == 'someotherkey'
+ assert tags['Tags'][0]['Value'] == 'someothervalue'
+
+ # And again:
+ conn.untag_role(RoleName='my-role', TagKeys=['someotherkey'])
+ tags = conn.list_role_tags(RoleName='my-role')
+ assert not tags['Tags']
+
+ # Test removing tags with invalid values:
+ # With more than 50 tags:
+ with assert_raises(ClientError) as ce:
+ conn.untag_role(RoleName='my-role', TagKeys=[str(x) for x in range(0, 51)])
+ assert 'failed to satisfy constraint: Member must have length less than or equal to 50.' \
+ in ce.exception.response['Error']['Message']
+ assert 'tagKeys' in ce.exception.response['Error']['Message']
+
+ # With a really big key:
+ with assert_raises(ClientError) as ce:
+ conn.untag_role(RoleName='my-role', TagKeys=['0' * 129])
+ assert 'Member must have length less than or equal to 128.' in ce.exception.response['Error']['Message']
+ assert 'tagKeys' in ce.exception.response['Error']['Message']
+
+ # With an invalid character:
+ with assert_raises(ClientError) as ce:
+ conn.untag_role(RoleName='my-role', TagKeys=['NOWAY!'])
+ assert 'Member must satisfy regular expression pattern: [\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]+' \
+ in ce.exception.response['Error']['Message']
+ assert 'tagKeys' in ce.exception.response['Error']['Message']
+
+ # With a role that doesn't exist:
+ with assert_raises(ClientError):
+ conn.untag_role(RoleName='notarole', TagKeys=['somevalue'])