Skip to content

Commit

Permalink
Merge pull request #1773 from alinabuzachis/promote_iam_access_key
Browse files Browse the repository at this point in the history
Promote iam_access_key and the corresponding _info module
  • Loading branch information
gravesm authored Oct 9, 2023
2 parents bb742a5 + 62ae056 commit bcd74e1
Show file tree
Hide file tree
Showing 8 changed files with 1,185 additions and 0 deletions.
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

0 comments on commit bcd74e1

Please sign in to comment.