From ff016adfdb0c0494eeaa97b4a9497530ede70e7b Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Fri, 3 Dec 2021 14:28:49 +0000 Subject: [PATCH 1/9] Add iam_user password functionality --- plugins/modules/iam_user.py | 97 ++++++- .../targets/iam_user/defaults/main.yml | 2 + .../targets/iam_user/tasks/main.yml | 246 ++++++++++-------- 3 files changed, 234 insertions(+), 111 deletions(-) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index 2a7998a6d10..5cacbbe00ab 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,16 @@ - The name of the user to create. required: true type: str + password: + description: + - The password to apply to the user. + required: false + type: str + new_password: + description: + - The new password to update for the existing user. + required: false + type: str managed_policies: description: - A list of managed policy ARNs or friendly names to attach to the user. @@ -36,7 +47,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 +64,17 @@ 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 + wait_timeout: + description: + - How long (in seconds) to wait for creation / updates to complete. + default: 120 + type: int extends_documentation_fragment: - amazon.aws.aws - amazon.aws.ec2 @@ -179,15 +201,67 @@ 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 password / update password for the user + + user_params = dict() + user_params['UserName'] = module.params.get('name') + + if module.params.get('new_password') is not None: + user_params['Password'] = module.params.get('new_password') + + try: + connection.update_login_profile(**user_params) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + module.fail_json_aws(e, msg="Unable to update user login profile") + else: + user_params['Password'] = module.params.get('password') + + 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") + + 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 +279,21 @@ 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.get('new_password') is not None: + login_profile_result = create_or_update_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,11 +475,15 @@ def main(): argument_spec = dict( name=dict(required=True, type='str'), + password=dict(type='str', no_log=True), + new_password=dict(type='str', no_log=True), 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( diff --git a/tests/integration/targets/iam_user/defaults/main.yml b/tests/integration/targets/iam_user/defaults/main.yml index 8a69ca0931c..940b57081df 100644 --- a/tests/integration/targets/iam_user/defaults/main.yml +++ b/tests/integration/targets/iam_user/defaults/main.yml @@ -2,6 +2,8 @@ test_group: '{{ resource_prefix }}-group' test_path: '/' test_user: '{{ test_users[0] }}' +test_password: ATotallySecureUncrackablePassword1! +test_new_password: ATotallyNewSecureUncrackablePassword1! test_users: - '{{ resource_prefix }}-user-a' - '{{ resource_prefix }}-user-b' diff --git a/tests/integration/targets/iam_user/tasks/main.yml b/tests/integration/targets/iam_user/tasks/main.yml index 76d13e57f19..d8bb02aa545 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,8 @@ - name: create test user (check mode) iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" + password: "{{ test_password }}" state: present check_mode: yes register: iam_user @@ -34,7 +36,8 @@ - name: create test user iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" + password: "{{ test_password }}" state: present register: iam_user @@ -45,7 +48,7 @@ - name: ensure test user exists (no change) iam_user: - name: '{{ test_user }}' + name: "{{ test_user }}" state: present register: iam_user @@ -56,7 +59,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 +74,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 @@ -91,9 +98,10 @@ - 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 +112,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 +155,102 @@ "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: update IAM password + iam_user: + name: "{{ test_user }}" + new_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_user # =========================================== # Test Managed Policy management @@ -232,7 +262,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 +275,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 +288,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 +302,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 +316,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 +330,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 +345,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 +360,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 +376,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 +390,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 +404,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 +418,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 +431,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 +447,7 @@ - name: remove user from group iam_group: - name: '{{ test_group }}' + name: "{{ test_group }}" purge_users: True users: [] state: present @@ -425,8 +455,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 +466,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 +479,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 +495,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 +510,7 @@ - name: purge users from group iam_group: - name: '{{ test_group }}' + name: "{{ test_group }}" purge_users: True users: [] state: present @@ -488,7 +518,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 +526,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 +536,7 @@ - name: remove group iam_group: - name: '{{ test_group }}' + name: "{{ test_group }}" state: absent register: iam_group @@ -535,7 +565,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 @@ -558,15 +588,15 @@ - not iam_user.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 + - 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 From 08d975b58a4867e46c51509180700f4b5cfed6d0 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Fri, 3 Dec 2021 14:51:43 +0000 Subject: [PATCH 2/9] Update login profile setup --- plugins/modules/iam_user.py | 32 ++++++++----------- .../targets/iam_user/tasks/main.yml | 14 +++++++- 2 files changed, 27 insertions(+), 19 deletions(-) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index 5cacbbe00ab..165ef708fc4 100644 --- a/plugins/modules/iam_user.py +++ b/plugins/modules/iam_user.py @@ -26,10 +26,13 @@ - The password to apply to the user. required: false type: str - new_password: + update_password: + default: always + choices: ['always', 'on_create'] description: - - The new password to update for the existing user. - required: false + - 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 managed_policies: description: @@ -227,25 +230,18 @@ def wait_iam_exists(connection, module): def create_or_update_login_profile(connection, module): - # Apply password / update password for the user - + # 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') - if module.params.get('new_password') is not None: - user_params['Password'] = module.params.get('new_password') - - try: - connection.update_login_profile(**user_params) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: - module.fail_json_aws(e, msg="Unable to update user login profile") - else: - user_params['Password'] = module.params.get('password') - + try: + connection.update_login_profile(**user_params) + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: 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") + module.fail_json_aws(e, msg="Unable to create / update user login profile") return True @@ -290,7 +286,7 @@ def create_or_update_user(connection, module): login_profile_result = None update_result = update_user_tags(connection, module, params, user) - if module.params.get('new_password') is not None: + if module.params['update_password'] == "always" and module.params.get('password') is not None: login_profile_result = create_or_update_login_profile(connection, module) changed = bool(update_result) or bool(login_profile_result) @@ -476,7 +472,7 @@ def main(): argument_spec = dict( name=dict(required=True, type='str'), password=dict(type='str', no_log=True), - new_password=dict(type='str', no_log=True), + update_password=dict(default='always', choices=['always', 'on_create'], no_log=False), 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']), diff --git a/tests/integration/targets/iam_user/tasks/main.yml b/tests/integration/targets/iam_user/tasks/main.yml index d8bb02aa545..18d95b2cc31 100644 --- a/tests/integration/targets/iam_user/tasks/main.yml +++ b/tests/integration/targets/iam_user/tasks/main.yml @@ -240,10 +240,22 @@ - 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_user }}" + 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_user }}" - new_password: "{{ test_new_password }}" + password: "{{ test_new_password }}" state: present register: iam_user_update From 74c0f17cccb11ff40b484066cb28f780192fb9d8 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Fri, 3 Dec 2021 14:57:39 +0000 Subject: [PATCH 3/9] add changelog fragment --- changelogs/fragments/822-add-password-support-iam_user.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelogs/fragments/822-add-password-support-iam_user.yml diff --git a/changelogs/fragments/822-add-password-support-iam_user.yml b/changelogs/fragments/822-add-password-support-iam_user.yml new file mode 100644 index 00000000000..f02312d536e --- /dev/null +++ b/changelogs/fragments/822-add-password-support-iam_user.yml @@ -0,0 +1,3 @@ +minor_changes: + - iam_user - add password management support bringing parity with `iam` module (https://github.com/ansible-collections/community.aws/pull/822). + - iam_user - add boto3 waiter for iam user creation (https://github.com/ansible-collections/community.aws/pull/822). From ff0d73d924eea5acdb45d27dda1499b91459ee4f Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Fri, 3 Dec 2021 20:53:25 +0000 Subject: [PATCH 4/9] add version_added key to docnote --- plugins/modules/iam_user.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index 165ef708fc4..da636b9589d 100644 --- a/plugins/modules/iam_user.py +++ b/plugins/modules/iam_user.py @@ -26,6 +26,7 @@ - The password to apply to the user. required: false type: str + version_added: 2.2.0 update_password: default: always choices: ['always', 'on_create'] @@ -34,6 +35,7 @@ - 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 managed_policies: description: - A list of managed policy ARNs or friendly names to attach to the user. @@ -73,11 +75,13 @@ 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 From a03b7ead1bd92058dd95159ba0c5e8330b430e2e Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Mon, 6 Dec 2021 11:01:11 +0000 Subject: [PATCH 5/9] PR feedback --- plugins/modules/iam_user.py | 37 +++++++++++++-- .../targets/iam_user/defaults/main.yml | 2 + .../targets/iam_user/tasks/main.yml | 45 ++++++++++++++++--- 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index da636b9589d..902f5f96ed1 100644 --- a/plugins/modules/iam_user.py +++ b/plugins/modules/iam_user.py @@ -36,6 +36,12 @@ - I(update_password=on_create) will only set the password for newly created users. type: str version_added: 2.2.0 + remove_password: + description: + - When to update 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. @@ -99,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 @@ -241,11 +253,26 @@ def create_or_update_login_profile(connection, module): try: connection.update_login_profile(**user_params) - except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + 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 / update user login profile") + module.fail_json_aws(e, msg="Unable to create user login profile") + except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: + 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 @@ -292,6 +319,8 @@ def create_or_update_user(connection, module): 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') is not None: + login_profile_result = delete_login_profile(connection, module) changed = bool(update_result) or bool(login_profile_result) @@ -477,6 +506,7 @@ def main(): 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']), @@ -488,7 +518,8 @@ def main(): 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 940b57081df..1bbb5df0bf0 100644 --- a/tests/integration/targets/iam_user/defaults/main.yml +++ b/tests/integration/targets/iam_user/defaults/main.yml @@ -2,8 +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 18d95b2cc31..397ba70a23e 100644 --- a/tests/integration/targets/iam_user/tasks/main.yml +++ b/tests/integration/targets/iam_user/tasks/main.yml @@ -24,7 +24,6 @@ - name: create test user (check mode) iam_user: name: "{{ test_user }}" - password: "{{ test_password }}" state: present check_mode: yes register: iam_user @@ -37,7 +36,6 @@ - name: create test user iam_user: name: "{{ test_user }}" - password: "{{ test_password }}" state: present register: iam_user @@ -96,6 +94,31 @@ - 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 }}" @@ -242,7 +265,7 @@ ## Test user password update - name: test update IAM password with on_create only iam_user: - name: "{{ test_user }}" + name: "{{ test_user3 }}" password: "{{ test_new_password }}" update_password: "on_create" state: present @@ -254,7 +277,7 @@ - name: update IAM password iam_user: - name: "{{ test_user }}" + name: "{{ test_user3 }}" password: "{{ test_new_password }}" state: present register: iam_user_update @@ -262,7 +285,7 @@ - assert: that: - iam_user_update is changed - - iam_user_update.iam_user.user.user_name == test_user + - iam_user_update.iam_user.user.user_name == test_user3 # =========================================== # Test Managed Policy management @@ -599,6 +622,18 @@ that: - not iam_user.changed + ## Test user password removal + - name: Delete IAM password + iam_user: + 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: From e9a0fe5971b3c0b5079a3036aa35bf41a8b10eb8 Mon Sep 17 00:00:00 2001 From: Mark Woolley Date: Mon, 6 Dec 2021 11:06:25 +0000 Subject: [PATCH 6/9] Update iam_user.py --- plugins/modules/iam_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index 902f5f96ed1..a8d86b1a9b0 100644 --- a/plugins/modules/iam_user.py +++ b/plugins/modules/iam_user.py @@ -38,7 +38,7 @@ version_added: 2.2.0 remove_password: description: - - When to update delete user login passwords. + - Option to delete user login passwords. - This field is mutually exclusive to I(password). type: 'bool' version_added: 2.2.0 From 04450a4eec1a8045ad4d450a12d059c1d9f42740 Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Mon, 6 Dec 2021 12:00:40 +0000 Subject: [PATCH 7/9] Ignore pylint error --- plugins/modules/iam_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index a8d86b1a9b0..23462f796b8 100644 --- a/plugins/modules/iam_user.py +++ b/plugins/modules/iam_user.py @@ -258,7 +258,7 @@ def create_or_update_login_profile(connection, module): 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: + 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 From 817aec5be5fa2d74158c6850c652a3a09fd5d65b Mon Sep 17 00:00:00 2001 From: mark-woolley Date: Mon, 6 Dec 2021 13:52:56 +0000 Subject: [PATCH 8/9] linting fix --- plugins/modules/iam_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index 23462f796b8..b29272187bf 100644 --- a/plugins/modules/iam_user.py +++ b/plugins/modules/iam_user.py @@ -258,7 +258,7 @@ def create_or_update_login_profile(connection, module): 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 + 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 From 0850b3f9eb896c18508ca6e259565aadd92ca596 Mon Sep 17 00:00:00 2001 From: Markus Bergholz Date: Mon, 13 Dec 2021 10:03:07 +0100 Subject: [PATCH 9/9] Update plugins/modules/iam_user.py Co-authored-by: Mark Chappell --- plugins/modules/iam_user.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/modules/iam_user.py b/plugins/modules/iam_user.py index b29272187bf..e308c3cf0cb 100644 --- a/plugins/modules/iam_user.py +++ b/plugins/modules/iam_user.py @@ -319,7 +319,7 @@ def create_or_update_user(connection, module): 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') is not None: + elif module.params.get('remove_password'): login_profile_result = delete_login_profile(connection, module) changed = bool(update_result) or bool(login_profile_result)