aws_kms enhancements (#31960)
* Allow creation and deletion of keys (deletion just schedules for deletion, recreating an old key is just cancelling its deletion) * Allow grants to be set, thus enabling encryption contexts to be used with keys * Allow tags to be added and modified * Add testing for KMS module * Tidy up aws_kms module to latest standards
This commit is contained in:
parent
1f3a74c0c8
commit
46fbcf08bc
7 changed files with 1136 additions and 54 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
@ -86,6 +86,7 @@ packaging/release/ansible_release
|
|||
/test/results/junit/*.xml
|
||||
/test/results/logs/*.log
|
||||
/test/results/data/*.json
|
||||
/test/integration/cloud-config-aws.yml
|
||||
/test/integration/inventory.remote
|
||||
/test/integration/inventory.networking
|
||||
/test/integration/inventory.winrm
|
||||
|
|
2
changelogs/fragments/aws_kms_grants.yml
Normal file
2
changelogs/fragments/aws_kms_grants.yml
Normal file
|
@ -0,0 +1,2 @@
|
|||
minor_changes:
|
||||
- aws_kms is now able to create keys and manage grants and tags
|
54
hacking/aws_config/testing_policies/kms-policy.json
Normal file
54
hacking/aws_config/testing_policies/kms-policy.json
Normal file
|
@ -0,0 +1,54 @@
|
|||
{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowAccessToUnspecifiedKMSResources",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"iam:ListRoles",
|
||||
"kms:CancelKeyDeletion",
|
||||
"kms:CreateAlias",
|
||||
"kms:CreateGrant",
|
||||
"kms:CreateKey",
|
||||
"kms:DeleteAlias",
|
||||
"kms:Describe*",
|
||||
"kms:DisableKey",
|
||||
"kms:EnableKey",
|
||||
"kms:GenerateRandom",
|
||||
"kms:Get*",
|
||||
"kms:List*",
|
||||
"kms:RetireGrant",
|
||||
"kms:ScheduleKeyDeletion",
|
||||
"kms:TagResource",
|
||||
"kms:UntagResource",
|
||||
"kms:UpdateGrant",
|
||||
"kms:UpdateKeyDescription"
|
||||
],
|
||||
"Resource": "*"
|
||||
},
|
||||
{
|
||||
"Sid": "AllowAccessToSpecifiedIAMResources",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"iam:CreateRole",
|
||||
"iam:DeleteRole",
|
||||
"iam:GetRole",
|
||||
"iam:ListAttachedRolePolicies",
|
||||
"iam:ListInstanceProfilesForRole",
|
||||
"iam:PassRole",
|
||||
"iam:UpdateAssumeRolePolicy"
|
||||
],
|
||||
"Resource": "arn:aws:iam::{{aws_account}}:role/ansible-test-*"
|
||||
},
|
||||
{
|
||||
"Sid": "AllowInstanceProfileCreation",
|
||||
"Effect": "Allow",
|
||||
"Action": [
|
||||
"iam:AddRoleToInstanceProfile",
|
||||
"iam:CreateInstanceProfile",
|
||||
"iam:RemoveRoleFromInstanceProfile"
|
||||
],
|
||||
"Resource": "arn:aws:iam::{{aws_account}}:instance-profile/ansible-test-*"
|
||||
}
|
||||
]
|
||||
}
|
|
@ -26,22 +26,29 @@ short_description: Perform various KMS management tasks.
|
|||
description:
|
||||
- Manage role/user access to a KMS key. Not designed for encrypting/decrypting.
|
||||
version_added: "2.3"
|
||||
requirements: [ boto3 ]
|
||||
options:
|
||||
mode:
|
||||
description:
|
||||
- Grant or deny access.
|
||||
required: true
|
||||
default: grant
|
||||
choices: [ grant, deny ]
|
||||
key_alias:
|
||||
description:
|
||||
- Alias label to the key. One of C(key_alias) or C(key_arn) are required.
|
||||
alias:
|
||||
description: An alias for a key. For safety, even though KMS does not require keys
|
||||
to have an alias, this module expects all new keys to be given an alias
|
||||
to make them easier to manage. Existing keys without an alias may be
|
||||
referred to by I(key_id). Use M(aws_kms_facts) to find key ids. Required
|
||||
if I(key_id) is not given. Note that passing a I(key_id) and I(alias)
|
||||
will only cause a new alias to be added, an alias will never be renamed.
|
||||
The 'alias/' prefix is optional.
|
||||
required: false
|
||||
key_arn:
|
||||
aliases:
|
||||
- key_alias
|
||||
key_id:
|
||||
description:
|
||||
- Full ARN to the key. One of C(key_alias) or C(key_arn) are required.
|
||||
- Key ID or ARN of the key. One of C(alias) or C(key_id) are required.
|
||||
required: false
|
||||
aliases:
|
||||
- key_arn
|
||||
role_name:
|
||||
description:
|
||||
- Role to allow/deny access. One of C(role_name) or C(role_arn) are required.
|
||||
|
@ -60,8 +67,62 @@ options:
|
|||
- Only cleans if changes are being made.
|
||||
type: bool
|
||||
default: true
|
||||
|
||||
author: Ted Timmons (@tedder)
|
||||
state:
|
||||
description: Whether a key should be present or absent. Note that making an
|
||||
existing key absent only schedules a key for deletion. Passing a key that
|
||||
is scheduled for deletion with state present will cancel key deletion.
|
||||
required: False
|
||||
choices:
|
||||
- present
|
||||
- absent
|
||||
default: present
|
||||
version_added: 2.8
|
||||
enabled:
|
||||
description: Whether or not a key is enabled
|
||||
default: True
|
||||
version_added: 2.8
|
||||
type: bool
|
||||
description:
|
||||
description:
|
||||
A description of the CMK. Use a description that helps you decide
|
||||
whether the CMK is appropriate for a task.
|
||||
version_added: 2.8
|
||||
tags:
|
||||
description: A dictionary of tags to apply to a key.
|
||||
version_added: 2.8
|
||||
purge_tags:
|
||||
description: Whether the I(tags) argument should cause tags not in the list to
|
||||
be removed
|
||||
version_added: 2.8
|
||||
default: False
|
||||
type: bool
|
||||
purge_grants:
|
||||
description: Whether the I(grants) argument should cause grants not in the list to
|
||||
be removed
|
||||
default: False
|
||||
version_added: 2.8
|
||||
type: bool
|
||||
grants:
|
||||
description:
|
||||
- A list of grants to apply to the key. Each item must contain I(grantee_principal).
|
||||
Each item can optionally contain I(retiring_principal), I(operations), I(constraints),
|
||||
I(name).
|
||||
- Valid operations are C(Decrypt), C(Encrypt), C(GenerateDataKey), C(GenerateDataKeyWithoutPlaintext),
|
||||
C(ReEncryptFrom), C(ReEncryptTo), C(CreateGrant), C(RetireGrant), C(DescribeKey), C(Verify) and
|
||||
C(Sign)
|
||||
- Constraints is a dict containing C(encryption_context_subset) or C(encryption_context_equals),
|
||||
either or both being a dict specifying an encryption context match.
|
||||
See U(https://docs.aws.amazon.com/kms/latest/APIReference/API_GrantConstraints.html)
|
||||
- I(grantee_principal) and I(retiring_principal) must be ARNs
|
||||
version_added: 2.8
|
||||
policy:
|
||||
description:
|
||||
- policy to apply to the KMS key
|
||||
- See U(https://docs.aws.amazon.com/kms/latest/developerguide/key-policies.html)
|
||||
version_added: 2.8
|
||||
author:
|
||||
- Ted Timmons (@tedder)
|
||||
- Will Thames (@willthames)
|
||||
extends_documentation_fragment:
|
||||
- aws
|
||||
- ec2
|
||||
|
@ -72,18 +133,199 @@ EXAMPLES = '''
|
|||
aws_kms:
|
||||
args:
|
||||
mode: grant
|
||||
key_alias: "alias/my_production_secrets"
|
||||
alias: "alias/my_production_secrets"
|
||||
role_name: "prod-appServerRole-1R5AQG2BSEL6L"
|
||||
grant_types: "role,role grant"
|
||||
- name: remove access to production secrets from role
|
||||
aws_kms:
|
||||
args:
|
||||
mode: deny
|
||||
key_alias: "alias/my_production_secrets"
|
||||
alias: "alias/my_production_secrets"
|
||||
role_name: "prod-appServerRole-1R5AQG2BSEL6L"
|
||||
|
||||
# Create a new KMS key
|
||||
- aws_kms:
|
||||
alias: mykey
|
||||
tags:
|
||||
Name: myKey
|
||||
Purpose: protect_stuff
|
||||
|
||||
# Update previous key with more tags
|
||||
- aws_kms:
|
||||
alias: mykey
|
||||
tags:
|
||||
Name: myKey
|
||||
Purpose: protect_stuff
|
||||
Owner: security_team
|
||||
|
||||
# Update a known key with grants allowing an instance with the billing-prod IAM profile
|
||||
# to decrypt data encrypted with the environment: production, application: billing
|
||||
# encryption context
|
||||
- aws_kms:
|
||||
key_id: abcd1234-abcd-1234-5678-ef1234567890
|
||||
grants:
|
||||
- name: billing_prod
|
||||
grantee_principal: arn:aws:iam::1234567890123:role/billing_prod
|
||||
constraints:
|
||||
encryption_context_equals:
|
||||
environment: production
|
||||
application: billing
|
||||
operations:
|
||||
- Decrypt
|
||||
- RetireGrant
|
||||
'''
|
||||
|
||||
RETURN = '''
|
||||
key_id:
|
||||
description: ID of key
|
||||
type: str
|
||||
returned: always
|
||||
sample: abcd1234-abcd-1234-5678-ef1234567890
|
||||
key_arn:
|
||||
description: ARN of key
|
||||
type: str
|
||||
returned: always
|
||||
sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890
|
||||
key_state:
|
||||
description: The state of the key
|
||||
type: str
|
||||
returned: always
|
||||
sample: PendingDeletion
|
||||
key_usage:
|
||||
description: The cryptographic operations for which you can use the key.
|
||||
type: str
|
||||
returned: always
|
||||
sample: ENCRYPT_DECRYPT
|
||||
origin:
|
||||
description: The source of the key's key material. When this value is C(AWS_KMS),
|
||||
AWS KMS created the key material. When this value is C(EXTERNAL), the
|
||||
key material was imported or the CMK lacks key material.
|
||||
type: str
|
||||
returned: always
|
||||
sample: AWS_KMS
|
||||
aws_account_id:
|
||||
description: The AWS Account ID that the key belongs to
|
||||
type: str
|
||||
returned: always
|
||||
sample: 1234567890123
|
||||
creation_date:
|
||||
description: Date of creation of the key
|
||||
type: str
|
||||
returned: always
|
||||
sample: "2017-04-18T15:12:08.551000+10:00"
|
||||
description:
|
||||
description: Description of the key
|
||||
type: str
|
||||
returned: always
|
||||
sample: "My Key for Protecting important stuff"
|
||||
enabled:
|
||||
description: Whether the key is enabled. True if C(KeyState) is true.
|
||||
type: str
|
||||
returned: always
|
||||
sample: false
|
||||
aliases:
|
||||
description: list of aliases associated with the key
|
||||
type: list
|
||||
returned: always
|
||||
sample:
|
||||
- aws/acm
|
||||
- aws/ebs
|
||||
policies:
|
||||
description: list of policy documents for the keys. Empty when access is denied even if there are policies.
|
||||
type: list
|
||||
returned: always
|
||||
sample:
|
||||
Version: "2012-10-17"
|
||||
Id: "auto-ebs-2"
|
||||
Statement:
|
||||
- Sid: "Allow access through EBS for all principals in the account that are authorized to use EBS"
|
||||
Effect: "Allow"
|
||||
Principal:
|
||||
AWS: "*"
|
||||
Action:
|
||||
- "kms:Encrypt"
|
||||
- "kms:Decrypt"
|
||||
- "kms:ReEncrypt*"
|
||||
- "kms:GenerateDataKey*"
|
||||
- "kms:CreateGrant"
|
||||
- "kms:DescribeKey"
|
||||
Resource: "*"
|
||||
Condition:
|
||||
StringEquals:
|
||||
kms:CallerAccount: "111111111111"
|
||||
kms:ViaService: "ec2.ap-southeast-2.amazonaws.com"
|
||||
- Sid: "Allow direct access to key metadata to the account"
|
||||
Effect: "Allow"
|
||||
Principal:
|
||||
AWS: "arn:aws:iam::111111111111:root"
|
||||
Action:
|
||||
- "kms:Describe*"
|
||||
- "kms:Get*"
|
||||
- "kms:List*"
|
||||
- "kms:RevokeGrant"
|
||||
Resource: "*"
|
||||
tags:
|
||||
description: dictionary of tags applied to the key
|
||||
type: dict
|
||||
returned: always
|
||||
sample:
|
||||
Name: myKey
|
||||
Purpose: protecting_stuff
|
||||
grants:
|
||||
description: list of grants associated with a key
|
||||
type: complex
|
||||
returned: always
|
||||
contains:
|
||||
constraints:
|
||||
description: Constraints on the encryption context that the grant allows.
|
||||
See U(https://docs.aws.amazon.com/kms/latest/APIReference/API_GrantConstraints.html) for further details
|
||||
type: dict
|
||||
returned: always
|
||||
sample:
|
||||
encryption_context_equals:
|
||||
"aws:lambda:_function_arn": "arn:aws:lambda:ap-southeast-2:012345678912:function:xyz"
|
||||
creation_date:
|
||||
description: Date of creation of the grant
|
||||
type: str
|
||||
returned: always
|
||||
sample: 2017-04-18T15:12:08+10:00
|
||||
grant_id:
|
||||
description: The unique ID for the grant
|
||||
type: str
|
||||
returned: always
|
||||
sample: abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234
|
||||
grantee_principal:
|
||||
description: The principal that receives the grant's permissions
|
||||
type: str
|
||||
returned: always
|
||||
sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz
|
||||
issuing_account:
|
||||
description: The AWS account under which the grant was issued
|
||||
type: str
|
||||
returned: always
|
||||
sample: arn:aws:iam::01234567890:root
|
||||
key_id:
|
||||
description: The key ARN to which the grant applies.
|
||||
type: str
|
||||
returned: always
|
||||
sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890
|
||||
name:
|
||||
description: The friendly name that identifies the grant
|
||||
type: str
|
||||
returned: always
|
||||
sample: xyz
|
||||
operations:
|
||||
description: The list of operations permitted by the grant
|
||||
type: list
|
||||
returned: always
|
||||
sample:
|
||||
- Decrypt
|
||||
- RetireGrant
|
||||
retiring_principal:
|
||||
description: The principal that can retire the grant
|
||||
type: str
|
||||
returned: always
|
||||
sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz
|
||||
changes_needed:
|
||||
description: grant types that would be changed/were changed.
|
||||
type: dict
|
||||
|
@ -103,22 +345,385 @@ statement_label = {
|
|||
'admin': 'Allow access for Key Administrators'
|
||||
}
|
||||
|
||||
# import module snippets
|
||||
from ansible.module_utils.basic import AnsibleModule
|
||||
from ansible.module_utils.ec2 import boto_exception
|
||||
from ansible.module_utils.aws.core import AnsibleAWSModule, is_boto3_error_code
|
||||
from ansible.module_utils.ec2 import ec2_argument_spec
|
||||
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 compare_aws_tags
|
||||
from ansible.module_utils.six import string_types
|
||||
|
||||
# import a class, we'll use a fully qualified path
|
||||
import ansible.module_utils.ec2
|
||||
|
||||
import traceback
|
||||
import json
|
||||
|
||||
try:
|
||||
import botocore
|
||||
HAS_BOTO3 = True
|
||||
except ImportError:
|
||||
HAS_BOTO3 = False
|
||||
pass # caught by AnsibleAWSModule
|
||||
|
||||
|
||||
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||
def get_iam_roles_with_backoff(connection):
|
||||
paginator = connection.get_paginator('list_roles')
|
||||
return paginator.paginate().build_full_result()
|
||||
|
||||
|
||||
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||
def get_kms_keys_with_backoff(connection):
|
||||
paginator = connection.get_paginator('list_keys')
|
||||
return paginator.paginate().build_full_result()
|
||||
|
||||
|
||||
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||
def get_kms_aliases_with_backoff(connection):
|
||||
paginator = connection.get_paginator('list_aliases')
|
||||
return paginator.paginate().build_full_result()
|
||||
|
||||
|
||||
def get_kms_aliases_lookup(connection):
|
||||
_aliases = dict()
|
||||
for alias in get_kms_aliases_with_backoff(connection)['Aliases']:
|
||||
# Not all aliases are actually associated with a key
|
||||
if 'TargetKeyId' in alias:
|
||||
# strip off leading 'alias/' and add it to key's aliases
|
||||
if alias['TargetKeyId'] in _aliases:
|
||||
_aliases[alias['TargetKeyId']].append(alias['AliasName'][6:])
|
||||
else:
|
||||
_aliases[alias['TargetKeyId']] = [alias['AliasName'][6:]]
|
||||
return _aliases
|
||||
|
||||
|
||||
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||
def get_kms_tags_with_backoff(connection, key_id, **kwargs):
|
||||
return connection.list_resource_tags(KeyId=key_id, **kwargs)
|
||||
|
||||
|
||||
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||
def get_kms_grants_with_backoff(connection, key_id):
|
||||
params = dict(KeyId=key_id)
|
||||
paginator = connection.get_paginator('list_grants')
|
||||
return paginator.paginate(**params).build_full_result()
|
||||
|
||||
|
||||
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||
def get_kms_metadata_with_backoff(connection, key_id):
|
||||
return connection.describe_key(KeyId=key_id)
|
||||
|
||||
|
||||
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||
def list_key_policies_with_backoff(connection, key_id):
|
||||
paginator = connection.get_paginator('list_key_policies')
|
||||
return paginator.paginate(KeyId=key_id).build_full_result()
|
||||
|
||||
|
||||
@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
|
||||
def get_key_policy_with_backoff(connection, key_id, policy_name):
|
||||
return connection.get_key_policy(KeyId=key_id, PolicyName=policy_name)
|
||||
|
||||
|
||||
def get_kms_tags(connection, module, key_id):
|
||||
# Handle pagination here as list_resource_tags does not have
|
||||
# a paginator
|
||||
kwargs = {}
|
||||
tags = []
|
||||
more = True
|
||||
while more:
|
||||
try:
|
||||
tag_response = get_kms_tags_with_backoff(connection, key_id, **kwargs)
|
||||
tags.extend(tag_response['Tags'])
|
||||
except is_boto3_error_code('AccessDeniedException'):
|
||||
tag_response = {}
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e: # pylint: disable=duplicate-except
|
||||
module.fail_json_aws(e, msg="Failed to obtain key tags")
|
||||
if tag_response.get('NextMarker'):
|
||||
kwargs['Marker'] = tag_response['NextMarker']
|
||||
else:
|
||||
more = False
|
||||
return tags
|
||||
|
||||
|
||||
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]
|
||||
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 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):
|
||||
''' 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:
|
||||
result['constraints']['encryption_context_equals'] = constraints['EncryptionContextEquals']
|
||||
if 'EncryptionContextSubset' in constraints:
|
||||
result['constraints']['encryption_context_subset'] = constraints['EncryptionContextSubset']
|
||||
return result
|
||||
|
||||
|
||||
def get_key_details(connection, module, key_id):
|
||||
try:
|
||||
result = get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata']
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
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")
|
||||
|
||||
result['aliases'] = aliases.get(result['KeyId'], [])
|
||||
|
||||
result = camel_dict_to_snake_dict(result)
|
||||
|
||||
# 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']]
|
||||
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)
|
||||
result['tags'] = boto3_tag_list_to_ansible_dict(tags, 'TagKey', 'TagValue')
|
||||
result['policies'] = get_kms_policies(connection, module, key_id)
|
||||
return result
|
||||
|
||||
|
||||
def get_kms_facts(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]
|
||||
|
||||
|
||||
def convert_grant_params(grant, key):
|
||||
grant_params = dict(KeyId=key['key_id'],
|
||||
GranteePrincipal=grant['grantee_principal'])
|
||||
if grant.get('operations'):
|
||||
grant_params['Operations'] = grant['operations']
|
||||
if grant.get('retiring_principal'):
|
||||
grant_params['RetiringPrincipal'] = grant['retiring_principal']
|
||||
if grant.get('name'):
|
||||
grant_params['Name'] = grant['name']
|
||||
if grant.get('constraints'):
|
||||
grant_params['Constraints'] = dict()
|
||||
if grant['constraints'].get('encryption_context_subset'):
|
||||
grant_params['Constraints']['EncryptionContextSubset'] = grant['constraints']['encryption_context_subset']
|
||||
if grant['constraints'].get('encryption_context_equals'):
|
||||
grant_params['Constraints']['EncryptionContextEquals'] = grant['constraints']['encryption_context_equals']
|
||||
return grant_params
|
||||
|
||||
|
||||
def different_grant(existing_grant, desired_grant):
|
||||
if existing_grant.get('grantee_principal') != desired_grant.get('grantee_principal'):
|
||||
return True
|
||||
if existing_grant.get('retiring_principal') != desired_grant.get('retiring_principal'):
|
||||
return True
|
||||
if set(existing_grant.get('operations', [])) != set(desired_grant.get('operations')):
|
||||
return True
|
||||
if existing_grant.get('constraints') != desired_grant.get('constraints'):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def compare_grants(existing_grants, desired_grants, purge_grants=False):
|
||||
existing_dict = dict((eg['name'], eg) for eg in existing_grants)
|
||||
desired_dict = dict((dg['name'], dg) for dg in desired_grants)
|
||||
to_add_keys = set(desired_dict.keys()) - set(existing_dict.keys())
|
||||
if purge_grants:
|
||||
to_remove_keys = set(existing_dict.keys()) - set(desired_dict.keys())
|
||||
else:
|
||||
to_remove_keys = set()
|
||||
to_change_candidates = set(existing_dict.keys()) & set(desired_dict.keys())
|
||||
for candidate in to_change_candidates:
|
||||
if different_grant(existing_dict[candidate], desired_dict[candidate]):
|
||||
to_add_keys.add(candidate)
|
||||
to_remove_keys.add(candidate)
|
||||
|
||||
to_add = []
|
||||
to_remove = []
|
||||
for key in to_add_keys:
|
||||
grant = desired_dict[key]
|
||||
to_add.append(grant)
|
||||
for key in to_remove_keys:
|
||||
grant = existing_dict[key]
|
||||
to_remove.append(grant)
|
||||
return to_add, to_remove
|
||||
|
||||
|
||||
def ensure_enabled_disabled(connection, module, key):
|
||||
changed = False
|
||||
if key['key_state'] == 'Disabled' and module.params['enabled']:
|
||||
try:
|
||||
connection.enable_key(KeyId=key['key_id'])
|
||||
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_id'])
|
||||
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_key(connection, module, key):
|
||||
changed = False
|
||||
alias = module.params['alias']
|
||||
if not alias.startswith('alias/'):
|
||||
alias = 'alias/' + alias
|
||||
aliases = get_kms_aliases_with_backoff(connection)['Aliases']
|
||||
key_id = module.params.get('key_id')
|
||||
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)
|
||||
changed = True
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(msg="Failed create key alias")
|
||||
|
||||
if key['key_state'] == 'PendingDeletion':
|
||||
try:
|
||||
connection.cancel_key_deletion(KeyId=key['key_id'])
|
||||
# key is disabled after deletion cancellation
|
||||
# set this so that ensure_enabled_disabled works correctly
|
||||
key['key_state'] = 'Disabled'
|
||||
changed = True
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg="Failed to cancel key deletion")
|
||||
|
||||
changed = ensure_enabled_disabled(connection, module, key) or changed
|
||||
|
||||
description = module.params.get('description')
|
||||
# don't update description if description is not set
|
||||
# (means you can't remove a description completely)
|
||||
if description and key['description'] != description:
|
||||
try:
|
||||
connection.update_key_description(KeyId=key['key_id'], Description=description)
|
||||
changed = True
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg="Failed to update key description")
|
||||
|
||||
desired_tags = module.params.get('tags')
|
||||
to_add, to_remove = compare_aws_tags(key['tags'], desired_tags,
|
||||
module.params.get('purge_tags'))
|
||||
if to_remove:
|
||||
try:
|
||||
connection.untag_resource(KeyId=key['key_id'], TagKeys=to_remove)
|
||||
changed = True
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg="Unable to remove or update tag")
|
||||
if to_add:
|
||||
try:
|
||||
connection.tag_resource(KeyId=key['key_id'],
|
||||
Tags=[{'TagKey': tag_key, 'TagValue': desired_tags[tag_key]}
|
||||
for tag_key in to_add])
|
||||
changed = True
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg="Unable to add tag to key")
|
||||
|
||||
desired_grants = module.params.get('grants')
|
||||
existing_grants = key['grants']
|
||||
|
||||
to_add, to_remove = compare_grants(existing_grants, desired_grants,
|
||||
module.params.get('purge_grants'))
|
||||
if to_remove:
|
||||
for grant in to_remove:
|
||||
try:
|
||||
connection.retire_grant(KeyId=key['key_arn'], GrantId=grant['grant_id'])
|
||||
changed = True
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg="Unable to retire grant")
|
||||
|
||||
if to_add:
|
||||
for grant in to_add:
|
||||
grant_params = convert_grant_params(grant, key)
|
||||
try:
|
||||
connection.create_grant(**grant_params)
|
||||
changed = True
|
||||
except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
|
||||
module.fail_json_aws(e, msg="Unable to create grant")
|
||||
|
||||
# make results consistent with kms_facts
|
||||
result = get_key_details(connection, module, key['key_id'])
|
||||
module.exit_json(changed=changed, **camel_dict_to_snake_dict(result))
|
||||
|
||||
|
||||
def create_key(connection, module):
|
||||
params = dict(BypassPolicyLockoutSafetyCheck=False,
|
||||
Tags=ansible_dict_to_boto3_tag_list(module.params['tags']),
|
||||
KeyUsage='ENCRYPT_DECRYPT',
|
||||
Origin='AWS_KMS')
|
||||
if module.params.get('description'):
|
||||
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:
|
||||
module.fail_json_aws(e, msg="Failed to create initial key")
|
||||
key = get_key_details(connection, module, result['KeyId'])
|
||||
|
||||
alias = 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)
|
||||
for grant in module.params.get('grants'):
|
||||
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
|
||||
result = get_key_details(connection, module, key['key_id'])
|
||||
module.exit_json(changed=True, **camel_dict_to_snake_dict(result))
|
||||
|
||||
|
||||
def delete_key(connection, module, key):
|
||||
changed = False
|
||||
|
||||
if key['key_state'] != 'PendingDeletion':
|
||||
try:
|
||||
connection.schedule_key_deletion(KeyId=key['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['key_id'])
|
||||
module.exit_json(changed=changed, **camel_dict_to_snake_dict(result))
|
||||
|
||||
|
||||
def get_arn_from_kms_alias(kms, aliasname):
|
||||
|
@ -184,16 +789,19 @@ def do_grant(kms, keyarn, role_arn, granttypes, mode='grant', dry_run=True, clea
|
|||
|
||||
if role_arn not in statement['Principal']['AWS']: # needs to be added.
|
||||
changes_needed[granttype] = 'add'
|
||||
statement['Principal']['AWS'].append(role_arn)
|
||||
if not dry_run:
|
||||
statement['Principal']['AWS'].append(role_arn)
|
||||
elif role_arn in statement['Principal']['AWS']: # not one the places the role should be
|
||||
changes_needed[granttype] = 'remove'
|
||||
statement['Principal']['AWS'].remove(role_arn)
|
||||
if not dry_run:
|
||||
statement['Principal']['AWS'].remove(role_arn)
|
||||
|
||||
elif mode == 'deny' and statement['Sid'] == statement_label[granttype] and role_arn in statement['Principal']['AWS']:
|
||||
# we don't selectively deny. that's a grant with a
|
||||
# smaller list. so deny=remove all of this arn.
|
||||
changes_needed[granttype] = 'remove'
|
||||
statement['Principal']['AWS'].remove(role_arn)
|
||||
if not dry_run:
|
||||
statement['Principal']['AWS'].remove(role_arn)
|
||||
|
||||
try:
|
||||
if len(changes_needed) and not dry_run:
|
||||
|
@ -236,43 +844,40 @@ def assert_policy_shape(policy):
|
|||
|
||||
|
||||
def main():
|
||||
argument_spec = ansible.module_utils.ec2.ec2_argument_spec()
|
||||
argument_spec.update(dict(
|
||||
mode=dict(choices=['grant', 'deny'], default='grant'),
|
||||
key_alias=dict(required=False, type='str'),
|
||||
key_arn=dict(required=False, type='str'),
|
||||
role_name=dict(required=False, type='str'),
|
||||
role_arn=dict(required=False, type='str'),
|
||||
grant_types=dict(required=False, type='list'),
|
||||
clean_invalid_entries=dict(type='bool', default=True),
|
||||
)
|
||||
argument_spec = ec2_argument_spec()
|
||||
argument_spec.update(
|
||||
dict(
|
||||
mode=dict(choices=['grant', 'deny'], default='grant'),
|
||||
alias=dict(aliases=['key_alias']),
|
||||
role_name=dict(),
|
||||
role_arn=dict(),
|
||||
grant_types=dict(type='list'),
|
||||
clean_invalid_entries=dict(type='bool', default=True),
|
||||
key_id=dict(aliases=['key_arn']),
|
||||
description=dict(),
|
||||
enabled=dict(type='bool', default=True),
|
||||
tags=dict(type='dict', default={}),
|
||||
purge_tags=dict(type='bool', default=False),
|
||||
grants=dict(type='list', default=[]),
|
||||
policy=dict(),
|
||||
purge_grants=dict(type='bool', default=False),
|
||||
state=dict(default='present', choices=['present', 'absent']),
|
||||
)
|
||||
)
|
||||
|
||||
module = AnsibleModule(
|
||||
module = AnsibleAWSModule(
|
||||
supports_check_mode=True,
|
||||
argument_spec=argument_spec,
|
||||
required_one_of=[['key_alias', 'key_arn'], ['role_name', 'role_arn']],
|
||||
required_if=[['mode', 'grant', ['grant_types']]]
|
||||
required_one_of=[['alias', 'key_id']],
|
||||
)
|
||||
if not HAS_BOTO3:
|
||||
module.fail_json(msg='boto3 required for this module')
|
||||
|
||||
result = {}
|
||||
mode = module.params['mode']
|
||||
|
||||
try:
|
||||
region, ec2_url, aws_connect_kwargs = ansible.module_utils.ec2.get_aws_connection_info(module, boto3=True)
|
||||
kms = ansible.module_utils.ec2.boto3_conn(module, conn_type='client', resource='kms', region=region, endpoint=ec2_url, **aws_connect_kwargs)
|
||||
iam = ansible.module_utils.ec2.boto3_conn(module, conn_type='client', resource='iam', region=region, endpoint=ec2_url, **aws_connect_kwargs)
|
||||
except botocore.exceptions.NoCredentialsError as e:
|
||||
module.fail_json(msg='cannot connect to AWS', exception=traceback.format_exc())
|
||||
|
||||
try:
|
||||
if module.params['key_alias'] and not module.params['key_arn']:
|
||||
module.params['key_arn'] = get_arn_from_kms_alias(kms, module.params['key_alias'])
|
||||
if not module.params['key_arn']:
|
||||
module.fail_json(msg='key_arn or key_alias is required to {}'.format(mode))
|
||||
kms = module.client('kms')
|
||||
iam = module.client('iam')
|
||||
|
||||
if module.params['grant_types'] or mode == 'deny':
|
||||
if module.params['role_name'] and not module.params['role_arn']:
|
||||
module.params['role_arn'] = get_arn_from_role_name(iam, module.params['role_name'])
|
||||
if not module.params['role_arn']:
|
||||
|
@ -290,11 +895,31 @@ def main():
|
|||
clean_invalid_entries=module.params['clean_invalid_entries'])
|
||||
result.update(ret)
|
||||
|
||||
except Exception as err:
|
||||
error_msg = boto_exception(err)
|
||||
module.fail_json(msg=error_msg, exception=traceback.format_exc())
|
||||
module.exit_json(**result)
|
||||
else:
|
||||
all_keys = get_kms_facts(kms, module)
|
||||
key_id = module.params.get('key_id')
|
||||
alias = module.params.get('alias')
|
||||
if key_id:
|
||||
filtr = ('key-id', key_id)
|
||||
elif module.params.get('alias'):
|
||||
filtr = ('alias', alias)
|
||||
|
||||
module.exit_json(**result)
|
||||
candidate_keys = [key for key in all_keys if key_matches_filter(key, filtr)]
|
||||
|
||||
if module.params.get('state') == 'present':
|
||||
if candidate_keys:
|
||||
update_key(kms, module, candidate_keys[0])
|
||||
else:
|
||||
if module.params.get('key_id'):
|
||||
module.fail_json(msg="Could not find key with id %s to update")
|
||||
else:
|
||||
create_key(kms, module)
|
||||
else:
|
||||
if candidate_keys:
|
||||
delete_key(kms, module, candidate_keys[0])
|
||||
else:
|
||||
module.exit_json(changed=False)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
|
3
test/integration/targets/aws_kms/aliases
Normal file
3
test/integration/targets/aws_kms/aliases
Normal file
|
@ -0,0 +1,3 @@
|
|||
cloud/aws
|
||||
aws_kms_facts
|
||||
unsupported
|
3
test/integration/targets/aws_kms/meta/main.yml
Normal file
3
test/integration/targets/aws_kms/meta/main.yml
Normal file
|
@ -0,0 +1,3 @@
|
|||
dependencies:
|
||||
- prepare_tests
|
||||
- setup_ec2
|
394
test/integration/targets/aws_kms/tasks/main.yml
Normal file
394
test/integration/targets/aws_kms/tasks/main.yml
Normal file
|
@ -0,0 +1,394 @@
|
|||
- block:
|
||||
|
||||
# ============================================================
|
||||
- name: See whether key exists and its current state
|
||||
aws_kms_facts:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
filters:
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
|
||||
- name: create a key
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
enabled: yes
|
||||
register: create_kms
|
||||
|
||||
- name: assert that state is enabled
|
||||
assert:
|
||||
that:
|
||||
- create_kms.key_state == "Enabled"
|
||||
|
||||
- name: find facts about the key
|
||||
aws_kms_facts:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
filters:
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
register: new_key
|
||||
|
||||
- name: check that a key was found
|
||||
assert:
|
||||
that:
|
||||
- new_key["keys"]|length == 1
|
||||
|
||||
- name: create an IAM role that can do nothing
|
||||
iam_role:
|
||||
name: "{{ resource_prefix }}-kms-role"
|
||||
state: present
|
||||
assume_role_policy_document: '{"Version": "2012-10-17", "Statement": {"Action": "sts:AssumeRole", "Principal": {"Service": "ec2.amazonaws.com"}, "Effect": "Deny"} }'
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
register: iam_role_result
|
||||
|
||||
- name: grant user-style access to production secrets
|
||||
aws_kms:
|
||||
mode: grant
|
||||
key_alias: "alias/{{ resource_prefix }}-kms"
|
||||
role_name: "{{ resource_prefix }}-kms-role"
|
||||
grant_types: "role,role grant"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
region: "{{ aws_region }}"
|
||||
|
||||
- name: find facts about the key
|
||||
aws_kms_facts:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
filters:
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
register: new_key
|
||||
|
||||
- name: remove access to production secrets from role
|
||||
aws_kms:
|
||||
mode: deny
|
||||
key_alias: "alias/{{ resource_prefix }}-kms"
|
||||
role_arn: "{{ iam_role_result.iam_role.arn }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
region: "{{ aws_region }}"
|
||||
|
||||
- name: find facts about the key
|
||||
aws_kms_facts:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
filters:
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
register: new_key
|
||||
|
||||
- fail:
|
||||
|
||||
- name: set aws environment base fact
|
||||
set_fact:
|
||||
aws_environment_base:
|
||||
AWS_ACCESS_KEY_ID: "{{ aws_access_key }}"
|
||||
AWS_SECRET_ACCESS_KEY: "{{ aws_secret_key }}"
|
||||
no_log: True
|
||||
|
||||
- name: set aws environment fact
|
||||
set_fact:
|
||||
aws_environment: "{{ aws_environment_base|combine(security_token|ternary({'AWS_SECURITY_TOKEN': security_token}, {})) }}"
|
||||
no_log: True
|
||||
|
||||
- name: get ARN of calling user
|
||||
command: python -c 'import boto3,json; sts = boto3.client("sts"); print json.dumps(sts.get_caller_identity())'
|
||||
changed_when: False
|
||||
environment: "{{ aws_environment }}"
|
||||
register: sts_get_caller_results
|
||||
|
||||
- name: set caller_arn
|
||||
set_fact:
|
||||
caller_arn: "{{ (sts_get_caller_results.stdout|from_json).Arn }}"
|
||||
|
||||
- name: Allow the IAM role to use a specific Encryption Context
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
purge_grants: yes
|
||||
purge_tags: yes
|
||||
grants:
|
||||
- name: test_grant
|
||||
grantee_principal: "{{ iam_role_result.iam_role.arn }}"
|
||||
retiring_principal: "{{ caller_arn }}"
|
||||
constraints:
|
||||
encryption_context_equals:
|
||||
environment: test
|
||||
application: testapp
|
||||
operations:
|
||||
- Decrypt
|
||||
- RetireGrant
|
||||
register: grant_one
|
||||
|
||||
- name: assert grant added
|
||||
assert:
|
||||
that:
|
||||
- grant_one.changed
|
||||
- grant_one.grants|length == 1
|
||||
|
||||
- name: Add a second grant
|
||||
kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
grants:
|
||||
- name: another_grant
|
||||
grantee_principal: "{{ iam_role_result.iam_role.arn }}"
|
||||
retiring_principal: "{{ caller_arn }}"
|
||||
constraints:
|
||||
encryption_context_equals:
|
||||
Environment: second
|
||||
Application: anotherapp
|
||||
operations:
|
||||
- Decrypt
|
||||
- RetireGrant
|
||||
register: grant_two
|
||||
|
||||
- name: assert grant added
|
||||
assert:
|
||||
that:
|
||||
- grant_two.changed
|
||||
- grant_two.grants|length == 2
|
||||
|
||||
- name: Add a second grant again
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
grants:
|
||||
- name: another_grant
|
||||
grantee_principal: "{{ iam_role_result.iam_role.arn }}"
|
||||
retiring_principal: "{{ caller_arn }}"
|
||||
constraints:
|
||||
encryption_context_equals:
|
||||
Environment: second
|
||||
Application: anotherapp
|
||||
operations:
|
||||
- Decrypt
|
||||
- RetireGrant
|
||||
register: grant_two_again
|
||||
|
||||
- name: assert grant added
|
||||
assert:
|
||||
that:
|
||||
- not grant_two_again.changed
|
||||
- grant_two_again.grants|length == 2
|
||||
|
||||
- name: Update the grants with purge_grants set
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
purge_grants: yes
|
||||
grants:
|
||||
- name: third_grant
|
||||
grantee_principal: "{{ iam_role_result.iam_role.arn }}"
|
||||
retiring_principal: "{{ caller_arn }}"
|
||||
constraints:
|
||||
encryption_context_equals:
|
||||
environment: third
|
||||
application: onemoreapp
|
||||
operations:
|
||||
- Decrypt
|
||||
- RetireGrant
|
||||
register: grant_three
|
||||
|
||||
- name: assert grants replaced
|
||||
assert:
|
||||
that:
|
||||
- grant_three.changed
|
||||
- grant_three.grants|length == 1
|
||||
|
||||
- name: update third grant to change encryption context equals to subset
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
grants:
|
||||
- name: third_grant
|
||||
grantee_principal: "{{ iam_role_result.iam_role.arn }}"
|
||||
retiring_principal: "{{ caller_arn }}"
|
||||
constraints:
|
||||
encryption_context_subset:
|
||||
environment: third
|
||||
application: onemoreapp
|
||||
operations:
|
||||
- Decrypt
|
||||
- RetireGrant
|
||||
register: grant_three_update
|
||||
|
||||
- name: assert grants replaced
|
||||
assert:
|
||||
that:
|
||||
- "grant_three_update.changed"
|
||||
- "grant_three_update.grants|length == 1"
|
||||
- "'encryption_context_equals' not in grant_three_update.grants[0].constraints"
|
||||
- "'encryption_context_subset' in grant_three_update.grants[0].constraints"
|
||||
|
||||
- name: tag encryption key
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
tags:
|
||||
tag_one: tag_one
|
||||
tag_two: tag_two
|
||||
register: tag_kms
|
||||
|
||||
- name: assert tags added and grants remain in place
|
||||
assert:
|
||||
that:
|
||||
- "tag_kms.changed"
|
||||
- "tag_kms.grants|length == 1"
|
||||
- "'tag_one' in tag_kms.tags"
|
||||
- "'tag_two' in tag_kms.tags"
|
||||
|
||||
- name: add, replace, remove tags
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
purge_tags: yes
|
||||
tags:
|
||||
tag_two: tag_two_updated
|
||||
tag_three: tag_three
|
||||
register: tag_kms_update
|
||||
|
||||
- name: assert tags correctly changed
|
||||
assert:
|
||||
that:
|
||||
- "tag_kms_update.changed"
|
||||
- "'tag_one' not in tag_kms_update.tags"
|
||||
- "'tag_two' in tag_kms_update.tags"
|
||||
- "tag_kms_update.tags.tag_two == 'tag_two_updated'"
|
||||
- "'tag_three' in tag_kms_update.tags"
|
||||
|
||||
- name: make no real tag change
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
register: tag_kms_no_update
|
||||
|
||||
- name: assert no change to tags
|
||||
assert:
|
||||
that:
|
||||
- "not tag_kms_no_update.changed"
|
||||
- "'tag_one' not in tag_kms_no_update.tags"
|
||||
- "'tag_two' in tag_kms_no_update.tags"
|
||||
- "tag_kms_no_update.tags.tag_two == 'tag_two_updated'"
|
||||
- "'tag_three' in tag_kms_no_update.tags"
|
||||
|
||||
- name: update the key's description and disable it
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
description: test key for testing
|
||||
enabled: no
|
||||
register: update_key
|
||||
|
||||
- name: assert that state is enabled
|
||||
assert:
|
||||
that:
|
||||
- update_key.description == "test key for testing"
|
||||
- update_key.key_state == "Disabled"
|
||||
- update_key.changed
|
||||
|
||||
- name: delete the key
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: absent
|
||||
register: delete_kms
|
||||
|
||||
- name: assert that state is pending deletion
|
||||
assert:
|
||||
that:
|
||||
- delete_kms.key_state == "PendingDeletion"
|
||||
- delete_kms.changed
|
||||
|
||||
- name: undelete and enable the key
|
||||
aws_kms:
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
state: present
|
||||
enabled: yes
|
||||
register: undelete_kms
|
||||
|
||||
- name: assert that state is enabled
|
||||
assert:
|
||||
that:
|
||||
- undelete_kms.key_state == "Enabled"
|
||||
- undelete_kms.changed
|
||||
|
||||
always:
|
||||
|
||||
# ============================================================
|
||||
- name: finish off by deleting key
|
||||
aws_kms:
|
||||
state: absent
|
||||
region: "{{ aws_region }}"
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
alias: "{{ resource_prefix }}-kms"
|
||||
register: destroy_result
|
||||
|
||||
- name: remove the IAM role
|
||||
iam_role:
|
||||
name: "{{ resource_prefix }}-kms-role"
|
||||
state: absent
|
||||
aws_access_key: "{{ aws_access_key }}"
|
||||
aws_secret_key: "{{ aws_secret_key }}"
|
||||
security_token: "{{ security_token }}"
|
||||
register: iam_role_result
|
Loading…
Reference in a new issue