Refactor aws_kms to bring down the complexity score (#66037)
* Remove dead code key_matches_filter/key_matches_filters * Fail more cleanly when we don't recognise the 'shape' of KMS policy * Refactor aws_kms to bring down the complexity * Minor docs tweaks * Changelog fragment * Fixups from review
This commit is contained in:
parent
0dc08f6b97
commit
335108ac62
2 changed files with 309 additions and 287 deletions
2
changelogs/fragments/66037-aws_kms.yml
Normal file
2
changelogs/fragments/66037-aws_kms.yml
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
minor_changes:
|
||||||
|
- 'aws_kms: code refactor, some error messages updated'
|
|
@ -32,7 +32,8 @@ options:
|
||||||
type: str
|
type: str
|
||||||
key_id:
|
key_id:
|
||||||
description:
|
description:
|
||||||
- Key ID or ARN of the key. One of C(alias) or C(key_id) are required.
|
- Key ID or ARN of the key.
|
||||||
|
- One of I(alias) or I(key_id) are required.
|
||||||
required: false
|
required: false
|
||||||
aliases:
|
aliases:
|
||||||
- key_arn
|
- key_arn
|
||||||
|
@ -50,7 +51,8 @@ options:
|
||||||
type: str
|
type: str
|
||||||
policy_role_name:
|
policy_role_name:
|
||||||
description:
|
description:
|
||||||
- (deprecated) Role to allow/deny access. One of C(policy_role_name) or C(policy_role_arn) are required.
|
- (deprecated) Role to allow/deny access.
|
||||||
|
- One of I(policy_role_name) or I(policy_role_arn) are required.
|
||||||
- Used for modifying the Key Policy rather than modifying a grant and only
|
- Used for modifying the Key Policy rather than modifying a grant and only
|
||||||
works on the default policy created through the AWS Console.
|
works on the default policy created through the AWS Console.
|
||||||
- This option has been deprecated, and will be removed in 2.13. Use I(policy) instead.
|
- This option has been deprecated, and will be removed in 2.13. Use I(policy) instead.
|
||||||
|
@ -60,7 +62,8 @@ options:
|
||||||
type: str
|
type: str
|
||||||
policy_role_arn:
|
policy_role_arn:
|
||||||
description:
|
description:
|
||||||
- (deprecated) ARN of role to allow/deny access. One of C(policy_role_name) or C(policy_role_arn) are required.
|
- (deprecated) ARN of role to allow/deny access.
|
||||||
|
- One of I(policy_role_name) or I(policy_role_arn) are required.
|
||||||
- Used for modifying the Key Policy rather than modifying a grant and only
|
- Used for modifying the Key Policy rather than modifying a grant and only
|
||||||
works on the default policy created through the AWS Console.
|
works on the default policy created through the AWS Console.
|
||||||
- This option has been deprecated, and will be removed in 2.13. Use I(policy) instead.
|
- This option has been deprecated, and will be removed in 2.13. Use I(policy) instead.
|
||||||
|
@ -70,7 +73,8 @@ options:
|
||||||
- role_arn
|
- role_arn
|
||||||
policy_grant_types:
|
policy_grant_types:
|
||||||
description:
|
description:
|
||||||
- (deprecated) List of grants to give to user/role. Likely "role,role grant" or "role,role grant,admin". Required when C(policy_mode=grant).
|
- (deprecated) List of grants to give to user/role. Likely "role,role grant" or "role,role grant,admin".
|
||||||
|
- Required when I(policy_mode=grant).
|
||||||
- Used for modifying the Key Policy rather than modifying a grant and only
|
- Used for modifying the Key Policy rather than modifying a grant and only
|
||||||
works on the default policy created through the AWS Console.
|
works on the default policy created through the AWS Console.
|
||||||
- This option has been deprecated, and will be removed in 2.13. Use I(policy) instead.
|
- This option has been deprecated, and will be removed in 2.13. Use I(policy) instead.
|
||||||
|
@ -400,9 +404,7 @@ from ansible.module_utils.ec2 import AWSRetry, camel_dict_to_snake_dict
|
||||||
from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list
|
from ansible.module_utils.ec2 import boto3_tag_list_to_ansible_dict, ansible_dict_to_boto3_tag_list
|
||||||
from ansible.module_utils.ec2 import compare_aws_tags, compare_policies
|
from ansible.module_utils.ec2 import compare_aws_tags, compare_policies
|
||||||
from ansible.module_utils.six import string_types
|
from ansible.module_utils.six import string_types
|
||||||
from ansible.module_utils._text import to_native
|
|
||||||
|
|
||||||
import traceback
|
|
||||||
import json
|
import json
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
@ -502,26 +504,6 @@ def get_kms_policies(connection, module, key_id):
|
||||||
module.fail_json_aws(e, msg="Failed to obtain key policies")
|
module.fail_json_aws(e, msg="Failed to obtain key policies")
|
||||||
|
|
||||||
|
|
||||||
def key_matches_filter(key, filtr):
|
|
||||||
if filtr[0] == 'key-id':
|
|
||||||
return filtr[1] == key['key_id']
|
|
||||||
if filtr[0] == 'tag-key':
|
|
||||||
return filtr[1] in key['tags']
|
|
||||||
if filtr[0] == 'tag-value':
|
|
||||||
return filtr[1] in key['tags'].values()
|
|
||||||
if filtr[0] == 'alias':
|
|
||||||
return filtr[1] in key['aliases']
|
|
||||||
if filtr[0].startswith('tag:'):
|
|
||||||
return key['Tags'][filtr[0][4:]] == filtr[1]
|
|
||||||
|
|
||||||
|
|
||||||
def key_matches_filters(key, filters):
|
|
||||||
if not filters:
|
|
||||||
return True
|
|
||||||
else:
|
|
||||||
return all([key_matches_filter(key, filtr) for filtr in filters.items()])
|
|
||||||
|
|
||||||
|
|
||||||
def camel_to_snake_grant(grant):
|
def camel_to_snake_grant(grant):
|
||||||
''' camel_to_snake_grant snakifies everything except the encryption context '''
|
''' camel_to_snake_grant snakifies everything except the encryption context '''
|
||||||
constraints = grant.get('Constraints', {})
|
constraints = grant.get('Constraints', {})
|
||||||
|
@ -625,134 +607,193 @@ def compare_grants(existing_grants, desired_grants, purge_grants=False):
|
||||||
return to_add, to_remove
|
return to_add, to_remove
|
||||||
|
|
||||||
|
|
||||||
def ensure_enabled_disabled(connection, module, key):
|
def start_key_deletion(connection, module, key_metadata):
|
||||||
changed = False
|
if key_metadata['KeyState'] == 'PendingDeletion':
|
||||||
if key['key_state'] == 'Disabled' and module.params['enabled']:
|
|
||||||
try:
|
|
||||||
connection.enable_key(KeyId=key['key_arn'])
|
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
||||||
module.fail_json_aws(e, msg="Failed to enable key")
|
|
||||||
|
|
||||||
if key['key_state'] == 'Enabled' and not module.params['enabled']:
|
|
||||||
try:
|
|
||||||
connection.disable_key(KeyId=key['key_arn'])
|
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
||||||
module.fail_json_aws(e, msg="Failed to disable key")
|
|
||||||
return changed
|
|
||||||
|
|
||||||
|
|
||||||
def update_alias(connection, module, key_id, alias):
|
|
||||||
if not alias.startswith('alias/'):
|
|
||||||
alias = 'alias/' + alias
|
|
||||||
aliases = get_kms_aliases_with_backoff(connection)['Aliases']
|
|
||||||
if key_id:
|
|
||||||
# We will only add new aliases, not rename existing ones
|
|
||||||
if alias not in [_alias['AliasName'] for _alias in aliases]:
|
|
||||||
try:
|
|
||||||
connection.create_alias(KeyId=key_id, AliasName=alias)
|
|
||||||
return True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
||||||
module.fail_json_aws(e, msg="Failed create key alias")
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
if module.check_mode:
|
||||||
|
return True
|
||||||
|
|
||||||
def update_key(connection, module, key):
|
try:
|
||||||
changed = False
|
connection.schedule_key_deletion(KeyId=key_metadata['Arn'])
|
||||||
alias = module.params.get('alias')
|
return True
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, msg="Failed to schedule key for deletion")
|
||||||
|
|
||||||
|
|
||||||
|
def cancel_key_deletion(connection, module, key):
|
||||||
key_id = key['key_arn']
|
key_id = key['key_arn']
|
||||||
|
if key['key_state'] != 'PendingDeletion':
|
||||||
|
return False
|
||||||
|
|
||||||
if alias:
|
if module.check_mode:
|
||||||
changed = update_alias(connection, module, key_id, alias) or changed
|
return True
|
||||||
|
|
||||||
if key['key_state'] == 'PendingDeletion':
|
|
||||||
try:
|
try:
|
||||||
connection.cancel_key_deletion(KeyId=key_id)
|
connection.cancel_key_deletion(KeyId=key_id)
|
||||||
# key is disabled after deletion cancellation
|
# key is disabled after deletion cancellation
|
||||||
# set this so that ensure_enabled_disabled works correctly
|
# set this so that ensure_enabled_disabled works correctly
|
||||||
key['key_state'] = 'Disabled'
|
key['key_state'] = 'Disabled'
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
module.fail_json_aws(e, msg="Failed to cancel key deletion")
|
module.fail_json_aws(e, msg="Failed to cancel key deletion")
|
||||||
|
|
||||||
changed = ensure_enabled_disabled(connection, module, key) or changed
|
return True
|
||||||
|
|
||||||
description = module.params.get('description')
|
|
||||||
# don't update description if description is not set
|
def ensure_enabled_disabled(connection, module, key, enabled):
|
||||||
# (means you can't remove a description completely)
|
desired_state = 'Enabled'
|
||||||
if description and key['description'] != description:
|
if not enabled:
|
||||||
|
desired_state = 'Disabled'
|
||||||
|
|
||||||
|
if key['key_state'] == desired_state:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key_id = key['key_arn']
|
||||||
|
if not module.check_mode:
|
||||||
|
if enabled:
|
||||||
|
try:
|
||||||
|
connection.enable_key(KeyId=key_id)
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, msg="Failed to enable key")
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
connection.disable_key(KeyId=key_id)
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, msg="Failed to disable key")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def update_alias(connection, module, key, alias):
|
||||||
|
alias = canonicalize_alias_name(alias)
|
||||||
|
|
||||||
|
if alias is None:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key_id = key['key_arn']
|
||||||
|
aliases = get_kms_aliases_with_backoff(connection)['Aliases']
|
||||||
|
# We will only add new aliases, not rename existing ones
|
||||||
|
if alias in [_alias['AliasName'] for _alias in aliases]:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if not module.check_mode:
|
||||||
|
try:
|
||||||
|
connection.create_alias(TargetKeyId=key_id, AliasName=alias)
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, msg="Failed create key alias")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def update_description(connection, module, key, description):
|
||||||
|
if description is None:
|
||||||
|
return False
|
||||||
|
if key['description'] == description:
|
||||||
|
return False
|
||||||
|
|
||||||
|
key_id = key['key_arn']
|
||||||
|
if not module.check_mode:
|
||||||
try:
|
try:
|
||||||
connection.update_key_description(KeyId=key_id, Description=description)
|
connection.update_key_description(KeyId=key_id, Description=description)
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
module.fail_json_aws(e, msg="Failed to update key description")
|
module.fail_json_aws(e, msg="Failed to update key description")
|
||||||
|
|
||||||
desired_tags = module.params.get('tags')
|
return True
|
||||||
to_add, to_remove = compare_aws_tags(key['tags'], desired_tags,
|
|
||||||
module.params.get('purge_tags'))
|
|
||||||
|
def update_tags(connection, module, key, desired_tags, purge_tags):
|
||||||
|
# purge_tags needs to be explicitly set, so an empty tags list means remove
|
||||||
|
# all tags
|
||||||
|
|
||||||
|
to_add, to_remove = compare_aws_tags(key['tags'], desired_tags, purge_tags)
|
||||||
|
if not (bool(to_add) or bool(to_remove)):
|
||||||
|
return False
|
||||||
|
|
||||||
|
key_id = key['key_arn']
|
||||||
|
if not module.check_mode:
|
||||||
if to_remove:
|
if to_remove:
|
||||||
try:
|
try:
|
||||||
connection.untag_resource(KeyId=key_id, TagKeys=to_remove)
|
connection.untag_resource(KeyId=key_id, TagKeys=to_remove)
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
module.fail_json_aws(e, msg="Unable to remove or update tag")
|
module.fail_json_aws(e, msg="Unable to remove tag")
|
||||||
if to_add:
|
if to_add:
|
||||||
try:
|
try:
|
||||||
connection.tag_resource(KeyId=key_id,
|
tags = ansible_dict_to_boto3_tag_list(module.params['tags'], tag_name_key_name='TagKey', tag_value_key_name='TagValue')
|
||||||
Tags=[{'TagKey': tag_key, 'TagValue': desired_tags[tag_key]}
|
connection.tag_resource(KeyId=key_id, Tags=tags)
|
||||||
for tag_key in to_add])
|
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
module.fail_json_aws(e, msg="Unable to add tag to key")
|
module.fail_json_aws(e, msg="Unable to add tag to key")
|
||||||
|
|
||||||
# Update existing policy before trying to tweak grants
|
return True
|
||||||
if module.params.get('policy'):
|
|
||||||
policy = module.params.get('policy')
|
|
||||||
try:
|
def update_policy(connection, module, key, policy):
|
||||||
keyret = connection.get_key_policy(KeyId=key_id, PolicyName='default')
|
if policy is None:
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
return False
|
||||||
# If we can't fetch the current policy assume we're making a change
|
|
||||||
# Could occur if we have PutKeyPolicy without GetKeyPolicy
|
|
||||||
original_policy = {}
|
|
||||||
original_policy = json.loads(keyret['Policy'])
|
|
||||||
try:
|
try:
|
||||||
new_policy = json.loads(policy)
|
new_policy = json.loads(policy)
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
module.fail_json_aws(e, msg="Unable to parse new policy as JSON")
|
module.fail_json_aws(e, msg="Unable to parse new policy as JSON")
|
||||||
if compare_policies(original_policy, new_policy):
|
|
||||||
changed = True
|
key_id = key['key_arn']
|
||||||
|
try:
|
||||||
|
keyret = connection.get_key_policy(KeyId=key_id, PolicyName='default')
|
||||||
|
original_policy = json.loads(keyret['Policy'])
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError):
|
||||||
|
# If we can't fetch the current policy assume we're making a change
|
||||||
|
# Could occur if we have PutKeyPolicy without GetKeyPolicy
|
||||||
|
original_policy = {}
|
||||||
|
|
||||||
|
if not compare_policies(original_policy, new_policy):
|
||||||
|
return False
|
||||||
|
|
||||||
if not module.check_mode:
|
if not module.check_mode:
|
||||||
try:
|
try:
|
||||||
connection.put_key_policy(KeyId=key_id, PolicyName='default', Policy=policy)
|
connection.put_key_policy(KeyId=key_id, PolicyName='default', Policy=policy)
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
module.fail_json_aws(e, msg="Unable to update key policy")
|
module.fail_json_aws(e, msg="Unable to update key policy")
|
||||||
|
|
||||||
desired_grants = module.params.get('grants')
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def update_grants(connection, module, key, desired_grants, purge_grants):
|
||||||
existing_grants = key['grants']
|
existing_grants = key['grants']
|
||||||
|
|
||||||
to_add, to_remove = compare_grants(existing_grants, desired_grants,
|
to_add, to_remove = compare_grants(existing_grants, desired_grants, purge_grants)
|
||||||
module.params.get('purge_grants'))
|
if not (bool(to_add) or bool(to_remove)):
|
||||||
if to_remove:
|
return False
|
||||||
|
|
||||||
|
key_id = key['key_arn']
|
||||||
|
if not module.check_mode:
|
||||||
for grant in to_remove:
|
for grant in to_remove:
|
||||||
try:
|
try:
|
||||||
connection.retire_grant(KeyId=key_id, GrantId=grant['grant_id'])
|
connection.retire_grant(KeyId=key_id, GrantId=grant['grant_id'])
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
module.fail_json_aws(e, msg="Unable to retire grant")
|
module.fail_json_aws(e, msg="Unable to retire grant")
|
||||||
|
|
||||||
if to_add:
|
|
||||||
for grant in to_add:
|
for grant in to_add:
|
||||||
grant_params = convert_grant_params(grant, key)
|
grant_params = convert_grant_params(grant, key)
|
||||||
try:
|
try:
|
||||||
connection.create_grant(**grant_params)
|
connection.create_grant(**grant_params)
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
module.fail_json_aws(e, msg="Unable to create grant")
|
module.fail_json_aws(e, msg="Unable to create grant")
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def update_key(connection, module, key):
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
changed |= cancel_key_deletion(connection, module, key)
|
||||||
|
changed |= ensure_enabled_disabled(connection, module, key, module.params['enabled'])
|
||||||
|
changed |= update_alias(connection, module, key, module.params['alias'])
|
||||||
|
changed |= update_description(connection, module, key, module.params['description'])
|
||||||
|
changed |= update_tags(connection, module, key, module.params['tags'], module.params.get('purge_tags'))
|
||||||
|
changed |= update_policy(connection, module, key, module.params.get('policy'))
|
||||||
|
changed |= update_grants(connection, module, key, module.params.get('grants'), module.params.get('purge_grants'))
|
||||||
|
|
||||||
# make results consistent with kms_facts before returning
|
# make results consistent with kms_facts before returning
|
||||||
result = get_key_details(connection, module, key_id)
|
result = get_key_details(connection, module, key['key_arn'])
|
||||||
module.exit_json(changed=changed, **result)
|
result['changed'] = changed
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def create_key(connection, module):
|
def create_key(connection, module):
|
||||||
|
@ -771,58 +812,25 @@ def create_key(connection, module):
|
||||||
module.fail_json_aws(e, msg="Failed to create initial key")
|
module.fail_json_aws(e, msg="Failed to create initial key")
|
||||||
key = get_key_details(connection, module, result['KeyId'])
|
key = get_key_details(connection, module, result['KeyId'])
|
||||||
|
|
||||||
alias = module.params['alias']
|
update_alias(connection, module, key, module.params['alias'])
|
||||||
if not alias.startswith('alias/'):
|
|
||||||
alias = 'alias/' + alias
|
|
||||||
try:
|
|
||||||
connection.create_alias(AliasName=alias, TargetKeyId=key['key_id'])
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
||||||
module.fail_json_aws(e, msg="Failed to create alias")
|
|
||||||
|
|
||||||
ensure_enabled_disabled(connection, module, key)
|
ensure_enabled_disabled(connection, module, key, module.params.get('enabled'))
|
||||||
for grant in module.params.get('grants'):
|
update_grants(connection, module, key, module.params.get('grants'), False)
|
||||||
grant_params = convert_grant_params(grant, key)
|
|
||||||
try:
|
|
||||||
connection.create_grant(**grant_params)
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
||||||
module.fail_json_aws(e, msg="Failed to add grant to key")
|
|
||||||
|
|
||||||
# make results consistent with kms_facts
|
# make results consistent with kms_facts
|
||||||
result = get_key_details(connection, module, key['key_id'])
|
result = get_key_details(connection, module, key['key_id'])
|
||||||
module.exit_json(changed=True, **result)
|
result['changed'] = True
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
def delete_key(connection, module, key_metadata, key_id):
|
def delete_key(connection, module, key_metadata):
|
||||||
changed = False
|
changed = False
|
||||||
|
|
||||||
if key_metadata['KeyState'] != 'PendingDeletion':
|
changed |= start_key_deletion(connection, module, key_metadata)
|
||||||
try:
|
|
||||||
connection.schedule_key_deletion(KeyId=key_id)
|
|
||||||
changed = True
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
||||||
module.fail_json_aws(e, msg="Failed to schedule key for deletion")
|
|
||||||
|
|
||||||
result = get_key_details(connection, module, key_id)
|
result = get_key_details(connection, module, key_metadata['Arn'])
|
||||||
module.exit_json(changed=changed, **result)
|
result['changed'] = changed
|
||||||
|
return result
|
||||||
|
|
||||||
def get_arn_from_kms_alias(kms, aliasname):
|
|
||||||
ret = kms.list_aliases()
|
|
||||||
key_id = None
|
|
||||||
for a in ret['Aliases']:
|
|
||||||
if a['AliasName'] == aliasname:
|
|
||||||
key_id = a['TargetKeyId']
|
|
||||||
break
|
|
||||||
if not key_id:
|
|
||||||
raise Exception('could not find alias {0}'.format(aliasname))
|
|
||||||
|
|
||||||
# now that we have the ID for the key, we need to get the key's ARN. The alias
|
|
||||||
# has an ARN but we need the key itself.
|
|
||||||
ret = kms.list_keys()
|
|
||||||
for k in ret['Keys']:
|
|
||||||
if k['KeyId'] == key_id:
|
|
||||||
return k['KeyArn']
|
|
||||||
raise Exception('could not find key from id: {0}'.format(key_id))
|
|
||||||
|
|
||||||
|
|
||||||
def get_arn_from_role_name(iam, rolename):
|
def get_arn_from_role_name(iam, rolename):
|
||||||
|
@ -832,78 +840,85 @@ def get_arn_from_role_name(iam, rolename):
|
||||||
raise Exception('could not find arn for name {0}.'.format(rolename))
|
raise Exception('could not find arn for name {0}.'.format(rolename))
|
||||||
|
|
||||||
|
|
||||||
def do_policy_grant(module, kms, keyarn, role_arn, granttypes, mode='grant', dry_run=True, clean_invalid_entries=True):
|
def _clean_statement_principals(statement, clean_invalid_entries):
|
||||||
ret = {}
|
|
||||||
keyret = get_key_policy_with_backoff(kms, keyarn, 'default')
|
|
||||||
policy = json.loads(keyret['Policy'])
|
|
||||||
|
|
||||||
changes_needed = {}
|
|
||||||
assert_policy_shape(policy)
|
|
||||||
had_invalid_entries = False
|
|
||||||
for statement in policy['Statement']:
|
|
||||||
for granttype in ['role', 'role grant', 'admin']:
|
|
||||||
# do we want this grant type? Are we on its statement?
|
|
||||||
# and does the role have this grant type?
|
|
||||||
|
|
||||||
# create Principal and 'AWS' so we can safely use them later.
|
# create Principal and 'AWS' so we can safely use them later.
|
||||||
if not isinstance(statement.get('Principal'), dict):
|
if not isinstance(statement.get('Principal'), dict):
|
||||||
statement['Principal'] = dict()
|
statement['Principal'] = dict()
|
||||||
|
|
||||||
|
# If we have a single AWS Principal, ensure we still have a list (to manipulate)
|
||||||
if 'AWS' in statement['Principal'] and isinstance(statement['Principal']['AWS'], string_types):
|
if 'AWS' in statement['Principal'] and isinstance(statement['Principal']['AWS'], string_types):
|
||||||
# convert to list
|
|
||||||
statement['Principal']['AWS'] = [statement['Principal']['AWS']]
|
statement['Principal']['AWS'] = [statement['Principal']['AWS']]
|
||||||
if not isinstance(statement['Principal'].get('AWS'), list):
|
if not isinstance(statement['Principal'].get('AWS'), list):
|
||||||
statement['Principal']['AWS'] = list()
|
statement['Principal']['AWS'] = list()
|
||||||
|
|
||||||
if mode == 'grant' and statement['Sid'] == statement_label[granttype]:
|
|
||||||
# we're granting and we recognize this statement ID.
|
|
||||||
|
|
||||||
if granttype in granttypes:
|
|
||||||
invalid_entries = [item for item in statement['Principal']['AWS'] if not item.startswith('arn:aws:iam::')]
|
invalid_entries = [item for item in statement['Principal']['AWS'] if not item.startswith('arn:aws:iam::')]
|
||||||
if clean_invalid_entries and invalid_entries:
|
|
||||||
# we have bad/invalid entries. These are roles that were deleted.
|
|
||||||
# prune the list.
|
|
||||||
valid_entries = [item for item in statement['Principal']['AWS'] if item.startswith('arn:aws:iam::')]
|
valid_entries = [item for item in statement['Principal']['AWS'] if item.startswith('arn:aws:iam::')]
|
||||||
|
|
||||||
|
if bool(invalid_entries) and clean_invalid_entries:
|
||||||
statement['Principal']['AWS'] = valid_entries
|
statement['Principal']['AWS'] = valid_entries
|
||||||
had_invalid_entries = True
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def _do_statement_grant(statement, role_arn, grant_types, mode, grant_type):
|
||||||
|
|
||||||
|
if mode == 'grant':
|
||||||
|
if grant_type in grant_types:
|
||||||
if role_arn not in statement['Principal']['AWS']: # needs to be added.
|
if role_arn not in statement['Principal']['AWS']: # needs to be added.
|
||||||
changes_needed[granttype] = 'add'
|
|
||||||
if not dry_run:
|
|
||||||
statement['Principal']['AWS'].append(role_arn)
|
statement['Principal']['AWS'].append(role_arn)
|
||||||
|
return 'add'
|
||||||
elif role_arn in statement['Principal']['AWS']: # not one the places the role should be
|
elif role_arn in statement['Principal']['AWS']: # not one the places the role should be
|
||||||
changes_needed[granttype] = 'remove'
|
|
||||||
if not dry_run:
|
|
||||||
statement['Principal']['AWS'].remove(role_arn)
|
statement['Principal']['AWS'].remove(role_arn)
|
||||||
|
return 'remove'
|
||||||
|
return None
|
||||||
|
|
||||||
elif mode == 'deny' and statement['Sid'] == statement_label[granttype] and role_arn in statement['Principal']['AWS']:
|
if mode == 'deny' and role_arn in statement['Principal']['AWS']:
|
||||||
# we don't selectively deny. that's a grant with a
|
# we don't selectively deny. that's a grant with a
|
||||||
# smaller list. so deny=remove all of this arn.
|
# smaller list. so deny=remove all of this arn.
|
||||||
changes_needed[granttype] = 'remove'
|
|
||||||
if not dry_run:
|
|
||||||
statement['Principal']['AWS'].remove(role_arn)
|
statement['Principal']['AWS'].remove(role_arn)
|
||||||
|
return 'remove'
|
||||||
|
return None
|
||||||
|
|
||||||
try:
|
|
||||||
if len(changes_needed) and not dry_run:
|
def do_policy_grant(module, kms, keyarn, role_arn, grant_types, mode='grant', dry_run=True, clean_invalid_entries=True):
|
||||||
policy_json_string = json.dumps(policy)
|
ret = {}
|
||||||
kms.put_key_policy(KeyId=keyarn, PolicyName='default', Policy=policy_json_string)
|
policy = json.loads(get_key_policy_with_backoff(kms, keyarn, 'default')['Policy'])
|
||||||
# returns nothing, so we have to just assume it didn't throw
|
|
||||||
ret['changed'] = True
|
changes_needed = {}
|
||||||
except Exception as e:
|
assert_policy_shape(module, policy)
|
||||||
module.fail_json(msg='Could not update key_policy', new_policy=policy_json_string, details=to_native(e), exception=traceback.format_exc())
|
had_invalid_entries = False
|
||||||
raise
|
for statement in policy['Statement']:
|
||||||
|
# We already tested that these are the only types in the statements
|
||||||
|
for grant_type in statement_label:
|
||||||
|
# Are we on this grant type's statement?
|
||||||
|
if statement['Sid'] != statement_label[grant_type]:
|
||||||
|
continue
|
||||||
|
|
||||||
|
had_invalid_entries |= _clean_statement_principals(statement, clean_invalid_entries)
|
||||||
|
change = _do_statement_grant(statement, role_arn, grant_types, mode, grant_type)
|
||||||
|
if change:
|
||||||
|
changes_needed[grant_type] = change
|
||||||
|
|
||||||
ret['changes_needed'] = changes_needed
|
ret['changes_needed'] = changes_needed
|
||||||
ret['had_invalid_entries'] = had_invalid_entries
|
ret['had_invalid_entries'] = had_invalid_entries
|
||||||
ret['new_policy'] = policy
|
ret['new_policy'] = policy
|
||||||
if dry_run:
|
ret['changed'] = bool(changes_needed)
|
||||||
# true if changes > 0
|
|
||||||
ret['changed'] = len(changes_needed) > 0
|
if dry_run or not ret['changed']:
|
||||||
|
return ret
|
||||||
|
|
||||||
|
try:
|
||||||
|
policy_json_string = json.dumps(policy)
|
||||||
|
kms.put_key_policy(KeyId=keyarn, PolicyName='default', Policy=policy_json_string)
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, msg='Could not update key_policy', new_policy=policy_json_string)
|
||||||
|
|
||||||
return ret
|
return ret
|
||||||
|
|
||||||
|
|
||||||
def assert_policy_shape(policy):
|
def assert_policy_shape(module, policy):
|
||||||
'''Since the policy seems a little, uh, fragile, make sure we know approximately what we're looking at.'''
|
'''Since the policy seems a little, uh, fragile, make sure we know approximately what we're looking at.'''
|
||||||
errors = []
|
errors = []
|
||||||
if policy['Version'] != "2012-10-17":
|
if policy['Version'] != "2012-10-17":
|
||||||
|
@ -915,13 +930,61 @@ def assert_policy_shape(policy):
|
||||||
if statement['Sid'] == sidlabel:
|
if statement['Sid'] == sidlabel:
|
||||||
found_statement_type[label] = True
|
found_statement_type[label] = True
|
||||||
|
|
||||||
for statementtype in statement_label.keys():
|
for statementtype in statement_label:
|
||||||
if not found_statement_type.get(statementtype):
|
if not found_statement_type.get(statementtype):
|
||||||
errors.append('Policy is missing {0}.'.format(statementtype))
|
errors.append('Policy is missing {0}.'.format(statementtype))
|
||||||
|
|
||||||
if len(errors):
|
if errors:
|
||||||
raise Exception('Problems asserting policy shape. Cowardly refusing to modify it: {0}'.format(' '.join(errors)) + "\n" + str(policy))
|
module.fail_json(msg='Problems asserting policy shape. Cowardly refusing to modify it', errors=errors, policy=policy)
|
||||||
|
|
||||||
|
|
||||||
|
def canonicalize_alias_name(alias):
|
||||||
|
if alias is None:
|
||||||
return None
|
return None
|
||||||
|
if alias.startswith('alias/'):
|
||||||
|
return alias
|
||||||
|
return 'alias/' + alias
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_key_metadata(connection, module, key_id, alias):
|
||||||
|
|
||||||
|
alias = canonicalize_alias_name(module.params.get('alias'))
|
||||||
|
|
||||||
|
try:
|
||||||
|
# Fetch by key_id where possible
|
||||||
|
if key_id:
|
||||||
|
return get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata']
|
||||||
|
# Or try alias as a backup
|
||||||
|
return get_kms_metadata_with_backoff(connection, alias)['KeyMetadata']
|
||||||
|
|
||||||
|
except connection.exceptions.NotFoundException:
|
||||||
|
return None
|
||||||
|
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||||
|
module.fail_json_aws(e, 'Failed to fetch key metadata.')
|
||||||
|
|
||||||
|
|
||||||
|
def update_policy_grants(connection, module, key_metadata, mode):
|
||||||
|
iam = module.client('iam')
|
||||||
|
key_id = key_metadata['Arn']
|
||||||
|
|
||||||
|
if module.params.get('policy_role_name') and not module.params.get('policy_role_arn'):
|
||||||
|
module.params['policy_role_arn'] = get_arn_from_role_name(iam, module.params['policy_role_name'])
|
||||||
|
if not module.params.get('policy_role_arn'):
|
||||||
|
module.fail_json(msg='policy_role_arn or policy_role_name is required to {0}'.format(module.params['policy_mode']))
|
||||||
|
|
||||||
|
# check the grant types for 'grant' only.
|
||||||
|
if mode == 'grant':
|
||||||
|
for grant_type in module.params['policy_grant_types']:
|
||||||
|
if grant_type not in statement_label:
|
||||||
|
module.fail_json(msg='{0} is an unknown grant type.'.format(grant_type))
|
||||||
|
|
||||||
|
return do_policy_grant(module, connection,
|
||||||
|
key_id,
|
||||||
|
module.params['policy_role_arn'],
|
||||||
|
module.params['policy_grant_types'],
|
||||||
|
mode=mode,
|
||||||
|
dry_run=module.check_mode,
|
||||||
|
clean_invalid_entries=module.params['policy_clean_invalid_entries'])
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
@ -949,77 +1012,34 @@ def main():
|
||||||
required_one_of=[['alias', 'key_id']],
|
required_one_of=[['alias', 'key_id']],
|
||||||
)
|
)
|
||||||
|
|
||||||
result = {}
|
|
||||||
mode = module.params['policy_mode']
|
mode = module.params['policy_mode']
|
||||||
|
|
||||||
kms = module.client('kms')
|
kms = module.client('kms')
|
||||||
iam = module.client('iam')
|
|
||||||
|
|
||||||
key_id = module.params.get('key_id')
|
key_metadata = fetch_key_metadata(kms, module, module.params.get('key_id'), module.params.get('alias'))
|
||||||
alias = module.params.get('alias')
|
# We can't create keys with a specific ID, if we can't access the key we'll have to fail
|
||||||
if alias and alias.startswith('alias/'):
|
if module.params.get('state') == 'present' and module.params.get('key_id') and not key_metadata:
|
||||||
alias = alias[6:]
|
|
||||||
|
|
||||||
# Fetch/Canonicalize key_id where possible
|
|
||||||
if key_id:
|
|
||||||
try:
|
|
||||||
# Don't use get_key_details it triggers module.fail when the key
|
|
||||||
# doesn't exist
|
|
||||||
key_metadata = get_kms_metadata_with_backoff(kms, key_id)['KeyMetadata']
|
|
||||||
key_id = key_metadata['Arn']
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
||||||
# We can't create keys with a specific ID, if we can't access the
|
|
||||||
# key we'll have to fail
|
|
||||||
if module.params.get('state') == 'present':
|
|
||||||
module.fail_json(msg="Could not find key with id %s to update")
|
module.fail_json(msg="Could not find key with id %s to update")
|
||||||
key_metadata = None
|
|
||||||
elif alias:
|
|
||||||
try:
|
|
||||||
key_metadata = get_kms_metadata_with_backoff(kms, 'alias/%s' % alias)['KeyMetadata']
|
|
||||||
key_id = key_metadata['Arn']
|
|
||||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
|
||||||
key_metadata = None
|
|
||||||
|
|
||||||
if module.params.get('policy_grant_types') or mode == 'deny':
|
if module.params.get('policy_grant_types') or mode == 'deny':
|
||||||
module.deprecate('Managing the KMS IAM Policy via policy_mode and policy_grant_types is fragile'
|
module.deprecate('Managing the KMS IAM Policy via policy_mode and policy_grant_types is fragile'
|
||||||
' and has been deprecated in favour of the policy option.', version='2.13')
|
' and has been deprecated in favour of the policy option.', version='2.13')
|
||||||
if module.params.get('policy_role_name') and not module.params.get('policy_role_arn'):
|
result = update_policy_grants(kms, module, key_metadata, mode)
|
||||||
module.params['policy_role_arn'] = get_arn_from_role_name(iam, module.params['policy_role_name'])
|
|
||||||
if not module.params.get('policy_role_arn'):
|
|
||||||
module.fail_json(msg='policy_role_arn or policy_role_name is required to {0}'.format(module.params['policy_mode']))
|
|
||||||
|
|
||||||
# check the grant types for 'grant' only.
|
|
||||||
if mode == 'grant':
|
|
||||||
for g in module.params['policy_grant_types']:
|
|
||||||
if g not in statement_label:
|
|
||||||
module.fail_json(msg='{0} is an unknown grant type.'.format(g))
|
|
||||||
|
|
||||||
ret = do_policy_grant(module, kms,
|
|
||||||
key_id,
|
|
||||||
module.params['policy_role_arn'],
|
|
||||||
module.params['policy_grant_types'],
|
|
||||||
mode=mode,
|
|
||||||
dry_run=module.check_mode,
|
|
||||||
clean_invalid_entries=module.params['policy_clean_invalid_entries'])
|
|
||||||
result.update(ret)
|
|
||||||
|
|
||||||
module.exit_json(**result)
|
module.exit_json(**result)
|
||||||
|
|
||||||
else:
|
if module.params.get('state') == 'absent':
|
||||||
if module.params.get('state') == 'present':
|
if key_metadata is None:
|
||||||
if key_metadata:
|
|
||||||
key_details = get_key_details(kms, module, key_id)
|
|
||||||
update_key(kms, module, key_details)
|
|
||||||
else:
|
|
||||||
if key_id:
|
|
||||||
module.fail_json(msg="Could not find key with id %s to update" % key_id)
|
|
||||||
else:
|
|
||||||
create_key(kms, module)
|
|
||||||
else:
|
|
||||||
if key_metadata:
|
|
||||||
delete_key(kms, module, key_metadata, key_id)
|
|
||||||
else:
|
|
||||||
module.exit_json(changed=False)
|
module.exit_json(changed=False)
|
||||||
|
result = delete_key(kms, module, key_metadata)
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
if key_metadata:
|
||||||
|
key_details = get_key_details(kms, module, key_metadata['Arn'])
|
||||||
|
result = update_key(kms, module, key_details)
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
result = create_key(kms, module)
|
||||||
|
module.exit_json(**result)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|
Loading…
Reference in a new issue