Skip to content

Commit

Permalink
aws_kms: Support setting PendingWindowInDays (Deletion Delay) and fix…
Browse files Browse the repository at this point in the history
… tests (ansible-collections#200)

* Ensure we can still update / delete KMS keys when we can't access the key rotation status

* Fix and enable KMS tests

* Add support for setting the deletion schedule window

* Ignore failures during cleanup

* changelog

* Change role name to match those permitted by CI policies

* Split imports - easier to rebase

* Make sure key rotation enable/disable errors don't drop through to main()

* Allow STS principals as well as IAM principals

* Add support for direct lookup by alias/id
Use it in test suite (filters are done client side and are SLOW)

* Ensure we don't throw an exception when a tag doesn't exist

* Add docs

* changelog

* Flag aws_kms tests as unstable

* lint fixups

* Consistently handle 'UnsupportedOperationException' on key rotation

* Update version added

* Allow a little flexibility for deletion times

* Update version_added
  • Loading branch information
tremble authored and danielcotton committed Nov 23, 2021
1 parent be30f35 commit 50c0729
Show file tree
Hide file tree
Showing 6 changed files with 266 additions and 79 deletions.
6 changes: 6 additions & 0 deletions changelogs/fragments/200-aws_kms-deletion.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
bugfixes:
- aws_kms - fixes issue where module execution fails without the kms:GetKeyRotationStatus permission. (https://github.com/ansible-collections/community.aws/pull/200).
- aws_kms_info - ensure that searching by tag works when tag only exists on some CMKs (https://github.com/ansible-collections/community.aws/issues/276).
minor_changes:
- aws_kms - add support for setting the deletion window using `pending_window` (PendingWindowInDays) (https://github.com/ansible-collections/community.aws/pull/200).
- aws_kms_info - Add ``key_id`` and ``alias`` parameters to support fetching a single key (https://github.com/ansible-collections/community.aws/pull/200).
70 changes: 52 additions & 18 deletions plugins/modules/aws_kms.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,15 @@
tags:
description: A dictionary of tags to apply to a key.
type: dict
pending_window:
description:
- The number of days between requesting deletion of the CMK and when it will actually be deleted.
- Only used when I(state=absent) and the CMK has not yet been deleted.
- Valid values are between 7 and 30 (inclusive).
- 'See also: U(https://docs.aws.amazon.com/kms/latest/APIReference/API_ScheduleKeyDeletion.html#KMS-ScheduleKeyDeletion-request-PendingWindowInDays)'
type: int
aliases: ['deletion_delay']
version_added: 1.4.0
purge_tags:
description: Whether the I(tags) argument should cause tags not in the list to
be removed
Expand Down Expand Up @@ -405,19 +414,25 @@
'admin': 'Allow access for Key Administrators'
}

from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule, is_boto3_error_code
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_aws_tags, compare_policies
from ansible.module_utils.six import string_types

import json
import re

try:
import botocore
except ImportError:
pass # caught by AnsibleAWSModule

from ansible.module_utils.six import string_types

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 ansible_dict_to_boto3_tag_list
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 camel_dict_to_snake_dict
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_aws_tags
from ansible_collections.amazon.aws.plugins.module_utils.ec2 import compare_policies


@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
def get_iam_roles_with_backoff(connection):
Expand Down Expand Up @@ -533,8 +548,11 @@ def get_key_details(connection, module, key_id):
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to obtain aliases")

current_rotation_status = connection.get_key_rotation_status(KeyId=key_id)
result['enable_key_rotation'] = current_rotation_status.get('KeyRotationEnabled')
try:
current_rotation_status = connection.get_key_rotation_status(KeyId=key_id)
result['enable_key_rotation'] = current_rotation_status.get('KeyRotationEnabled')
except is_boto3_error_code(['AccessDeniedException', 'UnsupportedOperationException']) as e:
result['enable_key_rotation'] = None
result['aliases'] = aliases.get(result['KeyId'], [])

result = camel_dict_to_snake_dict(result)
Expand Down Expand Up @@ -622,8 +640,12 @@ def start_key_deletion(connection, module, key_metadata):
if module.check_mode:
return True

deletion_params = {'KeyId': key_metadata['Arn']}
if module.params.get('pending_window'):
deletion_params['PendingWindowInDays'] = module.params.get('pending_window')

try:
connection.schedule_key_deletion(KeyId=key_metadata['Arn'])
connection.schedule_key_deletion(**deletion_params)
return True
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to schedule key for deletion")
Expand Down Expand Up @@ -767,14 +789,23 @@ def update_key_rotation(connection, module, key, enable_key_rotation):
if enable_key_rotation is None:
return False
key_id = key['key_arn']
current_rotation_status = connection.get_key_rotation_status(KeyId=key_id)
if current_rotation_status.get('KeyRotationEnabled') == enable_key_rotation:
return False

if enable_key_rotation:
connection.enable_key_rotation(KeyId=key_id)
else:
connection.disable_key_rotation(KeyId=key_id)
try:
current_rotation_status = connection.get_key_rotation_status(KeyId=key_id)
if current_rotation_status.get('KeyRotationEnabled') == enable_key_rotation:
return False
except is_boto3_error_code('AccessDeniedException'):
pass
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Unable to get current key rotation status")

try:
if enable_key_rotation:
connection.enable_key_rotation(KeyId=key_id)
else:
connection.disable_key_rotation(KeyId=key_id)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to enable/disable key rotation")
return True


Expand Down Expand Up @@ -881,8 +912,10 @@ def _clean_statement_principals(statement, clean_invalid_entries):
if not isinstance(statement['Principal'].get('AWS'), list):
statement['Principal']['AWS'] = list()

invalid_entries = [item for item in statement['Principal']['AWS'] if not item.startswith('arn:aws:iam::')]
valid_entries = [item for item in statement['Principal']['AWS'] if item.startswith('arn:aws:iam::')]
valid_princ = re.compile('^arn:aws:(iam|sts)::')

invalid_entries = [item for item in statement['Principal']['AWS'] if not valid_princ.match(item)]
valid_entries = [item for item in statement['Principal']['AWS'] if valid_princ.match(item)]

if bool(invalid_entries) and clean_invalid_entries:
statement['Principal']['AWS'] = valid_entries
Expand Down Expand Up @@ -1024,6 +1057,7 @@ def main():
policy_role_arn=dict(aliases=['role_arn']),
policy_grant_types=dict(aliases=['grant_types'], type='list', elements='str'),
policy_clean_invalid_entries=dict(aliases=['clean_invalid_entries'], type='bool', default=True),
pending_window=dict(aliases=['deletion_delay'], type='int'),
key_id=dict(aliases=['key_arn']),
description=dict(),
enabled=dict(type='bool', default=True),
Expand Down
74 changes: 60 additions & 14 deletions plugins/modules/aws_kms_info.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,31 @@
- This module was called C(aws_kms_facts) before Ansible 2.9. The usage did not change.
author: "Will Thames (@willthames)"
options:
alias:
description:
- Alias for key.
- Mutually exclusive with I(key_id) and I(filters).
required: false
aliases:
- key_alias
type: str
version_added: 1.4.0
key_id:
description:
- Key ID or ARN of the key.
- Mutually exclusive with I(alias) and I(filters).
required: false
aliases:
- key_arn
type: str
version_added: 1.4.0
filters:
description:
- A dict of filters to apply. Each dict item consists of a filter key and a filter value.
The filters aren't natively supported by boto3, but are supported to provide similar
functionality to other modules. Standard tag filters (C(tag-key), C(tag-value) and
C(tag:tagName)) are available, as are C(key-id) and C(alias)
- Mutually exclusive with I(alias) and I(key_id).
type: dict
pending_deletion:
description: Whether to get full details (tags, grants etc.) of keys pending deletion
Expand Down Expand Up @@ -290,12 +309,20 @@ def get_key_policy_with_backoff(connection, key_id, policy_name):
def get_enable_key_rotation_with_backoff(connection, key_id):
try:
current_rotation_status = connection.get_key_rotation_status(KeyId=key_id)
except is_boto3_error_code('AccessDeniedException'):
except is_boto3_error_code(['AccessDeniedException', 'UnsupportedOperationException']) as e:
return None

return current_rotation_status.get('KeyRotationEnabled')


def canonicalize_alias_name(alias):
if alias is None:
return None
if alias.startswith('alias/'):
return alias
return 'alias/' + alias


def get_kms_tags(connection, module, key_id):
# Handle pagination here as list_resource_tags does not have
# a paginator
Expand Down Expand Up @@ -338,7 +365,10 @@ def key_matches_filter(key, filtr):
if filtr[0] == 'alias':
return filtr[1] in key['aliases']
if filtr[0].startswith('tag:'):
return key['tags'][filtr[0][4:]] == filtr[1]
tag_key = filtr[0][4:]
if tag_key not in key['tags']:
return False
return key['tags'].get(tag_key) == filtr[1]


def key_matches_filters(key, filters):
Expand All @@ -353,20 +383,21 @@ def get_key_details(connection, module, key_id, tokens=None):
tokens = []
try:
result = get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata']
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
# Make sure we have the canonical ARN, we might have been passed an alias
key_id = result['Arn']
except is_boto3_error_code('NotFoundException'):
return None
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Failed to obtain key metadata")
result['KeyArn'] = result.pop('Arn')

try:
aliases = get_kms_aliases_lookup(connection)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to obtain aliases")
# We can only get aliases for our own account, so we don't need the full ARN
result['aliases'] = aliases.get(result['KeyId'], [])

if result['Origin'] == 'AWS_KMS':
result['enable_key_rotation'] = get_enable_key_rotation_with_backoff(connection, key_id)
else:
result['enable_key_rotation'] = None
result['enable_key_rotation'] = get_enable_key_rotation_with_backoff(connection, key_id)

if module.params.get('pending_deletion'):
return camel_dict_to_snake_dict(result)
Expand All @@ -384,21 +415,36 @@ def get_key_details(connection, module, key_id, tokens=None):


def get_kms_info(connection, module):
try:
keys = get_kms_keys_with_backoff(connection)['Keys']
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to obtain keys")

return [get_key_details(connection, module, key['KeyId']) for key in keys]
if module.params.get('key_id'):
key_id = module.params.get('key_id')
details = get_key_details(connection, module, key_id)
if details:
return [details]
return []
elif module.params.get('alias'):
alias = canonicalize_alias_name(module.params.get('alias'))
details = get_key_details(connection, module, alias)
if details:
return [details]
return []
else:
try:
keys = get_kms_keys_with_backoff(connection)['Keys']
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to obtain keys")
return [get_key_details(connection, module, key['KeyId']) for key in keys]


def main():
argument_spec = dict(
alias=dict(aliases=['key_alias']),
key_id=dict(aliases=['key_arn']),
filters=dict(type='dict'),
pending_deletion=dict(type='bool', default=False),
)

module = AnsibleAWSModule(argument_spec=argument_spec,
mutually_exclusive=[['alias', 'filters', 'key_id']],
supports_check_mode=True)
if module._name == 'aws_kms_facts':
module.deprecate("The 'aws_kms_facts' module has been renamed to 'aws_kms_info'", date='2021-12-01', collection_name='community.aws')
Expand Down
4 changes: 3 additions & 1 deletion tests/integration/targets/aws_kms/aliases
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
cloud/aws
aws_kms_info
unsupported
shippable/aws/group2
# Various race conditions - likely needs waiters
unstable
3 changes: 3 additions & 0 deletions tests/integration/targets/aws_kms/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
kms_role_name: 'ansible-test-{{ resource_prefix }}-kms'
kms_key_alias: '{{ resource_prefix }}-kms'
Loading

0 comments on commit 50c0729

Please sign in to comment.