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

Promote iam_access_key and the corresponding _info module #1773

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelogs/fragments/migrate_iam_access_key.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
major_changes:
- iam_access_key - The module has been migrated from the ``community.aws`` collection.
Playbooks using the Fully Qualified Collection Name for this module should be updated
to use ``amazon.aws.iam_access_key``.
- iam_access_key_info - The module has been migrated from the ``community.aws`` collection.
Playbooks using the Fully Qualified Collection Name for this module should be updated
to use ``amazon.aws.iam_access_key_info``.
2 changes: 2 additions & 0 deletions meta/runtime.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ action_groups:
- elb_application_lb_info
- elb_classic_lb
- execute_lambda
- iam_access_key
- iam_access_key_info
- iam_group
- iam_instance_profile
- iam_instance_profile_info
Expand Down
314 changes: 314 additions & 0 deletions plugins/modules/iam_access_key.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,314 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-

# Copyright (c) 2021 Ansible Project
# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)

DOCUMENTATION = r"""
---
module: iam_access_key
version_added: 2.1.0
version_added_collection: community.aws
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
notes:
- For security reasons, this module should be used with B(no_log=true) and (register) functionalities
when creating new access key.
extends_documentation_fragment:
- amazon.aws.common.modules
- amazon.aws.region.modules
- amazon.aws.boto3
"""

EXAMPLES = r"""
# Note: These examples do not set authentication details, see the AWS Guide for details.

- name: Create a new access key
amazon.aws.iam_access_key:
user_name: example_user
state: present
no_log: true

- name: Delete the access_key
amazon.aws.iam_access_key:
user_name: example_user
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.botocore import is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.botocore import normalize_boto3_result
from ansible_collections.amazon.aws.plugins.module_utils.modules import AnsibleAWSModule
from ansible_collections.amazon.aws.plugins.module_utils.retries import AWSRetry
from ansible_collections.amazon.aws.plugins.module_utils.transformation import scrub_none_parameters


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=f'Failed to delete access key "{access_key_id}" for user "{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=f'Access key "{access_key_id}" not found attached to User "{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=f'Failed to update access key "{access_key_id}" for user "{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=f'Failed to create access key for user "{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=f'Failed to get access keys for user "{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()
Loading