From 84100c44831a2060ceb33a36ecdab339bbf68d0b Mon Sep 17 00:00:00 2001 From: usmankb Date: Wed, 29 Apr 2020 00:28:19 +0530 Subject: [PATCH 01/19] enhancement Create-VPC-endpoint --- moto/ec2/models.py | 98 +++++++++++++++++++++++++++++ moto/ec2/responses/vpcs.py | 65 +++++++++++++++++++ moto/ec2/utils.py | 16 +++++ tests/test_ec2/test_route_tables.py | 36 +++++++++++ 4 files changed, 215 insertions(+) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index ce9c3ef5b..118fc6804 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -104,6 +104,7 @@ from .utils import ( random_internet_gateway_id, random_ip, random_ipv6_cidr, + randor_ipv4_cidr, random_launch_template_id, random_nat_gateway_id, random_key_pair, @@ -112,6 +113,8 @@ from .utils import ( random_reservation_id, random_route_table_id, generate_route_id, + generate_vpc_end_point_id, + create_dns_entries, split_route_id, random_security_group_id, random_snapshot_id, @@ -2735,6 +2738,7 @@ class VPCBackend(object): def __init__(self): self.vpcs = {} + self.vpc_end_points = {} self.vpc_refs[self.__class__].add(weakref.ref(self)) super(VPCBackend, self).__init__() @@ -2877,6 +2881,66 @@ class VPCBackend(object): vpc = self.get_vpc(vpc_id) return vpc.associate_vpc_cidr_block(cidr_block, amazon_provided_ipv6_cidr_block) + def create_vpc_endpoint(self, + vpc_id, + service_name, + type=None, + policy_document=False, + route_table_ids=None, + subnet_ids=[], + network_interface_ids=[], + dns_entries=None, + client_token=None, + security_group=None, + tag_specifications=None, + private_dns_enabled=None + ): + + vpc_endpoint_id = generate_vpc_end_point_id(vpc_id) + + #validates if vpc is present or not. + self.get_vpc(vpc_id) + + if type == "interface" or "Interface ": + + network_interface_ids = [] + for subnet_id in subnet_ids: + self.get_subnet(subnet_id) + eni = self.create_network_interface(subnet_id, random_private_ip()) + network_interface_ids.append(eni.id) + + dns_entries = create_dns_entries(service_name, vpc_endpoint_id) + + else : + # considering gateway if type is not mentioned. + service_destination_cidr = randor_ipv4_cidr() + + for route_table_id in route_table_ids: + self.create_route( + route_table_id, + service_destination_cidr + ) + + vpc_end_point = VPCEndPoint( + vpc_endpoint_id, + vpc_id, + service_name, + type, + policy_document, + route_table_ids, + subnet_ids, + network_interface_ids, + [dns_entries], + client_token, + security_group, + tag_specifications, + private_dns_enabled + ) + + self.vpc_end_points[vpc_endpoint_id] = vpc_end_point + + return vpc_end_point + class VPCPeeringConnectionStatus(object): def __init__(self, code="initiating-request", message=""): @@ -3485,6 +3549,40 @@ class Route(object): return route_table +class VPCEndPoint(TaggedEC2Resource): + def __init__( + self, + id, + vpc_id, + service_name, + type=None, + policy_document=False, + route_table_ids=None, + subnet_ids =None, + network_interface_ids=None, + dns_entries=None, + client_token=None, + security_group=None, + tag_specifications=None, + private_dns_enabled=None, + ): + + self.id = id + self.vpc_id = vpc_id + self.service_name = service_name + self.type = type + self.policy_document = policy_document + self.route_table_ids = route_table_ids + self.network_interface_ids = network_interface_ids + self.subnet_ids = subnet_ids + self.client_token = client_token + self.security_group = security_group + self.tag_specifications = tag_specifications + self.private_dns_enabled = private_dns_enabled + self.created_at = datetime.utcnow() + self.dns_entries = dns_entries + + class RouteBackend(object): def __init__(self): super(RouteBackend, self).__init__() diff --git a/moto/ec2/responses/vpcs.py b/moto/ec2/responses/vpcs.py index 0fd198378..2af4c0b29 100644 --- a/moto/ec2/responses/vpcs.py +++ b/moto/ec2/responses/vpcs.py @@ -163,6 +163,36 @@ class VPCs(BaseResponse): cidr_block_state="disassociating", ) + def create_vpc_endpoint(self): + vpc_id = self._get_param("VpcId") + service_name = self._get_param("ServiceName") + route_table_ids = self._get_multi_param("RouteTableId") + subnet_ids = self._get_multi_param("SubnetId") + type = self._get_param("VpcEndpointType") + policy_document = self._get_param("PolicyDocument") + client_token = self._get_param("ClientToken") + tag_specifications = self._get_param("TagSpecifications") + private_dns_enabled = self._get_param("PrivateDNSEnabled") + security_group = self._get_param("SecurityGroup") + + vpc_end_point = self.ec2_backend.create_vpc_endpoint( + vpc_id=vpc_id, + service_name=service_name, + type=type, + policy_document=policy_document, + route_table_ids=route_table_ids, + subnet_ids=subnet_ids, + client_token=client_token, + security_group=security_group, + tag_specifications=tag_specifications, + private_dns_enabled=private_dns_enabled + ) + + template = self.response_template(CREATE_VPC_END_POINT) + return template.render( + vpc_end_point=vpc_end_point + ) + CREATE_VPC_RESPONSE = """ @@ -384,3 +414,38 @@ IPV6_DISASSOCIATE_VPC_CIDR_BLOCK_RESPONSE = """ """ + +CREATE_VPC_END_POINT = """ + + {{ vpc_end_point.policy_document }} + available + false + {{ vpc_end_point.service_name }} + {{ vpc_end_point.vpc_id }} + {{ vpc_end_point.id }} + + {% for routeid in vpc_end_point.route_table_ids %} + {{ routeid }} + {% endfor %} + + + {% for network_interface_id in vpc_end_point.network_interface_ids %} + {{ network_interface_id }} + {% endfor %} + + + {% for subnetId in vpc_end_point.subnet_ids %} + {{ subnetId }} + {% endfor %} + + + {% for entry in vpc_end_point.dns_entries %} + + {{ entry["hosted_zone_id"] }} + {{ entry["dns_name"] }} + + {% endfor %} + + {{ vpc_end_point.created_at }} + +""" \ No newline at end of file diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 3b363e45d..408ee9be5 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -181,6 +181,10 @@ def random_ip(): ) +def randor_ipv4_cidr(): + return "10.0.{}.{}/16".format(random.randint(0, 255), random.randint(0, 255)) + + def random_ipv6_cidr(): return "2400:6500:{}:{}::/56".format(random_resource_id(4), random_resource_id(4)) @@ -189,6 +193,18 @@ def generate_route_id(route_table_id, cidr_block): return "%s~%s" % (route_table_id, cidr_block) +def generate_vpc_end_point_id(vpc_id): + return "%s-%s" % ('vpce', vpc_id[4:]) + + +def create_dns_entries(service_name, vpc_endpoint_id): + dns_entries = {} + dns_entries["dns_name"] = "{}-{}.{}".format(vpc_endpoint_id, + random_resource_id(8), service_name) + dns_entries["hosted_zone_id"] = random_resource_id(13).upper() + return dns_entries + + def split_route_id(route_id): values = route_id.split("~") return values[0], values[1] diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index 347464691..182776f95 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -618,3 +618,39 @@ def test_describe_route_tables_with_nat_gateway(): nat_gw_routes[0]["DestinationCidrBlock"].should.equal("0.0.0.0/0") nat_gw_routes[0]["NatGatewayId"].should.equal(nat_gw_id) nat_gw_routes[0]["State"].should.equal("active") + + +@mock_ec2 +def test_create_vpc_end_point(): + + ec2 = boto3.client("ec2", region_name="us-west-1") + vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") + subnet = ec2.create_subnet(VpcId=vpc["Vpc"]["VpcId"], + CidrBlock="10.0.0.0/24") + + route_table = ec2.create_route_table(VpcId=vpc["Vpc"]["VpcId"]) + + vpc_end_point = ec2.create_vpc_endpoint( + VpcId=vpc["Vpc"]["VpcId"], + ServiceName="com.amazonaws.us-east-1.s3", + RouteTableIds=[route_table["RouteTable"]["RouteTableId"]] + ) + + vpc_end_point["VpcEndpoint"]["ServiceName"].\ + should.equal("com.amazonaws.us-east-1.s3") + vpc_end_point["VpcEndpoint"]["RouteTableIds"][0].\ + should.equal(route_table["RouteTable"]["RouteTableId"]) + vpc_end_point["VpcEndpoint"]["VpcId"].should.equal(vpc["Vpc"]["VpcId"]) + + vpc_end_point = ec2.create_vpc_endpoint( + VpcId=vpc["Vpc"]["VpcId"], + ServiceName="com.amazonaws.us-east-1.s3", + SubnetIds=[subnet["Subnet"]["SubnetId"]], + VpcEndpointType="interface" + ) + + vpc_end_point["VpcEndpoint"]["ServiceName"].\ + should.equal("com.amazonaws.us-east-1.s3") + vpc_end_point["VpcEndpoint"]["SubnetIds"][0].\ + should.equal(subnet["Subnet"]["SubnetId"]) + vpc_end_point["VpcEndpoint"]["VpcId"].should.equal(vpc["Vpc"]["VpcId"]) From f4888da33498592d6a1e25a073ddf4673947afa6 Mon Sep 17 00:00:00 2001 From: usmankb Date: Wed, 29 Apr 2020 18:02:02 +0530 Subject: [PATCH 02/19] added test asserts and review comments --- moto/ec2/models.py | 6 ++++-- moto/ec2/responses/vpcs.py | 2 ++ tests/test_ec2/test_route_tables.py | 19 +++++++++++++++++++ 3 files changed, 25 insertions(+), 2 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index 118fc6804..c35fa339d 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -2901,7 +2901,7 @@ class VPCBackend(object): #validates if vpc is present or not. self.get_vpc(vpc_id) - if type == "interface" or "Interface ": + if type and type.lower() == "interface": network_interface_ids = [] for subnet_id in subnet_ids: @@ -2920,6 +2920,8 @@ class VPCBackend(object): route_table_id, service_destination_cidr ) + if dns_entries: + dns_entries = [dns_entries] vpc_end_point = VPCEndPoint( vpc_endpoint_id, @@ -2930,7 +2932,7 @@ class VPCBackend(object): route_table_ids, subnet_ids, network_interface_ids, - [dns_entries], + dns_entries, client_token, security_group, tag_specifications, diff --git a/moto/ec2/responses/vpcs.py b/moto/ec2/responses/vpcs.py index 2af4c0b29..4b0aa76d8 100644 --- a/moto/ec2/responses/vpcs.py +++ b/moto/ec2/responses/vpcs.py @@ -439,12 +439,14 @@ CREATE_VPC_END_POINT = """ {{ entry["dns_name"] }} {% endfor %} + {% endif %} {{ vpc_end_point.created_at }} diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index 182776f95..f8be8dc80 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -630,6 +630,7 @@ def test_create_vpc_end_point(): route_table = ec2.create_route_table(VpcId=vpc["Vpc"]["VpcId"]) + # test without any end point type specified vpc_end_point = ec2.create_vpc_endpoint( VpcId=vpc["Vpc"]["VpcId"], ServiceName="com.amazonaws.us-east-1.s3", @@ -641,7 +642,24 @@ def test_create_vpc_end_point(): vpc_end_point["VpcEndpoint"]["RouteTableIds"][0].\ should.equal(route_table["RouteTable"]["RouteTableId"]) vpc_end_point["VpcEndpoint"]["VpcId"].should.equal(vpc["Vpc"]["VpcId"]) + vpc_end_point["VpcEndpoint"]["DnsEntries"].should.have.length_of(0) + # test with any end point type as gateway + vpc_end_point = ec2.create_vpc_endpoint( + VpcId=vpc["Vpc"]["VpcId"], + ServiceName="com.amazonaws.us-east-1.s3", + RouteTableIds=[route_table["RouteTable"]["RouteTableId"]], + VpcEndpointType="gateway" + ) + + vpc_end_point["VpcEndpoint"]["ServiceName"]. \ + should.equal("com.amazonaws.us-east-1.s3") + vpc_end_point["VpcEndpoint"]["RouteTableIds"][0]. \ + should.equal(route_table["RouteTable"]["RouteTableId"]) + vpc_end_point["VpcEndpoint"]["VpcId"].should.equal(vpc["Vpc"]["VpcId"]) + vpc_end_point["VpcEndpoint"]["DnsEntries"].should.have.length_of(0) + + # test with end point type as interface vpc_end_point = ec2.create_vpc_endpoint( VpcId=vpc["Vpc"]["VpcId"], ServiceName="com.amazonaws.us-east-1.s3", @@ -654,3 +672,4 @@ def test_create_vpc_end_point(): vpc_end_point["VpcEndpoint"]["SubnetIds"][0].\ should.equal(subnet["Subnet"]["SubnetId"]) vpc_end_point["VpcEndpoint"]["VpcId"].should.equal(vpc["Vpc"]["VpcId"]) + len(vpc_end_point["VpcEndpoint"]["DnsEntries"]).should.be.greater_than(0) \ No newline at end of file From 2d0087d500ad392fd017b603f94b9030a63f5a52 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Wed, 29 Apr 2020 16:29:25 +0100 Subject: [PATCH 03/19] Linting --- moto/ec2/models.py | 42 ++++++++++++++-------------- moto/ec2/responses/vpcs.py | 26 ++++++++--------- moto/ec2/utils.py | 7 +++-- tests/test_ec2/test_route_tables.py | 43 ++++++++++++++++------------- 4 files changed, 60 insertions(+), 58 deletions(-) diff --git a/moto/ec2/models.py b/moto/ec2/models.py index c35fa339d..edc216eb3 100644 --- a/moto/ec2/models.py +++ b/moto/ec2/models.py @@ -2881,24 +2881,25 @@ class VPCBackend(object): vpc = self.get_vpc(vpc_id) return vpc.associate_vpc_cidr_block(cidr_block, amazon_provided_ipv6_cidr_block) - def create_vpc_endpoint(self, - vpc_id, - service_name, - type=None, - policy_document=False, - route_table_ids=None, - subnet_ids=[], - network_interface_ids=[], - dns_entries=None, - client_token=None, - security_group=None, - tag_specifications=None, - private_dns_enabled=None - ): + def create_vpc_endpoint( + self, + vpc_id, + service_name, + type=None, + policy_document=False, + route_table_ids=None, + subnet_ids=[], + network_interface_ids=[], + dns_entries=None, + client_token=None, + security_group=None, + tag_specifications=None, + private_dns_enabled=None, + ): vpc_endpoint_id = generate_vpc_end_point_id(vpc_id) - #validates if vpc is present or not. + # validates if vpc is present or not. self.get_vpc(vpc_id) if type and type.lower() == "interface": @@ -2911,15 +2912,12 @@ class VPCBackend(object): dns_entries = create_dns_entries(service_name, vpc_endpoint_id) - else : + else: # considering gateway if type is not mentioned. service_destination_cidr = randor_ipv4_cidr() for route_table_id in route_table_ids: - self.create_route( - route_table_id, - service_destination_cidr - ) + self.create_route(route_table_id, service_destination_cidr) if dns_entries: dns_entries = [dns_entries] @@ -2936,7 +2934,7 @@ class VPCBackend(object): client_token, security_group, tag_specifications, - private_dns_enabled + private_dns_enabled, ) self.vpc_end_points[vpc_endpoint_id] = vpc_end_point @@ -3560,7 +3558,7 @@ class VPCEndPoint(TaggedEC2Resource): type=None, policy_document=False, route_table_ids=None, - subnet_ids =None, + subnet_ids=None, network_interface_ids=None, dns_entries=None, client_token=None, diff --git a/moto/ec2/responses/vpcs.py b/moto/ec2/responses/vpcs.py index 4b0aa76d8..59222207d 100644 --- a/moto/ec2/responses/vpcs.py +++ b/moto/ec2/responses/vpcs.py @@ -176,22 +176,20 @@ class VPCs(BaseResponse): security_group = self._get_param("SecurityGroup") vpc_end_point = self.ec2_backend.create_vpc_endpoint( - vpc_id=vpc_id, - service_name=service_name, - type=type, - policy_document=policy_document, - route_table_ids=route_table_ids, - subnet_ids=subnet_ids, - client_token=client_token, - security_group=security_group, - tag_specifications=tag_specifications, - private_dns_enabled=private_dns_enabled + vpc_id=vpc_id, + service_name=service_name, + type=type, + policy_document=policy_document, + route_table_ids=route_table_ids, + subnet_ids=subnet_ids, + client_token=client_token, + security_group=security_group, + tag_specifications=tag_specifications, + private_dns_enabled=private_dns_enabled, ) template = self.response_template(CREATE_VPC_END_POINT) - return template.render( - vpc_end_point=vpc_end_point - ) + return template.render(vpc_end_point=vpc_end_point) CREATE_VPC_RESPONSE = """ @@ -450,4 +448,4 @@ CREATE_VPC_END_POINT = """ """ diff --git a/moto/ec2/utils.py b/moto/ec2/utils.py index 408ee9be5..c07c470a9 100644 --- a/moto/ec2/utils.py +++ b/moto/ec2/utils.py @@ -194,13 +194,14 @@ def generate_route_id(route_table_id, cidr_block): def generate_vpc_end_point_id(vpc_id): - return "%s-%s" % ('vpce', vpc_id[4:]) + return "%s-%s" % ("vpce", vpc_id[4:]) def create_dns_entries(service_name, vpc_endpoint_id): dns_entries = {} - dns_entries["dns_name"] = "{}-{}.{}".format(vpc_endpoint_id, - random_resource_id(8), service_name) + dns_entries["dns_name"] = "{}-{}.{}".format( + vpc_endpoint_id, random_resource_id(8), service_name + ) dns_entries["hosted_zone_id"] = random_resource_id(13).upper() return dns_entries diff --git a/tests/test_ec2/test_route_tables.py b/tests/test_ec2/test_route_tables.py index f8be8dc80..a64fbae1a 100644 --- a/tests/test_ec2/test_route_tables.py +++ b/tests/test_ec2/test_route_tables.py @@ -625,8 +625,7 @@ def test_create_vpc_end_point(): ec2 = boto3.client("ec2", region_name="us-west-1") vpc = ec2.create_vpc(CidrBlock="10.0.0.0/16") - subnet = ec2.create_subnet(VpcId=vpc["Vpc"]["VpcId"], - CidrBlock="10.0.0.0/24") + subnet = ec2.create_subnet(VpcId=vpc["Vpc"]["VpcId"], CidrBlock="10.0.0.0/24") route_table = ec2.create_route_table(VpcId=vpc["Vpc"]["VpcId"]) @@ -634,13 +633,15 @@ def test_create_vpc_end_point(): vpc_end_point = ec2.create_vpc_endpoint( VpcId=vpc["Vpc"]["VpcId"], ServiceName="com.amazonaws.us-east-1.s3", - RouteTableIds=[route_table["RouteTable"]["RouteTableId"]] - ) + RouteTableIds=[route_table["RouteTable"]["RouteTableId"]], + ) - vpc_end_point["VpcEndpoint"]["ServiceName"].\ - should.equal("com.amazonaws.us-east-1.s3") - vpc_end_point["VpcEndpoint"]["RouteTableIds"][0].\ - should.equal(route_table["RouteTable"]["RouteTableId"]) + vpc_end_point["VpcEndpoint"]["ServiceName"].should.equal( + "com.amazonaws.us-east-1.s3" + ) + vpc_end_point["VpcEndpoint"]["RouteTableIds"][0].should.equal( + route_table["RouteTable"]["RouteTableId"] + ) vpc_end_point["VpcEndpoint"]["VpcId"].should.equal(vpc["Vpc"]["VpcId"]) vpc_end_point["VpcEndpoint"]["DnsEntries"].should.have.length_of(0) @@ -649,13 +650,15 @@ def test_create_vpc_end_point(): VpcId=vpc["Vpc"]["VpcId"], ServiceName="com.amazonaws.us-east-1.s3", RouteTableIds=[route_table["RouteTable"]["RouteTableId"]], - VpcEndpointType="gateway" + VpcEndpointType="gateway", ) - vpc_end_point["VpcEndpoint"]["ServiceName"]. \ - should.equal("com.amazonaws.us-east-1.s3") - vpc_end_point["VpcEndpoint"]["RouteTableIds"][0]. \ - should.equal(route_table["RouteTable"]["RouteTableId"]) + vpc_end_point["VpcEndpoint"]["ServiceName"].should.equal( + "com.amazonaws.us-east-1.s3" + ) + vpc_end_point["VpcEndpoint"]["RouteTableIds"][0].should.equal( + route_table["RouteTable"]["RouteTableId"] + ) vpc_end_point["VpcEndpoint"]["VpcId"].should.equal(vpc["Vpc"]["VpcId"]) vpc_end_point["VpcEndpoint"]["DnsEntries"].should.have.length_of(0) @@ -664,12 +667,14 @@ def test_create_vpc_end_point(): VpcId=vpc["Vpc"]["VpcId"], ServiceName="com.amazonaws.us-east-1.s3", SubnetIds=[subnet["Subnet"]["SubnetId"]], - VpcEndpointType="interface" + VpcEndpointType="interface", ) - vpc_end_point["VpcEndpoint"]["ServiceName"].\ - should.equal("com.amazonaws.us-east-1.s3") - vpc_end_point["VpcEndpoint"]["SubnetIds"][0].\ - should.equal(subnet["Subnet"]["SubnetId"]) + vpc_end_point["VpcEndpoint"]["ServiceName"].should.equal( + "com.amazonaws.us-east-1.s3" + ) + vpc_end_point["VpcEndpoint"]["SubnetIds"][0].should.equal( + subnet["Subnet"]["SubnetId"] + ) vpc_end_point["VpcEndpoint"]["VpcId"].should.equal(vpc["Vpc"]["VpcId"]) - len(vpc_end_point["VpcEndpoint"]["DnsEntries"]).should.be.greater_than(0) \ No newline at end of file + len(vpc_end_point["VpcEndpoint"]["DnsEntries"]).should.be.greater_than(0) From d6d2a38c76ba15051424c237ece0b12b6c1d5d11 Mon Sep 17 00:00:00 2001 From: Bert Blommers Date: Thu, 30 Apr 2020 12:11:33 +0100 Subject: [PATCH 04/19] Fix circular import issue --- moto/core/responses.py | 5 ++++- moto/{core => iam}/access_control.py | 3 +-- 2 files changed, 5 insertions(+), 3 deletions(-) rename moto/{core => iam}/access_control.py (99%) diff --git a/moto/core/responses.py b/moto/core/responses.py index 9a46f8ac5..508bd8c59 100644 --- a/moto/core/responses.py +++ b/moto/core/responses.py @@ -11,7 +11,6 @@ import requests import pytz -from moto.core.access_control import IAMRequest, S3IAMRequest from moto.core.exceptions import DryRunClientError from jinja2 import Environment, DictLoader, TemplateNotFound @@ -134,9 +133,13 @@ class ActionAuthenticatorMixin(object): ActionAuthenticatorMixin.request_count += 1 def _authenticate_and_authorize_normal_action(self): + from moto.iam.access_control import IAMRequest + self._authenticate_and_authorize_action(IAMRequest) def _authenticate_and_authorize_s3_action(self): + from moto.iam.access_control import S3IAMRequest + self._authenticate_and_authorize_action(S3IAMRequest) @staticmethod diff --git a/moto/core/access_control.py b/moto/iam/access_control.py similarity index 99% rename from moto/core/access_control.py rename to moto/iam/access_control.py index 8ba0c3ba1..bcde25d9e 100644 --- a/moto/core/access_control.py +++ b/moto/iam/access_control.py @@ -25,8 +25,6 @@ from botocore.credentials import Credentials from six import string_types from moto.core import ACCOUNT_ID -from moto.iam.models import Policy -from moto.iam import iam_backend from moto.core.exceptions import ( SignatureDoesNotMatchError, AccessDeniedError, @@ -44,6 +42,7 @@ from moto.s3.exceptions import ( S3SignatureDoesNotMatchError, ) from moto.sts import sts_backend +from .models import iam_backend, Policy log = logging.getLogger(__name__) From 72bc07f1129f87c89166fec37c953509ebac9948 Mon Sep 17 00:00:00 2001 From: zscholl Date: Wed, 11 Mar 2020 14:54:58 -0500 Subject: [PATCH 05/19] get access key create date for cred report --- .gitignore | 1 + moto/iam/models.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index fb9bd51de..deb9d9840 100644 --- a/.gitignore +++ b/.gitignore @@ -22,3 +22,4 @@ tests/file.tmp .eggs/ .mypy_cache/ *.tmp +.venv/ \ No newline at end of file diff --git a/moto/iam/models.py b/moto/iam/models.py index 08a1eb36a..dd197c872 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -680,14 +680,14 @@ class User(BaseModel): access_key_2_last_rotated = "N/A" elif len(self.access_keys) == 1: access_key_1_active = "true" - access_key_1_last_rotated = date_created.strftime(date_format) + access_key_1_last_rotated = self.access_keys[0].create_date.strftime(date_format) access_key_2_active = "false" access_key_2_last_rotated = "N/A" else: access_key_1_active = "true" - access_key_1_last_rotated = date_created.strftime(date_format) + access_key_1_last_rotated = self.access_keys[0].create_date.strftime(date_format) access_key_2_active = "true" - access_key_2_last_rotated = date_created.strftime(date_format) + access_key_2_last_rotated = self.access_keys[1].create_date.strftime(date_format) return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},{9},false,N/A,false,N/A".format( self.name, From fc5e6ebf512694a78228347b43024e7038cd3431 Mon Sep 17 00:00:00 2001 From: zscholl Date: Wed, 11 Mar 2020 15:00:47 -0500 Subject: [PATCH 06/19] formatting --- moto/iam/models.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index dd197c872..dfa6fd36a 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -680,14 +680,20 @@ class User(BaseModel): access_key_2_last_rotated = "N/A" elif len(self.access_keys) == 1: access_key_1_active = "true" - access_key_1_last_rotated = self.access_keys[0].create_date.strftime(date_format) + access_key_1_last_rotated = self.access_keys[0].create_date.strftime( + date_format + ) access_key_2_active = "false" access_key_2_last_rotated = "N/A" else: access_key_1_active = "true" - access_key_1_last_rotated = self.access_keys[0].create_date.strftime(date_format) + access_key_1_last_rotated = self.access_keys[0].create_date.strftime( + date_format + ) access_key_2_active = "true" - access_key_2_last_rotated = self.access_keys[1].create_date.strftime(date_format) + access_key_2_last_rotated = self.access_keys[1].create_date.strftime( + date_format + ) return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},{9},false,N/A,false,N/A".format( self.name, From 2f2d6dc3fecb30e7420678d7731ac2bfc9ac336d Mon Sep 17 00:00:00 2001 From: zscholl Date: Wed, 11 Mar 2020 15:07:08 -0500 Subject: [PATCH 07/19] newline --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index deb9d9840..02e812c5b 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,4 @@ tests/file.tmp .eggs/ .mypy_cache/ *.tmp -.venv/ \ No newline at end of file +.venv/ From 35fde06381a910331410105be05bf4b28a37eb49 Mon Sep 17 00:00:00 2001 From: zscholl Date: Thu, 12 Mar 2020 13:07:30 -0500 Subject: [PATCH 08/19] update last_used for access keys --- moto/core/utils.py | 2 +- moto/iam/models.py | 23 ++++++++++++++++++++--- moto/iam/responses.py | 5 +++++ 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/moto/core/utils.py b/moto/core/utils.py index dce9f675c..921f64be2 100644 --- a/moto/core/utils.py +++ b/moto/core/utils.py @@ -187,7 +187,7 @@ def iso_8601_datetime_with_milliseconds(datetime): def iso_8601_datetime_without_milliseconds(datetime): - return datetime.strftime("%Y-%m-%dT%H:%M:%S") + "Z" + return None if datetime is None else datetime.strftime("%Y-%m-%dT%H:%M:%S") + "Z" RFC1123 = "%a, %d %b %Y %H:%M:%S GMT" diff --git a/moto/iam/models.py b/moto/iam/models.py index dfa6fd36a..c84e664c3 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -464,7 +464,7 @@ class AccessKey(BaseModel): self.secret_access_key = random_alphanumeric(40) self.status = "Active" self.create_date = datetime.utcnow() - self.last_used = datetime.utcnow() + self.last_used = None @property def created_iso_8601(self): @@ -683,6 +683,11 @@ class User(BaseModel): access_key_1_last_rotated = self.access_keys[0].create_date.strftime( date_format ) + access_key_2_last_rotated = ( + "N/A" + if self.access_key[0].last_used is None + else self.access_key[0].last_used.strftime(date_format) + ) access_key_2_active = "false" access_key_2_last_rotated = "N/A" else: @@ -690,12 +695,22 @@ class User(BaseModel): access_key_1_last_rotated = self.access_keys[0].create_date.strftime( date_format ) + access_key_1_last_used = ( + "N/A" + if self.access_key[0].last_used is None + else self.access_key[0].last_used.strftime(date_format) + ) access_key_2_active = "true" access_key_2_last_rotated = self.access_keys[1].create_date.strftime( date_format ) + access_key_2_last_used = ( + "N/A" + if self.access_key[1].last_used is None + else self.access_key[1].last_used.strftime(date_format) + ) - return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},{9},false,N/A,false,N/A".format( + return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},not_supported,not_supported,{9},{10},{11},false,N/A,false,N/A".format( self.name, self.arn, date_created.strftime(date_format), @@ -704,8 +719,10 @@ class User(BaseModel): date_created.strftime(date_format), access_key_1_active, access_key_1_last_rotated, + access_key_1_last_used, access_key_2_active, access_key_2_last_rotated, + access_key_2_last_used, ) @@ -1805,7 +1822,7 @@ class IAMBackend(BaseBackend): def get_credential_report(self): if not self.credential_report: raise IAMReportNotPresentException("Credential report not present") - report = "user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_2_active,access_key_2_last_rotated,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated\n" + report = "user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated\n" for user in self.users: report += self.users[user].to_csv() return base64.b64encode(report.encode("ascii")).decode("ascii") diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 12501769e..947cccf33 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -1779,7 +1779,12 @@ GET_ACCESS_KEY_LAST_USED_TEMPLATE = """ {{ user_name }} + {{% if last_used % }} {{ last_used }} + {{% else % }} + N/A + N/A + {{% endif %}} From 54d816f09fbc1ca9e883be02249e66e6c5dbf120 Mon Sep 17 00:00:00 2001 From: zscholl Date: Thu, 12 Mar 2020 13:33:20 -0500 Subject: [PATCH 09/19] fix typo --- moto/iam/models.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index c84e664c3..6256d437a 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -697,8 +697,8 @@ class User(BaseModel): ) access_key_1_last_used = ( "N/A" - if self.access_key[0].last_used is None - else self.access_key[0].last_used.strftime(date_format) + if self.access_keys[0].last_used is None + else self.access_keys[0].last_used.strftime(date_format) ) access_key_2_active = "true" access_key_2_last_rotated = self.access_keys[1].create_date.strftime( @@ -706,8 +706,8 @@ class User(BaseModel): ) access_key_2_last_used = ( "N/A" - if self.access_key[1].last_used is None - else self.access_key[1].last_used.strftime(date_format) + if self.access_keys[1].last_used is None + else self.access_keys[1].last_used.strftime(date_format) ) return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},not_supported,not_supported,{9},{10},{11},false,N/A,false,N/A".format( From 9821eff1284109e0135b2486ce55acd418ca899d Mon Sep 17 00:00:00 2001 From: zscholl Date: Thu, 12 Mar 2020 15:06:40 -0500 Subject: [PATCH 10/19] add newline --- moto/iam/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index 6256d437a..304a06504 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -710,7 +710,7 @@ class User(BaseModel): else self.access_keys[1].last_used.strftime(date_format) ) - return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},not_supported,not_supported,{9},{10},{11},false,N/A,false,N/A".format( + return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},not_supported,not_supported,{9},{10},{11},false,N/A,false,N/A\n".format( self.name, self.arn, date_created.strftime(date_format), From b342a96cb0079de8ce27f5acfaba5c4496211bad Mon Sep 17 00:00:00 2001 From: zscholl Date: Thu, 12 Mar 2020 15:15:14 -0500 Subject: [PATCH 11/19] add fields --- moto/iam/models.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index 304a06504..b92c1f293 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -710,7 +710,7 @@ class User(BaseModel): else self.access_keys[1].last_used.strftime(date_format) ) - return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},not_supported,not_supported,{9},{10},{11},false,N/A,false,N/A\n".format( + return "{0},{1},{2},{3},{4},{5},not_supported,false,{6},{7},{8},not_supported,not_supported,{9},{10},{11},not_supported,not_supported,false,N/A,false,N/A\n".format( self.name, self.arn, date_created.strftime(date_format), From 09109f336c14285a3ea805127e994d7851f1d84f Mon Sep 17 00:00:00 2001 From: zscholl Date: Thu, 12 Mar 2020 15:57:54 -0500 Subject: [PATCH 12/19] more fixes --- moto/iam/models.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index b92c1f293..a01b8655a 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -676,20 +676,23 @@ class User(BaseModel): if len(self.access_keys) == 0: access_key_1_active = "false" access_key_1_last_rotated = "N/A" + access_key_1_last_used = "N/A" access_key_2_active = "false" access_key_2_last_rotated = "N/A" + access_key_2_last_used = "N/A" elif len(self.access_keys) == 1: access_key_1_active = "true" access_key_1_last_rotated = self.access_keys[0].create_date.strftime( date_format ) - access_key_2_last_rotated = ( + access_key_1_last_used = ( "N/A" - if self.access_key[0].last_used is None - else self.access_key[0].last_used.strftime(date_format) + if self.access_keys[0].last_used is None + else self.access_keys[0].last_used.strftime(date_format) ) access_key_2_active = "false" access_key_2_last_rotated = "N/A" + access_key_2_last_used = "N/A" else: access_key_1_active = "true" access_key_1_last_rotated = self.access_keys[0].create_date.strftime( From 1abff5727581286ac0fec63e96092aed65795e3e Mon Sep 17 00:00:00 2001 From: zscholl Date: Mon, 23 Mar 2020 14:46:46 -0500 Subject: [PATCH 13/19] add status to credential report --- moto/iam/models.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index a01b8655a..6da6c6742 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -681,7 +681,7 @@ class User(BaseModel): access_key_2_last_rotated = "N/A" access_key_2_last_used = "N/A" elif len(self.access_keys) == 1: - access_key_1_active = "true" + access_key_1_active = "true" if self.access_keys[0].status == "Active" else "false" access_key_1_last_rotated = self.access_keys[0].create_date.strftime( date_format ) @@ -694,7 +694,7 @@ class User(BaseModel): access_key_2_last_rotated = "N/A" access_key_2_last_used = "N/A" else: - access_key_1_active = "true" + access_key_1_active = "true" if self.access_keys[0].status == "Active" else "false" access_key_1_last_rotated = self.access_keys[0].create_date.strftime( date_format ) @@ -703,7 +703,7 @@ class User(BaseModel): if self.access_keys[0].last_used is None else self.access_keys[0].last_used.strftime(date_format) ) - access_key_2_active = "true" + access_key_2_active = "true" if self.access_keys[1].status == "Active" else "false" access_key_2_last_rotated = self.access_keys[1].create_date.strftime( date_format ) From 48304f81b18fbe75f306c90068796ef5653fddfe Mon Sep 17 00:00:00 2001 From: zscholl Date: Fri, 24 Apr 2020 13:16:13 -0500 Subject: [PATCH 14/19] fix last_used template --- moto/iam/responses.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 947cccf33..086ba508b 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -1779,12 +1779,12 @@ GET_ACCESS_KEY_LAST_USED_TEMPLATE = """ {{ user_name }} - {{% if last_used % }} + {% if last_used is defined %} {{ last_used }} - {{% else % }} + {% else %} N/A N/A - {{% endif %}} + {% endif %} From 51e7002cbb84c9524372f216115461e2ce5c3f12 Mon Sep 17 00:00:00 2001 From: zscholl Date: Wed, 29 Apr 2020 15:49:14 -0500 Subject: [PATCH 15/19] add tests --- moto/iam/models.py | 12 +++++-- moto/iam/responses.py | 9 +++--- tests/test_iam/test_iam.py | 64 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 74 insertions(+), 11 deletions(-) diff --git a/moto/iam/models.py b/moto/iam/models.py index 6da6c6742..d3907da26 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -681,7 +681,9 @@ class User(BaseModel): access_key_2_last_rotated = "N/A" access_key_2_last_used = "N/A" elif len(self.access_keys) == 1: - access_key_1_active = "true" if self.access_keys[0].status == "Active" else "false" + access_key_1_active = ( + "true" if self.access_keys[0].status == "Active" else "false" + ) access_key_1_last_rotated = self.access_keys[0].create_date.strftime( date_format ) @@ -694,7 +696,9 @@ class User(BaseModel): access_key_2_last_rotated = "N/A" access_key_2_last_used = "N/A" else: - access_key_1_active = "true" if self.access_keys[0].status == "Active" else "false" + access_key_1_active = ( + "true" if self.access_keys[0].status == "Active" else "false" + ) access_key_1_last_rotated = self.access_keys[0].create_date.strftime( date_format ) @@ -703,7 +707,9 @@ class User(BaseModel): if self.access_keys[0].last_used is None else self.access_keys[0].last_used.strftime(date_format) ) - access_key_2_active = "true" if self.access_keys[1].status == "Active" else "false" + access_key_2_active = ( + "true" if self.access_keys[1].status == "Active" else "false" + ) access_key_2_last_rotated = self.access_keys[1].create_date.strftime( date_format ) diff --git a/moto/iam/responses.py b/moto/iam/responses.py index 086ba508b..667a6d13b 100644 --- a/moto/iam/responses.py +++ b/moto/iam/responses.py @@ -1779,12 +1779,11 @@ GET_ACCESS_KEY_LAST_USED_TEMPLATE = """ {{ user_name }} - {% if last_used is defined %} - {{ last_used }} - {% else %} - N/A - N/A + {% if last_used %} + {{ last_used }} {% endif %} + N/A + N/A diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 995895437..986809bd5 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -4,6 +4,7 @@ import json import boto import boto3 +import csv import os import sure # noqa import sys @@ -12,12 +13,14 @@ from botocore.exceptions import ClientError from dateutil.tz import tzutc from moto import mock_iam, mock_iam_deprecated -from moto.iam.models import aws_managed_policies from moto.core import ACCOUNT_ID +from moto.iam.models import aws_managed_policies +from moto.backends import get_backend from nose.tools import assert_raises, assert_equals from nose.tools import raises from datetime import datetime +from datetime import timezone from tests.helpers import requires_boto_gte from uuid import uuid4 @@ -1215,6 +1218,44 @@ def test_boto3_get_credential_report(): report.should.match(r".*my-user.*") +@mock_iam +def test_boto3_get_credential_report_content(): + conn = boto3.client("iam", region_name="us-east-1") + username = "my-user" + conn.create_user(UserName=username) + key1 = conn.create_access_key(UserName=username)["AccessKey"] + conn.update_access_key( + UserName=username, AccessKeyId=key1["AccessKeyId"], Status="Inactive" + ) + key1 = conn.create_access_key(UserName=username)["AccessKey"] + iam_backend = get_backend("iam")["global"] + timestamp = datetime.now(tz=timezone.utc) + iam_backend.users[username].access_keys[1].last_used = timestamp + with assert_raises(ClientError): + conn.get_credential_report() + result = conn.generate_credential_report() + while result["State"] != "COMPLETE": + result = conn.generate_credential_report() + result = conn.get_credential_report() + report = result["Content"].decode("utf-8") + header = report.split("\n")[0] + header.should.equal( + "user,arn,user_creation_time,password_enabled,password_last_used,password_last_changed,password_next_rotation,mfa_active,access_key_1_active,access_key_1_last_rotated,access_key_1_last_used_date,access_key_1_last_used_region,access_key_1_last_used_service,access_key_2_active,access_key_2_last_rotated,access_key_2_last_used_date,access_key_2_last_used_region,access_key_2_last_used_service,cert_1_active,cert_1_last_rotated,cert_2_active,cert_2_last_rotated" + ) + report_dict = csv.DictReader(report.split("\n")) + user = next(report_dict) + user["user"].should.equal("my-user") + user["access_key_1_active"].should.equal("false") + user["access_key_1_last_rotated"].should.equal( + timestamp.isoformat(timespec="seconds") + ) + user["access_key_1_last_used_date"].should.equal("N/A") + user["access_key_2_active"].should.equal("true") + user["access_key_2_last_used_date"].should.equal( + timestamp.isoformat(timespec="seconds") + ) + + @requires_boto_gte("2.39") @mock_iam_deprecated() def test_managed_policy(): @@ -1382,7 +1423,7 @@ def test_update_access_key(): @mock_iam -def test_get_access_key_last_used(): +def test_get_access_key_last_used_when_unused(): iam = boto3.resource("iam", region_name="us-east-1") client = iam.meta.client username = "test-user" @@ -1393,11 +1434,28 @@ def test_get_access_key_last_used(): resp = client.get_access_key_last_used( AccessKeyId=create_key_response["AccessKeyId"] ) + resp["AccessKeyLastUsed"].should_not.contain("LastUsedDate") + resp["UserName"].should.equal(create_key_response["UserName"]) + +@mock_iam +def test_get_access_key_last_used_when_used(): + iam = boto3.resource("iam", region_name="us-east-1") + client = iam.meta.client + username = "test-user" + iam.create_user(UserName=username) + with assert_raises(ClientError): + client.get_access_key_last_used(AccessKeyId="non-existent-key-id") + create_key_response = client.create_access_key(UserName=username)["AccessKey"] + # Set last used date using the IAM backend. Moto currently does not have a mechanism for tracking usage of access keys + iam_backend = get_backend("iam")["global"] + iam_backend.users[username].access_keys[0].last_used = datetime.utcnow() + resp = client.get_access_key_last_used( + AccessKeyId=create_key_response["AccessKeyId"] + ) datetime.strftime( resp["AccessKeyLastUsed"]["LastUsedDate"], "%Y-%m-%d" ).should.equal(datetime.strftime(datetime.utcnow(), "%Y-%m-%d")) - resp["UserName"].should.equal(create_key_response["UserName"]) @mock_iam From 0423be259ada80786ea6b7b1d272470e5edf79b5 Mon Sep 17 00:00:00 2001 From: zscholl Date: Thu, 30 Apr 2020 08:44:45 -0500 Subject: [PATCH 16/19] remove backend logic & py27 incompatible timezone --- tests/test_iam/test_iam.py | 34 ++++------------------------------ 1 file changed, 4 insertions(+), 30 deletions(-) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 986809bd5..5a8ffe709 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -15,12 +15,10 @@ from dateutil.tz import tzutc from moto import mock_iam, mock_iam_deprecated from moto.core import ACCOUNT_ID from moto.iam.models import aws_managed_policies -from moto.backends import get_backend from nose.tools import assert_raises, assert_equals from nose.tools import raises from datetime import datetime -from datetime import timezone from tests.helpers import requires_boto_gte from uuid import uuid4 @@ -1228,9 +1226,7 @@ def test_boto3_get_credential_report_content(): UserName=username, AccessKeyId=key1["AccessKeyId"], Status="Inactive" ) key1 = conn.create_access_key(UserName=username)["AccessKey"] - iam_backend = get_backend("iam")["global"] - timestamp = datetime.now(tz=timezone.utc) - iam_backend.users[username].access_keys[1].last_used = timestamp + timestamp = datetime.utcnow() with assert_raises(ClientError): conn.get_credential_report() result = conn.generate_credential_report() @@ -1246,14 +1242,12 @@ def test_boto3_get_credential_report_content(): user = next(report_dict) user["user"].should.equal("my-user") user["access_key_1_active"].should.equal("false") - user["access_key_1_last_rotated"].should.equal( - timestamp.isoformat(timespec="seconds") + user["access_key_1_last_rotated"].should.match( + timestamp.strftime(datetime.utcnow(), "%Y-%m-%d") ) user["access_key_1_last_used_date"].should.equal("N/A") user["access_key_2_active"].should.equal("true") - user["access_key_2_last_used_date"].should.equal( - timestamp.isoformat(timespec="seconds") - ) + user["access_key_2_last_used_date"].should.equal("N/A") @requires_boto_gte("2.39") @@ -1438,26 +1432,6 @@ def test_get_access_key_last_used_when_unused(): resp["UserName"].should.equal(create_key_response["UserName"]) -@mock_iam -def test_get_access_key_last_used_when_used(): - iam = boto3.resource("iam", region_name="us-east-1") - client = iam.meta.client - username = "test-user" - iam.create_user(UserName=username) - with assert_raises(ClientError): - client.get_access_key_last_used(AccessKeyId="non-existent-key-id") - create_key_response = client.create_access_key(UserName=username)["AccessKey"] - # Set last used date using the IAM backend. Moto currently does not have a mechanism for tracking usage of access keys - iam_backend = get_backend("iam")["global"] - iam_backend.users[username].access_keys[0].last_used = datetime.utcnow() - resp = client.get_access_key_last_used( - AccessKeyId=create_key_response["AccessKeyId"] - ) - datetime.strftime( - resp["AccessKeyLastUsed"]["LastUsedDate"], "%Y-%m-%d" - ).should.equal(datetime.strftime(datetime.utcnow(), "%Y-%m-%d")) - - @mock_iam def test_upload_ssh_public_key(): iam = boto3.resource("iam", region_name="us-east-1") From 1f1404352e91a6e7f13801f9233a10299dcdece4 Mon Sep 17 00:00:00 2001 From: zscholl Date: Thu, 30 Apr 2020 09:42:22 -0500 Subject: [PATCH 17/19] use conditional TEST_SERVER_MODE for backend tests --- tests/test_iam/test_iam.py | 40 +++++++++++++++++++++++++++++++++----- 1 file changed, 35 insertions(+), 5 deletions(-) diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 5a8ffe709..6792d8f52 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -12,9 +12,10 @@ from boto.exception import BotoServerError from botocore.exceptions import ClientError from dateutil.tz import tzutc -from moto import mock_iam, mock_iam_deprecated +from moto import mock_iam, mock_iam_deprecated, settings from moto.core import ACCOUNT_ID from moto.iam.models import aws_managed_policies +from moto.backends import get_backend from nose.tools import assert_raises, assert_equals from nose.tools import raises @@ -1227,6 +1228,9 @@ def test_boto3_get_credential_report_content(): ) key1 = conn.create_access_key(UserName=username)["AccessKey"] timestamp = datetime.utcnow() + if not settings.TEST_SERVER_MODE: + iam_backend = get_backend("iam")["global"] + iam_backend.users[username].access_keys[1].last_used = timestamp with assert_raises(ClientError): conn.get_credential_report() result = conn.generate_credential_report() @@ -1242,12 +1246,38 @@ def test_boto3_get_credential_report_content(): user = next(report_dict) user["user"].should.equal("my-user") user["access_key_1_active"].should.equal("false") - user["access_key_1_last_rotated"].should.match( - timestamp.strftime(datetime.utcnow(), "%Y-%m-%d") - ) + user["access_key_1_last_rotated"].should.match(timestamp.strftime("%Y-%m-%d")) user["access_key_1_last_used_date"].should.equal("N/A") user["access_key_2_active"].should.equal("true") - user["access_key_2_last_used_date"].should.equal("N/A") + if not settings.TEST_SERVER_MODE: + user["access_key_2_last_used_date"].should.match(timestamp.strftime("%Y-%m-%d")) + else: + user["access_key_2_last_used_date"].should.equal("N/A") + + +@mock_iam +def test_get_access_key_last_used_when_used(): + iam = boto3.resource("iam", region_name="us-east-1") + client = iam.meta.client + username = "test-user" + iam.create_user(UserName=username) + with assert_raises(ClientError): + client.get_access_key_last_used(AccessKeyId="non-existent-key-id") + create_key_response = client.create_access_key(UserName=username)["AccessKey"] + # Set last used date using the IAM backend. Moto currently does not have a mechanism for tracking usage of access keys + if not settings.TEST_SERVER_MODE: + timestamp = datetime.utcnow() + iam_backend = get_backend("iam")["global"] + iam_backend.users[username].access_keys[0].last_used = timestamp + resp = client.get_access_key_last_used( + AccessKeyId=create_key_response["AccessKeyId"] + ) + if not settings.TEST_SERVER_MODE: + datetime.strftime( + resp["AccessKeyLastUsed"]["LastUsedDate"], "%Y-%m-%d" + ).should.equal(timestamp.strftime("%Y-%m-%d")) + else: + resp["AccessKeyLastUsed"].should_not.contain("LastUsedDate") @requires_boto_gte("2.39") From 95c459a86de317b390fdd3a9038bfb859b98e7d0 Mon Sep 17 00:00:00 2001 From: Chagui- Date: Thu, 30 Apr 2020 17:21:45 -0400 Subject: [PATCH 18/19] Added exception for deleting a group which has childs. Added better tests for delete_thing_group --- moto/iot/models.py | 9 +++++++++ tests/test_iot/test_iot.py | 41 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+) diff --git a/moto/iot/models.py b/moto/iot/models.py index 51a23b6c6..1f0623eb3 100644 --- a/moto/iot/models.py +++ b/moto/iot/models.py @@ -842,6 +842,15 @@ class IoTBackend(BaseBackend): return thing_group.thing_group_name, thing_group.arn, thing_group.thing_group_id def delete_thing_group(self, thing_group_name, expected_version): + child_groups = [ + thing_group + for _, thing_group in self.thing_groups.items() + if thing_group.parent_group_name == thing_group_name + ] + if len(child_groups) > 0: + raise InvalidRequestException( + f" Cannot delete thing group : {thing_group_name} when there are still child groups attached to it" + ) thing_group = self.describe_thing_group(thing_group_name) del self.thing_groups[thing_group.arn] diff --git a/tests/test_iot/test_iot.py b/tests/test_iot/test_iot.py index 2f43de5b9..58a820fee 100644 --- a/tests/test_iot/test_iot.py +++ b/tests/test_iot/test_iot.py @@ -756,6 +756,47 @@ def test_delete_principal_thing(): client.delete_certificate(certificateId=cert_id) +@mock_iot +def test_delete_thing_group(): + client = boto3.client("iot", region_name="ap-northeast-1") + group_name_1a = "my-group-name-1a" + group_name_2a = "my-group-name-2a" + # --1a + # |--2a + + # create thing groups tree + # 1 + thing_group1a = client.create_thing_group(thingGroupName=group_name_1a) + thing_group1a.should.have.key("thingGroupName").which.should.equal(group_name_1a) + thing_group1a.should.have.key("thingGroupArn") + # 2 + thing_group2a = client.create_thing_group( + thingGroupName=group_name_2a, parentGroupName=group_name_1a + ) + thing_group2a.should.have.key("thingGroupName").which.should.equal(group_name_2a) + thing_group2a.should.have.key("thingGroupArn") + + # delete group with child + try: + client.delete_thing_group(thingGroupName=group_name_1a) + except client.exceptions.InvalidRequestException as exc: + error_code = exc.response["Error"]["Code"] + error_code.should.equal("InvalidRequestException") + else: + raise Exception("Should have raised error") + + # delete child group + client.delete_thing_group(thingGroupName=group_name_2a) + res = client.list_thing_groups() + res.should.have.key("thingGroups").which.should.have.length_of(1) + res["thingGroups"].should_not.have.key(group_name_2a) + + # now that there is no child group, we can delete the previus group safely + client.delete_thing_group(thingGroupName=group_name_1a) + res = client.list_thing_groups() + res.should.have.key("thingGroups").which.should.have.length_of(0) + + @mock_iot def test_describe_thing_group_metadata_hierarchy(): client = boto3.client("iot", region_name="ap-northeast-1") From 5e4451b0d50b90c36ccbd0fdedea75406bf78ade Mon Sep 17 00:00:00 2001 From: Chagui- Date: Thu, 30 Apr 2020 20:54:51 -0400 Subject: [PATCH 19/19] Removed f string so python 2 can work correctly --- moto/iot/models.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/moto/iot/models.py b/moto/iot/models.py index 1f0623eb3..2e9979bda 100644 --- a/moto/iot/models.py +++ b/moto/iot/models.py @@ -849,7 +849,9 @@ class IoTBackend(BaseBackend): ] if len(child_groups) > 0: raise InvalidRequestException( - f" Cannot delete thing group : {thing_group_name} when there are still child groups attached to it" + " Cannot delete thing group : " + + thing_group_name + + " when there are still child groups attached to it" ) thing_group = self.describe_thing_group(thing_group_name) del self.thing_groups[thing_group.arn]