From fe6af9cebaa4c5f9e87015af44d039c6fcfbf614 Mon Sep 17 00:00:00 2001 From: Mark Woolley Date: Mon, 13 Dec 2021 09:59:52 +0000 Subject: [PATCH] iam_user password management support (#822) iam_user password management support SUMMARY The iam module currently supports password management for IAM users, but the newer iam_user module does not currently. This PR adds the password management functionality to bring parity with the old module. To ensure the IAM user is properly created before adding a login profile, the waiter for the IAM creation has also been added. ISSUE TYPE Feature Pull Request COMPONENT NAME iam_user ADDITIONAL INFORMATION The added functionality uses the create_login_profile and update_login_profile methods: https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html#IAM.Client.create_login_profile https://boto3.amazonaws.com/v1/documentation/api/latest/reference/services/iam.html#IAM.Client.update_login_profile Local integration tests run: ansible-test integration --docker centos8 -vv iam_user --allow-unsupported ... PLAY RECAP ********************************************************************* testhost : ok=92 changed=24 unreachable=0 failed=0 skipped=0 rescued=0 ignored=2 AWS ACTIONS: ['iam:AddUserToGroup', 'iam:AttachUserPolicy', 'iam:CreateGroup', 'iam:CreateLoginProfile', 'iam:CreateUser', 'iam:DeleteGroup', 'iam:DeleteLoginProfile', 'iam:DeleteUser', 'iam:DetachUserPolicy', 'iam:GetGroup', 'iam:GetUser', 'iam:ListAccessKeys', 'iam:ListAttachedGroupPolicies', 'iam:ListAttachedUserPolicies', 'iam:ListGroupsForUser', 'iam:ListMFADevices', 'iam:ListPolicies', 'iam:ListSSHPublicKeys', 'iam:ListServiceSpecificCredentials', 'iam:ListSigningCertificates', 'iam:ListUserPolicies', 'iam:ListUsers', 'iam:RemoveUserFromGroup', 'iam:TagUser', 'iam:UntagUser', 'iam:UpdateLoginProfile'] Reviewed-by: Markus Bergholz Reviewed-by: Mark Chappell Reviewed-by: None This commit was initially merged in https://github.com/ansible-collections/community.aws See: https://github.com/ansible-collections/community.aws/commit/a3d940af4a717327f3bce0726a2c23b6890e8609 --- plugins/modules/iam_user.py | 130 +++++++- .../targets/iam_user/defaults/main.yml | 4 + .../targets/iam_user/tasks/main.yml | 293 +++++++++++------- 3 files changed, 315 insertions(+), 112 deletions(-) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index 2a7998a6d10..e308c3cf0cb 100644 --- a/plugins/modules/iam_user.py +++ b/plugins/modules/iam_user.py @@ -12,7 +12,8 @@ version_added: 1.0.0 short_description: Manage AWS IAM users description: - - Manage AWS IAM users. + - A module to manage AWS IAM users. + - The module does not manage groups that users belong to, groups memberships can be managed using `iam_group`. author: Josh Souza (@joshsouza) options: name: @@ -20,6 +21,27 @@ - The name of the user to create. required: true type: str + password: + description: + - The password to apply to the user. + required: false + type: str + version_added: 2.2.0 + update_password: + default: always + choices: ['always', 'on_create'] + description: + - When to update user passwords. + - I(update_password=always) will ensure the password is set to I(password). + - I(update_password=on_create) will only set the password for newly created users. + type: str + version_added: 2.2.0 + remove_password: + description: + - Option to delete user login passwords. + - This field is mutually exclusive to I(password). + type: 'bool' + version_added: 2.2.0 managed_policies: description: - A list of managed policy ARNs or friendly names to attach to the user. @@ -36,7 +58,7 @@ type: str purge_policies: description: - - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detatched. + - When I(purge_policies=true) any managed policies not listed in I(managed_policies) will be detached. required: false default: false type: bool @@ -53,6 +75,19 @@ default: true type: bool version_added: 2.1.0 + wait: + description: + - When I(wait=True) the module will wait for up to I(wait_timeout) seconds + for IAM user creation before returning. + default: True + type: bool + version_added: 2.2.0 + wait_timeout: + description: + - How long (in seconds) to wait for creation / updates to complete. + default: 120 + type: int + version_added: 2.2.0 extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 @@ -70,6 +105,12 @@ name: testuser1 state: present +- name: Create a user with a password + community.aws.iam_user: + name: testuser1 + password: SomeSecurePassword + state: present + - name: Create a user and attach a managed policy using its ARN community.aws.iam_user: name: testuser1 @@ -179,15 +220,75 @@ def convert_friendly_names_to_arns(connection, module, policy_names): module.fail_json(msg="Couldn't find policy: " + str(e)) +def wait_iam_exists(connection, module): + if module.check_mode: + return + if not module.params.get('wait'): + return + + user_name = module.params.get('name') + wait_timeout = module.params.get('wait_timeout') + + delay = min(wait_timeout, 5) + max_attempts = wait_timeout // delay + + try: + waiter = connection.get_waiter('user_exists') + waiter.wait( + WaiterConfig={'Delay': delay, 'MaxAttempts': max_attempts}, + UserName=user_name, + ) + except botocore.exceptions.WaiterError as e: + module.fail_json_aws(e, msg='Timeout while waiting on IAM user creation') + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg='Failed while waiting on IAM user creation') + + +def create_or_update_login_profile(connection, module): + + # Apply new password / update password for the user + user_params = dict() + user_params['UserName'] = module.params.get('name') + user_params['Password'] = module.params.get('password') + + try: + connection.update_login_profile(**user_params) + except is_boto3_error_code('NoSuchEntity'): + try: + connection.create_login_profile(**user_params) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Unable to create user login profile") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except + module.fail_json_aws(e, msg="Unable to update user login profile") + + return True + + +def delete_login_profile(connection, module): + + user_params = dict() + user_params['UserName'] = module.params.get('name') + + try: + connection.delete_login_profile(**user_params) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Unable to delete user login profile") + + return True + + def create_or_update_user(connection, module): params = dict() params['UserName'] = module.params.get('name') managed_policies = module.params.get('managed_policies') purge_policies = module.params.get('purge_policies') + if module.params.get('tags') is not None: params["Tags"] = ansible_dict_to_boto3_tag_list(module.params.get('tags')) + changed = False + if managed_policies: managed_policies = convert_friendly_names_to_arns(connection, module, managed_policies) @@ -205,8 +306,23 @@ def create_or_update_user(connection, module): changed = True except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: module.fail_json_aws(e, msg="Unable to create user") + + # Wait for user to be fully available before continuing + if module.params.get('wait'): + wait_iam_exists(connection, module) + + if module.params.get('password') is not None: + create_or_update_login_profile(connection, module) else: - changed = update_user_tags(connection, module, params, user) + login_profile_result = None + update_result = update_user_tags(connection, module, params, user) + + if module.params['update_password'] == "always" and module.params.get('password') is not None: + login_profile_result = create_or_update_login_profile(connection, module) + elif module.params.get('remove_password'): + login_profile_result = delete_login_profile(connection, module) + + changed = bool(update_result) or bool(login_profile_result) # Manage managed policies current_attached_policies = get_attached_policy_list(connection, module, params['UserName']) @@ -388,16 +504,22 @@ def main(): argument_spec = dict( name=dict(required=True, type='str'), + password=dict(type='str', no_log=True), + update_password=dict(default='always', choices=['always', 'on_create'], no_log=False), + remove_password=dict(type='bool'), managed_policies=dict(default=[], type='list', aliases=['managed_policy'], elements='str'), state=dict(choices=['present', 'absent'], required=True), purge_policies=dict(default=False, type='bool', aliases=['purge_policy', 'purge_managed_policies']), tags=dict(type='dict'), purge_tags=dict(type='bool', default=True), + wait=dict(type='bool', default=True), + wait_timeout=dict(default=120, type='int'), ) module = AnsibleAWSModule( argument_spec=argument_spec, - supports_check_mode=True + supports_check_mode=True, + mutually_exclusive=[['password', 'remove_password']] ) connection = module.client('iam') diff --git a/tests/integration/targets/iam_user/defaults/main.yml b/tests/integration/targets/iam_user/defaults/main.yml index 8a69ca0931c..1bbb5df0bf0 100644 --- a/tests/integration/targets/iam_user/defaults/main.yml +++ b/tests/integration/targets/iam_user/defaults/main.yml @@ -2,6 +2,10 @@ test_group: '{{ resource_prefix }}-group' test_path: '/' test_user: '{{ test_users[0] }}' +test_user3: '{{ test_users[2] }}' +test_password: ATotallySecureUncrackablePassword1! +test_new_password: ATotallyNewSecureUncrackablePassword1! test_users: - '{{ resource_prefix }}-user-a' - '{{ resource_prefix }}-user-b' + - '{{ resource_prefix }}-user-c' diff --git a/tests/integration/targets/iam_user/tasks/main.yml b/tests/integration/targets/iam_user/tasks/main.yml index 76d13e57f19..397ba70a23e 100644 --- a/tests/integration/targets/iam_user/tasks/main.yml +++ b/tests/integration/targets/iam_user/tasks/main.yml @@ -11,10 +11,11 @@ block: - name: ensure improper usage of parameters fails gracefully iam_user_info: - path: '{{ test_path }}' - group: '{{ test_group }}' + path: "{{ test_path }}" + group: "{{ test_group }}" ignore_errors: yes register: iam_user_info_path_group + - assert: that: - iam_user_info_path_group is failed @@ -22,7 +23,7 @@ - name: create test user (check mode) iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present check_mode: yes register: iam_user @@ -34,7 +35,7 @@ - name: create test user iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present register: iam_user @@ -45,7 +46,7 @@ - name: ensure test user exists (no change) iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present register: iam_user @@ -56,7 +57,8 @@ - name: ensure the info used to validate other tests is valid set_fact: - test_iam_user: '{{ iam_user.iam_user.user }}' + test_iam_user: "{{ iam_user.iam_user.user }}" + - assert: that: - 'test_iam_user.arn.startswith("arn:aws:iam")' @@ -70,15 +72,18 @@ - name: get info on IAM user(s) iam_user_info: register: iam_user_info + - assert: that: - iam_user_info.iam_users | length != 0 - name: get info on IAM user(s) with name iam_user_info: - name: '{{ test_user }}' + name: "{{ test_user }}" register: iam_user_info + - debug: var=iam_user_info + - assert: that: - iam_user_info.iam_users | length == 1 @@ -89,11 +94,37 @@ - iam_user_info.iam_users[0].user_name == test_iam_user.user_name - iam_user_info.iam_users[0].tags | length == 0 + - name: create test user with password (check mode) + iam_user: + name: "{{ test_user3 }}" + password: "{{ test_password }}" + state: present + check_mode: yes + register: iam_user + + - name: assert that the second user would be created + assert: + that: + - iam_user is changed + + - name: create second test user with password + iam_user: + name: "{{ test_user3 }}" + password: "{{ test_password }}" + state: present + register: iam_user + + - name: assert that the second user is created + assert: + that: + - iam_user is changed + - name: get info on IAM user(s) on path iam_user_info: - path: '{{ test_path }}' - name: '{{ test_user }}' + path: "{{ test_path }}" + name: "{{ test_user }}" register: iam_user_info + - assert: that: - iam_user_info.iam_users | length == 1 @@ -104,39 +135,42 @@ - iam_user_info.iam_users[0].user_name == test_iam_user.user_name - iam_user_info.iam_users[0].tags | length == 0 - - name: 'Add Tag' + ## Test tags creation / updates + - name: "Add Tag" iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present tags: TagA: ValueA register: iam_user + - assert: that: - - iam_user is changed - - iam_user.iam_user.user.user_name == test_user - - iam_user.iam_user.user.tags | length == 1 - - '"TagA" in iam_user.iam_user.user.tags' - - iam_user.iam_user.user.tags.TagA == "ValueA" + - iam_user is changed + - iam_user.iam_user.user.user_name == test_user + - iam_user.iam_user.user.tags | length == 1 + - '"TagA" in iam_user.iam_user.user.tags' + - iam_user.iam_user.user.tags.TagA == "ValueA" - - name: 'Add Tag (no change)' + - name: "Add Tag (no change)" iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present tags: TagA: ValueA register: iam_user + - assert: that: - - iam_user is not changed - - iam_user.iam_user.user.user_name == test_user - - iam_user.iam_user.user.tags | length == 1 - - '"TagA" in iam_user.iam_user.user.tags' - - iam_user.iam_user.user.tags.TagA == "ValueA" + - iam_user is not changed + - iam_user.iam_user.user.user_name == test_user + - iam_user.iam_user.user.tags | length == 1 + - '"TagA" in iam_user.iam_user.user.tags' + - iam_user.iam_user.user.tags.TagA == "ValueA" - - name: 'Extend Tags' + - name: "Extend Tags" iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present purge_tags: no tags: @@ -144,83 +178,114 @@ "Tag C": "Value C" "tag d": "value d" register: iam_user + - assert: that: - - iam_user is changed - - iam_user.iam_user.user.user_name == test_user - - iam_user.iam_user.user.tags | length == 4 - - '"TagA" in iam_user.iam_user.user.tags' - - '"tag_b" in iam_user.iam_user.user.tags' - - '"Tag C" in iam_user.iam_user.user.tags' - - '"tag d" in iam_user.iam_user.user.tags' - - iam_user.iam_user.user.tags.TagA == "ValueA" - - iam_user.iam_user.user.tags.tag_b == "value_b" - - iam_user.iam_user.user.tags["Tag C"] == "Value C" - - iam_user.iam_user.user.tags["tag d"] == "value d" - - - name: 'Create user without Tag (no change)' + - iam_user is changed + - iam_user.iam_user.user.user_name == test_user + - iam_user.iam_user.user.tags | length == 4 + - '"TagA" in iam_user.iam_user.user.tags' + - '"tag_b" in iam_user.iam_user.user.tags' + - '"Tag C" in iam_user.iam_user.user.tags' + - '"tag d" in iam_user.iam_user.user.tags' + - iam_user.iam_user.user.tags.TagA == "ValueA" + - iam_user.iam_user.user.tags.tag_b == "value_b" + - iam_user.iam_user.user.tags["Tag C"] == "Value C" + - iam_user.iam_user.user.tags["tag d"] == "value d" + + - name: "Create user without Tag (no change)" iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present register: iam_user + - assert: that: - - iam_user is not changed - - iam_user.iam_user.user.user_name == test_user - - iam_user.iam_user.user.tags | length == 4 + - iam_user is not changed + - iam_user.iam_user.user.user_name == test_user + - iam_user.iam_user.user.tags | length == 4 - - name: 'Remove all Tags (check mode)' + - name: "Remove all Tags (check mode)" iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present tags: {} check_mode: yes register: iam_user + - assert: that: - - iam_user is changed + - iam_user is changed - - name: 'Remove 3 Tags' + - name: "Remove 3 Tags" iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present tags: TagA: ValueA register: iam_user + - assert: that: - - iam_user is changed - - iam_user.iam_user.user.user_name == test_user - - iam_user.iam_user.user.tags | length == 1 - - '"TagA" in iam_user.iam_user.user.tags' - - iam_user.iam_user.user.tags.TagA == "ValueA" + - iam_user is changed + - iam_user.iam_user.user.user_name == test_user + - iam_user.iam_user.user.tags | length == 1 + - '"TagA" in iam_user.iam_user.user.tags' + - iam_user.iam_user.user.tags.TagA == "ValueA" - - name: 'Change Tag' + - name: "Change Tag" iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present tags: TagA: AnotherValueA register: iam_user + - assert: that: - - iam_user is changed - - iam_user.iam_user.user.user_name == test_user - - iam_user.iam_user.user.tags | length == 1 - - '"TagA" in iam_user.iam_user.user.tags' - - iam_user.iam_user.user.tags.TagA == "AnotherValueA" - - - name: 'Remove All Tags' + - iam_user is changed + - iam_user.iam_user.user.user_name == test_user + - iam_user.iam_user.user.tags | length == 1 + - '"TagA" in iam_user.iam_user.user.tags' + - iam_user.iam_user.user.tags.TagA == "AnotherValueA" + + - name: "Remove All Tags" iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present tags: {} register: iam_user + - assert: that: - - iam_user is changed - - iam_user.iam_user.user.user_name == test_user - - iam_user.iam_user.user.tags | length == 0 + - iam_user is changed + - iam_user.iam_user.user.user_name == test_user + - iam_user.iam_user.user.tags | length == 0 + + ## Test user password update + - name: test update IAM password with on_create only + iam_user: + name: "{{ test_user3 }}" + password: "{{ test_new_password }}" + update_password: "on_create" + state: present + register: iam_user_update + + - assert: + that: + - iam_user_update is not changed + + - name: update IAM password + iam_user: + name: "{{ test_user3 }}" + password: "{{ test_new_password }}" + state: present + register: iam_user_update + + - assert: + that: + - iam_user_update is changed + - iam_user_update.iam_user.user.user_name == test_user3 # =========================================== # Test Managed Policy management @@ -232,7 +297,7 @@ - name: attach managed policy to user (check mode) check_mode: yes iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/AWSDenyAll @@ -245,7 +310,7 @@ - name: attach managed policy to user iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/AWSDenyAll @@ -258,7 +323,7 @@ - name: ensure managed policy is attached to user (no change) iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/AWSDenyAll @@ -272,7 +337,7 @@ - name: attach different managed policy to user (check mode) check_mode: yes iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/ServiceQuotasReadOnlyAccess @@ -286,7 +351,7 @@ - name: attach different managed policy to user iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/ServiceQuotasReadOnlyAccess @@ -300,7 +365,7 @@ - name: Check first policy wasn't purged iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/ServiceQuotasReadOnlyAccess @@ -315,7 +380,7 @@ - name: Check that managed policy order doesn't matter iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/AWSDenyAll @@ -330,7 +395,7 @@ - name: Check that policy doesn't require full ARN path iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - AWSDenyAll @@ -346,7 +411,7 @@ - name: Remove one of the managed policies - with purge (check mode) check_mode: yes iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/ServiceQuotasReadOnlyAccess @@ -360,7 +425,7 @@ - name: Remove one of the managed policies - with purge iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/ServiceQuotasReadOnlyAccess @@ -374,7 +439,7 @@ - name: Check we only have the one policy attached iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present managed_policy: - arn:aws:iam::aws:policy/ServiceQuotasReadOnlyAccess @@ -388,9 +453,9 @@ - name: ensure group exists iam_group: - name: '{{ test_group }}' + name: "{{ test_group }}" users: - - '{{ test_user }}' + - "{{ test_user }}" state: present register: iam_group @@ -401,8 +466,8 @@ - name: get info on IAM user(s) in group iam_user_info: - group: '{{ test_group }}' - name: '{{ test_user }}' + group: "{{ test_group }}" + name: "{{ test_user }}" register: iam_user_info - assert: @@ -417,7 +482,7 @@ - name: remove user from group iam_group: - name: '{{ test_group }}' + name: "{{ test_group }}" purge_users: True users: [] state: present @@ -425,8 +490,8 @@ - name: get info on IAM user(s) after removing from group iam_user_info: - group: '{{ test_group }}' - name: '{{ test_user }}' + group: "{{ test_group }}" + name: "{{ test_user }}" register: iam_user_info - name: assert empty list of users for group are returned @@ -436,9 +501,9 @@ - name: ensure ansible users exist iam_user: - name: '{{ item }}' + name: "{{ item }}" state: present - with_items: '{{ test_users }}' + with_items: "{{ test_users }}" - name: get info on multiple IAM user(s) iam_user_info: @@ -449,15 +514,15 @@ - name: ensure multiple user group exists with single user iam_group: - name: '{{ test_group }}' + name: "{{ test_group }}" users: - - '{{ test_user }}' + - "{{ test_user }}" state: present register: iam_group - name: get info on IAM user(s) in group iam_user_info: - group: '{{ test_group }}' + group: "{{ test_group }}" register: iam_user_info - assert: that: @@ -465,14 +530,14 @@ - name: add all users to group iam_group: - name: '{{ test_group }}' - users: '{{ test_users }}' + name: "{{ test_group }}" + users: "{{ test_users }}" state: present register: iam_group - name: get info on multiple IAM user(s) in group iam_user_info: - group: '{{ test_group }}' + group: "{{ test_group }}" register: iam_user_info - assert: that: @@ -480,7 +545,7 @@ - name: purge users from group iam_group: - name: '{{ test_group }}' + name: "{{ test_group }}" purge_users: True users: [] state: present @@ -488,7 +553,7 @@ - name: ensure info is empty for empty group iam_user_info: - group: '{{ test_group }}' + group: "{{ test_group }}" register: iam_user_info - assert: that: @@ -496,7 +561,7 @@ - name: get info on IAM user(s) after removing from group iam_user_info: - group: '{{ test_group }}' + group: "{{ test_group }}" register: iam_user_info - name: assert empty list of users for group are returned @@ -506,7 +571,7 @@ - name: remove group iam_group: - name: '{{ test_group }}' + name: "{{ test_group }}" state: absent register: iam_group @@ -535,7 +600,7 @@ - name: get info on IAM user(s) after deleting iam_user_info: - group: '{{ test_user }}' + group: "{{ test_user }}" ignore_errors: yes register: iam_user_info @@ -557,16 +622,28 @@ that: - not iam_user.changed - always: - - name: remove group - iam_group: - name: '{{ test_group }}' - state: absent - ignore_errors: yes - - - name: remove ansible users + ## Test user password removal + - name: Delete IAM password iam_user: - name: '{{ item }}' - state: absent - with_items: '{{ test_users }}' - ignore_errors: yes + name: "{{ test_user3 }}" + remove_password: yes + state: present + register: iam_user_password_removal + + - assert: + that: + - iam_user_password_removal is changed + + always: + - name: remove group + iam_group: + name: "{{ test_group }}" + state: absent + ignore_errors: yes + + - name: remove ansible users + iam_user: + name: "{{ item }}" + state: absent + with_items: "{{ test_users }}" + ignore_errors: yes