From e109f32ce27de8613c9c57f7891c322c432cbb9e Mon Sep 17 00:00:00 2001 From: Bikouo Aubin <79859644+abikouo@users.noreply.github.com> Date: Wed, 27 Sep 2023 08:03:10 +0200 Subject: [PATCH] Migrate iam_role* modules and tests (#1760) Migrate iam_role* modules and tests SUMMARY Migrate modules iam_role and iam_role_info from community.aws ISSUE TYPE New Module Pull Request COMPONENT NAME iam_role iam_role_info Reviewed-by: Mark Chappell Reviewed-by: Alina Buzachis --- changelogs/fragments/migrate_iam_role.yml | 7 + meta/runtime.yml | 4 +- plugins/modules/iam_role.py | 773 ++++++++++++++++++ plugins/modules/iam_role_info.py | 289 +++++++ tests/integration/targets/iam_role/aliases | 9 + .../targets/iam_role/defaults/main.yml | 5 + .../targets/iam_role/files/deny-all-a.json | 13 + .../targets/iam_role/files/deny-all-b.json | 13 + .../targets/iam_role/files/deny-all.json | 12 + .../targets/iam_role/files/deny-assume.json | 10 + .../targets/iam_role/meta/main.yml | 1 + .../iam_role/tasks/boundary_policy.yml | 86 ++ .../iam_role/tasks/complex_role_creation.yml | 128 +++ .../iam_role/tasks/creation_deletion.yml | 376 +++++++++ .../iam_role/tasks/description_update.yml | 143 ++++ .../iam_role/tasks/inline_policy_update.yml | 49 ++ .../targets/iam_role/tasks/main.yml | 83 ++ .../iam_role/tasks/max_session_update.yml | 66 ++ .../iam_role/tasks/parameter_checks.yml | 82 ++ .../targets/iam_role/tasks/policy_update.yml | 246 ++++++ .../targets/iam_role/tasks/role_removal.yml | 59 ++ .../targets/iam_role/tasks/tags_update.yml | 328 ++++++++ 22 files changed, 2781 insertions(+), 1 deletion(-) create mode 100644 changelogs/fragments/migrate_iam_role.yml create mode 100644 plugins/modules/iam_role.py create mode 100644 plugins/modules/iam_role_info.py create mode 100644 tests/integration/targets/iam_role/aliases create mode 100644 tests/integration/targets/iam_role/defaults/main.yml create mode 100644 tests/integration/targets/iam_role/files/deny-all-a.json create mode 100644 tests/integration/targets/iam_role/files/deny-all-b.json create mode 100644 tests/integration/targets/iam_role/files/deny-all.json create mode 100644 tests/integration/targets/iam_role/files/deny-assume.json create mode 100644 tests/integration/targets/iam_role/meta/main.yml create mode 100644 tests/integration/targets/iam_role/tasks/boundary_policy.yml create mode 100644 tests/integration/targets/iam_role/tasks/complex_role_creation.yml create mode 100644 tests/integration/targets/iam_role/tasks/creation_deletion.yml create mode 100644 tests/integration/targets/iam_role/tasks/description_update.yml create mode 100644 tests/integration/targets/iam_role/tasks/inline_policy_update.yml create mode 100644 tests/integration/targets/iam_role/tasks/main.yml create mode 100644 tests/integration/targets/iam_role/tasks/max_session_update.yml create mode 100644 tests/integration/targets/iam_role/tasks/parameter_checks.yml create mode 100644 tests/integration/targets/iam_role/tasks/policy_update.yml create mode 100644 tests/integration/targets/iam_role/tasks/role_removal.yml create mode 100644 tests/integration/targets/iam_role/tasks/tags_update.yml diff --git a/changelogs/fragments/migrate_iam_role.yml b/changelogs/fragments/migrate_iam_role.yml new file mode 100644 index 00000000000..9b8110ba7d8 --- /dev/null +++ b/changelogs/fragments/migrate_iam_role.yml @@ -0,0 +1,7 @@ +major_changes: +- iam_role - The module has been migrated from the ``community.aws`` collection. Playbooks + using the Fully Qualified Collection Name for this module should be updated to use + ``amazon.aws.iam_role`` (https://github.com/ansible-collections/amazon.aws/pull/1760). +- iam_role_info - The module has been migrated from the ``community.aws`` collection. + Playbooks using the Fully Qualified Collection Name for this module should be updated + to use ``amazon.aws.iam_role_info`` (https://github.com/ansible-collections/amazon.aws/pull/1760). diff --git a/meta/runtime.yml b/meta/runtime.yml index 007f42aaa72..7f75e0d5224 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -73,6 +73,8 @@ action_groups: - iam_instance_profile_info - iam_policy - iam_policy_info + - iam_role + - iam_role_info - iam_user - iam_user_info - kms_key @@ -144,4 +146,4 @@ plugin_routing: redirect: amazon.aws.ssm_parameter aws_secret: # Deprecation for this alias should not *start* prior to 2024-09-01 - redirect: amazon.aws.secretsmanager_secret + redirect: amazon.aws.secretsmanager_secret \ No newline at end of file diff --git a/plugins/modules/iam_role.py b/plugins/modules/iam_role.py new file mode 100644 index 00000000000..404e4aa4e5f --- /dev/null +++ b/plugins/modules/iam_role.py @@ -0,0 +1,773 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Contributors to the Ansible project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: iam_role +version_added: 1.0.0 +version_added_collection: community.aws +short_description: Manage AWS IAM roles +description: + - Manage AWS IAM roles. +author: + - "Rob White (@wimnat)" +options: + path: + description: + - The path to the role. For more information about paths, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_identifiers.html). + default: "/" + type: str + name: + description: + - The name of the role to create. + required: true + type: str + description: + description: + - Provides a description of the role. + type: str + boundary: + description: + - The ARN of an IAM managed policy to use to restrict the permissions this role can pass on to IAM roles/users that it creates. + - Boundaries cannot be set on Instance Profiles, as such if this option is specified then I(create_instance_profile) must be C(false). + - This is intended for roles/users that have permissions to create new IAM objects. + - For more information on boundaries, see U(https://docs.aws.amazon.com/IAM/latest/UserGuide/access_policies_boundaries.html). + aliases: [boundary_policy_arn] + type: str + assume_role_policy_document: + description: + - The trust relationship policy document that grants an entity permission to assume the role. + - This parameter is required when I(state=present). + type: json + managed_policies: + description: + - A list of managed policy ARNs, managed policy ARNs or friendly names. + - To remove all policies set I(purge_polices=true) and I(managed_policies=[None]). + - To embed an inline policy, use M(amazon.aws.iam_policy). + aliases: ['managed_policy'] + type: list + elements: str + max_session_duration: + description: + - The maximum duration (in seconds) of a session when assuming the role. + - Valid values are between 1 and 12 hours (3600 and 43200 seconds). + type: int + purge_policies: + description: + - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched. + type: bool + aliases: ['purge_policy', 'purge_managed_policies'] + default: true + state: + description: + - Create or remove the IAM role. + default: present + choices: [ present, absent ] + type: str + create_instance_profile: + description: + - Creates an IAM instance profile along with the role. + default: true + type: bool + delete_instance_profile: + description: + - When I(delete_instance_profile=true) and I(state=absent) deleting a role will also delete the instance + profile created with the same I(name) as the role. + - Only applies when I(state=absent). + default: false + type: bool + wait_timeout: + description: + - How long (in seconds) to wait for creation / update to complete. + default: 120 + type: int + wait: + description: + - When I(wait=True) the module will wait for up to I(wait_timeout) seconds + for IAM role creation before returning. + default: True + type: bool +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.tags + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Create a role with description and tags + amazon.aws.iam_role: + name: mynewrole + assume_role_policy_document: "{{ lookup('file','policy.json') }}" + description: This is My New Role + tags: + env: dev + +- name: "Create a role and attach a managed policy called 'PowerUserAccess'" + amazon.aws.iam_role: + name: mynewrole + assume_role_policy_document: "{{ lookup('file','policy.json') }}" + managed_policies: + - arn:aws:iam::aws:policy/PowerUserAccess + +- name: Keep the role created above but remove all managed policies + amazon.aws.iam_role: + name: mynewrole + assume_role_policy_document: "{{ lookup('file','policy.json') }}" + managed_policies: [] + +- name: Delete the role + amazon.aws.iam_role: + name: mynewrole + assume_role_policy_document: "{{ lookup('file', 'policy.json') }}" + state: absent + +""" +RETURN = r""" +iam_role: + description: dictionary containing the IAM Role data + returned: success + type: complex + contains: + path: + description: the path to the role + type: str + returned: always + sample: / + role_name: + description: the friendly name that identifies the role + type: str + returned: always + sample: myrole + role_id: + description: the stable and unique string identifying the role + type: str + returned: always + sample: ABCDEFF4EZ4ABCDEFV4ZC + arn: + description: the Amazon Resource Name (ARN) specifying the role + type: str + returned: always + sample: "arn:aws:iam::1234567890:role/mynewrole" + create_date: + description: the date and time, in ISO 8601 date-time format, when the role was created + type: str + returned: always + sample: "2016-08-14T04:36:28+00:00" + assume_role_policy_document: + description: + - the policy that grants an entity permission to assume the role + - | + note: the case of keys in this dictionary are currently converted from CamelCase to + snake_case. In a release after 2023-12-01 this behaviour will change + type: dict + returned: always + sample: { + 'statement': [ + { + 'action': 'sts:AssumeRole', + 'effect': 'Allow', + 'principal': { + 'service': 'ec2.amazonaws.com' + }, + 'sid': '' + } + ], + 'version': '2012-10-17' + } + assume_role_policy_document_raw: + description: the policy that grants an entity permission to assume the role + type: dict + returned: always + version_added: 5.3.0 + sample: { + 'Statement': [ + { + 'Action': 'sts:AssumeRole', + 'Effect': 'Allow', + 'Principal': { + 'Service': 'ec2.amazonaws.com' + }, + 'Sid': '' + } + ], + 'Version': '2012-10-17' + } + + attached_policies: + description: a list of dicts containing the name and ARN of the managed IAM policies attached to the role + type: list + returned: always + sample: [ + { + 'policy_arn': 'arn:aws:iam::aws:policy/PowerUserAccess', + 'policy_name': 'PowerUserAccess' + } + ] + tags: + description: role tags + type: dict + returned: always + sample: '{"Env": "Prod"}' +""" + +import json + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.arn import validate_aws_arn +from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.policy import compare_policies +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.tagging import ansible_dict_to_boto3_tag_list +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict +from ansible_collections.amazon.aws.plugins.module_utils.tagging import compare_aws_tags + +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule + + +@AWSRetry.jittered_backoff() +def _list_policies(client): + paginator = client.get_paginator("list_policies") + return paginator.paginate().build_full_result()["Policies"] + + +def wait_iam_exists(module, client): + if module.check_mode: + return + if not module.params.get("wait"): + return + + role_name = module.params.get("name") + wait_timeout = module.params.get("wait_timeout") + + delay = min(wait_timeout, 5) + max_attempts = wait_timeout // delay + + try: + waiter = client.get_waiter("role_exists") + waiter.wait( + WaiterConfig={"Delay": delay, "MaxAttempts": max_attempts}, + RoleName=role_name, + ) + except botocore.exceptions.WaiterError as e: + module.fail_json_aws(e, msg="Timeout while waiting on IAM role creation") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Failed while waiting on IAM role creation") + + +def convert_friendly_names_to_arns(module, client, policy_names): + if all(validate_aws_arn(policy, service="iam") for policy in policy_names if policy is not None): + return policy_names + + allpolicies = {} + policies = _list_policies(client) + + for policy in policies: + allpolicies[policy["PolicyName"]] = policy["Arn"] + allpolicies[policy["Arn"]] = policy["Arn"] + try: + return [allpolicies[policy] for policy in policy_names if policy is not None] + except KeyError as e: + module.fail_json_aws(e, msg="Couldn't find policy") + + +def attach_policies(module, client, policies_to_attach, role_name): + if module.check_mode and policies_to_attach: + return True + + changed = False + for policy_arn in policies_to_attach: + try: + client.attach_role_policy(RoleName=role_name, PolicyArn=policy_arn, aws_retry=True) + changed = True + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to attach policy {policy_arn} to role {role_name}") + return changed + + +def remove_policies(module, client, policies_to_remove, role_name): + if module.check_mode and policies_to_remove: + return True + + changed = False + for policy in policies_to_remove: + try: + client.detach_role_policy(RoleName=role_name, PolicyArn=policy, aws_retry=True) + changed = True + except is_boto3_error_code("NoSuchEntityException"): + pass + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to detach policy {policy} from {role_name}") + return changed + + +def remove_inline_policies(module, client, role_name): + current_inline_policies = get_inline_policy_list(module, client, role_name) + for policy in current_inline_policies: + try: + client.delete_role_policy(RoleName=role_name, PolicyName=policy, aws_retry=True) + except is_boto3_error_code("NoSuchEntityException"): + pass + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to delete policy {policy} embedded in {role_name}") + + +def generate_create_params(module): + params = dict() + params["Path"] = module.params.get("path") + params["RoleName"] = module.params.get("name") + params["AssumeRolePolicyDocument"] = module.params.get("assume_role_policy_document") + if module.params.get("description") is not None: + params["Description"] = module.params.get("description") + if module.params.get("max_session_duration") is not None: + params["MaxSessionDuration"] = module.params.get("max_session_duration") + if module.params.get("boundary") is not None: + params["PermissionsBoundary"] = module.params.get("boundary") + if module.params.get("tags") is not None: + params["Tags"] = ansible_dict_to_boto3_tag_list(module.params.get("tags")) + + return params + + +def create_basic_role(module, client): + """ + Perform the Role creation. + Assumes tests for the role existing have already been performed. + """ + if module.check_mode: + module.exit_json(changed=True) + + try: + params = generate_create_params(module) + role = client.create_role(aws_retry=True, **params) + # 'Description' is documented as key of the role returned by create_role + # but appears to be an AWS bug (the value is not returned using the AWS CLI either). + # Get the role after creating it. + role = get_role_with_backoff(module, client, params["RoleName"]) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Unable to create role") + + return role + + +def update_role_assumed_policy(module, client, role_name, target_assumed_policy, current_assumed_policy): + # Check Assumed Policy document + if target_assumed_policy is None or not compare_policies(current_assumed_policy, json.loads(target_assumed_policy)): + return False + + if module.check_mode: + return True + + try: + client.update_assume_role_policy(RoleName=role_name, PolicyDocument=target_assumed_policy, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to update assume role policy for role {role_name}") + return True + + +def update_role_description(module, client, role_name, target_description, current_description): + # Check Description update + if target_description is None or current_description == target_description: + return False + + if module.check_mode: + return True + + try: + client.update_role(RoleName=role_name, Description=target_description, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to update description for role {role_name}") + return True + + +def update_role_max_session_duration(module, client, role_name, target_duration, current_duration): + # Check MaxSessionDuration update + if target_duration is None or current_duration == target_duration: + return False + + if module.check_mode: + return True + + try: + client.update_role(RoleName=role_name, MaxSessionDuration=target_duration, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to update maximum session duration for role {role_name}") + return True + + +def update_role_permissions_boundary( + module, client, role_name, target_permissions_boundary, current_permissions_boundary +): + # Check PermissionsBoundary + if target_permissions_boundary is None or target_permissions_boundary == current_permissions_boundary: + return False + + if module.check_mode: + return True + + if target_permissions_boundary == "": + try: + client.delete_role_permissions_boundary(RoleName=role_name, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to remove permission boundary for role {role_name}") + else: + try: + client.put_role_permissions_boundary( + RoleName=role_name, PermissionsBoundary=target_permissions_boundary, aws_retry=True + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to update permission boundary for role {role_name}") + return True + + +def update_managed_policies(module, client, role_name, managed_policies, purge_policies): + # Check Managed Policies + if managed_policies is None: + return False + + # Get list of current attached managed policies + current_attached_policies = get_attached_policy_list(module, client, role_name) + current_attached_policies_arn_list = [policy["PolicyArn"] for policy in current_attached_policies] + + if len(managed_policies) == 1 and managed_policies[0] is None: + managed_policies = [] + + policies_to_remove = set(current_attached_policies_arn_list) - set(managed_policies) + policies_to_attach = set(managed_policies) - set(current_attached_policies_arn_list) + + changed = False + if purge_policies and policies_to_remove: + if module.check_mode: + return True + else: + changed |= remove_policies(module, client, policies_to_remove, role_name) + + if policies_to_attach: + if module.check_mode: + return True + else: + changed |= attach_policies(module, client, policies_to_attach, role_name) + + return changed + + +def create_or_update_role(module, client): + role_name = module.params.get("name") + assumed_policy = module.params.get("assume_role_policy_document") + create_instance_profile = module.params.get("create_instance_profile") + description = module.params.get("description") + duration = module.params.get("max_session_duration") + path = module.params.get("path") + permissions_boundary = module.params.get("boundary") + purge_tags = module.params.get("purge_tags") + tags = ansible_dict_to_boto3_tag_list(module.params.get("tags")) if module.params.get("tags") else None + purge_policies = module.params.get("purge_policies") + managed_policies = module.params.get("managed_policies") + if managed_policies: + # Attempt to list the policies early so we don't leave things behind if we can't find them. + managed_policies = convert_friendly_names_to_arns(module, client, managed_policies) + + changed = False + + # Get role + role = get_role(module, client, role_name) + + # If role is None, create it + if role is None: + role = create_basic_role(module, client) + + if not module.check_mode and module.params.get("wait"): + wait_iam_exists(module, client) + + changed = True + else: + # Role exists - get current attributes + current_assumed_policy = role.get("AssumeRolePolicyDocument") + current_description = role.get("Description") + current_duration = role.get("MaxSessionDuration") + current_permissions_boundary = role.get("PermissionsBoundary", {}).get("PermissionsBoundaryArn", "") + + # Update attributes + changed |= update_role_tags(module, client, role_name, tags, purge_tags) + changed |= update_role_assumed_policy(module, client, role_name, assumed_policy, current_assumed_policy) + changed |= update_role_description(module, client, role_name, description, current_description) + changed |= update_role_max_session_duration(module, client, role_name, duration, current_duration) + changed |= update_role_permissions_boundary( + module, client, role_name, permissions_boundary, current_permissions_boundary + ) + + if not module.check_mode and module.params.get("wait"): + wait_iam_exists(module, client) + + if create_instance_profile: + changed |= create_instance_profiles(module, client, role_name, path) + + if not module.check_mode and module.params.get("wait"): + wait_iam_exists(module, client) + + changed |= update_managed_policies(module, client, role_name, managed_policies, purge_policies) + wait_iam_exists(module, client) + + # Get the role again + role = get_role(module, client, role_name) + role["AttachedPolicies"] = get_attached_policy_list(module, client, role_name) + role["tags"] = get_role_tags(module, client) + + camel_role = camel_dict_to_snake_dict(role, ignore_list=["tags"]) + camel_role["assume_role_policy_document_raw"] = role.get("AssumeRolePolicyDocument", {}) + module.exit_json(changed=changed, iam_role=camel_role, **camel_role) + + +def create_instance_profiles(module, client, role_name, path): + # Fetch existing Profiles + try: + instance_profiles = client.list_instance_profiles_for_role(RoleName=role_name, aws_retry=True)[ + "InstanceProfiles" + ] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {role_name}") + + # Profile already exists + if any(p["InstanceProfileName"] == role_name for p in instance_profiles): + return False + + if module.check_mode: + return True + + # Make sure an instance profile is created + try: + client.create_instance_profile(InstanceProfileName=role_name, Path=path, aws_retry=True) + except is_boto3_error_code("EntityAlreadyExists"): + # If the profile already exists, no problem, move on. + # Implies someone's changing things at the same time... + return False + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to create instance profile for role {role_name}") + + # And attach the role to the profile + try: + client.add_role_to_instance_profile(InstanceProfileName=role_name, RoleName=role_name, aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to attach role {role_name} to instance profile {role_name}") + + return True + + +def remove_instance_profiles(module, client, role_name): + delete_profiles = module.params.get("delete_instance_profile") + + try: + instance_profiles = client.list_instance_profiles_for_role(aws_retry=True, RoleName=role_name)[ + "InstanceProfiles" + ] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list instance profiles for role {role_name}") + + # Remove the role from the instance profile(s) + for profile in instance_profiles: + profile_name = profile["InstanceProfileName"] + try: + if not module.check_mode: + client.remove_role_from_instance_profile( + aws_retry=True, InstanceProfileName=profile_name, RoleName=role_name + ) + if profile_name == role_name: + if delete_profiles: + try: + client.delete_instance_profile(InstanceProfileName=profile_name, aws_retry=True) + except is_boto3_error_code("NoSuchEntityException"): + pass + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to remove instance profile {profile_name}") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to remove role {role_name} from instance profile {profile_name}") + + +def destroy_role(module, client): + role_name = module.params.get("name") + role = get_role(module, client, role_name) + + if role is None: + module.exit_json(changed=False) + + if not module.check_mode: + # Before we try to delete the role we need to remove any + # - attached instance profiles + # - attached managed policies + # - embedded inline policies + remove_instance_profiles(module, client, role_name) + update_managed_policies(module, client, role_name, [], True) + remove_inline_policies(module, client, role_name) + try: + client.delete_role(aws_retry=True, RoleName=role_name) + except is_boto3_error_code("NoSuchEntityException"): + module.exit_json(changed=False) + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Unable to delete role") + + module.exit_json(changed=True) + + +def get_role_with_backoff(module, client, name): + try: + return AWSRetry.jittered_backoff(catch_extra_error_codes=["NoSuchEntity"])(client.get_role)(RoleName=name)[ + "Role" + ] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to get role {name}") + + +def get_role(module, client, name): + try: + return client.get_role(RoleName=name, aws_retry=True)["Role"] + except is_boto3_error_code("NoSuchEntity"): + return None + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Unable to get role {name}") + + +def get_attached_policy_list(module, client, name): + try: + return client.list_attached_role_policies(RoleName=name, aws_retry=True)["AttachedPolicies"] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list attached policies for role {name}") + + +def get_inline_policy_list(module, client, name): + try: + return client.list_role_policies(RoleName=name, aws_retry=True)["PolicyNames"] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list attached policies for role {name}") + + +def get_role_tags(module, client): + role_name = module.params.get("name") + try: + return boto3_tag_list_to_ansible_dict(client.list_role_tags(RoleName=role_name, aws_retry=True)["Tags"]) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to list tags for role {role_name}") + + +def update_role_tags(module, client, role_name, new_tags, purge_tags): + if new_tags is None: + return False + new_tags = boto3_tag_list_to_ansible_dict(new_tags) + + try: + existing_tags = boto3_tag_list_to_ansible_dict( + client.list_role_tags(RoleName=role_name, aws_retry=True)["Tags"] + ) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError, KeyError): + existing_tags = {} + + tags_to_add, tags_to_remove = compare_aws_tags(existing_tags, new_tags, purge_tags=purge_tags) + + if not module.check_mode: + try: + if tags_to_remove: + client.untag_role(RoleName=role_name, TagKeys=tags_to_remove, aws_retry=True) + if tags_to_add: + client.tag_role(RoleName=role_name, Tags=ansible_dict_to_boto3_tag_list(tags_to_add), aws_retry=True) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Unable to set tags for role {role_name}") + + changed = bool(tags_to_add) or bool(tags_to_remove) + return changed + + +def main(): + argument_spec = dict( + name=dict(type="str", required=True), + path=dict(type="str", default="/"), + assume_role_policy_document=dict(type="json"), + managed_policies=dict(type="list", aliases=["managed_policy"], elements="str"), + max_session_duration=dict(type="int"), + state=dict(type="str", choices=["present", "absent"], default="present"), + description=dict(type="str"), + boundary=dict(type="str", aliases=["boundary_policy_arn"]), + create_instance_profile=dict(type="bool", default=True), + delete_instance_profile=dict(type="bool", default=False), + purge_policies=dict(default=True, type="bool", aliases=["purge_policy", "purge_managed_policies"]), + tags=dict(type="dict", aliases=["resource_tags"]), + purge_tags=dict(type="bool", default=True), + wait=dict(type="bool", default=True), + wait_timeout=dict(default=120, type="int"), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + required_if=[("state", "present", ["assume_role_policy_document"])], + supports_check_mode=True, + ) + + module.deprecate( + "All return values other than iam_role and changed have been deprecated and " + "will be removed in a release after 2023-12-01.", + date="2023-12-01", + collection_name="amazon.aws", + ) + + module.deprecate( + "In a release after 2023-12-01 the contents of iam_role.assume_role_policy_document " + "will no longer be converted from CamelCase to snake_case. The " + "iam_role.assume_role_policy_document_raw return value already returns the " + "policy document in this future format.", + date="2023-12-01", + collection_name="amazon.aws", + ) + + if module.params.get("boundary"): + if module.params.get("create_instance_profile"): + module.fail_json(msg="When using a boundary policy, `create_instance_profile` must be set to `false`.") + if not validate_aws_arn(module.params.get("boundary"), service="iam"): + module.fail_json(msg="Boundary policy must be an ARN") + if module.params.get("max_session_duration"): + max_session_duration = module.params.get("max_session_duration") + if max_session_duration < 3600 or max_session_duration > 43200: + module.fail_json(msg="max_session_duration must be between 1 and 12 hours (3600 and 43200 seconds)") + if module.params.get("path"): + path = module.params.get("path") + if not path.endswith("/") or not path.startswith("/"): + module.fail_json(msg="path must begin and end with /") + + client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) + + state = module.params.get("state") + + if state == "present": + create_or_update_role(module, client) + elif state == "absent": + destroy_role(module, client) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/iam_role_info.py b/plugins/modules/iam_role_info.py new file mode 100644 index 00000000000..b87a281287f --- /dev/null +++ b/plugins/modules/iam_role_info.py @@ -0,0 +1,289 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright: Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: iam_role_info +version_added: 1.0.0 +version_added_collection: community.aws +short_description: Gather information on IAM roles +description: + - Gathers information about IAM roles. +author: + - "Will Thames (@willthames)" +options: + name: + description: + - Name of a role to search for. + - Mutually exclusive with I(path_prefix). + aliases: + - role_name + type: str + path_prefix: + description: + - Prefix of role to restrict IAM role search for. + - Mutually exclusive with I(name). + type: str +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +- name: find all existing IAM roles + amazon.aws.iam_role_info: + register: result + +- name: describe a single role + amazon.aws.iam_role_info: + name: MyIAMRole + +- name: describe all roles matching a path prefix + amazon.aws.iam_role_info: + path_prefix: /application/path +""" + +RETURN = r""" +iam_roles: + description: List of IAM roles + returned: always + type: complex + contains: + arn: + description: Amazon Resource Name for IAM role. + returned: always + type: str + sample: arn:aws:iam::123456789012:role/AnsibleTestRole + assume_role_policy_document: + description: + - The policy that grants an entity permission to assume the role + - | + Note: the case of keys in this dictionary are currently converted from CamelCase to + snake_case. In a release after 2023-12-01 this behaviour will change. + returned: always + type: dict + assume_role_policy_document_raw: + description: The policy document describing what can assume the role. + returned: always + type: dict + version_added: 5.3.0 + create_date: + description: Date IAM role was created. + returned: always + type: str + sample: '2017-10-23T00:05:08+00:00' + inline_policies: + description: List of names of inline policies. + returned: always + type: list + sample: [] + managed_policies: + description: List of attached managed policies. + returned: always + type: complex + contains: + policy_arn: + description: Amazon Resource Name for the policy. + returned: always + type: str + sample: arn:aws:iam::123456789012:policy/AnsibleTestEC2Policy + policy_name: + description: Name of managed policy. + returned: always + type: str + sample: AnsibleTestEC2Policy + instance_profiles: + description: List of attached instance profiles. + returned: always + type: complex + contains: + arn: + description: Amazon Resource Name for the instance profile. + returned: always + type: str + sample: arn:aws:iam::123456789012:instance-profile/AnsibleTestEC2Policy + create_date: + description: Date instance profile was created. + returned: always + type: str + sample: '2017-10-23T00:05:08+00:00' + instance_profile_id: + description: Amazon Identifier for the instance profile. + returned: always + type: str + sample: AROAII7ABCD123456EFGH + instance_profile_name: + description: Name of instance profile. + returned: always + type: str + sample: AnsibleTestEC2Policy + path: + description: Path of instance profile. + returned: always + type: str + sample: / + roles: + description: List of roles associated with this instance profile. + returned: always + type: list + sample: [] + path: + description: Path of role. + returned: always + type: str + sample: / + role_id: + description: Amazon Identifier for the role. + returned: always + type: str + sample: AROAII7ABCD123456EFGH + role_name: + description: Name of the role. + returned: always + type: str + sample: AnsibleTestRole + tags: + description: Role tags. + type: dict + returned: always + sample: '{"Env": "Prod"}' +""" + +try: + import botocore +except ImportError: + pass # caught by AnsibleAWSModule + +from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict + +from ansible_collections.amazon.aws.plugins.module_utils.botocore import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.tagging import boto3_tag_list_to_ansible_dict + +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule + + +@AWSRetry.jittered_backoff() +def list_iam_roles_with_backoff(client, **kwargs): + paginator = client.get_paginator("list_roles") + return paginator.paginate(**kwargs).build_full_result() + + +@AWSRetry.jittered_backoff() +def list_iam_role_policies_with_backoff(client, role_name): + paginator = client.get_paginator("list_role_policies") + return paginator.paginate(RoleName=role_name).build_full_result()["PolicyNames"] + + +@AWSRetry.jittered_backoff() +def list_iam_attached_role_policies_with_backoff(client, role_name): + paginator = client.get_paginator("list_attached_role_policies") + return paginator.paginate(RoleName=role_name).build_full_result()["AttachedPolicies"] + + +@AWSRetry.jittered_backoff() +def list_iam_instance_profiles_for_role_with_backoff(client, role_name): + paginator = client.get_paginator("list_instance_profiles_for_role") + return paginator.paginate(RoleName=role_name).build_full_result()["InstanceProfiles"] + + +def describe_iam_role(module, client, role): + name = role["RoleName"] + try: + role["InlinePolicies"] = list_iam_role_policies_with_backoff(client, name) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Couldn't get inline policies for role {name}") + try: + role["ManagedPolicies"] = list_iam_attached_role_policies_with_backoff(client, name) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Couldn't get managed policies for role {name}") + try: + role["InstanceProfiles"] = list_iam_instance_profiles_for_role_with_backoff(client, name) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f"Couldn't get instance profiles for role {name}") + try: + role["tags"] = boto3_tag_list_to_ansible_dict(role["Tags"]) + del role["Tags"] + except KeyError: + role["tags"] = {} + return role + + +def describe_iam_roles(module, client): + name = module.params["name"] + path_prefix = module.params["path_prefix"] + if name: + try: + roles = [client.get_role(RoleName=name, aws_retry=True)["Role"]] + except is_boto3_error_code("NoSuchEntity"): + return [] + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f"Couldn't get IAM role {name}") + else: + params = dict() + if path_prefix: + if not path_prefix.startswith("/"): + path_prefix = "/" + path_prefix + if not path_prefix.endswith("/"): + path_prefix = path_prefix + "/" + params["PathPrefix"] = path_prefix + try: + roles = list_iam_roles_with_backoff(client, **params)["Roles"] + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Couldn't list IAM roles") + return [normalize_role(describe_iam_role(module, client, role)) for role in roles] + + +def normalize_profile(profile): + new_profile = camel_dict_to_snake_dict(profile) + if profile.get("Roles"): + profile["roles"] = [normalize_role(role) for role in profile.get("Roles")] + return new_profile + + +def normalize_role(role): + new_role = camel_dict_to_snake_dict(role, ignore_list=["tags"]) + new_role["assume_role_policy_document_raw"] = role.get("AssumeRolePolicyDocument") + if role.get("InstanceProfiles"): + role["instance_profiles"] = [normalize_profile(profile) for profile in role.get("InstanceProfiles")] + return new_role + + +def main(): + """ + Module action handler + """ + argument_spec = dict( + name=dict(aliases=["role_name"]), + path_prefix=dict(), + ) + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + mutually_exclusive=[["name", "path_prefix"]], + ) + + client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) + + module.deprecate( + "In a release after 2023-12-01 the contents of assume_role_policy_document " + "will no longer be converted from CamelCase to snake_case. The " + ".assume_role_policy_document_raw return value already returns the " + "policy document in this future format.", + date="2023-12-01", + collection_name="amazon.aws", + ) + + module.exit_json(changed=False, iam_roles=describe_iam_roles(module, client)) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/iam_role/aliases b/tests/integration/targets/iam_role/aliases new file mode 100644 index 00000000000..483c861158c --- /dev/null +++ b/tests/integration/targets/iam_role/aliases @@ -0,0 +1,9 @@ +# reason: missing-policy +# It should be possible to test iam_role by limiting which policies can be +# attached to the roles. +# Careful review is needed prior to adding this to the main CI. +unsupported + +cloud/aws + +iam_role_info diff --git a/tests/integration/targets/iam_role/defaults/main.yml b/tests/integration/targets/iam_role/defaults/main.yml new file mode 100644 index 00000000000..e83a563990b --- /dev/null +++ b/tests/integration/targets/iam_role/defaults/main.yml @@ -0,0 +1,5 @@ +test_role: '{{ resource_prefix }}-role' +test_path: /{{ resource_prefix }}/ +safe_managed_policy: AWSDenyAll +custom_policy_name: '{{ resource_prefix }}-denyall' +boundary_policy: arn:aws:iam::aws:policy/AWSDenyAll diff --git a/tests/integration/targets/iam_role/files/deny-all-a.json b/tests/integration/targets/iam_role/files/deny-all-a.json new file mode 100644 index 00000000000..ae62fd1975d --- /dev/null +++ b/tests/integration/targets/iam_role/files/deny-all-a.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*", + "Sid": "DenyA" + } + ] +} diff --git a/tests/integration/targets/iam_role/files/deny-all-b.json b/tests/integration/targets/iam_role/files/deny-all-b.json new file mode 100644 index 00000000000..3a4704a46ab --- /dev/null +++ b/tests/integration/targets/iam_role/files/deny-all-b.json @@ -0,0 +1,13 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*", + "Sid": "DenyB" + } + ] +} diff --git a/tests/integration/targets/iam_role/files/deny-all.json b/tests/integration/targets/iam_role/files/deny-all.json new file mode 100644 index 00000000000..3d324b9b9c6 --- /dev/null +++ b/tests/integration/targets/iam_role/files/deny-all.json @@ -0,0 +1,12 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": [ + "*" + ], + "Effect": "Deny", + "Resource": "*" + } + ] +} diff --git a/tests/integration/targets/iam_role/files/deny-assume.json b/tests/integration/targets/iam_role/files/deny-assume.json new file mode 100644 index 00000000000..73e87715862 --- /dev/null +++ b/tests/integration/targets/iam_role/files/deny-assume.json @@ -0,0 +1,10 @@ +{ + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Principal": { "Service": "ec2.amazonaws.com" }, + "Effect": "Deny" + } + ] +} diff --git a/tests/integration/targets/iam_role/meta/main.yml b/tests/integration/targets/iam_role/meta/main.yml new file mode 100644 index 00000000000..32cf5dda7ed --- /dev/null +++ b/tests/integration/targets/iam_role/meta/main.yml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/iam_role/tasks/boundary_policy.yml b/tests/integration/targets/iam_role/tasks/boundary_policy.yml new file mode 100644 index 00000000000..818b701f1ef --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/boundary_policy.yml @@ -0,0 +1,86 @@ +- name: Create minimal role with no boundary policy + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: Configure Boundary Policy (CHECK MODE) + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + boundary: '{{ boundary_policy }}' + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Configure Boundary Policy + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + boundary: '{{ boundary_policy }}' + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: Configure Boundary Policy (no change) - check mode + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + boundary: '{{ boundary_policy }}' + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Configure Boundary Policy (no change) + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + boundary: '{{ boundary_policy }}' + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: iam_role_info after adding boundary policy + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_arn == boundary_policy + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_type == 'Policy' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + +- name: Remove IAM Role + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role +- assert: + that: + - iam_role is changed diff --git a/tests/integration/targets/iam_role/tasks/complex_role_creation.yml b/tests/integration/targets/iam_role/tasks/complex_role_creation.yml new file mode 100644 index 00000000000..59db5d156fe --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/complex_role_creation.yml @@ -0,0 +1,128 @@ +- name: Complex IAM Role (CHECK MODE) + iam_role: + name: '{{ test_role }}' + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: '{{ boundary_policy }}' + create_instance_profile: no + description: Ansible Test Role {{ resource_prefix }} + managed_policy: + - '{{ safe_managed_policy }}' + - '{{ custom_policy_name }}' + max_session_duration: 43200 + path: '{{ test_path }}' + tags: + TagA: ValueA + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: iam_role_info after Complex Role creation in check_mode + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +- name: Complex IAM Role + iam_role: + name: '{{ test_role }}' + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: '{{ boundary_policy }}' + create_instance_profile: no + description: Ansible Test Role {{ resource_prefix }} + managed_policy: + - '{{ safe_managed_policy }}' + - '{{ custom_policy_name }}' + max_session_duration: 43200 + path: '{{ test_path }}' + tags: + TagA: ValueA + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.arn.startswith("arn") + - iam_role.iam_role.arn.endswith("role" + test_path + test_role ) + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 2 + - iam_role.iam_role.max_session_duration == 43200 + - iam_role.iam_role.path == test_path + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + +- name: Complex IAM role (no change) - check mode + iam_role: + name: '{{ test_role }}' + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: '{{ boundary_policy }}' + create_instance_profile: no + description: Ansible Test Role {{ resource_prefix }} + managed_policy: + - '{{ safe_managed_policy }}' + - '{{ custom_policy_name }}' + max_session_duration: 43200 + path: '{{ test_path }}' + tags: + TagA: ValueA + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Complex IAM role (no change) + iam_role: + name: '{{ test_role }}' + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + boundary: '{{ boundary_policy }}' + create_instance_profile: no + description: Ansible Test Role {{ resource_prefix }} + managed_policy: + - '{{ safe_managed_policy }}' + - '{{ custom_policy_name }}' + max_session_duration: 43200 + path: '{{ test_path }}' + tags: + TagA: ValueA + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: iam_role_info after Role creation + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role" + test_path + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 2 + - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == test_path + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_arn == boundary_policy + - role_info.iam_roles[0].permissions_boundary.permissions_boundary_type == 'Policy' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - '"TagA" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagA == "ValueA" diff --git a/tests/integration/targets/iam_role/tasks/creation_deletion.yml b/tests/integration/targets/iam_role/tasks/creation_deletion.yml new file mode 100644 index 00000000000..166c7f1236e --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/creation_deletion.yml @@ -0,0 +1,376 @@ +- name: Try running some rapid fire create/delete tests + block: + - name: Minimal IAM Role without instance profile (rapid) + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + - name: Minimal IAM Role without instance profile (rapid) + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role_again + - assert: + that: + - iam_role is changed + - iam_role_again is not changed + + - name: Remove IAM Role (rapid) + iam_role: + state: absent + name: '{{ test_role }}' + register: iam_role + - name: Remove IAM Role (rapid) + iam_role: + state: absent + name: '{{ test_role }}' + register: iam_role_again + - assert: + that: + - iam_role is changed + - iam_role_again is not changed + + - name: Minimal IAM Role without instance profile (rapid) + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + - name: Remove IAM Role (rapid) + iam_role: + state: absent + name: '{{ test_role }}' + register: iam_role_again + - assert: + that: + - iam_role is changed + - iam_role_again is changed + +# =================================================================== +# Role Creation +# (without Instance profile) +- name: iam_role_info before Role creation (no args) + iam_role_info: + register: role_info +- assert: + that: + - role_info is succeeded + +- name: iam_role_info before Role creation (search for test role) + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +- name: Minimal IAM Role (CHECK MODE) + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is changed + +- name: iam_role_info after Role creation in check_mode + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +- name: Minimal IAM Role without instance profile + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.arn.startswith("arn") + - iam_role.iam_role.arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in iam_role.iam_role' + - '"assume_role_policy_document_raw" in iam_role.iam_role' + - iam_role.iam_role.assume_role_policy_document_raw == assume_deny_policy + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '/' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + +- name: Minimal IAM Role without instance profile (no change) - check mode + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Minimal IAM Role without instance profile (no change) + iam_role: + name: '{{ test_role }}' + create_instance_profile: no + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: iam_role_info after Role creation + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"assume_role_policy_document_raw" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].assume_role_policy_document_raw == assume_deny_policy + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 0 + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 + +- name: Remove IAM Role + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: iam_role_info after Role deletion + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +# ------------------------------------------------------------------------------------------ + +# (with path) +- name: Minimal IAM Role with path (CHECK MODE) + iam_role: + name: '{{ test_role }}' + path: '{{ test_path }}' + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is changed + +- name: Minimal IAM Role with path + iam_role: + name: '{{ test_role }}' + path: '{{ test_path }}' + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.arn.startswith("arn") + - iam_role.iam_role.arn.endswith("role" + test_path + test_role ) + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '{{ test_path }}' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + +- name: Minimal IAM Role with path (no change) - check mode + iam_role: + name: '{{ test_role }}' + path: '{{ test_path }}' + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Minimal IAM Role with path (no change) + iam_role: + name: '{{ test_role }}' + path: '{{ test_path }}' + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: iam_role_info after Role creation + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role" + test_path + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile" + + test_path + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '{{ test_path }}' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 + +- name: iam_role_info after Role creation (searching a path) + iam_role_info: + path_prefix: '{{ test_path }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role" + test_path + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile" + + test_path + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].path == '{{ test_path }}' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 + +- name: Remove IAM Role + iam_role: + state: absent + name: '{{ test_role }}' + path: '{{ test_path }}' + delete_instance_profile: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: iam_role_info after Role deletion + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +# ------------------------------------------------------------------------------------------ + +# (with Instance profile) +- name: Minimal IAM Role with instance profile - check mode + iam_role: + name: '{{ test_role }}' + create_instance_profile: yes + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is changed + +- name: Minimal IAM Role with instance profile + iam_role: + name: '{{ test_role }}' + create_instance_profile: yes + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.arn.startswith("arn") + - iam_role.iam_role.arn.endswith("role/" + test_role ) + # Would be nice to test the contents... + - '"assume_role_policy_document" in iam_role.iam_role' + - iam_role.iam_role.attached_policies | length == 0 + - iam_role.iam_role.max_session_duration == 3600 + - iam_role.iam_role.path == '/' + - iam_role.iam_role.role_name == test_role + - '"create_date" in iam_role.iam_role' + - '"role_id" in iam_role.iam_role' + +- name: Minimal IAM Role wth instance profile (no change) - check mode + iam_role: + name: '{{ test_role }}' + create_instance_profile: yes + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Minimal IAM Role wth instance profile (no change) + iam_role: + name: '{{ test_role }}' + create_instance_profile: yes + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: iam_role_info after Role creation + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 3600 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 diff --git a/tests/integration/targets/iam_role/tasks/description_update.yml b/tests/integration/targets/iam_role/tasks/description_update.yml new file mode 100644 index 00000000000..198104134fb --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/description_update.yml @@ -0,0 +1,143 @@ +- name: Add Description (CHECK MODE) + iam_role: + name: '{{ test_role }}' + description: Ansible Test Role {{ resource_prefix }} + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Add Description + iam_role: + name: '{{ test_role }}' + description: Ansible Test Role {{ resource_prefix }} + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role {{ resource_prefix }}' + +- name: Add Description (no change) - check mode + iam_role: + name: '{{ test_role }}' + description: Ansible Test Role {{ resource_prefix }} + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Add Description (no change) + iam_role: + name: '{{ test_role }}' + description: Ansible Test Role {{ resource_prefix }} + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role {{ resource_prefix }}' + +- name: iam_role_info after adding Description + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 + +# ------------------------------------------------------------------------------------------ + +- name: Update Description (CHECK MODE) + iam_role: + name: '{{ test_role }}' + description: Ansible Test Role (updated) {{ resource_prefix }} + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Update Description + iam_role: + name: '{{ test_role }}' + description: Ansible Test Role (updated) {{ resource_prefix }} + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role (updated) {{ resource_prefix + }}' + +- name: Update Description (no change) - check mode + iam_role: + name: '{{ test_role }}' + description: Ansible Test Role (updated) {{ resource_prefix }} + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Update Description (no change) + iam_role: + name: '{{ test_role }}' + description: Ansible Test Role (updated) {{ resource_prefix }} + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.description == 'Ansible Test Role (updated) {{ resource_prefix + }}' + +- name: iam_role_info after updating Description + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 diff --git a/tests/integration/targets/iam_role/tasks/inline_policy_update.yml b/tests/integration/targets/iam_role/tasks/inline_policy_update.yml new file mode 100644 index 00000000000..3c82196dd8d --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/inline_policy_update.yml @@ -0,0 +1,49 @@ +- name: Attach inline policy a + iam_policy: + state: present + iam_type: role + iam_name: '{{ test_role }}' + policy_name: inline-policy-a + policy_json: '{{ lookup("file", "deny-all-a.json") }}' +- name: Attach inline policy b + iam_policy: + state: present + iam_type: role + iam_name: '{{ test_role }}' + policy_name: inline-policy-b + policy_json: '{{ lookup("file", "deny-all-b.json") }}' +- name: iam_role_info after attaching inline policies (using iam_policy) + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 2 + - '"inline-policy-a" in role_info.iam_roles[0].inline_policies' + - '"inline-policy-b" in role_info.iam_roles[0].inline_policies' + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" diff --git a/tests/integration/targets/iam_role/tasks/main.yml b/tests/integration/targets/iam_role/tasks/main.yml new file mode 100644 index 00000000000..a787d5cfac1 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/main.yml @@ -0,0 +1,83 @@ +# Tests for iam_role and iam_role_info +# +# Tests: +# - Minimal Role creation +# - Role deletion +# - Fetching a specific role +# - Creating roles w/ and w/o instance profiles +# - Creating roles w/ a path +# - Updating Max Session Duration +# - Updating Description +# - Managing list of managed policies +# - Managing list of inline policies (for testing _info) +# - Managing boundary policy +# +# Notes: +# - Only tests *documented* return values ( RESULT.iam_role ) +# - There are some known timing issues with boto3 returning before actions +# complete in the case of problems with "changed" status it's worth enabling +# the standard_pauses and paranoid_pauses options as a first step in debugging + + +- name: Setup AWS connection info + module_defaults: + group/aws: + access_key: '{{ aws_access_key }}' + secret_key: '{{ aws_secret_key }}' + session_token: '{{ security_token | default(omit) }}' + region: '{{ aws_region }}' + iam_role: + assume_role_policy_document: '{{ lookup("file", "deny-assume.json") }}' + collections: + - community.general + block: + - set_fact: + assume_deny_policy: '{{ lookup("file", "deny-assume.json") | from_json }}' + - include_tasks: parameter_checks.yml + - name: Create Safe IAM Managed Policy + iam_managed_policy: + state: present + policy_name: '{{ custom_policy_name }}' + policy_description: A safe (deny-all) managed policy + policy: "{{ lookup('file', 'deny-all.json') }}" + register: create_managed_policy + - assert: + that: + - create_managed_policy is succeeded + + # =================================================================== + # Rapid Role Creation and deletion + - include_tasks: creation_deletion.yml + - include_tasks: max_session_update.yml + - include_tasks: description_update.yml + - include_tasks: tags_update.yml + - include_tasks: policy_update.yml + - include_tasks: inline_policy_update.yml + - include_tasks: role_removal.yml + - include_tasks: boundary_policy.yml + - include_tasks: complex_role_creation.yml + always: + # =================================================================== + # Cleanup + + - name: Remove IAM Role + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + ignore_errors: true + - name: Remove IAM Role (with path) + iam_role: + state: absent + name: '{{ test_role }}' + path: '{{ test_path }}' + delete_instance_profile: yes + ignore_errors: true + - name: iam_role_info after Role deletion + iam_role_info: + name: '{{ test_role }}' + ignore_errors: true + - name: Remove test managed policy + iam_managed_policy: + state: absent + policy_name: '{{ custom_policy_name }}' diff --git a/tests/integration/targets/iam_role/tasks/max_session_update.yml b/tests/integration/targets/iam_role/tasks/max_session_update.yml new file mode 100644 index 00000000000..a850de70264 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/max_session_update.yml @@ -0,0 +1,66 @@ +- name: Update Max Session Duration (CHECK MODE) + iam_role: + name: '{{ test_role }}' + max_session_duration: 43200 + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Update Max Session Duration + iam_role: + name: '{{ test_role }}' + max_session_duration: 43200 + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.max_session_duration == 43200 + +- name: Update Max Session Duration (no change) + iam_role: + name: '{{ test_role }}' + max_session_duration: 43200 + register: iam_role +- assert: + that: + - iam_role is not changed + +- name: Update Max Session Duration (no change) - check mode + iam_role: + name: '{{ test_role }}' + max_session_duration: 43200 + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: iam_role_info after updating Max Session Duration + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - '"description" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 0 diff --git a/tests/integration/targets/iam_role/tasks/parameter_checks.yml b/tests/integration/targets/iam_role/tasks/parameter_checks.yml new file mode 100644 index 00000000000..74d3294b1c5 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/parameter_checks.yml @@ -0,0 +1,82 @@ +# Parameter Checks +- name: Friendly message when creating an instance profile and adding a boundary profile + iam_role: + name: '{{ test_role }}' + boundary: '{{ boundary_policy }}' + register: iam_role + ignore_errors: yes +- assert: + that: + - iam_role is failed + - '"boundary policy" in iam_role.msg' + - '"create_instance_profile" in iam_role.msg' + - '"false" in iam_role.msg' + +- name: Friendly message when boundary profile is not an ARN + iam_role: + name: '{{ test_role }}' + boundary: AWSDenyAll + create_instance_profile: no + register: iam_role + ignore_errors: yes +- assert: + that: + - iam_role is failed + - '"Boundary policy" in iam_role.msg' + - '"ARN" in iam_role.msg' + +- name: Friendly message when "present" without assume_role_policy_document + module_defaults: {iam_role: {}} + iam_role: + name: '{{ test_role }}' + register: iam_role + ignore_errors: yes +- assert: + that: + - iam_role is failed + - iam_role.msg.startswith("state is present but all of the following are missing") + - '"assume_role_policy_document" in iam_role.msg' + +- name: Maximum Session Duration needs to be between 1 and 12 hours + iam_role: + name: '{{ test_role }}' + max_session_duration: 3599 + register: iam_role + ignore_errors: yes +- assert: + that: + - iam_role is failed + - '"max_session_duration must be between" in iam_role.msg' + +- name: Maximum Session Duration needs to be between 1 and 12 hours + iam_role: + name: '{{ test_role }}' + max_session_duration: 43201 + register: iam_role + ignore_errors: yes +- assert: + that: + - iam_role is failed + - '"max_session_duration must be between" in iam_role.msg' + +- name: Role Paths must start with / + iam_role: + name: '{{ test_role }}' + path: test/ + register: iam_role + ignore_errors: yes +- assert: + that: + - iam_role is failed + - '"path must begin and end with /" in iam_role.msg' + +- name: Role Paths must end with / + iam_role: + name: '{{ test_role }}' + path: /test + register: iam_role + ignore_errors: yes +- assert: + that: + - iam_role is failed + - '"path must begin and end with /" in iam_role.msg' diff --git a/tests/integration/targets/iam_role/tasks/policy_update.yml b/tests/integration/targets/iam_role/tasks/policy_update.yml new file mode 100644 index 00000000000..ab16ea81f39 --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/policy_update.yml @@ -0,0 +1,246 @@ +- name: Add Managed Policy (CHECK MODE) + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ safe_managed_policy }}' + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Add Managed Policy + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ safe_managed_policy }}' + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: Add Managed Policy (no change) - check mode + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ safe_managed_policy }}' + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Add Managed Policy (no change) + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ safe_managed_policy }}' + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: iam_role_info after adding Managed Policy + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - custom_policy_name not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" + +# ------------------------------------------------------------------------------------------ + +- name: Update Managed Policy without purge (CHECK MODE) + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ custom_policy_name }}' + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Update Managed Policy without purge + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: Update Managed Policy without purge (no change) - check mode + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Update Managed Policy without purge (no change) + iam_role: + name: '{{ test_role }}' + purge_policies: no + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: iam_role_info after updating Managed Policy without purge + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 2 + - safe_managed_policy in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" + +# ------------------------------------------------------------------------------------------ + +# Managed Policies are purged by default +- name: Update Managed Policy with purge (CHECK MODE) + iam_role: + name: '{{ test_role }}' + managed_policy: + - '{{ custom_policy_name }}' + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Update Managed Policy with purge + iam_role: + name: '{{ test_role }}' + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + +- name: Update Managed Policy with purge (no change) - check mode + iam_role: + name: '{{ test_role }}' + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Update Managed Policy with purge (no change) + iam_role: + name: '{{ test_role }}' + managed_policy: + - '{{ custom_policy_name }}' + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + +- name: iam_role_info after updating Managed Policy with purge + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 1 + - safe_managed_policy not in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - custom_policy_name in ( role_info | community.general.json_query("iam_roles[*].managed_policies[*].policy_name") + | list | flatten ) + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" diff --git a/tests/integration/targets/iam_role/tasks/role_removal.yml b/tests/integration/targets/iam_role/tasks/role_removal.yml new file mode 100644 index 00000000000..7450fb9685c --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/role_removal.yml @@ -0,0 +1,59 @@ +- name: Remove IAM Role (CHECK MODE) + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: iam_role_info after deleting role in check mode + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + +- name: Remove IAM Role + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: iam_role_info after deleting role + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 0 + +- name: Remove IAM Role (should be gone already) - check mode + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Remove IAM Role (should be gone already) + iam_role: + state: absent + name: '{{ test_role }}' + delete_instance_profile: yes + register: iam_role +- assert: + that: + - iam_role is not changed diff --git a/tests/integration/targets/iam_role/tasks/tags_update.yml b/tests/integration/targets/iam_role/tasks/tags_update.yml new file mode 100644 index 00000000000..b68013212dd --- /dev/null +++ b/tests/integration/targets/iam_role/tasks/tags_update.yml @@ -0,0 +1,328 @@ +- name: Add Tag (CHECK MODE) + iam_role: + name: '{{ test_role }}' + tags: + TagA: ValueA + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Add Tag + iam_role: + name: '{{ test_role }}' + tags: + TagA: ValueA + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - iam_role.iam_role.tags | length == 1 + - '"TagA" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagA == "ValueA" + +- name: Add Tag (no change) - check mode + iam_role: + name: '{{ test_role }}' + tags: + TagA: ValueA + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Add Tag (no change) + iam_role: + name: '{{ test_role }}' + tags: + TagA: ValueA + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - '"TagA" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagA == "ValueA" + +- name: iam_role_info after adding Tags + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagA" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagA == "ValueA" + +# ------------------------------------------------------------------------------------------ + +- name: Update Tag (CHECK MODE) + iam_role: + name: '{{ test_role }}' + tags: + TagA: AValue + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Update Tag + iam_role: + name: '{{ test_role }}' + tags: + TagA: AValue + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - '"TagA" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagA == "AValue" + +- name: Update Tag (no change) - check mode + iam_role: + name: '{{ test_role }}' + tags: + TagA: AValue + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Update Tag (no change) + iam_role: + name: '{{ test_role }}' + tags: + TagA: AValue + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - '"TagA" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagA == "AValue" + +- name: iam_role_info after updating Tag + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagA" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagA == "AValue" + +# ------------------------------------------------------------------------------------------ + +- name: Add second Tag without purge (CHECK MODE) + iam_role: + name: '{{ test_role }}' + purge_tags: no + tags: + TagB: ValueB + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Add second Tag without purge + iam_role: + name: '{{ test_role }}' + purge_tags: no + tags: + TagB: ValueB + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - '"TagB" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagB == "ValueB" + +- name: Add second Tag without purge (no change) - check mode + iam_role: + name: '{{ test_role }}' + purge_tags: no + tags: + TagB: ValueB + register: iam_role + check_mode: yes +- assert: + that: + - iam_role is not changed + +- name: Add second Tag without purge (no change) + iam_role: + name: '{{ test_role }}' + purge_tags: no + tags: + TagB: ValueB + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - '"TagB" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagB == "ValueB" + +- name: iam_role_info after adding second Tag without purge + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 2 + - '"TagA" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagA == "AValue" + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB" + +# ------------------------------------------------------------------------------------------ + +- name: Purge first tag (CHECK MODE) + iam_role: + name: '{{ test_role }}' + purge_tags: yes + tags: + TagB: ValueB + check_mode: yes + register: iam_role +- assert: + that: + - iam_role is changed + +- name: Purge first tag + iam_role: + name: '{{ test_role }}' + purge_tags: yes + tags: + TagB: ValueB + register: iam_role +- assert: + that: + - iam_role is changed + - iam_role.iam_role.role_name == test_role + - '"TagB" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagB == "ValueB" + +- name: Purge first tag (no change) - check mode + iam_role: + name: '{{ test_role }}' + purge_tags: yes + tags: + TagB: ValueB + register: iam_role +- assert: + that: + - iam_role is not changed + +- name: Purge first tag (no change) + iam_role: + name: '{{ test_role }}' + purge_tags: yes + tags: + TagB: ValueB + register: iam_role +- assert: + that: + - iam_role is not changed + - iam_role.iam_role.role_name == test_role + - '"TagB" in iam_role.iam_role.tags' + - iam_role.iam_role.tags.TagB == "ValueB" + +- name: iam_role_info after purging first Tag + iam_role_info: + name: '{{ test_role }}' + register: role_info +- assert: + that: + - role_info is succeeded + - role_info.iam_roles | length == 1 + - role_info.iam_roles[0].arn.startswith("arn") + - role_info.iam_roles[0].arn.endswith("role/" + test_role ) + - '"assume_role_policy_document" in role_info.iam_roles[0]' + - '"create_date" in role_info.iam_roles[0]' + - role_info.iam_roles[0].description == "Ansible Test Role (updated) {{ resource_prefix + }}" + - role_info.iam_roles[0].inline_policies | length == 0 + - role_info.iam_roles[0].instance_profiles | length == 1 + - role_info.iam_roles[0].instance_profiles[0].instance_profile_name == test_role + - role_info.iam_roles[0].instance_profiles[0].arn.startswith("arn") + - role_info.iam_roles[0].instance_profiles[0].arn.endswith("instance-profile/" + + test_role) + - role_info.iam_roles[0].managed_policies | length == 0 + - role_info.iam_roles[0].max_session_duration == 43200 + - role_info.iam_roles[0].path == '/' + - '"permissions_boundary" not in role_info.iam_roles[0]' + - role_info.iam_roles[0].role_id == iam_role.iam_role.role_id + - role_info.iam_roles[0].role_name == test_role + - role_info.iam_roles[0].tags | length == 1 + - '"TagA" not in role_info.iam_roles[0].tags' + - '"TagB" in role_info.iam_roles[0].tags' + - role_info.iam_roles[0].tags.TagB == "ValueB"