Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

iam_user - stabilize for migration to amazon.aws #1059

Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
minor_changes:
- iam_user - wrap client with backoff logic to catch EntityTemporarilyUnmodifiable exception (https://github.com/ansible-collections/community.aws/pull/1059).
- iam_user - add `user` value to return data structure to deprecate old `iam_user` (https://github.com/ansible-collections/community.aws/pull/1059).
bugfixes:
- iam_user_info - gracefully handle when no users are found (https://github.com/ansible-collections/community.aws/pull/1059).
88 changes: 68 additions & 20 deletions plugins/modules/iam_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,8 @@
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.
- When I(wait=True) the module will wait for up to I(wait_timeout) seconds for
different IAM user actions to be reflected in AWS (user creation/deletion, login profile update, etc).
default: True
type: bool
version_added: 2.2.0
Expand Down Expand Up @@ -161,20 +161,20 @@
user_id:
description: the stable and unique string identifying the user
type: str
sample: AGPAIDBWE12NSFINE55TM
sample: "AGPAIDBWE12NSFINE55TM"
user_name:
description: the friendly name that identifies the user
type: str
sample: testuser1
sample: "testuser1"
path:
description: the path to the user
type: str
sample: /
sample: "/"
tags:
description: user tags
type: dict
returned: always
sample: '{"Env": "Prod"}'
sample: {"Env": "Prod"}
'''

try:
Expand All @@ -187,6 +187,7 @@
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.ec2 import ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_aws_tags

Expand Down Expand Up @@ -228,10 +229,6 @@ def convert_friendly_names_to_arns(connection, module, policy_names):


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')
Expand Down Expand Up @@ -261,10 +258,17 @@ def create_or_update_login_profile(connection, module):
retval = {}

try:
retval = connection.update_login_profile(**user_params)
retval = connection.update_login_profile(aws_retry=True, **user_params)
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved
if module.params.get('wait'):
# Wait for login profile to finish updating
connection.update_login_profile(aws_retry=True, **user_params)
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved
except is_boto3_error_code('NoSuchEntity'):
# Login profile does not yet exist - create it
try:
retval = connection.create_login_profile(**user_params)
retval = connection.create_login_profile(aws_retry=True, **user_params)
if module.params.get('wait'):
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved
# Wait for login profile to finish creating
connection.update_login_profile(aws_retry=True, **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
Expand All @@ -274,14 +278,31 @@ def create_or_update_login_profile(connection, module):


def delete_login_profile(connection, module):

'''
Deletes a users login profile.
Parameters:
connection: IAM client
module: AWSModule
Returns:
(bool): True if login profile deleted, False if no login profile found to delete
'''
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")
# User does not have login profile - nothing to delete
if not user_has_login_profile(connection, module, user_params['UserName']):
return False

if not module.check_mode:
try:
connection.delete_login_profile(**user_params, aws_retry=True)
if module.params.get('wait'):
# Wait for login profile to be completely removed
connection.delete_login_profile(**user_params, aws_retry=True)
except is_boto3_error_code('NoSuchEntity'):
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved
pass
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Unable to delete user login profile")

return True

Expand Down Expand Up @@ -331,6 +352,9 @@ def create_or_update_user(connection, module):
update_result = update_user_tags(connection, module, params, user)

if module.params['update_password'] == "always" and module.params.get('password') is not None:
# Can't compare passwords, so just return changed on check mode runs
if module.check_mode:
module.exit_json(changed=True)
login_profile_result, login_profile_data = create_or_update_login_profile(connection, module)

if login_profile_data.get('LoginProfile', {}).get('PasswordResetRequired', False):
Expand Down Expand Up @@ -382,7 +406,7 @@ def create_or_update_user(connection, module):
# `LoginProfile` is only returned on `create_login_profile` method
user['user']['password_reset_required'] = login_profile_data.get('LoginProfile', {}).get('PasswordResetRequired', False)

module.exit_json(changed=changed, iam_user=user)
module.exit_json(changed=changed, iam_user=user, user=user['user'])


def destroy_user(connection, module):
Expand Down Expand Up @@ -412,7 +436,7 @@ def destroy_user(connection, module):
connection.delete_access_key(UserName=user_name, AccessKeyId=access_key["AccessKeyId"])

# Remove user's login profile (console password)
delete_user_login_profile(connection, module, user_name)
delete_login_profile(connection, module)

# Remove user's ssh public keys
ssh_public_keys = connection.list_ssh_public_keys(UserName=user_name)["SSHPublicKeys"]
Expand Down Expand Up @@ -495,6 +519,25 @@ def delete_user_login_profile(connection, module, user_name):
module.fail_json_aws(e, msg="Unable to delete login profile for user {0}".format(user_name))


def user_has_login_profile(connection, module, name):
'''
Returns whether or not given user has a login profile.
Parameters:
connection: IAM client
module: AWSModule
name (str): Username of user
Returns:
(bool): True if user had login profile, False if not
'''
try:
connection.get_login_profile(UserName=name)
except is_boto3_error_code('NoSuchEntity'):
return False
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Unable to get login profile for user {0}".format(name))
return True


def update_user_tags(connection, module, params, user):
user_name = params['UserName']
existing_tags = user['user']['tags']
Expand Down Expand Up @@ -543,7 +586,12 @@ def main():
mutually_exclusive=[['password', 'remove_password']],
)

connection = module.client('iam')
module.deprecate("The 'iam_user' return key is deprecated and will be replaced by 'user'. Both values are returned for now.",
date='2022-12-01', collection_name='community.aws')
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved

# Catch EntityTemporarilyUnmodifiable exception when modifying user's login profile
retry_decorator = AWSRetry.jittered_backoff(delay=2, catch_extra_error_codes=['EntityTemporarilyUnmodifiable'])
connection = module.client('iam', retry_decorator=retry_decorator)

state = module.params.get("state")

Expand Down
13 changes: 10 additions & 3 deletions plugins/modules/iam_user_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,7 @@
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.ec2 import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict

Expand Down Expand Up @@ -142,14 +143,18 @@ def list_iam_users(connection, module):
params['UserName'] = name
try:
iam_users.append(connection.get_user(**params)['User'])
except (ClientError, BotoCoreError) as e:
except is_boto3_error_code('NoSuchEntity'):
pass
except (ClientError, BotoCoreError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Couldn't get IAM user info for user %s" % name)

if group:
params['GroupName'] = group
try:
iam_users = list_iam_users_with_backoff(connection, 'get_group', **params)['Users']
except (ClientError, BotoCoreError) as e:
except is_boto3_error_code('NoSuchEntity'):
pass
except (ClientError, BotoCoreError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Couldn't get IAM user info for group %s" % group)
if name:
iam_users = [user for user in iam_users if user['UserName'] == name]
Expand All @@ -158,7 +163,9 @@ def list_iam_users(connection, module):
params['PathPrefix'] = path
try:
iam_users = list_iam_users_with_backoff(connection, 'list_users', **params)['Users']
except (ClientError, BotoCoreError) as e:
except is_boto3_error_code('NoSuchEntity'):
pass
except (ClientError, BotoCoreError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Couldn't get IAM user info for path %s" % path)
if name:
iam_users = [user for user in iam_users if user['UserName'] == name]
Expand Down
Loading