Repository: ansible-collections/amazon.aws

Issue:
[PR #1701/19b89f68 backport][stable-6] Backup selection bugfix
**This is a backport of PR #1701 as merged into main (19b89f682227f8755322250c929355812dc37a2c).**

##### SUMMARY
Fix bug in backup_selection where an existing backup selection was not updated to add Conditions if that value was previously empty.

##### ISSUE TYPE
- Bugfix Pull Request

##### COMPONENT NAME
backup_selection

##### ADDITIONAL INFORMATION
Split into two commits, the first commit adds comprehensive integration tests to cover the scenarios that were failing and the second commit fixes the code. 



Discussion:

Resolving patch:
diff --git a/plugins/modules/iam_access_key.py b/plugins/modules/iam_access_key.py
new file mode 100644
--- /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
--- /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()


Test patch:
diff --git a/tests/integration/targets/iam_access_key/aliases b/tests/integration/targets/iam_access_key/aliases
new file mode 100644
--- /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
--- /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
--- /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
--- /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