diff --git a/meta/runtime.yml b/meta/runtime.yml index c1ca9228a31..39685c11f83 100644 --- a/meta/runtime.yml +++ b/meta/runtime.yml @@ -162,6 +162,8 @@ action_groups: - elb_target_info - execute_lambda - iam + - iam_access_key + - iam_access_key_info - iam_cert - iam_group - iam_managed_policy diff --git a/plugins/modules/iam_access_key.py b/plugins/modules/iam_access_key.py new file mode 100644 index 00000000000..1d5701e9d74 --- /dev/null +++ b/plugins/modules/iam_access_key.py @@ -0,0 +1,316 @@ +#!/usr/bin/python +# Copyright (c) 2021 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: iam_access_key +version_added: 2.1.0 +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 + +extends_documentation_fragment: +- amazon.aws.aws +- amazon.aws.ec2 +''' + +EXAMPLES = r''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Create a new access key + community.aws.iam_access_key: + user_name: example_user + state: present + +- name: Delete the access_key + community.aws.iam_access_key: + name: example_user + access_key_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.core import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code +from ansible_collections.amazon.aws.plugins.module_utils.core import normalize_boto3_result +from ansible_collections.amazon.aws.plugins.module_utils.core import scrub_none_parameters +from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry + + +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='Failed to delete access key "{0}" for user "{1}"'.format(access_key_id, 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='Access key "{0}" not found attached to User "{1}"'.format(access_key_id, 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='Failed to update access key "{0}" for user "{1}"'.format(access_key_id, 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='Failed to create access key for user "{0}"'.format(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='Failed to get access keys for user "{0}"'.format(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..9251cb846f6 --- /dev/null +++ b/plugins/modules/iam_access_key_info.py @@ -0,0 +1,127 @@ +#!/usr/bin/python +# Copyright (c) 2021 Ansible Project +# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt) + +from __future__ import absolute_import, division, print_function +__metaclass__ = type + + +DOCUMENTATION = r''' +--- +module: iam_access_key_info +version_added: 2.1.0 +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.aws +- amazon.aws.ec2 +''' + +EXAMPLES = r''' +# Note: These examples do not set authentication details, see the AWS Guide for details. + +- name: Fetch Access keys for a user + community.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.core import AnsibleAWSModule +from ansible_collections.amazon.aws.plugins.module_utils.core import normalize_boto3_result +from ansible_collections.amazon.aws.plugins.module_utils.ec2 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='Failed to get access keys for user "{0}"'.format(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..eaaa3523e19 --- /dev/null +++ b/tests/integration/targets/iam_access_key/defaults/main.yml @@ -0,0 +1,2 @@ +--- +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..1f64f1169a9 --- /dev/null +++ b/tests/integration/targets/iam_access_key/meta/main.yml @@ -0,0 +1,3 @@ +dependencies: + - prepare_tests + - setup_ec2 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..a7fcc633ce9 --- /dev/null +++ b/tests/integration/targets/iam_access_key/tasks/main.yml @@ -0,0 +1,808 @@ +--- +- name: AWS AuthN details + module_defaults: + group/aws: + aws_access_key: "{{ aws_access_key }}" + aws_secret_key: "{{ aws_secret_key }}" + security_token: "{{ security_token | default(omit) }}" + region: "{{ aws_region }}" + collections: + - amazon.aws + - 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 + 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 + 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] }}' + + # ================================================================================== + + # We don't block the attempt to create a third access key - should AWS change + # the limits this will "JustWork". + + # - name: Create a third key (check_mode) + # iam_access_key: + # user_name: '{{ test_user }}' + # state: present + # register: create_key_3 + # ignore_errors: True + # check_mode: true + + # - assert: + # that: + # - create_key_3 is successful + # - create_key_3 is changed + + - name: Create a third key without rotation + iam_access_key: + user_name: '{{ test_user }}' + state: present + 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 + 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: + aws_access_key: "{{ create_key_2.access_key.access_key_id }}" + aws_secret_key: "{{ create_key_2.secret_access_key }}" + security_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: + aws_access_key: "{{ create_key_1.access_key.access_key_id }}" + aws_secret_key: "{{ create_key_1.secret_access_key }}" + security_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: + aws_access_key: "{{ create_key_3.access_key.access_key_id }}" + aws_secret_key: "{{ create_key_3.secret_access_key }}" + security_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 + 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] }}' + + # We already tested the idempotency of disabling keys, use this to verify that + # the key is disabled + - 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: yes diff --git a/tests/integration/targets/iam_user/tasks/main.yml b/tests/integration/targets/iam_user/tasks/main.yml index c5be49ec4c0..4bdd6eebc2c 100644 --- a/tests/integration/targets/iam_user/tasks/main.yml +++ b/tests/integration/targets/iam_user/tasks/main.yml @@ -20,38 +20,6 @@ - iam_user_info_path_group is failed - 'iam_user_info_path_group.msg == "parameters are mutually exclusive: group|path"' - - name: ensure exception handling fails as expected - iam_user_info: - region: 'bogus' - path: '' - ignore_errors: yes - register: iam_user_info - - assert: - that: - - iam_user_info is failed - - '"user" in iam_user_info.msg' - - - name: ensure exception handling fails as expected with group - iam_user_info: - region: 'bogus' - group: '{{ test_group }}' - ignore_errors: yes - register: iam_user_info - - assert: - that: - - iam_user_info is failed - - '"group" in iam_user_info.msg' - - - name: ensure exception handling fails as expected with default path - iam_user_info: - region: 'bogus' - ignore_errors: yes - register: iam_user_info - - assert: - that: - - iam_user_info is failed - - '"path" in iam_user_info.msg' - - name: create test user (check mode) iam_user: name: '{{ test_user }}'