diff --git a/moto/config/models.py b/moto/config/models.py index 77f46e644..b8f31aa8d 100644 --- a/moto/config/models.py +++ b/moto/config/models.py @@ -48,6 +48,7 @@ from moto.config.exceptions import ( from moto.core import BaseBackend, BaseModel from moto.s3.config import s3_account_public_access_block_query, s3_config_query from moto.core import ACCOUNT_ID as DEFAULT_ACCOUNT_ID + from moto.iam.config import role_config_query, policy_config_query POP_STRINGS = [ @@ -68,6 +69,29 @@ RESOURCE_MAP = { "AWS::IAM::Policy": policy_config_query, } +CONFIG_REGIONS = [ + "af-south-1", + "ap-east-1", + "ap-northeast-1", + "ap-northeast-2", + "ap-south-1", + "ap-southeast-1", + "ap-southeast-2", + "ca-central-1", + "eu-central-1", + "eu-north-1", + "eu-south-1", + "eu-west-1", + "eu-west-2", + "eu-west-3", + "me-south-1", + "sa-east-1", + "us-east-1", + "us-east-2", + "us-west-1", + "us-west-2", +] + def datetime2int(date): return int(time.mktime(date.timetuple())) @@ -979,6 +1003,7 @@ class ConfigBackend(BaseBackend): limit, next_token, resource_region=resource_region, + aggregator=self.config_aggregators.get(aggregator_name).__dict__, ) resource_identifiers = [] @@ -989,7 +1014,6 @@ class ConfigBackend(BaseBackend): "ResourceType": identifier["type"], "ResourceId": identifier["id"], } - if identifier.get("name"): item["ResourceName"] = identifier["name"] diff --git a/moto/core/models.py b/moto/core/models.py index b8b4322be..a3f720658 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -28,7 +28,6 @@ from .utils import ( ) ACCOUNT_ID = os.environ.get("MOTO_ACCOUNT_ID", "123456789012") -CONFIG_BACKEND_DELIM = "\x1e" # Record Seperator "RS" ASCII Character class BaseMockAWS(object): @@ -723,6 +722,8 @@ class ConfigQueryModel(object): :param backend_region: The region for the backend to pull results from. Set to `None` if this is an aggregated query. :param resource_region: The region for where the resources reside to pull results from. Set to `None` if this is a non-aggregated query. + :param aggregator: If an aggregated query, this will be the `ConfigAggregator instance from the backend. Set to `None` + if a non-aggregated query. Useful if you need special logic based off the aggregator (ie IAM) :return: This should return a list of Dicts that have the following fields: [ { @@ -766,47 +767,6 @@ class ConfigQueryModel(object): """ raise NotImplementedError() - def aggregate_regions(self, path, backend_region, resource_region): - """ - This method is called for both aggregated and non-aggregated calls for config resources. - - It will figure out how to return the full list of resources for a given regional backend and append them to a final list. - It produces a list of both the region and the resource name with a delimiter character (CONFIG_BACKEND_DELIM, ASCII Record separator, \x1e). - IE: "us-east-1\x1ei-1234567800" - - You should only use this method if you need to aggregate resources over more than one region. - If your region is global, just query the global backend directly in the `list_config_service_resources` method - - If you use this method, your config-enabled resource must parse the delimited string in it's `list_config_service_resources` method. - ... - :param path: - A dict accessor string applied to the backend that locates resources inside that backend. -            For example, if you passed path="keypairs", and you were working with an ec2 moto backend, it would yield the contents from - ec2_moto_backend[region].keypairs - :param backend_region: - Only used for filtering; A string representing the region IE: us-east-1 - :param resource_region: - Only used for filtering; A string representing the region IE: us-east-1 - :return: - Returns a list of "region\x1eresourcename" strings - """ - - filter_region = backend_region or resource_region - if filter_region: - filter_resources = list(self.backends[filter_region].__dict__[path].keys()) - return list( - map( - lambda resource: "{}{}{}".format( - filter_region, CONFIG_BACKEND_DELIM, resource - ), - filter_resources, - ) - ) - - # If we don't have a filter region - ret = [] - for region in self.backends: - this_region_resources = list(self.backends[region].__dict__[path].keys()) - for resource in this_region_resources: - ret.append("{}{}{}".format(region, CONFIG_BACKEND_DELIM, resource)) - return ret - class base_decorator(object): mock_backend = MockAWS diff --git a/moto/iam/config.py b/moto/iam/config.py index fdf31b576..484153217 100644 --- a/moto/iam/config.py +++ b/moto/iam/config.py @@ -4,8 +4,6 @@ from moto.core.exceptions import InvalidNextTokenException from moto.core.models import ConfigQueryModel from moto.iam import iam_backends -CONFIG_BACKEND_DELIM = "\x1e" # Record Seperator "RS" ASCII Character - class RoleConfigQuery(ConfigQueryModel): def list_config_service_resources( @@ -16,6 +14,7 @@ class RoleConfigQuery(ConfigQueryModel): next_token, backend_region=None, resource_region=None, + aggregator=None, ): # IAM roles are "global" and aren't assigned into any availability zone # The resource ID is a AWS-assigned random string like "AROA0BSVNSZKXVHS00SBJ" @@ -31,12 +30,16 @@ class RoleConfigQuery(ConfigQueryModel): # Filter by resource name or ids if resource_name or resource_ids: filtered_roles = [] - # resource_name takes precendence over resource_ids + # resource_name takes precedence over resource_ids if resource_name: for role in role_list: if role.name == resource_name: filtered_roles = [role] break + # but if both are passed, it must be a subset + if filtered_roles and resource_ids: + if filtered_roles[0].id not in resource_ids: + return [], None else: for role in role_list: if role.id in resource_ids: @@ -45,10 +48,54 @@ class RoleConfigQuery(ConfigQueryModel): # Filtered roles are now the subject for the listing role_list = filtered_roles - # Pagination logic, sort by role id - sorted_roles = sorted(role_list, key=lambda role: role.id) - # sorted_role_ids matches indicies of sorted_roles - sorted_role_ids = list(map(lambda role: role.id, sorted_roles)) + if aggregator: + # IAM is a little special; Roles are created in us-east-1 (which AWS calls the "global" region) + # However, the resource will return in the aggregator (in duplicate) for each region in the aggregator + # Therefore, we'll need to find out the regions where the aggregators are running, and then duplicate the resource there + + # In practice, it looks like AWS will only duplicate these resources if you've "used" any roles in the region, but since + # we can't really tell if this has happened in moto, we'll just bind this to the regions in your aggregator + from moto.config.models import CONFIG_REGIONS + + aggregated_regions = [] + aggregator_sources = aggregator.get( + "account_aggregation_sources" + ) or aggregator.get("organization_aggregation_source") + for source in aggregator_sources: + source_dict = source.__dict__ + if source_dict["all_aws_regions"]: + aggregated_regions = CONFIG_REGIONS + break + for region in source_dict["aws_regions"]: + aggregated_regions.append(region) + + duplicate_role_list = [] + for region in list(set(aggregated_regions)): + for role in role_list: + duplicate_role_list.append( + { + "_id": "{}{}".format( + role.id, region + ), # this is only for sorting, isn't returned outside of this functin + "type": "AWS::IAM::Role", + "id": role.id, + "name": role.name, + "region": region, + } + ) + + # Pagination logic, sort by role id + sorted_roles = sorted(duplicate_role_list, key=lambda role: role["_id"]) + + # sorted_role_ids matches indicies of sorted_roles + sorted_role_ids = list(map(lambda role: role["_id"], sorted_roles)) + else: + # Non-aggregated queries are in the else block, and we can treat these like a normal config resource + # Pagination logic, sort by role id + sorted_roles = sorted(role_list, key=lambda role: role.id) + # sorted_role_ids matches indicies of sorted_roles + sorted_role_ids = list(map(lambda role: role.id, sorted_roles)) + new_token = None # Get the start: @@ -70,9 +117,9 @@ class RoleConfigQuery(ConfigQueryModel): [ { "type": "AWS::IAM::Role", - "id": role.id, - "name": role.name, - "region": "global", + "id": role["id"] if aggregator else role.id, + "name": role["name"] if aggregator else role.name, + "region": role["region"] if aggregator else "global", } for role in role_list ], @@ -114,6 +161,7 @@ class PolicyConfigQuery(ConfigQueryModel): next_token, backend_region=None, resource_region=None, + aggregator=None, ): # IAM policies are "global" and aren't assigned into any availability zone # The resource ID is a AWS-assigned random string like "ANPA0BSVNSZK00SJSPVUJ" @@ -126,8 +174,11 @@ class PolicyConfigQuery(ConfigQueryModel): # respect the configuration recorder's 'includeGlobalResourceTypes' setting, # but it's default set be default, and moto's config doesn't yet support # custom configuration recorders, we'll just behave as default. - policy_list = filter( - lambda policy: not policy.arn.startswith("arn:aws:iam::aws"), policy_list, + policy_list = list( + filter( + lambda policy: not policy.arn.startswith("arn:aws:iam::aws"), + policy_list, + ) ) if not policy_list: @@ -136,12 +187,17 @@ class PolicyConfigQuery(ConfigQueryModel): # Filter by resource name or ids if resource_name or resource_ids: filtered_policies = [] - # resource_name takes precendence over resource_ids + # resource_name takes precedence over resource_ids if resource_name: for policy in policy_list: if policy.name == resource_name: filtered_policies = [policy] break + # but if both are passed, it must be a subset + if filtered_policies and resource_ids: + if filtered_policies[0].id not in resource_ids: + return [], None + else: for policy in policy_list: if policy.id in resource_ids: @@ -150,10 +206,55 @@ class PolicyConfigQuery(ConfigQueryModel): # Filtered roles are now the subject for the listing policy_list = filtered_policies - # Pagination logic, sort by role id - sorted_policies = sorted(policy_list, key=lambda role: role.id) - # sorted_policy_ids matches indicies of sorted_policies - sorted_policy_ids = list(map(lambda policy: policy.id, sorted_policies)) + if aggregator: + # IAM is a little special; Policies are created in us-east-1 (which AWS calls the "global" region) + # However, the resource will return in the aggregator (in duplicate) for each region in the aggregator + # Therefore, we'll need to find out the regions where the aggregators are running, and then duplicate the resource there + + # In practice, it looks like AWS will only duplicate these resources if you've "used" any policies in the region, but since + # we can't really tell if this has happened in moto, we'll just bind this to the regions in your aggregator + from moto.config.models import CONFIG_REGIONS + + aggregated_regions = [] + aggregator_sources = aggregator.get( + "account_aggregation_sources" + ) or aggregator.get("organization_aggregation_source") + for source in aggregator_sources: + source_dict = source.__dict__ + if source_dict["all_aws_regions"]: + aggregated_regions = CONFIG_REGIONS + break + for region in source_dict["aws_regions"]: + aggregated_regions.append(region) + + duplicate_policy_list = [] + for region in list(set(aggregated_regions)): + for policy in policy_list: + duplicate_policy_list.append( + { + "_id": "{}{}".format( + policy.id, region + ), # this is only for sorting, isn't returned outside of this functin + "type": "AWS::IAM::Policy", + "id": policy.id, + "name": policy.name, + "region": region, + } + ) + + # Pagination logic, sort by role id + sorted_policies = sorted( + duplicate_policy_list, key=lambda policy: policy["_id"] + ) + + # sorted_policy_ids matches indicies of sorted_policies + sorted_policy_ids = list(map(lambda policy: policy["_id"], sorted_policies)) + else: + # Non-aggregated queries are in the else block, and we can treat these like a normal config resource + # Pagination logic, sort by role id + sorted_policies = sorted(policy_list, key=lambda role: role.id) + # sorted_policy_ids matches indicies of sorted_policies + sorted_policy_ids = list(map(lambda policy: policy.id, sorted_policies)) new_token = None @@ -176,9 +277,9 @@ class PolicyConfigQuery(ConfigQueryModel): [ { "type": "AWS::IAM::Policy", - "id": policy.id, - "name": policy.name, - "region": "global", + "id": policy["id"] if aggregator else policy.id, + "name": policy["name"] if aggregator else policy.name, + "region": policy["region"] if aggregator else "global", } for policy in policy_list ], diff --git a/moto/s3/config.py b/moto/s3/config.py index 04b4315f3..932ebc3be 100644 --- a/moto/s3/config.py +++ b/moto/s3/config.py @@ -19,6 +19,7 @@ class S3ConfigQuery(ConfigQueryModel): next_token, backend_region=None, resource_region=None, + aggregator=None, ): # The resource_region only matters for aggregated queries as you can filter on bucket regions for them. # For other resource types, you would need to iterate appropriately for the backend_region. @@ -132,6 +133,7 @@ class S3AccountPublicAccessBlockConfigQuery(ConfigQueryModel): next_token, backend_region=None, resource_region=None, + aggregator=None, ): # For the Account Public Access Block, they are the same for all regions. The resource ID is the AWS account ID # There is no resource name -- it should be a blank string "" if provided. diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index f71b96925..b662cc527 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -2905,27 +2905,62 @@ def test_role_list_config_discovered_resources(): None, ) - # Create a role - role_config_query.backends["global"].create_role( - role_name="something", - assume_role_policy_document=None, - path="/", - permissions_boundary=None, - description="something", - tags=[], - max_session_duration=3600, - ) + # Make 3 roles + roles = [] + num_roles = 3 + for ix in range(1, num_roles + 1): + this_role = role_config_query.backends["global"].create_role( + role_name="role{}".format(ix), + assume_role_policy_document=None, + path="/", + permissions_boundary=None, + description="role{}".format(ix), + tags=[{"Key": "foo", "Value": "bar"}], + max_session_duration=3600, + ) + roles.append( + {"id": this_role.id, "name": this_role.name,} + ) + + assert len(roles) == num_roles result = role_config_query.list_config_service_resources(None, None, 100, None)[0] - assert len(result) == 1 + assert len(result) == num_roles - # The role gets a random ID, so we have to grab it + # The roles gets a random ID, so we can't directly test it role = result[0] assert role["type"] == "AWS::IAM::Role" - assert len(role["id"]) == len(random_resource_id()) - assert role["name"] == "something" + assert role["id"] in list(map(lambda p: p["id"], roles)) + assert role["name"] in list(map(lambda p: p["name"], roles)) assert role["region"] == "global" + # test passing list of resource ids + resource_ids = role_config_query.list_config_service_resources( + [roles[0]["id"], roles[1]["id"]], None, 100, None + )[0] + assert len(resource_ids) == 2 + + # test passing a single resource name + resource_name = role_config_query.list_config_service_resources( + None, roles[0]["name"], 100, None + )[0] + assert len(resource_name) == 1 + assert resource_name[0]["id"] == roles[0]["id"] + assert resource_name[0]["name"] == roles[0]["name"] + + # test passing a single resource name AND some resource id's + both_filter_good = role_config_query.list_config_service_resources( + [roles[0]["id"], roles[1]["id"]], roles[0]["name"], 100, None + )[0] + assert len(both_filter_good) == 1 + assert both_filter_good[0]["id"] == roles[0]["id"] + assert both_filter_good[0]["name"] == roles[0]["name"] + + both_filter_bad = role_config_query.list_config_service_resources( + [roles[0]["id"], roles[1]["id"]], roles[2]["name"], 100, None + )[0] + assert len(both_filter_bad) == 0 + @mock_iam def test_role_config_dict(): @@ -3200,18 +3235,29 @@ def test_role_config_dict(): def test_role_config_client(): from moto.iam.models import ACCOUNT_ID from moto.iam.utils import random_resource_id + from moto.config.models import CONFIG_REGIONS iam_client = boto3.client("iam", region_name="us-west-2") config_client = boto3.client("config", region_name="us-west-2") - account_aggregation_source = { + all_account_aggregation_source = { "AccountIds": [ACCOUNT_ID], "AllAwsRegions": True, } + two_region_account_aggregation_source = { + "AccountIds": [ACCOUNT_ID], + "AwsRegions": ["us-east-1", "us-west-2"], + } + config_client.put_configuration_aggregator( ConfigurationAggregatorName="test_aggregator", - AccountAggregationSources=[account_aggregation_source], + AccountAggregationSources=[all_account_aggregation_source], + ) + + config_client.put_configuration_aggregator( + ConfigurationAggregatorName="test_aggregator_two_regions", + AccountAggregationSources=[two_region_account_aggregation_source], ) result = config_client.list_discovered_resources(resourceType="AWS::IAM::Role") @@ -3251,29 +3297,88 @@ def test_role_config_client(): )["resourceIdentifiers"][0]["resourceId"] ) != first_result - # Test aggregated query: (everything is getting a random id, so we can't test names by ordering) + # Test aggregated query - by `Limit=len(CONFIG_REGIONS)`, we should get a single policy duplicated across all regions agg_result = config_client.list_aggregate_discovered_resources( ResourceType="AWS::IAM::Role", ConfigurationAggregatorName="test_aggregator", - Limit=1, + Limit=len(CONFIG_REGIONS), ) - first_agg_result = agg_result["ResourceIdentifiers"][0]["ResourceId"] - assert agg_result["ResourceIdentifiers"][0]["ResourceType"] == "AWS::IAM::Role" - assert len(first_agg_result) == len(random_resource_id()) - assert agg_result["ResourceIdentifiers"][0]["SourceAccountId"] == ACCOUNT_ID - assert agg_result["ResourceIdentifiers"][0]["SourceRegion"] == "global" + assert len(agg_result["ResourceIdentifiers"]) == len(CONFIG_REGIONS) + + agg_name = None + agg_id = None + for resource in agg_result["ResourceIdentifiers"]: + assert resource["ResourceType"] == "AWS::IAM::Role" + assert resource["SourceRegion"] in CONFIG_REGIONS + assert resource["SourceAccountId"] == ACCOUNT_ID + if agg_id: + assert resource["ResourceId"] == agg_id + if agg_name: + assert resource["ResourceName"] == agg_name + agg_name = resource["ResourceName"] + agg_id = resource["ResourceId"] # Test aggregated pagination + for resource in config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator", + ResourceType="AWS::IAM::Role", + NextToken=agg_result["NextToken"], + )["ResourceIdentifiers"]: + assert resource["ResourceId"] != agg_id + + # Test non-aggregated resource name/id filter assert ( - config_client.list_aggregate_discovered_resources( - ConfigurationAggregatorName="test_aggregator", - ResourceType="AWS::IAM::Role", - Limit=1, - NextToken=agg_result["NextToken"], - )["ResourceIdentifiers"][0]["ResourceId"] - != first_agg_result + config_client.list_discovered_resources( + resourceType="AWS::IAM::Role", resourceName=roles[1]["name"], limit=1, + )["resourceIdentifiers"][0]["resourceName"] + == roles[1]["name"] ) + assert ( + config_client.list_discovered_resources( + resourceType="AWS::IAM::Role", resourceIds=[roles[0]["id"]], limit=1, + )["resourceIdentifiers"][0]["resourceName"] + == roles[0]["name"] + ) + + # Test aggregated resource name/id filter + agg_name_filter = config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator", + ResourceType="AWS::IAM::Role", + Filters={"ResourceName": roles[5]["name"]}, + ) + assert len(agg_name_filter["ResourceIdentifiers"]) == len(CONFIG_REGIONS) + assert agg_name_filter["ResourceIdentifiers"][0]["ResourceId"] == roles[5]["id"] + + agg_name_filter = config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator_two_regions", + ResourceType="AWS::IAM::Role", + Filters={"ResourceName": roles[5]["name"]}, + ) + assert len(agg_name_filter["ResourceIdentifiers"]) == len( + two_region_account_aggregation_source["AwsRegions"] + ) + assert agg_name_filter["ResourceIdentifiers"][0]["ResourceId"] == roles[5]["id"] + + agg_id_filter = config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator", + ResourceType="AWS::IAM::Role", + Filters={"ResourceId": roles[4]["id"]}, + ) + + assert len(agg_id_filter["ResourceIdentifiers"]) == len(CONFIG_REGIONS) + assert agg_id_filter["ResourceIdentifiers"][0]["ResourceName"] == roles[4]["name"] + + agg_name_filter = config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator_two_regions", + ResourceType="AWS::IAM::Role", + Filters={"ResourceId": roles[5]["id"]}, + ) + assert len(agg_name_filter["ResourceIdentifiers"]) == len( + two_region_account_aggregation_source["AwsRegions"] + ) + assert agg_name_filter["ResourceIdentifiers"][0]["ResourceName"] == roles[5]["name"] + # Test non-aggregated resource name/id filter assert ( config_client.list_discovered_resources( @@ -3354,7 +3459,7 @@ def test_role_config_client(): ResourceIdentifiers=[ { "SourceAccountId": ACCOUNT_ID, - "SourceRegion": "global", + "SourceRegion": "us-east-1", "ResourceId": roles[1]["id"], "ResourceType": "AWS::IAM::Role", } @@ -3382,13 +3487,21 @@ def test_policy_list_config_discovered_resources(): ], } - # Create a policy - policy_config_query.backends["global"].create_policy( - description="mypolicy", - path="", - policy_document=json.dumps(basic_policy), - policy_name="mypolicy", - ) + # Make 3 policies + policies = [] + num_policies = 3 + for ix in range(1, num_policies + 1): + this_policy = policy_config_query.backends["global"].create_policy( + description="policy{}".format(ix), + path="", + policy_document=json.dumps(basic_policy), + policy_name="policy{}".format(ix), + ) + policies.append( + {"id": this_policy.id, "name": this_policy.name,} + ) + + assert len(policies) == num_policies # We expect the backend to have arns as their keys for backend_key in list( @@ -3397,14 +3510,41 @@ def test_policy_list_config_discovered_resources(): assert backend_key.startswith("arn:aws:iam::") result = policy_config_query.list_config_service_resources(None, None, 100, None)[0] - assert len(result) == 1 + assert len(result) == num_policies policy = result[0] assert policy["type"] == "AWS::IAM::Policy" - assert len(policy["id"]) == len(random_policy_id()) - assert policy["name"] == "mypolicy" + assert policy["id"] in list(map(lambda p: p["id"], policies)) + assert policy["name"] in list(map(lambda p: p["name"], policies)) assert policy["region"] == "global" + # test passing list of resource ids + resource_ids = policy_config_query.list_config_service_resources( + [policies[0]["id"], policies[1]["id"]], None, 100, None + )[0] + assert len(resource_ids) == 2 + + # test passing a single resource name + resource_name = policy_config_query.list_config_service_resources( + None, policies[0]["name"], 100, None + )[0] + assert len(resource_name) == 1 + assert resource_name[0]["id"] == policies[0]["id"] + assert resource_name[0]["name"] == policies[0]["name"] + + # test passing a single resource name AND some resource id's + both_filter_good = policy_config_query.list_config_service_resources( + [policies[0]["id"], policies[1]["id"]], policies[0]["name"], 100, None + )[0] + assert len(both_filter_good) == 1 + assert both_filter_good[0]["id"] == policies[0]["id"] + assert both_filter_good[0]["name"] == policies[0]["name"] + + both_filter_bad = policy_config_query.list_config_service_resources( + [policies[0]["id"], policies[1]["id"]], policies[2]["name"], 100, None + )[0] + assert len(both_filter_bad) == 0 + @mock_iam def test_policy_config_dict(): @@ -3519,6 +3659,7 @@ def test_policy_config_dict(): def test_policy_config_client(): from moto.iam.models import ACCOUNT_ID from moto.iam.utils import random_policy_id + from moto.config.models import CONFIG_REGIONS basic_policy = { "Version": "2012-10-17", @@ -3528,14 +3669,24 @@ def test_policy_config_client(): iam_client = boto3.client("iam", region_name="us-west-2") config_client = boto3.client("config", region_name="us-west-2") - account_aggregation_source = { + all_account_aggregation_source = { "AccountIds": [ACCOUNT_ID], "AllAwsRegions": True, } + two_region_account_aggregation_source = { + "AccountIds": [ACCOUNT_ID], + "AwsRegions": ["us-east-1", "us-west-2"], + } + config_client.put_configuration_aggregator( ConfigurationAggregatorName="test_aggregator", - AccountAggregationSources=[account_aggregation_source], + AccountAggregationSources=[all_account_aggregation_source], + ) + + config_client.put_configuration_aggregator( + ConfigurationAggregatorName="test_aggregator_two_regions", + AccountAggregationSources=[two_region_account_aggregation_source], ) result = config_client.list_discovered_resources(resourceType="AWS::IAM::Policy") @@ -3575,28 +3726,35 @@ def test_policy_config_client(): )["resourceIdentifiers"][0]["resourceId"] ) != first_result - # Test aggregated query: (everything is getting a random id, so we can't test names by ordering) + # Test aggregated query - by `Limit=len(CONFIG_REGIONS)`, we should get a single policy duplicated across all regions agg_result = config_client.list_aggregate_discovered_resources( ResourceType="AWS::IAM::Policy", ConfigurationAggregatorName="test_aggregator", - Limit=1, + Limit=len(CONFIG_REGIONS), ) - first_agg_result = agg_result["ResourceIdentifiers"][0]["ResourceId"] - assert agg_result["ResourceIdentifiers"][0]["ResourceType"] == "AWS::IAM::Policy" - assert len(first_agg_result) == len(random_policy_id()) - assert agg_result["ResourceIdentifiers"][0]["SourceAccountId"] == ACCOUNT_ID - assert agg_result["ResourceIdentifiers"][0]["SourceRegion"] == "global" + assert len(agg_result["ResourceIdentifiers"]) == len(CONFIG_REGIONS) + + agg_name = None + agg_id = None + for resource in agg_result["ResourceIdentifiers"]: + assert resource["ResourceType"] == "AWS::IAM::Policy" + assert resource["SourceRegion"] in CONFIG_REGIONS + assert resource["SourceAccountId"] == ACCOUNT_ID + if agg_id: + assert resource["ResourceId"] == agg_id + if agg_name: + assert resource["ResourceName"] == agg_name + agg_name = resource["ResourceName"] + agg_id = resource["ResourceId"] # Test aggregated pagination - assert ( - config_client.list_aggregate_discovered_resources( - ConfigurationAggregatorName="test_aggregator", - ResourceType="AWS::IAM::Policy", - Limit=1, - NextToken=agg_result["NextToken"], - )["ResourceIdentifiers"][0]["ResourceId"] - != first_agg_result - ) + for resource in config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator", + ResourceType="AWS::IAM::Policy", + Limit=1, + NextToken=agg_result["NextToken"], + )["ResourceIdentifiers"]: + assert resource["ResourceId"] != agg_id # Test non-aggregated resource name/id filter assert ( @@ -3605,6 +3763,7 @@ def test_policy_config_client(): )["resourceIdentifiers"][0]["resourceName"] == policies[1]["name"] ) + assert ( config_client.list_discovered_resources( resourceType="AWS::IAM::Policy", resourceIds=[policies[0]["id"]], limit=1, @@ -3613,24 +3772,47 @@ def test_policy_config_client(): ) # Test aggregated resource name/id filter + agg_name_filter = config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator", + ResourceType="AWS::IAM::Policy", + Filters={"ResourceName": policies[5]["name"]}, + ) + assert len(agg_name_filter["ResourceIdentifiers"]) == len(CONFIG_REGIONS) assert ( - config_client.list_aggregate_discovered_resources( - ConfigurationAggregatorName="test_aggregator", - ResourceType="AWS::IAM::Policy", - Filters={"ResourceName": policies[5]["name"]}, - Limit=1, - )["ResourceIdentifiers"][0]["ResourceName"] - == policies[5]["name"] + agg_name_filter["ResourceIdentifiers"][0]["ResourceName"] == policies[5]["name"] ) + agg_name_filter = config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator_two_regions", + ResourceType="AWS::IAM::Policy", + Filters={"ResourceName": policies[5]["name"]}, + ) + assert len(agg_name_filter["ResourceIdentifiers"]) == len( + two_region_account_aggregation_source["AwsRegions"] + ) + assert agg_name_filter["ResourceIdentifiers"][0]["ResourceId"] == policies[5]["id"] + + agg_id_filter = config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator", + ResourceType="AWS::IAM::Policy", + Filters={"ResourceId": policies[4]["id"]}, + ) + + assert len(agg_id_filter["ResourceIdentifiers"]) == len(CONFIG_REGIONS) assert ( - config_client.list_aggregate_discovered_resources( - ConfigurationAggregatorName="test_aggregator", - ResourceType="AWS::IAM::Policy", - Filters={"ResourceId": policies[4]["id"]}, - Limit=1, - )["ResourceIdentifiers"][0]["ResourceName"] - == policies[4]["name"] + agg_id_filter["ResourceIdentifiers"][0]["ResourceName"] == policies[4]["name"] + ) + + agg_name_filter = config_client.list_aggregate_discovered_resources( + ConfigurationAggregatorName="test_aggregator_two_regions", + ResourceType="AWS::IAM::Policy", + Filters={"ResourceId": policies[5]["id"]}, + ) + assert len(agg_name_filter["ResourceIdentifiers"]) == len( + two_region_account_aggregation_source["AwsRegions"] + ) + assert ( + agg_name_filter["ResourceIdentifiers"][0]["ResourceName"] == policies[5]["name"] ) # Test name/id filter with pagination @@ -3678,7 +3860,7 @@ def test_policy_config_client(): ResourceIdentifiers=[ { "SourceAccountId": ACCOUNT_ID, - "SourceRegion": "global", + "SourceRegion": "us-east-2", "ResourceId": policies[8]["id"], "ResourceType": "AWS::IAM::Policy", }