From 7bc5b5c08f8f5f7f92312983c2dbccd43c59f41f Mon Sep 17 00:00:00 2001 From: Nick Stocchero Date: Thu, 30 Jul 2020 08:17:35 -0600 Subject: [PATCH] Add IAM Role and Policy to Config --- CONFIG_README.md | 4 +- moto/config/models.py | 4 +- moto/core/models.py | 21 ++ moto/iam/config.py | 173 ++++++++++++++ moto/iam/models.py | 148 +++++++++++- tests/test_iam/test_iam.py | 448 +++++++++++++++++++++++++++++++++++++ 6 files changed, 793 insertions(+), 5 deletions(-) create mode 100644 moto/iam/config.py diff --git a/CONFIG_README.md b/CONFIG_README.md index 356bb87a0..e223c8457 100644 --- a/CONFIG_README.md +++ b/CONFIG_README.md @@ -23,8 +23,8 @@ However, this will only work on resource types that have this enabled. ### Current enabled resource types: -1. S3 - +1. S3 (all) +1. IAM (Role, Policy) ## Developer Guide diff --git a/moto/config/models.py b/moto/config/models.py index b6dc4672d..77f46e644 100644 --- a/moto/config/models.py +++ b/moto/config/models.py @@ -47,8 +47,8 @@ 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 = [ "capitalizeStart", @@ -64,6 +64,8 @@ DEFAULT_PAGE_SIZE = 100 RESOURCE_MAP = { "AWS::S3::Bucket": s3_config_query, "AWS::S3::AccountPublicAccessBlock": s3_account_public_access_block_query, + "AWS::IAM::Role": role_config_query, + "AWS::IAM::Policy": policy_config_query, } diff --git a/moto/core/models.py b/moto/core/models.py index ae241322c..bc7d282fd 100644 --- a/moto/core/models.py +++ b/moto/core/models.py @@ -766,6 +766,27 @@ class ConfigQueryModel(object): """ raise NotImplementedError() + def aggregate_regions(self, path, backend_region, resource_region): + """ + Returns a list of "region\1eresourcename" strings + """ + + filter_region = backend_region or resource_region + if filter_region: + filter_resources = list(self.backends[filter_region].__dict__[path].keys()) + return map( + lambda resource: "{}\1e{}".format(filter_region, 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("{}\1e{}".format(region, resource)) + return ret + class base_decorator(object): mock_backend = MockAWS diff --git a/moto/iam/config.py b/moto/iam/config.py new file mode 100644 index 000000000..4bb381248 --- /dev/null +++ b/moto/iam/config.py @@ -0,0 +1,173 @@ +import json + +from moto.core.exceptions import InvalidNextTokenException +from moto.core.models import ConfigQueryModel +from moto.iam import iam_backends + + +class RoleConfigQuery(ConfigQueryModel): + def list_config_service_resources( + self, + resource_ids, + resource_name, + limit, + next_token, + backend_region=None, + resource_region=None, + ): + # For aggregation -- did we get both a resource ID and a resource name? + if resource_ids and resource_name: + # If the values are different, then return an empty list: + if resource_name not in resource_ids: + return [], None + + role_list = self.aggregate_regions("roles", backend_region, resource_region) + + if not role_list: + return [], None + + # Pagination logic: + sorted_roles = sorted(role_list) + new_token = None + + # Get the start: + if not next_token: + start = 0 + else: + # "Tokens" are region + \00 + resource ID. + if next_token not in sorted_roles: + raise InvalidNextTokenException() + + start = sorted_roles.index(next_token) + + # Get the list of items to collect: + role_list = sorted_roles[start : (start + limit)] + + if len(sorted_roles) > (start + limit): + new_token = sorted_roles[start + limit] + + return ( + [ + { + "type": "AWS::IAM::Role", + "id": role.split("\1e")[1], + "name": role.split("\1e")[1], + "region": role.split("\1e")[0], + } + for role in role_list + ], + new_token, + ) + + def get_config_resource( + self, resource_id, resource_name=None, backend_region=None, resource_region=None + ): + + role = self.backends["global"].roles.get(resource_id, {}) + + if not role: + return + + if resource_name and role.name != resource_name: + return + + # Format the bucket to the AWS Config format: + config_data = role.to_config_dict() + + # The 'configuration' field is also a JSON string: + config_data["configuration"] = json.dumps(config_data["configuration"]) + + # Supplementary config need all values converted to JSON strings if they are not strings already: + for field, value in config_data["supplementaryConfiguration"].items(): + if not isinstance(value, str): + config_data["supplementaryConfiguration"][field] = json.dumps(value) + + return config_data + + +class PolicyConfigQuery(ConfigQueryModel): + def list_config_service_resources( + self, + resource_ids, + resource_name, + limit, + next_token, + backend_region=None, + resource_region=None, + ): + # For aggregation -- did we get both a resource ID and a resource name? + if resource_ids and resource_name: + # If the values are different, then return an empty list: + if resource_name not in resource_ids: + return [], None + + # We don't want to include AWS Managed Policies + policy_list = filter( + lambda policy: not policy.split("\1e")[1].startswith("arn:aws:iam::aws"), + self.aggregate_regions("managed_policies", backend_region, resource_region), + ) + + if not policy_list: + return [], None + + # Pagination logic: + sorted_policies = sorted(policy_list) + new_token = None + + # Get the start: + if not next_token: + start = 0 + else: + # "Tokens" are region + \00 + resource ID. + if next_token not in sorted_policies: + raise InvalidNextTokenException() + + start = sorted_policies.index(next_token) + + # Get the list of items to collect: + policy_list = sorted_policies[start : (start + limit)] + + if len(sorted_policies) > (start + limit): + new_token = sorted_policies[start + limit] + + return ( + [ + { + "type": "AWS::IAM::Policy", + "id": policy.split("\1e")[1], + "name": policy.split("\1e")[1], + "region": policy.split("\1e")[0], + } + for policy in policy_list + ], + new_token, + ) + + def get_config_resource( + self, resource_id, resource_name=None, backend_region=None, resource_region=None + ): + + policy = self.backends["global"].managed_policies.get(resource_id, {}) + + if not policy: + return + + if resource_name and policy.name != resource_name: + return + + # Format the bucket to the AWS Config format: + config_data = policy.to_config_dict() + + # The 'configuration' field is also a JSON string: + config_data["configuration"] = json.dumps(config_data["configuration"]) + + # Supplementary config need all values converted to JSON strings if they are not strings already: + for field, value in config_data["supplementaryConfiguration"].items(): + if not isinstance(value, str): + config_data["supplementaryConfiguration"][field] = json.dumps(value) + + return config_data + + +role_config_query = RoleConfigQuery(iam_backends) +policy_config_query = PolicyConfigQuery(iam_backends) diff --git a/moto/iam/models.py b/moto/iam/models.py index 3a174e17b..9ae1ddcdf 100755 --- a/moto/iam/models.py +++ b/moto/iam/models.py @@ -8,11 +8,13 @@ import sys from datetime import datetime import json import re +import time from cryptography import x509 from cryptography.hazmat.backends import default_backend from six.moves.urllib.parse import urlparse +from six.moves.urllib import parse from moto.core.exceptions import RESTError from moto.core import BaseBackend, BaseModel, ACCOUNT_ID, CloudFormationModel from moto.core.utils import ( @@ -153,7 +155,7 @@ class OpenIDConnectProvider(BaseModel): self._errors = [] self._validate(url, thumbprint_list, client_id_list) - parsed_url = urlparse(url) + parsed_url = parse.urlparse(url) self.url = parsed_url.netloc + parsed_url.path self.thumbprint_list = thumbprint_list self.client_id_list = client_id_list @@ -201,7 +203,7 @@ class OpenIDConnectProvider(BaseModel): self._raise_errors() - parsed_url = urlparse(url) + parsed_url = parse.urlparse(url) if not parsed_url.scheme or not parsed_url.netloc: raise ValidationError("Invalid Open ID Connect Provider URL") @@ -265,6 +267,48 @@ class ManagedPolicy(Policy): def arn(self): return "arn:aws:iam::{0}:policy{1}{2}".format(ACCOUNT_ID, self.path, self.name) + def to_config_dict(self): + return { + "version": "1.3", + "configurationItemCaptureTime": str(self.create_date), + "configurationItemStatus": "OK", + "configurationStateId": str( + int(time.mktime(self.create_date.timetuple())) + ), # PY2 and 3 compatible + "arn": "arn:aws:iam::{}:policy/{}".format(ACCOUNT_ID, self.name), + "resourceType": "AWS::IAM::Policy", + "resourceId": self.id, + "resourceName": self.name, + "awsRegion": "global", + "availabilityZone": "Not Applicable", + "resourceCreationTime": str(self.create_date), + "configuration": { + "policyName": self.name, + "policyId": self.id, + "arn": "arn:aws:iam::{}:policy/{}".format(ACCOUNT_ID, self.name), + "path": self.path, + "defaultVersionId": self.default_version_id, + "attachmentCount": self.attachment_count, + "permissionsBoundaryUsageCount": 0, + "isAttachable": ManagedPolicy.is_attachable, + "description": self.description, + "createDate": str(self.create_date.isoformat()), + "updateDate": str(self.create_date.isoformat()), + "policyVersionList": list( + map( + lambda version: { + "document": parse.quote(version.document), + "versionId": version.version_id, + "isDefaultVersion": version.is_default, + "createDate": str(version.create_date), + }, + self.versions, + ) + ), + }, + "supplementaryConfiguration": {}, + } + class AWSManagedPolicy(ManagedPolicy): """AWS-managed policy.""" @@ -513,6 +557,69 @@ class Role(CloudFormationModel): def arn(self): return "arn:aws:iam::{0}:role{1}{2}".format(ACCOUNT_ID, self.path, self.name) + def to_config_dict(self): + _managed_policies = [] + for key in self.managed_policies.keys(): + _managed_policies.append( + {"policyArn": key, "policyName": iam_backend.managed_policies[key].name} + ) + + _role_policy_list = [] + for key, value in self.policies.items(): + _role_policy_list.append( + {"policyName": key, "policyDocument": parse.quote(value)} + ) + + _instance_profiles = [] + for key, instance_profile in iam_backend.instance_profiles.items(): + for role in instance_profile.roles: + _instance_profiles.append(instance_profile.to_embedded_config_dict()) + break + + config_dict = { + "version": "1.3", + "configurationItemCaptureTime": str(self.create_date), + "configurationItemStatus": "ResourceDiscovered", + "configurationStateId": str( + int(time.mktime(self.create_date.timetuple())) + ), # PY2 and 3 compatible + "arn": "arn:aws:iam::{}:role/{}".format(ACCOUNT_ID, self.name), + "resourceType": "AWS::IAM::Role", + "resourceId": self.name, + "resourceName": self.name, + "awsRegion": "global", + "availabilityZone": "Not Applicable", + "resourceCreationTime": str(self.create_date), + "relatedEvents": [], + "relationships": [], + "tags": self.tags, + "configuration": { + "path": self.path, + "roleName": self.name, + "roleId": self.id, + "arn": "arn:aws:iam::{}:role/{}".format(ACCOUNT_ID, self.name), + "assumeRolePolicyDocument": parse.quote( + self.assume_role_policy_document + ) + if self.assume_role_policy_document + else None, + "instanceProfileList": _instance_profiles, + "rolePolicyList": _role_policy_list, + "createDate": self.create_date.isoformat(), + "attachedManagedPolicies": _managed_policies, + "permissionsBoundary": self.permissions_boundary, + "tags": list( + map( + lambda key: {"key": key, "value": self.tags[key]["Value"]}, + self.tags, + ) + ), + "roleLastUsed": None, + }, + "supplementaryConfiguration": {}, + } + return config_dict + def put_policy(self, policy_name, policy_json): self.policies[policy_name] = policy_json @@ -590,6 +697,43 @@ class InstanceProfile(CloudFormationModel): return self.arn raise UnformattedGetAttTemplateException() + def to_embedded_config_dict(self): + # Instance Profiles aren't a config item itself, but they are returned in IAM roles with + # a "config like" json structure It's also different than Role.to_config_dict() + roles = [] + for role in self.roles: + roles.append( + { + "path": role.path, + "roleName": role.name, + "roleId": role.id, + "arn": "arn:aws:iam::{}:role/{}".format(ACCOUNT_ID, role.name), + "createDate": str(role.create_date), + "assumeRolePolicyDocument": parse.quote( + role.assume_role_policy_document + ), + "description": role.description, + "maxSessionDuration": None, + "permissionsBoundary": role.permissions_boundary, + "tags": list( + map( + lambda key: {"key": key, "value": role.tags[key]["Value"]}, + role.tags, + ) + ), + "roleLastUsed": None, + } + ) + + return { + "path": self.path, + "instanceProfileName": self.name, + "instanceProfileId": self.id, + "arn": "arn:aws:iam::{}:instance-profile/{}".format(ACCOUNT_ID, self.name), + "createDate": str(self.create_date), + "roles": roles, + } + class Certificate(BaseModel): def __init__(self, cert_name, cert_body, private_key, cert_chain=None, path=None): diff --git a/tests/test_iam/test_iam.py b/tests/test_iam/test_iam.py index 288825d6e..b42f3d76f 100644 --- a/tests/test_iam/test_iam.py +++ b/tests/test_iam/test_iam.py @@ -19,6 +19,7 @@ from nose.tools import raises from datetime import datetime from tests.helpers import requires_boto_gte from uuid import uuid4 +from six.moves.urllib import parse MOCK_CERT = """-----BEGIN CERTIFICATE----- @@ -2882,3 +2883,450 @@ def test_delete_role_with_instance_profiles_present(): role_names = [role["RoleName"] for role in iam.list_roles()["Roles"]] assert "Role1" in role_names assert "Role2" not in role_names + + +@mock_iam +def test_delete_account_password_policy_errors(): + client = boto3.client("iam", region_name="us-east-1") + + client.delete_account_password_policy.when.called_with().should.throw( + ClientError, "The account policy with name PasswordPolicy cannot be found." + ) + + +@mock_iam +def test_role_list_config_discovered_resources(): + from moto.iam.config import role_config_query + from moto.iam.utils import random_resource_id + + # Without any roles + assert role_config_query.list_config_service_resources(None, None, 100, None) == ( + [], + 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, + ) + + result = role_config_query.list_config_service_resources(None, None, 100, None)[0] + assert len(result) == 1 + + # The role gets a random ID, so we have to grab it + role = result[0] + assert role["type"] == "AWS::IAM::Role" + assert len(role["id"]) == len(random_resource_id()) + assert role["id"] == role["name"] + assert role["region"] == "global" + + +@mock_iam +def test_policy_list_config_discovered_resources(): + from moto.iam.config import policy_config_query + + # Without any policies + assert policy_config_query.list_config_service_resources(None, None, 100, None) == ( + [], + None, + ) + + basic_policy = { + "Version": "2012-10-17", + "Statement": [ + {"Action": ["ec2:DeleteKeyPair"], "Effect": "Deny", "Resource": "*"} + ], + } + + # Create a role + policy_config_query.backends["global"].create_policy( + description="mypolicy", + path="", + policy_document=json.dumps(basic_policy), + policy_name="mypolicy", + ) + + result = policy_config_query.list_config_service_resources(None, None, 100, None)[0] + assert len(result) == 1 + + policy = result[0] + assert policy["type"] == "AWS::IAM::Policy" + assert policy["id"] == policy["name"] == "arn:aws:iam::123456789012:policy/mypolicy" + assert policy["region"] == "global" + + +@mock_iam +def test_role_config_dict(): + from moto.iam.config import role_config_query, policy_config_query + from moto.iam.utils import random_resource_id + + # Without any roles + assert not role_config_query.get_config_resource("something") + assert role_config_query.list_config_service_resources(None, None, 100, None) == ( + [], + None, + ) + + basic_assume_role = { + "Version": "2012-10-17", + "Statement": [ + {"Effect": "Allow", "Principal": {"AWS": "*"}, "Action": "sts:AssumeRole"} + ], + } + + basic_policy = { + "Version": "2012-10-17", + "Statement": [{"Action": ["ec2:*"], "Effect": "Allow", "Resource": "*"}], + } + + # Create a policy for use in role permissions boundary + policy_config_query.backends["global"].create_policy( + description="basic_policy", + path="/", + policy_document=json.dumps(basic_policy), + policy_name="basic_policy", + ) + + policy_arn = policy_config_query.list_config_service_resources( + None, None, 100, None + )[0][0]["id"] + assert policy_arn is not None + + # Create some roles (and grab them repeatedly since they create with random names) + role_config_query.backends["global"].create_role( + role_name="plain_role", + assume_role_policy_document=None, + path="/", + permissions_boundary=None, + description="plain_role", + tags=[{"Key": "foo", "Value": "bar"}], + max_session_duration=3600, + ) + + plain_role = role_config_query.list_config_service_resources(None, None, 100, None)[ + 0 + ][0] + assert plain_role is not None + assert len(plain_role["id"]) == len(random_resource_id()) + + role_config_query.backends["global"].create_role( + role_name="assume_role", + assume_role_policy_document=json.dumps(basic_assume_role), + path="/", + permissions_boundary=None, + description="assume_role", + tags=[], + max_session_duration=3600, + ) + + assume_role = next( + role + for role in role_config_query.list_config_service_resources( + None, None, 100, None + )[0] + if role["id"] not in [plain_role["id"]] + ) + assert assume_role is not None + assert len(assume_role["id"]) == len(random_resource_id()) + assert assume_role["id"] is not plain_role["id"] + + role_config_query.backends["global"].create_role( + role_name="assume_and_permission_boundary_role", + assume_role_policy_document=json.dumps(basic_assume_role), + path="/", + permissions_boundary=policy_arn, + description="assume_and_permission_boundary_role", + tags=[], + max_session_duration=3600, + ) + + assume_and_permission_boundary_role = next( + role + for role in role_config_query.list_config_service_resources( + None, None, 100, None + )[0] + if role["id"] not in [plain_role["id"], assume_role["id"]] + ) + assert assume_and_permission_boundary_role is not None + assert len(assume_and_permission_boundary_role["id"]) == len(random_resource_id()) + assert assume_and_permission_boundary_role["id"] is not plain_role["id"] + assert assume_and_permission_boundary_role["id"] is not assume_role["id"] + + role_config_query.backends["global"].create_role( + role_name="role_with_attached_policy", + assume_role_policy_document=json.dumps(basic_assume_role), + path="/", + permissions_boundary=None, + description="role_with_attached_policy", + tags=[], + max_session_duration=3600, + ) + role_config_query.backends["global"].attach_role_policy( + policy_arn, "role_with_attached_policy" + ) + role_with_attached_policy = next( + role + for role in role_config_query.list_config_service_resources( + None, None, 100, None + )[0] + if role["id"] + not in [ + plain_role["id"], + assume_role["id"], + assume_and_permission_boundary_role["id"], + ] + ) + assert role_with_attached_policy is not None + assert len(role_with_attached_policy["id"]) == len(random_resource_id()) + assert role_with_attached_policy["id"] is not plain_role["id"] + assert role_with_attached_policy["id"] is not assume_role["id"] + assert ( + role_with_attached_policy["id"] is not assume_and_permission_boundary_role["id"] + ) + + role_config_query.backends["global"].create_role( + role_name="role_with_inline_policy", + assume_role_policy_document=json.dumps(basic_assume_role), + path="/", + permissions_boundary=None, + description="role_with_inline_policy", + tags=[], + max_session_duration=3600, + ) + role_config_query.backends["global"].put_role_policy( + "role_with_inline_policy", "inline_policy", json.dumps(basic_policy) + ) + + role_with_inline_policy = next( + role + for role in role_config_query.list_config_service_resources( + None, None, 100, None + )[0] + if role["id"] + not in [ + plain_role["id"], + assume_role["id"], + assume_and_permission_boundary_role["id"], + role_with_attached_policy["id"], + ] + ) + assert role_with_inline_policy is not None + assert len(role_with_inline_policy["id"]) == len(random_resource_id()) + assert role_with_inline_policy["id"] is not plain_role["id"] + assert role_with_inline_policy["id"] is not assume_role["id"] + assert ( + role_with_inline_policy["id"] is not assume_and_permission_boundary_role["id"] + ) + assert role_with_inline_policy["id"] is not role_with_attached_policy["id"] + + # plain role + plain_role_config = ( + role_config_query.backends["global"].roles[plain_role["id"]].to_config_dict() + ) + assert plain_role_config["version"] == "1.3" + assert plain_role_config["configurationItemStatus"] == "ResourceDiscovered" + assert plain_role_config["configurationStateId"] is not None + assert plain_role_config["arn"] == "arn:aws:iam::123456789012:role/plain_role" + assert plain_role_config["resourceType"] == "AWS::IAM::Role" + assert plain_role_config["resourceId"] == "plain_role" + assert plain_role_config["resourceName"] == "plain_role" + assert plain_role_config["awsRegion"] == "global" + assert plain_role_config["availabilityZone"] == "Not Applicable" + assert plain_role_config["resourceCreationTime"] is not None + assert plain_role_config["tags"] == {"foo": {"Key": "foo", "Value": "bar"}} + assert plain_role_config["configuration"]["path"] == "/" + assert plain_role_config["configuration"]["roleName"] == "plain_role" + assert plain_role_config["configuration"]["roleId"] == plain_role["id"] + assert plain_role_config["configuration"]["arn"] == plain_role_config["arn"] + assert plain_role_config["configuration"]["assumeRolePolicyDocument"] is None + assert plain_role_config["configuration"]["instanceProfileList"] == [] + assert plain_role_config["configuration"]["rolePolicyList"] == [] + assert plain_role_config["configuration"]["attachedManagedPolicies"] == [] + assert plain_role_config["configuration"]["permissionsBoundary"] is None + assert plain_role_config["configuration"]["tags"] == [ + {"key": "foo", "value": "bar"} + ] + assert plain_role_config["supplementaryConfiguration"] == {} + + # assume_role + assume_role_config = ( + role_config_query.backends["global"].roles[assume_role["id"]].to_config_dict() + ) + assert assume_role_config["arn"] == "arn:aws:iam::123456789012:role/assume_role" + assert assume_role_config["resourceId"] == "assume_role" + assert assume_role_config["resourceName"] == "assume_role" + assert assume_role_config["configuration"][ + "assumeRolePolicyDocument" + ] == parse.quote(json.dumps(basic_assume_role)) + + # assume_and_permission_boundary_role + assume_and_permission_boundary_role_config = ( + role_config_query.backends["global"] + .roles[assume_and_permission_boundary_role["id"]] + .to_config_dict() + ) + assert ( + assume_and_permission_boundary_role_config["arn"] + == "arn:aws:iam::123456789012:role/assume_and_permission_boundary_role" + ) + assert ( + assume_and_permission_boundary_role_config["resourceId"] + == "assume_and_permission_boundary_role" + ) + assert ( + assume_and_permission_boundary_role_config["resourceName"] + == "assume_and_permission_boundary_role" + ) + assert assume_and_permission_boundary_role_config["configuration"][ + "assumeRolePolicyDocument" + ] == parse.quote(json.dumps(basic_assume_role)) + assert ( + assume_and_permission_boundary_role_config["configuration"][ + "permissionsBoundary" + ] + == policy_arn + ) + + # role_with_attached_policy + role_with_attached_policy_config = ( + role_config_query.backends["global"] + .roles[role_with_attached_policy["id"]] + .to_config_dict() + ) + assert ( + role_with_attached_policy_config["arn"] + == "arn:aws:iam::123456789012:role/role_with_attached_policy" + ) + assert role_with_attached_policy_config["configuration"][ + "attachedManagedPolicies" + ] == [{"policyArn": policy_arn, "policyName": "basic_policy"}] + + # role_with_inline_policy + role_with_inline_policy_config = ( + role_config_query.backends["global"] + .roles[role_with_inline_policy["id"]] + .to_config_dict() + ) + assert ( + role_with_inline_policy_config["arn"] + == "arn:aws:iam::123456789012:role/role_with_inline_policy" + ) + assert role_with_inline_policy_config["configuration"]["rolePolicyList"] == [ + { + "policyName": "inline_policy", + "policyDocument": parse.quote(json.dumps(basic_policy)), + } + ] + + +@mock_iam +def test_policy_config_dict(): + from moto.iam.config import role_config_query, policy_config_query + from moto.iam.utils import random_policy_id + + # Without any roles + assert not policy_config_query.get_config_resource( + "arn:aws:iam::123456789012:policy/basic_policy" + ) + assert policy_config_query.list_config_service_resources(None, None, 100, None) == ( + [], + None, + ) + + basic_policy = { + "Version": "2012-10-17", + "Statement": [{"Action": ["ec2:*"], "Effect": "Allow", "Resource": "*"}], + } + + basic_policy_v2 = { + "Version": "2012-10-17", + "Statement": [ + {"Action": ["ec2:*", "s3:*"], "Effect": "Allow", "Resource": "*"} + ], + } + + policy_config_query.backends["global"].create_policy( + description="basic_policy", + path="/", + policy_document=json.dumps(basic_policy), + policy_name="basic_policy", + ) + + policy_arn = policy_config_query.list_config_service_resources( + None, None, 100, None + )[0][0]["id"] + assert policy_arn == "arn:aws:iam::123456789012:policy/basic_policy" + assert ( + policy_config_query.get_config_resource( + "arn:aws:iam::123456789012:policy/basic_policy" + ) + is not None + ) + + # Create a new version + policy_config_query.backends["global"].create_policy_version( + policy_arn, json.dumps(basic_policy_v2), "true" + ) + + # Create role to trigger attachment + role_config_query.backends["global"].create_role( + role_name="role_with_attached_policy", + assume_role_policy_document=None, + path="/", + permissions_boundary=None, + description="role_with_attached_policy", + tags=[], + max_session_duration=3600, + ) + role_config_query.backends["global"].attach_role_policy( + policy_arn, "role_with_attached_policy" + ) + + policy = ( + role_config_query.backends["global"] + .managed_policies["arn:aws:iam::123456789012:policy/basic_policy"] + .to_config_dict() + ) + assert policy["version"] == "1.3" + assert policy["configurationItemCaptureTime"] is not None + assert policy["configurationItemStatus"] == "OK" + assert policy["configurationStateId"] is not None + assert policy["arn"] == "arn:aws:iam::123456789012:policy/basic_policy" + assert policy["resourceType"] == "AWS::IAM::Policy" + assert len(policy["resourceId"]) == len(random_policy_id()) + assert policy["resourceName"] == "basic_policy" + assert policy["awsRegion"] == "global" + assert policy["availabilityZone"] == "Not Applicable" + assert policy["resourceCreationTime"] is not None + assert policy["configuration"]["policyName"] == policy["resourceName"] + assert policy["configuration"]["policyId"] == policy["resourceId"] + assert policy["configuration"]["arn"] == policy["arn"] + assert policy["configuration"]["path"] == "/" + assert policy["configuration"]["defaultVersionId"] == "v2" + assert policy["configuration"]["attachmentCount"] == 1 + assert policy["configuration"]["permissionsBoundaryUsageCount"] == 0 + assert policy["configuration"]["isAttachable"] == True + assert policy["configuration"]["description"] == "basic_policy" + assert policy["configuration"]["createDate"] is not None + assert policy["configuration"]["updateDate"] is not None + assert policy["configuration"]["policyVersionList"] == [ + { + "document": str(parse.quote(json.dumps(basic_policy))), + "versionId": "v1", + "isDefaultVersion": False, + "createDate": policy["configuration"]["policyVersionList"][0]["createDate"], + }, + { + "document": str(parse.quote(json.dumps(basic_policy_v2))), + "versionId": "v2", + "isDefaultVersion": True, + "createDate": policy["configuration"]["policyVersionList"][1]["createDate"], + }, + ] + assert policy["supplementaryConfiguration"] == {}