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

kms_key: Add multi region support to create_key #1290

2 changes: 2 additions & 0 deletions changelogs/fragments/1290-create_multi_region_key.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
minor_changes:
- kms_key - Add multi_region option to create_key (https://github.com/ansible-collections/amazon.aws/pull/1290).
128 changes: 103 additions & 25 deletions plugins/modules/kms_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,12 @@
- A description of the CMK.
- Use a description that helps you decide whether the CMK is appropriate for a task.
type: str
multi_region:
description:
- Whether to create a multi-Region primary key or not.
default: False
type: bool
GomathiselviS marked this conversation as resolved.
Show resolved Hide resolved
version_added: 5.2.0
pending_window:
description:
- The number of days between requesting deletion of the CMK and when it will actually be deleted.
Expand Down Expand Up @@ -158,6 +164,14 @@
Name: myKey
Purpose: protect_stuff

# Create a new multi-region KMS key
- amazon.aws.kms_key:
alias: mykey
multi_region: true
tags:
Name: myKey
Purpose: protect_stuff

# Update previous key with more tags
- amazon.aws.kms_key:
alias: mykey
Expand Down Expand Up @@ -409,6 +423,16 @@
description: Whether there are invalid (non-ARN) entries in the KMS entry. These don't count as a change, but will be removed if any changes are being made.
type: bool
returned: always
multi_region:
GomathiselviS marked this conversation as resolved.
Show resolved Hide resolved
description:
- Indicates whether the CMK is a multi-Region C(True) or regional C(False) key.
- This value is True for multi-Region primary and replica CMKs and False for regional CMKs.
type: bool
version_added: 5.2.0
returned: always
sample: False


'''

# these mappings are used to go from simple labels to the actual 'Sid' values returned
Expand Down Expand Up @@ -519,16 +543,18 @@ def get_kms_tags(connection, module, key_id):
def get_kms_policies(connection, module, key_id):
try:
policies = list_key_policies_with_backoff(connection, key_id)['PolicyNames']
return [get_key_policy_with_backoff(connection, key_id, policy)['Policy'] for
policy in policies]
return [
get_key_policy_with_backoff(connection, key_id, policy)['Policy']
for policy in policies
]
except is_boto3_error_code('AccessDeniedException'):
return []
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
module.fail_json_aws(e, msg="Failed to obtain key policies")


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', {})
result = camel_dict_to_snake_dict(grant)
if 'EncryptionContextEquals' in constraints:
Expand Down Expand Up @@ -561,8 +587,10 @@ def get_key_details(connection, module, key_id):

# grants and tags get snakified differently
try:
result['grants'] = [camel_to_snake_grant(grant) for grant in
get_kms_grants_with_backoff(connection, key_id)['Grants']]
result['grants'] = [
camel_to_snake_grant(grant)
for grant in get_kms_grants_with_backoff(connection, key_id)['Grants']
]
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Failed to obtain key grants")
tags = get_kms_tags(connection, module, key_id)
Expand All @@ -582,8 +610,9 @@ def get_kms_facts(connection, module):


def convert_grant_params(grant, key):
grant_params = dict(KeyId=key['key_arn'],
GranteePrincipal=grant['grantee_principal'])
grant_params = dict(
KeyId=key['key_arn'], GranteePrincipal=grant['grantee_principal']
)
if grant.get('operations'):
grant_params['Operations'] = grant['operations']
if grant.get('retiring_principal'):
Expand Down Expand Up @@ -751,7 +780,11 @@ def update_tags(connection, module, key, desired_tags, purge_tags):
module.fail_json_aws(e, msg="Unable to remove tag")
if to_add:
try:
tags = ansible_dict_to_boto3_tag_list(module.params['tags'], tag_name_key_name='TagKey', tag_value_key_name='TagValue')
tags = ansible_dict_to_boto3_tag_list(
module.params['tags'],
tag_name_key_name='TagKey',
tag_value_key_name='TagValue',
)
connection.tag_resource(KeyId=key_id, Tags=tags)
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
module.fail_json_aws(e, msg="Unable to add tag to key")
Expand Down Expand Up @@ -859,16 +892,21 @@ def update_key(connection, module, key):
def create_key(connection, module):
key_usage = module.params.get('key_usage')
key_spec = module.params.get('key_spec')
multi_region = module.params.get('multi_region')
tags_list = ansible_dict_to_boto3_tag_list(
module.params['tags'] or {},
# KMS doesn't use "Key" and "Value" as other APIs do.
tag_name_key_name='TagKey', tag_value_key_name='TagValue'
# KMS doesn't use 'Key' and 'Value' as other APIs do.
tag_name_key_name='TagKey',
tag_value_key_name='TagValue',
)
params = dict(
BypassPolicyLockoutSafetyCheck=False,
Tags=tags_list,
KeyUsage=key_usage,
CustomerMasterKeySpec=key_spec,
Origin='AWS_KMS',
MultiRegion=multi_region,
)
params = dict(BypassPolicyLockoutSafetyCheck=False,
Tags=tags_list,
KeyUsage=key_usage,
CustomerMasterKeySpec=key_spec,
Origin='AWS_KMS')

if module.check_mode:
return {'changed': True}
Expand All @@ -877,7 +915,6 @@ def create_key(connection, module):
params['Description'] = module.params['description']
if module.params.get('policy'):
params['Policy'] = module.params['policy']

try:
result = connection.create_key(**params)['KeyMetadata']
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
Expand Down Expand Up @@ -940,7 +977,29 @@ def fetch_key_metadata(connection, module, key_id, alias):
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.')
module.fail_json_aws(e, "Failed to fetch key metadata.")


def validate_params(module, key_metadata):
# 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'
and module.params.get('key_id')
and not key_metadata
):
module.fail_json(
msg='Could not find key with id {0} to update'.format(
module.params.get('key_id')
)
)
if (
module.params.get('multi_region')
and key_metadata
and module.params.get('state') == 'present'
):
module.fail_json(
msg='You cannot change the multi-region property on an existing key.'
)


def main():
Expand All @@ -950,16 +1009,34 @@ def main():
key_id=dict(aliases=['key_arn']),
description=dict(),
enabled=dict(type='bool', default=True),
multi_region=dict(type='bool', default=False),
tags=dict(type='dict', aliases=['resource_tags']),
purge_tags=dict(type='bool', default=True),
grants=dict(type='list', default=[], elements='dict'),
policy=dict(type='json'),
purge_grants=dict(type='bool', default=False),
state=dict(default='present', choices=['present', 'absent']),
enable_key_rotation=(dict(type='bool')),
key_spec=dict(type='str', default='SYMMETRIC_DEFAULT', aliases=['customer_master_key_spec'],
choices=['SYMMETRIC_DEFAULT', 'RSA_2048', 'RSA_3072', 'RSA_4096', 'ECC_NIST_P256', 'ECC_NIST_P384', 'ECC_NIST_P521', 'ECC_SECG_P256K1']),
key_usage=dict(type='str', default='ENCRYPT_DECRYPT', choices=['ENCRYPT_DECRYPT', 'SIGN_VERIFY']),
key_spec=dict(
type='str',
default='SYMMETRIC_DEFAULT',
aliases=['customer_master_key_spec'],
choices=[
'SYMMETRIC_DEFAULT',
'RSA_2048',
'RSA_3072',
'RSA_4096',
'ECC_NIST_P256',
'ECC_NIST_P384',
'ECC_NIST_P521',
'ECC_SECG_P256K1',
],
),
key_usage=dict(
type='str',
default='ENCRYPT_DECRYPT',
choices=['ENCRYPT_DECRYPT', 'SIGN_VERIFY'],
),
)

module = AnsibleAWSModule(
Expand All @@ -970,13 +1047,14 @@ def main():

kms = module.client('kms')

module.deprecate("The 'policies' return key is deprecated and will be replaced by 'key_policies'. Both values are returned for now.",
date='2024-05-01', collection_name='amazon.aws')
module.deprecate(
"The 'policies' return key is deprecated and will be replaced by 'key_policies'. Both values are returned for now.",
date='2024-05-01',
collection_name='amazon.aws',
)

key_metadata = fetch_key_metadata(kms, module, module.params.get('key_id'), 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 module.params.get('state') == 'present' and module.params.get('key_id') and not key_metadata:
module.fail_json(msg="Could not find key with id {0} to update".format(module.params.get('key_id')))
validate_params(module, key_metadata)

if module.params.get('state') == 'absent':
if key_metadata is None:
Expand Down
2 changes: 2 additions & 0 deletions tests/integration/targets/kms_key/inventory
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ states
grants
modify
tagging
# CI's AWS account doesnot support multi region
# multi_region

[all:vars]
ansible_connection=local
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
- block:
# ============================================================
# PREPARATION
#
# Get some information about who we are before starting our tests
# we'll need this as soon as we start working on the policies
- name: get ARN of calling user
aws_caller_info:
register: aws_caller_info
- name: See whether key exists and its current state
kms_key_info:
alias: '{{ kms_key_alias }}'
- name: create a multi region key - check mode
kms_key:
alias: '{{ kms_key_alias }}-check'
tags:
Hello: World
state: present
multi_region: True
enabled: yes
register: key_check
check_mode: yes
- name: find facts about the check mode key
kms_key_info:
alias: '{{ kms_key_alias }}-check'
register: check_key
- name: ensure that check mode worked as expected
assert:
that:
- check_key.kms_keys | length == 0
- key_check is changed

- name: create a multi region key
kms_key:
alias: '{{ kms_key_alias }}'
tags:
Hello: World
state: present
enabled: yes
multi_region: True
enable_key_rotation: no
register: key
- name: assert that state is enabled
assert:
that:
- key is changed
- '"key_id" in key'
- key.key_id | length >= 36
- not key.key_id.startswith("arn:aws")
- '"key_arn" in key'
- key.key_arn.endswith(key.key_id)
- key.key_arn.startswith("arn:aws")
- key.key_state == "Enabled"
- key.enabled == True
- key.tags | length == 1
- key.tags['Hello'] == 'World'
- key.enable_key_rotation == false
- key.key_usage == 'ENCRYPT_DECRYPT'
- key.customer_master_key_spec == 'SYMMETRIC_DEFAULT'
- key.grants | length == 0
- key.key_policies | length == 1
- key.key_policies[0].Id == 'key-default-1'
- key.description == ''
- key.multi_region == True

- name: Sleep to wait for updates to propagate
wait_for:
timeout: 45

- name: create a key (expect failure)
kms_key:
alias: '{{ kms_key_alias }}'
tags:
Hello: World
state: present
enabled: yes
multi_region: True
register: result
GomathiselviS marked this conversation as resolved.
Show resolved Hide resolved
ignore_errors: True

- assert:
that:
- result is failed
- result.msg != "MODULE FAILURE"
- result.changed == False
- '"You cannot change the multi-region property on an existing key." in result.msg'

always:
# ============================================================
# CLEAN-UP
- name: finish off by deleting keys
kms_key:
state: absent
alias: '{{ item }}'
pending_window: 7
ignore_errors: true
loop:
- '{{ kms_key_alias }}'
- '{{ kms_key_alias }}-diff-spec-usage'
- '{{ kms_key_alias }}-check'
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
- key.key_policies | length == 1
- key.key_policies[0].Id == 'key-default-1'
- key.description == ''
- key.multi_region == False

- name: Sleep to wait for updates to propagate
wait_for:
Expand Down Expand Up @@ -105,6 +106,7 @@
- key.key_policies | length == 1
- key.key_policies[0].Id == 'key-default-1'
- key.description == ''
- key.multi_region == False

# ------------------------------------------------------------------------------------------

Expand Down
Loading