def list_from_aws(cls: Type["IAMRoleResourceSpec"], client: BaseClient, account_id: str, region: str) -> ListFromAWSResult: """Return a dict of dicts of the format: {'role_1_arn': {role_1_dict}, 'role_2_arn': {role_2_dict}, ...} Where the dicts represent results from list_roles and additional info per role from list_targets_by_role.""" roles = {} paginator = client.get_paginator("list_roles") for resp in paginator.paginate(): for role in resp.get("Roles", []): role_name = role["RoleName"] assume_role_policy_document = copy.deepcopy( role["AssumeRolePolicyDocument"]) assume_role_policy_document_text = policy_doc_dict_to_sorted_str( assume_role_policy_document) role[ "AssumeRolePolicyDocumentText"] = assume_role_policy_document_text for statement in assume_role_policy_document.get( "Statement", []): for obj in statement.get("Condition", {}).values(): for obj_key in obj.keys(): if obj_key.lower() == "sts:externalid": obj[obj_key] = "REMOVED" policies_result = get_attached_role_policies(client, role_name) policies = policies_result role["PolicyAttachments"] = policies resource_arn = role["Arn"] roles[resource_arn] = role return ListFromAWSResult(resources=roles)
def list_from_aws(cls: Type["IAMPolicyResourceSpec"], client: BaseClient, account_id: str, region: str) -> ListFromAWSResult: """Return a dict of dicts of the format: {'role_1_arn': {role_1_dict}, 'role_2_arn': {role_2_dict}, ...} Where the dicts represent results from list_policies and additional info per role from list_targets_by_role.""" policies = {} paginator = client.get_paginator("list_policies") for resp in paginator.paginate(Scope="Local"): for policy in resp.get("Policies", []): resource_arn = policy["Arn"] default_policy_version = policy["DefaultVersionId"] policy_version_resp = client.get_policy_version( PolicyArn=resource_arn, VersionId=default_policy_version) default_policy_version_document_text = policy_version_resp[ "PolicyVersion"]["Document"] policy[ "DefaultVersionPolicyDocumentText"] = policy_doc_dict_to_sorted_str( default_policy_version_document_text) policies[resource_arn] = policy return ListFromAWSResult(resources=policies)
def list_from_aws(cls: Type["IAMPolicyResourceSpec"], client: BaseClient, account_id: str, region: str) -> ListFromAWSResult: """Return a dict of dicts of the format: {'role_1_arn': {role_1_dict}, 'role_2_arn': {role_2_dict}, ...} Where the dicts represent results from list_policies and additional info per role from list_targets_by_role.""" policies = {} paginator = client.get_paginator("list_policies") for resp in paginator.paginate(Scope="Local"): for policy in resp.get("Policies", []): resource_arn = policy["Arn"] default_policy_version = policy["DefaultVersionId"] try: default_policy_version_document_text = cls.get_policy_version_document_text( client=client, policy_arn=resource_arn, policy_version=default_policy_version, ) policy[ "DefaultVersionPolicyDocumentText"] = policy_doc_dict_to_sorted_str( default_policy_version_document_text) policies[resource_arn] = policy except ClientError as c_e: error_code = getattr(c_e, "response", {}).get("Error", {}).get("Code", {}) if error_code != "NoSuchEntity": raise c_e return ListFromAWSResult(resources=policies)
def compare_embedded_policy(source_policy, expected_policy_name, expected_policy_document): if source_policy.pred != "embedded_policy": return False if len(source_policy.obj.simple_links) != 2: return False embedded_policy = source_policy.obj.simple_links[0] if embedded_policy.pred != "policy_name": return False if embedded_policy.obj != expected_policy_name: return False embedded_policy_document = source_policy.obj.simple_links[1] if embedded_policy_document.pred != "policy_document": return False got_policy_document = policy_doc_dict_to_sorted_str( json.loads(embedded_policy_document.obj)) expected_policy_document = policy_doc_dict_to_sorted_str( json.loads(expected_policy_document)) return got_policy_document == expected_policy_document
def get_embedded_role_policy( client: BaseClient, role_name: str, policy_name: str ) -> Dict[str, str]: """Get attached embedded policies""" resp = client.get_role_policy(RoleName=role_name, PolicyName=policy_name) policy_document = resp.get("PolicyDocument") policy_name = resp.get("PolicyName") policy_document = policy_doc_dict_to_sorted_str(policy_document) return { "PolicyName": policy_name, "PolicyDocument": policy_document, }
def test(self): with tempfile.TemporaryDirectory() as temp_dir: resource_region_name = "us-east-1" # get moto"s enabled regions ec2_client = boto3.client("ec2", region_name=resource_region_name) all_regions = ec2_client.describe_regions( Filters=[{ "Name": "opt-in-status", "Values": ["opt-in-not-required", "opted-in"] }])["Regions"] account_id = get_account_id() all_region_names = tuple(region["RegionName"] for region in all_regions) enabled_region_names = tuple( region["RegionName"] for region in all_regions if region["OptInStatus"] != "not-opted-in") delete_vpcs(all_region_names) # add a diverse set of resources which are supported by moto ## dynamodb # TODO moto is not returning TableId in list/describe # dynamodb_table_1_arn = create_dynamodb_table( # name="test_table_1", # attr_name="test_hash_key_attr_1", # attr_type="S", # key_type="HASH", # region_name=region_name, # ) ## s3 bucket_1_name = "test_bucket" bucket_1_arn, bucket_1_creation_date = create_bucket( name=bucket_1_name, account_id=account_id, region_name=resource_region_name) ## ec2 vpc_1_cidr = "10.0.0.0/16" vpc_1_id = create_vpc(cidr_block=vpc_1_cidr, region_name=resource_region_name) vpc_1_arn = VPCResourceSpec.generate_arn( resource_id=vpc_1_id, account_id=account_id, region=resource_region_name) subnet_1_cidr = "10.0.0.0/24" subnet_1_cidr_network = ipaddress.IPv4Network(subnet_1_cidr, strict=False) subnet_1_first_ip, subnet_1_last_ip = ( int(subnet_1_cidr_network[0]), int(subnet_1_cidr_network[-1]), ) subnet_1_id = create_subnet(cidr_block=subnet_1_cidr, vpc_id=vpc_1_id, region_name=resource_region_name) subnet_1_arn = SubnetResourceSpec.generate_arn( resource_id=subnet_1_id, account_id=account_id, region=resource_region_name) fixed_bucket_1_arn = f"arn:aws:s3:::{bucket_1_name}" flow_log_1_id, flow_log_1_creation_time = create_flow_log( vpc_id=vpc_1_id, dest_bucket_arn=fixed_bucket_1_arn, region_name=resource_region_name, ) flow_log_1_arn = FlowLogResourceSpec.generate_arn( resource_id=flow_log_1_id, account_id=account_id, region=resource_region_name) ebs_volume_1_size = 128 ebs_volume_1_az = f"{resource_region_name}a" ebs_volume_1_arn, ebs_volume_1_create_time = create_volume( size=ebs_volume_1_size, az=ebs_volume_1_az, region_name=resource_region_name) ## iam policy_1_name = "test_policy_1" policy_1_arn, policy_1_id = create_iam_policy( name=policy_1_name, policy_doc={ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": "logs:CreateLogGroup", "Resource": "*" }, ], }, ) role_1_name = "test_role_1" role_1_assume_role_policy_doc = { "Version": "2012-10-17", "Statement": [{ "Action": "sts:AssumeRole", "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Sid": "", }], } role_1_description = "Test Role 1" role_1_max_session_duration = 3600 role_1_arn = create_iam_role( name=role_1_name, assume_role_policy_doc=role_1_assume_role_policy_doc, description=role_1_description, max_session_duration=role_1_max_session_duration, ) ## lambda lambda_function_1_name = "test_lambda_function_1" lambda_function_1_runtime = "python3.7" lambda_function_1_handler = "lambda_function.lambda_handler" lambda_function_1_description = "Test Lambda Function 1" lambda_function_1_timeout = 30 lambda_function_1_memory_size = 256 lambda_function_1_arn = create_lambda_function( name=lambda_function_1_name, runtime=lambda_function_1_runtime, role_name=role_1_arn, handler=lambda_function_1_handler, description=lambda_function_1_description, timeout=lambda_function_1_timeout, memory_size=lambda_function_1_memory_size, publish=False, region_name=resource_region_name, ) # scan test_scan_id = "test_scan_id" aws_config = AWSConfig( artifact_path=temp_dir, pruner_max_age_min=4320, graph_name="alti", concurrency=ConcurrencyConfig(max_account_scan_threads=1, max_svc_scan_threads=1, max_account_scan_tries=2), scan=ScanConfig( accounts=(), regions=(), scan_sub_accounts=False, preferred_account_scan_regions=( "us-west-1", "us-west-2", "us-east-1", "us-east-2", ), ), accessor=Accessor( credentials_cache=AWSCredentialsCache(cache={}), multi_hop_accessors=[], cache_creds=True, ), write_master_json=True, ) resource_spec_classes = ( # DynamoDbTableResourceSpec, TODO moto EBSVolumeResourceSpec, FlowLogResourceSpec, IAMPolicyResourceSpec, IAMRoleResourceSpec, LambdaFunctionResourceSpec, S3BucketResourceSpec, SubnetResourceSpec, VPCResourceSpec, ) muxer = LocalAWSScanMuxer( scan_id=test_scan_id, config=aws_config, resource_spec_classes=resource_spec_classes, ) with unittest.mock.patch( "altimeter.aws.scan.account_scanner.get_all_enabled_regions" ) as mock_get_all_enabled_regions: mock_get_all_enabled_regions.return_value = enabled_region_names aws2n_result = aws2n( scan_id=test_scan_id, config=aws_config, muxer=muxer, load_neptune=False, ) graph_set = GraphSet.from_json_file( Path(aws2n_result.json_path)) self.assertEqual(len(graph_set.errors), 0) self.assertEqual(graph_set.name, "alti") self.assertEqual(graph_set.version, "2") # now check each resource type self.maxDiff = None ## Accounts expected_account_resources = [ Resource( resource_id=f"arn:aws::::account/{account_id}", type="aws:account", link_collection=LinkCollection( simple_links=(SimpleLink(pred="account_id", obj=account_id), ), ), ) ] account_resources = [ resource for resource in graph_set.resources if resource.type == "aws:account" ] self.assertCountEqual(account_resources, expected_account_resources) ## Regions expected_region_resources = [ Resource( resource_id= f"arn:aws:::{account_id}:region/{region['RegionName']}", type="aws:region", link_collection=LinkCollection( simple_links=( SimpleLink(pred="name", obj=region["RegionName"]), SimpleLink(pred="opt_in_status", obj=region["OptInStatus"]), ), resource_links=(ResourceLink( pred="account", obj=f"arn:aws::::account/{account_id}"), ), ), ) for region in all_regions ] region_resources = [ resource for resource in graph_set.resources if resource.type == "aws:region" ] self.assertCountEqual(region_resources, expected_region_resources) ## IAM Policies expected_iam_policy_resources = [ Resource( resource_id=policy_1_arn, type="aws:iam:policy", link_collection=LinkCollection( simple_links=( SimpleLink(pred="name", obj=policy_1_name), SimpleLink(pred="policy_id", obj=policy_1_id), SimpleLink(pred="default_version_id", obj="v1"), SimpleLink( pred="default_version_policy_document_text", obj= '{"Statement": [{"Action": "logs:CreateLogGroup", "Effect": "Allow", "Resource": "*"}], "Version": "2012-10-17"}', ), ), resource_links=(ResourceLink( pred="account", obj=f"arn:aws::::account/{account_id}"), ), ), ) ] iam_policy_resources = [ resource for resource in graph_set.resources if resource.type == "aws:iam:policy" ] self.assertCountEqual(iam_policy_resources, expected_iam_policy_resources) ## IAM Roles expected_iam_role_resources = [ Resource( resource_id=role_1_arn, type="aws:iam:role", link_collection=LinkCollection( simple_links=( SimpleLink(pred="name", obj=role_1_name), SimpleLink(pred="max_session_duration", obj=role_1_max_session_duration), SimpleLink(pred="description", obj=role_1_description), SimpleLink( pred="assume_role_policy_document_text", obj=policy_doc_dict_to_sorted_str( role_1_assume_role_policy_doc), ), ), multi_links=(MultiLink( pred="assume_role_policy_document", obj=LinkCollection( simple_links=(SimpleLink( pred="version", obj="2012-10-17"), ), multi_links=(MultiLink( pred="statement", obj=LinkCollection( simple_links=( SimpleLink(pred="effect", obj="Allow"), SimpleLink( pred="action", obj="sts:AssumeRole"), ), multi_links=(MultiLink( pred="principal", obj=LinkCollection( simple_links=(SimpleLink( pred="service", obj= "lambda.amazonaws.com", ), )), ), ), ), ), ), ), ), ), resource_links=(ResourceLink( pred="account", obj="arn:aws::::account/123456789012"), ), ), ) ] iam_role_resources = [ resource for resource in graph_set.resources if resource.type == "aws:iam:role" ] self.assertCountEqual(iam_role_resources, expected_iam_role_resources) ## Lambda functions expected_lambda_function_resources = [ Resource( resource_id=lambda_function_1_arn, type="aws:lambda:function", link_collection=LinkCollection( simple_links=( SimpleLink(pred="function_name", obj=lambda_function_1_name), SimpleLink(pred="runtime", obj=lambda_function_1_runtime), ), resource_links=( ResourceLink( pred="account", obj=f"arn:aws::::account/{account_id}"), ResourceLink( pred="region", obj= f"arn:aws:::{account_id}:region/{resource_region_name}", ), ), transient_resource_links=(ResourceLink( pred="role", obj="arn:aws:iam::123456789012:role/test_role_1" ), ), ), ), ] lambda_function_resources = [ resource for resource in graph_set.resources if resource.type == "aws:lambda:function" ] self.assertCountEqual(lambda_function_resources, expected_lambda_function_resources) ## EC2 VPCs expected_ec2_vpc_resources = [ Resource( resource_id=vpc_1_arn, type="aws:ec2:vpc", link_collection=LinkCollection( simple_links=( SimpleLink(pred="is_default", obj=True), SimpleLink(pred="cidr_block", obj=vpc_1_cidr), SimpleLink(pred="state", obj="available"), ), resource_links=( ResourceLink( pred="account", obj=f"arn:aws::::account/{account_id}"), ResourceLink( pred="region", obj= f"arn:aws:::{account_id}:region/{resource_region_name}", ), ), ), ) ] ec2_vpc_resources = [ resource for resource in graph_set.resources if resource.type == "aws:ec2:vpc" ] self.assertCountEqual(ec2_vpc_resources, expected_ec2_vpc_resources) ## EC2 VPC Flow Logs expected_ec2_vpc_flow_log_resources = [ Resource( resource_id=flow_log_1_arn, type="aws:ec2:flow-log", link_collection=LinkCollection( simple_links=( SimpleLink( pred="creation_time", obj=flow_log_1_creation_time.replace( tzinfo=datetime.timezone.utc). isoformat(), ), SimpleLink(pred="deliver_logs_status", obj="SUCCESS"), SimpleLink(pred="flow_log_status", obj="ACTIVE"), SimpleLink(pred="traffic_type", obj="ALL"), SimpleLink(pred="log_destination_type", obj="s3"), SimpleLink(pred="log_destination", obj=fixed_bucket_1_arn), SimpleLink( pred="log_format", obj= "${version} ${account-id} ${interface-id} ${srcaddr} ${dstaddr} ${srcport} ${dstport} ${protocol} ${packets} ${bytes} ${start} ${end} ${action} ${log-status}", ), ), resource_links=( ResourceLink( pred="account", obj=f"arn:aws::::account/{account_id}"), ResourceLink( pred="region", obj= f"arn:aws:::{account_id}:region/{resource_region_name}", ), ), transient_resource_links=(TransientResourceLink( pred="vpc", obj=vpc_1_arn, ), ), ), ) ] ec2_vpc_flow_log_resources = [ resource for resource in graph_set.resources if resource.type == "aws:ec2:flow-log" ] self.assertCountEqual(ec2_vpc_flow_log_resources, expected_ec2_vpc_flow_log_resources) ## EC2 Subnets expected_ec2_subnet_resources = [ Resource( resource_id=subnet_1_arn, type="aws:ec2:subnet", link_collection=LinkCollection( simple_links=( SimpleLink(pred="cidr_block", obj=subnet_1_cidr), SimpleLink(pred="first_ip", obj=subnet_1_first_ip), SimpleLink(pred="last_ip", obj=subnet_1_last_ip), SimpleLink(pred="state", obj="available"), ), resource_links=( ResourceLink(pred="vpc", obj=vpc_1_arn), ResourceLink( pred="account", obj=f"arn:aws::::account/{account_id}"), ResourceLink( pred="region", obj= f"arn:aws:::{account_id}:region/{resource_region_name}", ), ), ), ) ] ec2_subnet_resources = [ resource for resource in graph_set.resources if resource.type == "aws:ec2:subnet" ] self.assertCountEqual(ec2_subnet_resources, expected_ec2_subnet_resources) ## EC2 EBS Volumes expected_ec2_ebs_volume_resources = [ Resource( resource_id=ebs_volume_1_arn, type="aws:ec2:volume", link_collection=LinkCollection( simple_links=( SimpleLink(pred="availability_zone", obj=ebs_volume_1_az), SimpleLink( pred="create_time", obj=ebs_volume_1_create_time.replace( tzinfo=datetime.timezone.utc). isoformat(), ), SimpleLink(pred="size", obj=ebs_volume_1_size), SimpleLink(pred="state", obj="available"), SimpleLink(pred="volume_type", obj="gp2"), SimpleLink(pred="encrypted", obj=False), ), resource_links=( ResourceLink( pred="account", obj=f"arn:aws::::account/{account_id}"), ResourceLink( pred="region", obj= f"arn:aws:::{account_id}:region/{resource_region_name}", ), ), ), ) ] ec2_ebs_volume_resources = [ resource for resource in graph_set.resources if resource.type == "aws:ec2:volume" ] self.assertCountEqual(ec2_ebs_volume_resources, expected_ec2_ebs_volume_resources) ## S3 Buckets expected_s3_bucket_resources = [ Resource( resource_id=bucket_1_arn, type="aws:s3:bucket", link_collection=LinkCollection( simple_links=( SimpleLink(pred="name", obj=bucket_1_name), SimpleLink( pred="creation_date", obj=bucket_1_creation_date.replace( tzinfo=datetime.timezone.utc). isoformat(), ), ), resource_links=( ResourceLink( pred="account", obj=f"arn:aws::::account/{account_id}"), ResourceLink( pred="region", obj= f"arn:aws:::{account_id}:region/{resource_region_name}", ), ), ), ) ] s3_bucket_resources = [ resource for resource in graph_set.resources if resource.type == "aws:s3:bucket" ] self.assertCountEqual(s3_bucket_resources, expected_s3_bucket_resources) expected_num_graph_set_resources = ( 0 + len(expected_account_resources) + len(expected_region_resources) + len(expected_iam_policy_resources) + len(expected_iam_role_resources) + len(expected_lambda_function_resources) + len(expected_ec2_ebs_volume_resources) + len(expected_ec2_subnet_resources) + len(expected_ec2_vpc_resources) + len(expected_ec2_vpc_flow_log_resources) + len(expected_s3_bucket_resources)) self.assertEqual(len(graph_set.resources), expected_num_graph_set_resources)
def test(self): policy_doc = { "Version": "2012-10-17", "Statement": [ { "Sid": "AllowViewAccountInfo", "Effect": "Allow", "Action": [ "iam:GetAccountPasswordPolicy", "iam:GetAccountSummary", "iam:ListVirtualMFADevices", ], "Resource": "*", }, { "Sid": "AllowManageOwnPasswords", "Effect": "Allow", "Action": ["iam:ChangePassword", "iam:GetUser"], "Resource": "arn:aws:iam::*:user/${aws:username}", }, { "Sid": "AllowManageOwnAccessKeys", "Effect": "Allow", "Action": [ "iam:CreateAccessKey", "iam:DeleteAccessKey", "iam:ListAccessKeys", "iam:UpdateAccessKey", ], "Resource": "arn:aws:iam::*:user/${aws:username}", }, { "Sid": "AllowManageOwnSigningCertificates", "Effect": "Allow", "Action": [ "iam:DeleteSigningCertificate", "iam:ListSigningCertificates", "iam:UpdateSigningCertificate", "iam:UploadSigningCertificate", ], "Resource": "arn:aws:iam::*:user/${aws:username}", }, { "Sid": "AllowManageOwnSSHPublicKeys", "Effect": "Allow", "Action": [ "iam:DeleteSSHPublicKey", "iam:GetSSHPublicKey", "iam:ListSSHPublicKeys", "iam:UpdateSSHPublicKey", "iam:UploadSSHPublicKey", ], "Resource": "arn:aws:iam::*:user/${aws:username}", }, { "Sid": "AllowManageOwnGitCredentials", "Effect": "Allow", "Action": [ "iam:CreateServiceSpecificCredential", "iam:DeleteServiceSpecificCredential", "iam:ListServiceSpecificCredentials", "iam:ResetServiceSpecificCredential", "iam:UpdateServiceSpecificCredential", ], "Resource": "arn:aws:iam::*:user/${aws:username}", }, { "Sid": "AllowManageOwnVirtualMFADevice", "Effect": "Allow", "Action": [ "iam:CreateVirtualMFADevice", "iam:DeleteVirtualMFADevice" ], "Resource": "arn:aws:iam::*:mfa/${aws:username}", }, { "Sid": "AllowManageOwnUserMFA", "Effect": "Allow", "Action": [ "iam:DeactivateMFADevice", "iam:EnableMFADevice", "iam:ListMFADevices", "iam:ResyncMFADevice", ], "Resource": "arn:aws:iam::*:user/${aws:username}", }, { "Sid": "DenyAllExceptListedIfNoMFA", "Effect": "Deny", "NotAction": [ "iam:CreateVirtualMFADevice", "iam:EnableMFADevice", "iam:GetUser", "iam:ListMFADevices", "iam:ListVirtualMFADevices", "iam:ResyncMFADevice", "sts:GetSessionToken", ], "Resource": "*", "Condition": { "BoolIfExists": { "aws:MultiFactorAuthPresent": "false" } }, }, ], } expected_policy_doc_sorted_str = '{"Statement": [{"Action": ["iam:ChangePassword", "iam:GetUser"], "Effect": "Allow", "Resource": "arn:aws:iam::*:user/${aws:username}", "Sid": "AllowManageOwnPasswords"}, {"Action": ["iam:CreateAccessKey", "iam:DeleteAccessKey", "iam:ListAccessKeys", "iam:UpdateAccessKey"], "Effect": "Allow", "Resource": "arn:aws:iam::*:user/${aws:username}", "Sid": "AllowManageOwnAccessKeys"}, {"Action": ["iam:CreateServiceSpecificCredential", "iam:DeleteServiceSpecificCredential", "iam:ListServiceSpecificCredentials", "iam:ResetServiceSpecificCredential", "iam:UpdateServiceSpecificCredential"], "Effect": "Allow", "Resource": "arn:aws:iam::*:user/${aws:username}", "Sid": "AllowManageOwnGitCredentials"}, {"Action": ["iam:CreateVirtualMFADevice", "iam:DeleteVirtualMFADevice"], "Effect": "Allow", "Resource": "arn:aws:iam::*:mfa/${aws:username}", "Sid": "AllowManageOwnVirtualMFADevice"}, {"Action": ["iam:DeactivateMFADevice", "iam:EnableMFADevice", "iam:ListMFADevices", "iam:ResyncMFADevice"], "Effect": "Allow", "Resource": "arn:aws:iam::*:user/${aws:username}", "Sid": "AllowManageOwnUserMFA"}, {"Action": ["iam:DeleteSSHPublicKey", "iam:GetSSHPublicKey", "iam:ListSSHPublicKeys", "iam:UpdateSSHPublicKey", "iam:UploadSSHPublicKey"], "Effect": "Allow", "Resource": "arn:aws:iam::*:user/${aws:username}", "Sid": "AllowManageOwnSSHPublicKeys"}, {"Action": ["iam:DeleteSigningCertificate", "iam:ListSigningCertificates", "iam:UpdateSigningCertificate", "iam:UploadSigningCertificate"], "Effect": "Allow", "Resource": "arn:aws:iam::*:user/${aws:username}", "Sid": "AllowManageOwnSigningCertificates"}, {"Action": ["iam:GetAccountPasswordPolicy", "iam:GetAccountSummary", "iam:ListVirtualMFADevices"], "Effect": "Allow", "Resource": "*", "Sid": "AllowViewAccountInfo"}, {"Condition": {"BoolIfExists": {"aws:MultiFactorAuthPresent": "false"}}, "Effect": "Deny", "NotAction": ["iam:CreateVirtualMFADevice", "iam:EnableMFADevice", "iam:GetUser", "iam:ListMFADevices", "iam:ListVirtualMFADevices", "iam:ResyncMFADevice", "sts:GetSessionToken"], "Resource": "*", "Sid": "DenyAllExceptListedIfNoMFA"}], "Version": "2012-10-17"}' policy_doc_sorted_str = policy_doc_dict_to_sorted_str(policy_doc) self.assertEqual(policy_doc_sorted_str, expected_policy_doc_sorted_str)