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,4 @@
minor_changes:
- iam_user - wrap login profile action API calls with AWSRetry (when wait is True), clean up return struct (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).
99 changes: 70 additions & 29 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 @@ -251,7 +252,7 @@ def wait_iam_exists(connection, module):
module.fail_json_aws(e, msg='Failed while waiting on IAM user creation')


def create_or_update_login_profile(connection, module):
def create_or_update_login_profile(connection, module, wait):
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved

# Apply new password / update password for the user
user_params = dict()
Expand All @@ -262,9 +263,20 @@ def create_or_update_login_profile(connection, module):

try:
retval = connection.update_login_profile(**user_params)
if wait:
AWSRetry.jittered_backoff(
delay=1,
catch_extra_error_codes=['EntityTemporarilyUnmodifiable']
)(connection.update_login_profile)(**user_params)
except is_boto3_error_code('NoSuchEntity'):
# Login profile does not yet exist - create it
try:
retval = connection.create_login_profile(**user_params)
if wait:
AWSRetry.jittered_backoff(
delay=1,
catch_extra_error_codes=['EntityTemporarilyUnmodifiable']
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved
)(connection.update_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
Expand All @@ -273,15 +285,31 @@ def create_or_update_login_profile(connection, module):
return True, retval


def delete_login_profile(connection, module):

user_params = dict()
user_params['UserName'] = module.params.get('name')
def delete_login_profile(connection, module, user_name, wait):
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved
'''
Deletes a users login profile.
Parameters:
connection: IAM client
module: AWSModule
user_name (str): Username of user
wait (bool): Whether or not the module waits for full deletion to reflect on AWS
Returns:
(bool): True if login profile deleted, False if no login profile found to delete
'''
# User does not have login profile - nothing to delete
if not user_has_login_policy(connection, module, user_name):
return False

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")
if not module.check_mode:
try:
if wait:
AWSRetry.jittered_backoff(catch_extra_error_codes=['EntityTemporarilyUnmodifiable'])(connection.delete_login_profile)(UserName=user_name)
else:
connection.delete_login_profile(UserName=user_name)
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 All @@ -292,6 +320,7 @@ def create_or_update_user(connection, module):
params['UserName'] = module.params.get('name')
managed_policies = module.params.get('managed_policies')
purge_policies = module.params.get('purge_policies')
wait = module.params.get('wait')

if module.params.get('tags') is not None:
params["Tags"] = ansible_dict_to_boto3_tag_list(module.params.get('tags'))
Expand All @@ -318,11 +347,11 @@ def create_or_update_user(connection, module):
module.fail_json_aws(e, msg="Unable to create user")

# Wait for user to be fully available before continuing
if module.params.get('wait'):
if wait:
wait_iam_exists(connection, module)

if module.params.get('password') is not None:
login_profile_result, login_profile_data = create_or_update_login_profile(connection, module)
login_profile_result, login_profile_data = create_or_update_login_profile(connection, module, wait)

if login_profile_data.get('LoginProfile', {}).get('PasswordResetRequired', False):
new_login_profile = True
Expand All @@ -331,13 +360,16 @@ 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:
login_profile_result, login_profile_data = create_or_update_login_profile(connection, module)
# 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, wait)

if login_profile_data.get('LoginProfile', {}).get('PasswordResetRequired', False):
new_login_profile = True

elif module.params.get('remove_password'):
login_profile_result = delete_login_profile(connection, module)
login_profile_result = delete_login_profile(connection, module, params['UserName'], wait)

changed = bool(update_result) or bool(login_profile_result)

Expand Down Expand Up @@ -380,9 +412,9 @@ def create_or_update_user(connection, module):
user = get_user(connection, module, params['UserName'])
if changed and new_login_profile:
# `LoginProfile` is only returned on `create_login_profile` method
user['user']['password_reset_required'] = login_profile_data.get('LoginProfile', {}).get('PasswordResetRequired', False)
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, user=user)
jatorcasso marked this conversation as resolved.
Show resolved Hide resolved


def destroy_user(connection, module):
Expand Down Expand Up @@ -412,7 +444,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, user_name, module.params.get('wait'))

# Remove user's ssh public keys
ssh_public_keys = connection.list_ssh_public_keys(UserName=user_name)["SSHPublicKeys"]
Expand Down Expand Up @@ -472,7 +504,7 @@ def get_user(connection, module, name):
tags = boto3_tag_list_to_ansible_dict(user['User'].pop('Tags', []))
user = camel_dict_to_snake_dict(user)
user['user']['tags'] = tags
return user
return user['user']


def get_attached_policy_list(connection, module, name):
Expand All @@ -485,19 +517,28 @@ def get_attached_policy_list(connection, module, name):
module.fail_json_aws(e, msg="Unable to get policies for user {0}".format(name))


def delete_user_login_profile(connection, module, user_name):

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


def update_user_tags(connection, module, params, user):
user_name = params['UserName']
existing_tags = user['user']['tags']
existing_tags = user['tags']
new_tags = params.get('Tags')
if new_tags is None:
return False
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