diff --git a/changelogs/fragments/migrate_iam_access_key.yml b/changelogs/fragments/migrate_iam_access_key.yml new file mode 100644 index 00000000000..4d1f5bea3a9 --- /dev/null +++ b/changelogs/fragments/migrate_iam_access_key.yml @@ -0,0 +1,7 @@ +major_changes: +- iam_access_key - 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_access_key``. +- iam_access_key_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_access_key_info``. diff --git a/meta/runtime.yml b/meta/runtime.yml index dc35a8ec4d2..62050ae54b0 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -68,6 +68,8 @@ action_groups: - elb_application_lb_info - elb_classic_lb - execute_lambda + - iam_access_key + - iam_access_key_info - iam_group - iam_instance_profile - iam_instance_profile_info diff --git a/plugins/modules/iam_access_key.py b/plugins/modules/iam_access_key.py new file mode 100644 index 00000000000..f486d57c091 --- /dev/null +++ b/plugins/modules/iam_access_key.py @@ -0,0 +1,314 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: iam_access_key +version_added: 2.1.0 +version_added_collection: community.aws +short_description: Manage AWS IAM User access keys +description: + - Manage AWS IAM user access keys. +author: + - Mark Chappell (@tremble) +options: + user_name: + description: + - The name of the IAM User to which the key belongs. + required: true + type: str + aliases: ['username'] + id: + description: + - The ID of the access key. + - Required when I(state=absent). + - Mutually exclusive with I(rotate_keys). + required: false + type: str + state: + description: + - Create or remove the access key. + - When I(state=present) and I(id) is not defined a new key will be created. + required: false + type: str + default: 'present' + choices: [ 'present', 'absent' ] + active: + description: + - Whether the key should be enabled or disabled. + - Defaults to C(true) when creating a new key. + required: false + type: bool + aliases: ['enabled'] + rotate_keys: + description: + - When there are already 2 access keys attached to the IAM user the oldest + key will be removed and a new key created. + - Ignored if I(state=absent) + - Mutually exclusive with I(id). + required: false + type: bool + default: false +notes: + - For security reasons, this module should be used with B(no_log=true) and (register) functionalities + when creating new access key. +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Create a new access key + amazon.aws.iam_access_key: + user_name: example_user + state: present + no_log: true + +- name: Delete the access_key + amazon.aws.iam_access_key: + user_name: example_user + id: AKIA1EXAMPLE1EXAMPLE + state: absent +""" + +RETURN = r""" +access_key: + description: A dictionary containing all the access key information. + returned: When the key exists. + type: complex + contains: + access_key_id: + description: The ID for the access key. + returned: success + type: str + sample: AKIA1EXAMPLE1EXAMPLE + create_date: + description: The date and time, in ISO 8601 date-time format, when the access key was created. + returned: success + type: str + sample: "2021-10-09T13:25:42+00:00" + user_name: + description: The name of the IAM user to which the key is attached. + returned: success + type: str + sample: example_user + status: + description: + - The status of the key. + - C(Active) means it can be used. + - C(Inactive) means it can not be used. + returned: success + type: str + sample: Inactive +secret_access_key: + description: + - The secret access key. + - A secret access key is the equivalent of a password which can not be changed and as such should be considered sensitive data. + - Secret access keys can only be accessed at creation time. + returned: When a new key is created. + type: str + sample: example/Example+EXAMPLE+example/Example +deleted_access_key_id: + description: + - The access key deleted during rotation. + returned: When a key was deleted during the rotation of access keys + type: str + sample: AKIA1EXAMPLE1EXAMPLE +""" + +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.botocore import normalize_boto3_result +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry +from ansible_collections.amazon.aws.plugins.module_utils.transformation import scrub_none_parameters + + +def delete_access_key(access_keys, user, access_key_id): + if not access_key_id: + return False + + if access_key_id not in access_keys: + return False + + if module.check_mode: + return True + + try: + client.delete_access_key( + aws_retry=True, + UserName=user, + AccessKeyId=access_key_id, + ) + except is_boto3_error_code("NoSuchEntityException"): + # Generally occurs when race conditions have happened and someone + # deleted the key while we were checking to see if it existed. + return False + except ( + botocore.exceptions.ClientError, + botocore.exceptions.BotoCoreError, + ) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg=f'Failed to delete access key "{access_key_id}" for user "{user}"') + + return True + + +def update_access_key(access_keys, user, access_key_id, enabled): + if access_key_id not in access_keys: + module.fail_json( + msg=f'Access key "{access_key_id}" not found attached to User "{user}"', + ) + + changes = dict() + access_key = access_keys.get(access_key_id) + + if enabled is not None: + desired_status = "Active" if enabled else "Inactive" + if access_key.get("status") != desired_status: + changes["Status"] = desired_status + + if not changes: + return False + + if module.check_mode: + return True + + try: + client.update_access_key(aws_retry=True, UserName=user, AccessKeyId=access_key_id, **changes) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws( + e, + changes=changes, + msg=f'Failed to update access key "{access_key_id}" for user "{user}"', + ) + return True + + +def create_access_key(access_keys, user, rotate_keys, enabled): + changed = False + oldest_key = False + + if len(access_keys) > 1 and rotate_keys: + sorted_keys = sorted(list(access_keys), key=lambda k: access_keys[k].get("create_date", None)) + oldest_key = sorted_keys[0] + changed |= delete_access_key(access_keys, user, oldest_key) + + if module.check_mode: + if changed: + return dict(deleted_access_key=oldest_key) + return True + + try: + results = client.create_access_key(aws_retry=True, UserName=user) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f'Failed to create access key for user "{user}"') + results = camel_dict_to_snake_dict(results) + access_key = results.get("access_key") + access_key = normalize_boto3_result(access_key) + + # Update settings which can't be managed on creation + if enabled is False: + access_key_id = access_key["access_key_id"] + access_keys = {access_key_id: access_key} + update_access_key(access_keys, user, access_key_id, enabled) + access_key["status"] = "Inactive" + + if oldest_key: + access_key["deleted_access_key"] = oldest_key + + return access_key + + +def get_access_keys(user): + try: + results = client.list_access_keys(aws_retry=True, UserName=user) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f'Failed to get access keys for user "{user}"') + if not results: + return None + + results = camel_dict_to_snake_dict(results) + access_keys = results.get("access_key_metadata", []) + if not access_keys: + return [] + + access_keys = normalize_boto3_result(access_keys) + access_keys = {k["access_key_id"]: k for k in access_keys} + return access_keys + + +def main(): + global module + global client + + argument_spec = dict( + user_name=dict(required=True, type="str", aliases=["username"]), + id=dict(required=False, type="str"), + state=dict(required=False, choices=["present", "absent"], default="present"), + active=dict(required=False, type="bool", aliases=["enabled"]), + rotate_keys=dict(required=False, type="bool", default=False), + ) + + required_if = [ + ["state", "absent", ("id",)], + ] + mutually_exclusive = [ + ["rotate_keys", "id"], + ] + + module = AnsibleAWSModule( + argument_spec=argument_spec, + supports_check_mode=True, + ) + + client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) + + changed = False + state = module.params.get("state") + user = module.params.get("user_name") + access_key_id = module.params.get("id") + rotate_keys = module.params.get("rotate_keys") + enabled = module.params.get("active") + + access_keys = get_access_keys(user) + results = dict() + + if state == "absent": + changed |= delete_access_key(access_keys, user, access_key_id) + else: + # If we have an ID then we should try to update it + if access_key_id: + changed |= update_access_key(access_keys, user, access_key_id, enabled) + access_keys = get_access_keys(user) + results["access_key"] = access_keys.get(access_key_id, None) + # Otherwise we try to create a new one + else: + secret_key = create_access_key(access_keys, user, rotate_keys, enabled) + if isinstance(secret_key, bool): + changed |= secret_key + else: + changed = True + results["access_key_id"] = secret_key.get("access_key_id", None) + results["secret_access_key"] = secret_key.pop("secret_access_key", None) + results["deleted_access_key_id"] = secret_key.pop("deleted_access_key", None) + if secret_key: + results["access_key"] = secret_key + results = scrub_none_parameters(results) + + module.exit_json(changed=changed, **results) + + +if __name__ == "__main__": + main() diff --git a/plugins/modules/iam_access_key_info.py b/plugins/modules/iam_access_key_info.py new file mode 100644 index 00000000000..2b7c064d690 --- /dev/null +++ b/plugins/modules/iam_access_key_info.py @@ -0,0 +1,122 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +# Copyright (c) 2021 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +DOCUMENTATION = r""" +--- +module: iam_access_key_info +version_added: 2.1.0 +version_added_collection: community.aws +short_description: fetch information about AWS IAM User access keys +description: + - 'Fetches information AWS IAM user access keys.' + - 'Note: It is not possible to fetch the secret access key.' +author: + - Mark Chappell (@tremble) +options: + user_name: + description: + - The name of the IAM User to which the keys belong. + required: true + type: str + aliases: ['username'] + +extends_documentation_fragment: + - amazon.aws.common.modules + - amazon.aws.region.modules + - amazon.aws.boto3 +""" + +EXAMPLES = r""" +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Fetch Access keys for a user + amazon.aws.iam_access_key_info: + user_name: example_user +""" + +RETURN = r""" +access_key: + description: A dictionary containing all the access key information. + returned: When the key exists. + type: list + elements: dict + contains: + access_key_id: + description: The ID for the access key. + returned: success + type: str + sample: AKIA1EXAMPLE1EXAMPLE + create_date: + description: The date and time, in ISO 8601 date-time format, when the access key was created. + returned: success + type: str + sample: "2021-10-09T13:25:42+00:00" + user_name: + description: The name of the IAM user to which the key is attached. + returned: success + type: str + sample: example_user + status: + description: + - The status of the key. + - C(Active) means it can be used. + - C(Inactive) means it can not be used. + returned: success + type: str + sample: Inactive +""" + +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 normalize_boto3_result +from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry + + +def get_access_keys(user): + try: + results = client.list_access_keys(aws_retry=True, UserName=user) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg=f'Failed to get access keys for user "{user}"') + if not results: + return None + + results = camel_dict_to_snake_dict(results) + access_keys = results.get("access_key_metadata", []) + if not access_keys: + return [] + + access_keys = normalize_boto3_result(access_keys) + access_keys = sorted(access_keys, key=lambda d: d.get("create_date", None)) + return access_keys + + +def main(): + global module + global client + + argument_spec = dict( + user_name=dict(required=True, type="str", aliases=["username"]), + ) + + module = AnsibleAWSModule(argument_spec=argument_spec, supports_check_mode=True) + + client = module.client("iam", retry_decorator=AWSRetry.jittered_backoff()) + + changed = False + user = module.params.get("user_name") + access_keys = get_access_keys(user) + + module.exit_json(changed=changed, access_keys=access_keys) + + +if __name__ == "__main__": + main() diff --git a/tests/integration/targets/iam_access_key/aliases b/tests/integration/targets/iam_access_key/aliases new file mode 100644 index 00000000000..ffceccfcc41 --- /dev/null +++ b/tests/integration/targets/iam_access_key/aliases @@ -0,0 +1,9 @@ +# reason: missing-policy +# It should be possible to test iam_user by limiting which policies can be +# attached to the users. +# Careful review is needed prior to adding this to the main CI. +unsupported + +cloud/aws + +iam_access_key_info diff --git a/tests/integration/targets/iam_access_key/defaults/main.yml b/tests/integration/targets/iam_access_key/defaults/main.yml new file mode 100644 index 00000000000..d5d0baf6f6d --- /dev/null +++ b/tests/integration/targets/iam_access_key/defaults/main.yml @@ -0,0 +1 @@ +test_user: '{{ resource_prefix }}' diff --git a/tests/integration/targets/iam_access_key/meta/main.yml b/tests/integration/targets/iam_access_key/meta/main.yml new file mode 100644 index 00000000000..32cf5dda7ed --- /dev/null +++ b/tests/integration/targets/iam_access_key/meta/main.yml @@ -0,0 +1 @@ +dependencies: [] diff --git a/tests/integration/targets/iam_access_key/tasks/main.yml b/tests/integration/targets/iam_access_key/tasks/main.yml new file mode 100644 index 00000000000..027c9bb0921 --- /dev/null +++ b/tests/integration/targets/iam_access_key/tasks/main.yml @@ -0,0 +1,729 @@ +- name: AWS AuthN details + module_defaults: + group/aws: + access_key: '{{ aws_access_key }}' + secret_key: '{{ aws_secret_key }}' + session_token: '{{ security_token | default(omit) }}' + region: '{{ aws_region }}' + collections: + - community.aws + block: + # ================================================================================== + # Preparation + # ================================================================================== + # We create an IAM user with no attached permissions. The *only* thing the + # user will be able to do is call sts.get_caller_identity + # https://docs.aws.amazon.com/STS/latest/APIReference/API_GetCallerIdentity.html + - name: Create test user + iam_user: + name: '{{ test_user }}' + state: present + register: iam_user + - assert: + that: + - iam_user is successful + - iam_user is changed + + # ================================================================================== + + - name: Fetch IAM key info (no keys) + iam_access_key_info: + user_name: '{{ test_user }}' + register: access_key_info + - assert: + that: + - access_key_info is successful + - '"access_keys" in access_key_info' + - access_key_info.access_keys | length == 0 + + # ================================================================================== + + - name: Create a key (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + state: present + register: create_key_1 + check_mode: true + - assert: + that: + - create_key_1 is successful + - create_key_1 is changed + + - name: Create a key + iam_access_key: + user_name: '{{ test_user }}' + state: present + no_log: true + register: create_key_1 + - assert: + that: + - create_key_1 is successful + - create_key_1 is changed + - '"access_key" in create_key_1' + - '"secret_access_key" in create_key_1' + - '"deleted_access_key_id" not in create_key_1' + - '"access_key_id" in create_key_1.access_key' + - '"create_date" in create_key_1.access_key' + - '"user_name" in create_key_1.access_key' + - '"status" in create_key_1.access_key' + - create_key_1.access_key.user_name == test_user + - create_key_1.access_key.status == 'Active' + + - name: Fetch IAM key info (1 key) + iam_access_key_info: + user_name: '{{ test_user }}' + register: access_key_info + - assert: + that: + - access_key_info is successful + - '"access_keys" in access_key_info' + - access_key_info.access_keys | length == 1 + - '"access_key_id" in access_key_1' + - '"create_date" in access_key_1' + - '"user_name" in access_key_1' + - '"status" in access_key_1' + - access_key_1.user_name == test_user + - access_key_1.access_key_id == create_key_1.access_key.access_key_id + - access_key_1.create_date == create_key_1.access_key.create_date + - access_key_1.status == 'Active' + vars: + access_key_1: '{{ access_key_info.access_keys[0] }}' + - name: Create a second key (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + state: present + register: create_key_2 + check_mode: true + - assert: + that: + - create_key_2 is successful + - create_key_2 is changed + + - name: Create a second key + iam_access_key: + user_name: '{{ test_user }}' + state: present + no_log: true + register: create_key_2 + - assert: + that: + - create_key_2 is successful + - create_key_2 is changed + - '"access_key" in create_key_2' + - '"secret_access_key" in create_key_2' + - '"deleted_access_key_id" not in create_key_2' + - '"access_key_id" in create_key_2.access_key' + - '"create_date" in create_key_2.access_key' + - '"user_name" in create_key_2.access_key' + - '"status" in create_key_2.access_key' + - create_key_2.access_key.user_name == test_user + - create_key_2.access_key.status == 'Active' + + - name: Fetch IAM key info (2 keys) + iam_access_key_info: + user_name: '{{ test_user }}' + register: access_key_info + - assert: + that: + - access_key_info is successful + - '"access_keys" in access_key_info' + - access_key_info.access_keys | length == 2 + - '"access_key_id" in access_key_1' + - '"create_date" in access_key_1' + - '"user_name" in access_key_1' + - '"status" in access_key_1' + - access_key_1.user_name == test_user + - access_key_1.access_key_id == create_key_1.access_key.access_key_id + - access_key_1.create_date == create_key_1.access_key.create_date + - access_key_1.status == 'Active' + - '"access_key_id" in access_key_2' + - '"create_date" in access_key_2' + - '"user_name" in access_key_2' + - '"status" in access_key_2' + - access_key_2.user_name == test_user + - access_key_2.access_key_id == create_key_2.access_key.access_key_id + - access_key_2.create_date == create_key_2.access_key.create_date + - access_key_2.status == 'Active' + vars: + access_key_1: '{{ access_key_info.access_keys[0] }}' + access_key_2: '{{ access_key_info.access_keys[1] }}' + - name: Create a third key without rotation + iam_access_key: + user_name: '{{ test_user }}' + state: present + no_log: true + register: create_key_3 + ignore_errors: true + - assert: + that: + # If Amazon update the limits we may need to change the expectation here. + - create_key_3 is failed + + - name: Fetch IAM key info (2 keys - not changed) + iam_access_key_info: + user_name: '{{ test_user }}' + register: access_key_info + - assert: + that: + - access_key_info is successful + - '"access_keys" in access_key_info' + - access_key_info.access_keys | length == 2 + - '"access_key_id" in access_key_1' + - '"create_date" in access_key_1' + - '"user_name" in access_key_1' + - '"status" in access_key_1' + - access_key_1.user_name == test_user + - access_key_1.access_key_id == create_key_1.access_key.access_key_id + - access_key_1.create_date == create_key_1.access_key.create_date + - access_key_1.status == 'Active' + - '"access_key_id" in access_key_2' + - '"create_date" in access_key_2' + - '"user_name" in access_key_2' + - '"status" in access_key_2' + - access_key_2.user_name == test_user + - access_key_2.access_key_id == create_key_2.access_key.access_key_id + - access_key_2.create_date == create_key_2.access_key.create_date + - access_key_2.status == 'Active' + vars: + access_key_1: '{{ access_key_info.access_keys[0] }}' + access_key_2: '{{ access_key_info.access_keys[1] }}' + - name: Create a third key - rotation enabled (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + state: present + rotate_keys: true + register: create_key_3 + check_mode: true + - assert: + that: + - create_key_3 is successful + - create_key_3 is changed + - '"deleted_access_key_id" in create_key_3' + - create_key_3.deleted_access_key_id == create_key_1.access_key.access_key_id + + - name: Create a second key + iam_access_key: + user_name: '{{ test_user }}' + state: present + rotate_keys: true + no_log: true + register: create_key_3 + - assert: + that: + - create_key_3 is successful + - create_key_3 is changed + - '"access_key" in create_key_3' + - '"secret_access_key" in create_key_3' + - '"deleted_access_key_id" in create_key_3' + - create_key_3.deleted_access_key_id == create_key_1.access_key.access_key_id + - '"access_key_id" in create_key_3.access_key' + - '"create_date" in create_key_3.access_key' + - '"user_name" in create_key_3.access_key' + - '"status" in create_key_3.access_key' + - create_key_3.access_key.user_name == test_user + - create_key_3.access_key.status == 'Active' + + - name: Fetch IAM key info (2 keys - oldest rotated) + iam_access_key_info: + user_name: '{{ test_user }}' + register: access_key_info + - assert: + that: + - access_key_info is successful + - '"access_keys" in access_key_info' + - access_key_info.access_keys | length == 2 + - '"access_key_id" in access_key_1' + - '"create_date" in access_key_1' + - '"user_name" in access_key_1' + - '"status" in access_key_1' + - access_key_1.user_name == test_user + - access_key_1.access_key_id == create_key_2.access_key.access_key_id + - access_key_1.create_date == create_key_2.access_key.create_date + - access_key_1.status == 'Active' + - '"access_key_id" in access_key_2' + - '"create_date" in access_key_2' + - '"user_name" in access_key_2' + - '"status" in access_key_2' + - access_key_2.user_name == test_user + - access_key_2.access_key_id == create_key_3.access_key.access_key_id + - access_key_2.create_date == create_key_3.access_key.create_date + - access_key_2.status == 'Active' + vars: + access_key_1: '{{ access_key_info.access_keys[0] }}' + access_key_2: '{{ access_key_info.access_keys[1] }}' + - name: Disable third key (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: false + register: disable_key + check_mode: true + - assert: + that: + - disable_key is successful + - disable_key is changed + + - name: Disable third key + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: false + register: disable_key + - assert: + that: + - disable_key is successful + - disable_key is changed + - '"access_key" in disable_key' + - '"secret_access_key" not in disable_key' + - '"deleted_access_key_id" not in disable_key' + - '"access_key_id" in disable_key.access_key' + - '"create_date" in disable_key.access_key' + - '"user_name" in disable_key.access_key' + - '"status" in disable_key.access_key' + - disable_key.access_key.user_name == test_user + - disable_key.access_key.status == 'Inactive' + + - name: Disable third key - idempotency (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: false + register: disable_key + check_mode: true + - assert: + that: + - disable_key is successful + - disable_key is not changed + + - name: Disable third key - idempotency + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: false + register: disable_key + - assert: + that: + - disable_key is successful + - disable_key is not changed + - '"access_key" in disable_key' + - '"secret_access_key" not in disable_key' + - '"deleted_access_key_id" not in disable_key' + - '"access_key_id" in disable_key.access_key' + - '"create_date" in disable_key.access_key' + - '"user_name" in disable_key.access_key' + - '"status" in disable_key.access_key' + - disable_key.access_key.user_name == test_user + - disable_key.access_key.status == 'Inactive' + + - name: Fetch IAM key info (2 keys - 1 disabled) + iam_access_key_info: + user_name: '{{ test_user }}' + register: access_key_info + - assert: + that: + - access_key_info is successful + - '"access_keys" in access_key_info' + - access_key_info.access_keys | length == 2 + - '"access_key_id" in access_key_1' + - '"create_date" in access_key_1' + - '"user_name" in access_key_1' + - '"status" in access_key_1' + - access_key_1.user_name == test_user + - access_key_1.access_key_id == create_key_2.access_key.access_key_id + - access_key_1.create_date == create_key_2.access_key.create_date + - access_key_1.status == 'Active' + - '"access_key_id" in access_key_2' + - '"create_date" in access_key_2' + - '"user_name" in access_key_2' + - '"status" in access_key_2' + - access_key_2.user_name == test_user + - access_key_2.access_key_id == create_key_3.access_key.access_key_id + - access_key_2.create_date == create_key_3.access_key.create_date + - access_key_2.status == 'Inactive' + vars: + access_key_1: '{{ access_key_info.access_keys[0] }}' + access_key_2: '{{ access_key_info.access_keys[1] }}' + - name: Touch third key - no change (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + register: touch_key + check_mode: true + - assert: + that: + - touch_key is successful + - touch_key is not changed + + - name: Touch third key - no change + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + register: touch_key + - assert: + that: + - touch_key is successful + - touch_key is not changed + - '"access_key" in touch_key' + - '"secret_access_key" not in touch_key' + - '"deleted_access_key_id" not in touch_key' + - '"access_key_id" in touch_key.access_key' + - '"create_date" in touch_key.access_key' + - '"user_name" in touch_key.access_key' + - '"status" in touch_key.access_key' + - touch_key.access_key.user_name == test_user + - touch_key.access_key.status == 'Inactive' + + # ================================================================================== + + - name: Enable third key (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: true + register: enable_key + check_mode: true + - assert: + that: + - enable_key is successful + - enable_key is changed + + - name: Enable third key + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: true + register: enable_key + - assert: + that: + - enable_key is successful + - enable_key is changed + - '"access_key" in enable_key' + - '"secret_access_key" not in enable_key' + - '"deleted_access_key_id" not in enable_key' + - '"access_key_id" in enable_key.access_key' + - '"create_date" in enable_key.access_key' + - '"user_name" in enable_key.access_key' + - '"status" in enable_key.access_key' + - enable_key.access_key.user_name == test_user + - enable_key.access_key.status == 'Active' + + - name: Enable third key - idempotency (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: true + register: enable_key + check_mode: true + - assert: + that: + - enable_key is successful + - enable_key is not changed + + - name: Enable third key - idempotency + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: true + register: enable_key + - assert: + that: + - enable_key is successful + - enable_key is not changed + - '"access_key" in enable_key' + - '"secret_access_key" not in enable_key' + - '"deleted_access_key_id" not in enable_key' + - '"access_key_id" in enable_key.access_key' + - '"create_date" in enable_key.access_key' + - '"user_name" in enable_key.access_key' + - '"status" in enable_key.access_key' + - enable_key.access_key.user_name == test_user + - enable_key.access_key.status == 'Active' + + # ================================================================================== + + - name: Touch third key again - no change (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + register: touch_key + check_mode: true + - assert: + that: + - touch_key is successful + - touch_key is not changed + + - name: Touch third key again - no change + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + register: touch_key + - assert: + that: + - touch_key is successful + - touch_key is not changed + - '"access_key" in touch_key' + - '"secret_access_key" not in touch_key' + - '"deleted_access_key_id" not in touch_key' + - '"access_key_id" in touch_key.access_key' + - '"create_date" in touch_key.access_key' + - '"user_name" in touch_key.access_key' + - '"status" in touch_key.access_key' + - touch_key.access_key.user_name == test_user + - touch_key.access_key.status == 'Active' + + # ================================================================================== + + - name: Re-Disable third key + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + enabled: false + register: redisable_key + - assert: + that: + - redisable_key is successful + - redisable_key is changed + - redisable_key.access_key.status == 'Inactive' + + - pause: + seconds: 10 + - name: Test GetCallerIdentity - Key 2 + aws_caller_info: + access_key: '{{ create_key_2.access_key.access_key_id }}' + secret_key: '{{ create_key_2.secret_access_key }}' + session_token: '{{ omit }}' + register: caller_identity_2 + - assert: + that: + - caller_identity_2 is successful + - caller_identity_2.arn == iam_user.iam_user.user.arn + + - name: Test GetCallerIdentity - Key 1 (gone) + aws_caller_info: + access_key: '{{ create_key_1.access_key.access_key_id }}' + secret_key: '{{ create_key_1.secret_access_key }}' + session_token: '{{ omit }}' + register: caller_identity_1 + ignore_errors: true + - assert: + that: + - caller_identity_1 is failed + - caller_identity_1.error.code == 'InvalidClientTokenId' + + - name: Test GetCallerIdentity - Key 3 (disabled) + aws_caller_info: + access_key: '{{ create_key_3.access_key.access_key_id }}' + secret_key: '{{ create_key_3.secret_access_key }}' + session_token: '{{ omit }}' + register: caller_identity_3 + ignore_errors: true + - assert: + that: + - caller_identity_3 is failed + - caller_identity_3.error.code == 'InvalidClientTokenId' + + # ================================================================================== + + - name: Delete active key (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_2.access_key.access_key_id }}' + state: absent + register: delete_active_key + check_mode: true + - assert: + that: + - delete_active_key is successful + - delete_active_key is changed + + - name: Delete active key + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_2.access_key.access_key_id }}' + state: absent + register: delete_active_key + - assert: + that: + - delete_active_key is successful + - delete_active_key is changed + + - name: Delete active key - idempotency (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_2.access_key.access_key_id }}' + state: absent + register: delete_active_key + check_mode: true + - assert: + that: + - delete_active_key is successful + - delete_active_key is not changed + + - name: Delete active key - idempotency + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_2.access_key.access_key_id }}' + state: absent + register: delete_active_key + - assert: + that: + - delete_active_key is successful + - delete_active_key is not changed + + # ================================================================================== + + - name: Delete inactive key (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + state: absent + register: delete_inactive_key + check_mode: true + - assert: + that: + - delete_inactive_key is successful + - delete_inactive_key is changed + + - name: Delete inactive key + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + state: absent + register: delete_inactive_key + - assert: + that: + - delete_inactive_key is successful + - delete_inactive_key is changed + + - name: Delete inactive key - idempotency (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + state: absent + register: delete_inactive_key + check_mode: true + - assert: + that: + - delete_inactive_key is successful + - delete_inactive_key is not changed + + - name: Delete inactive key - idempotency + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_3.access_key.access_key_id }}' + state: absent + register: delete_inactive_key + - assert: + that: + - delete_inactive_key is successful + - delete_inactive_key is not changed + + # ================================================================================== + + - name: Fetch IAM key info (no keys) + iam_access_key_info: + user_name: '{{ test_user }}' + register: access_key_info + - assert: + that: + - access_key_info is successful + - '"access_keys" in access_key_info' + - access_key_info.access_keys | length == 0 + + # ================================================================================== + + - name: Create an inactive key (check_mode) + iam_access_key: + user_name: '{{ test_user }}' + state: present + enabled: false + register: create_key_4 + check_mode: true + - assert: + that: + - create_key_4 is successful + - create_key_4 is changed + + - name: Create a key + iam_access_key: + user_name: '{{ test_user }}' + state: present + enabled: false + no_log: true + register: create_key_4 + - assert: + that: + - create_key_4 is successful + - create_key_4 is changed + - '"access_key" in create_key_4' + - '"secret_access_key" in create_key_4' + - '"deleted_access_key_id" not in create_key_4' + - '"access_key_id" in create_key_4.access_key' + - '"create_date" in create_key_4.access_key' + - '"user_name" in create_key_4.access_key' + - '"status" in create_key_4.access_key' + - create_key_4.access_key.user_name == test_user + - create_key_4.access_key.status == 'Inactive' + + - name: Fetch IAM key info (1 inactive key) + iam_access_key_info: + user_name: '{{ test_user }}' + register: access_key_info + - assert: + that: + - access_key_info is successful + - '"access_keys" in access_key_info' + - access_key_info.access_keys | length == 1 + - '"access_key_id" in access_key_1' + - '"create_date" in access_key_1' + - '"user_name" in access_key_1' + - '"status" in access_key_1' + - access_key_1.user_name == test_user + - access_key_1.access_key_id == create_key_4.access_key.access_key_id + - access_key_1.create_date == create_key_4.access_key.create_date + - access_key_1.status == 'Inactive' + vars: + access_key_1: '{{ access_key_info.access_keys[0] }}' + - name: Disable new key + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_4.access_key.access_key_id }}' + enabled: false + register: disable_new_key + - assert: + that: + - disable_new_key is successful + - disable_new_key is not changed + - '"access_key" in disable_new_key' + + # ================================================================================== + # Cleanup + + - name: Delete new key + iam_access_key: + user_name: '{{ test_user }}' + id: '{{ create_key_4.access_key.access_key_id }}' + state: absent + register: delete_new_key + - assert: + that: + - delete_new_key is successful + - delete_new_key is changed + + - name: Remove test user + iam_user: + name: '{{ test_user }}' + state: absent + register: delete_user + - assert: + that: + - delete_user is successful + - delete_user is changed + + always: + + - name: Remove test user + iam_user: + name: '{{ test_user }}' + state: absent + ignore_errors: true