1#!/usr/local/bin/python3.8
2#
3# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
4
5from __future__ import (absolute_import, division, print_function)
6__metaclass__ = type
7
8
9DOCUMENTATION = '''
10---
11module: aws_kms_info
12version_added: 1.0.0
13short_description: Gather information about AWS KMS keys
14description:
15    - Gather information about AWS KMS keys including tags and grants
16    - This module was called C(aws_kms_facts) before Ansible 2.9. The usage did not change.
17author: "Will Thames (@willthames)"
18options:
19  alias:
20    description:
21      - Alias for key.
22      - Mutually exclusive with I(key_id) and I(filters).
23    required: false
24    aliases:
25      - key_alias
26    type: str
27    version_added: 1.4.0
28  key_id:
29    description:
30      - Key ID or ARN of the key.
31      - Mutually exclusive with I(alias) and I(filters).
32    required: false
33    aliases:
34      - key_arn
35    type: str
36    version_added: 1.4.0
37  filters:
38    description:
39      - A dict of filters to apply. Each dict item consists of a filter key and a filter value.
40        The filters aren't natively supported by boto3, but are supported to provide similar
41        functionality to other modules. Standard tag filters (C(tag-key), C(tag-value) and
42        C(tag:tagName)) are available, as are C(key-id) and C(alias)
43      - Mutually exclusive with I(alias) and I(key_id).
44    type: dict
45  pending_deletion:
46    description: Whether to get full details (tags, grants etc.) of keys pending deletion
47    default: False
48    type: bool
49extends_documentation_fragment:
50- amazon.aws.aws
51- amazon.aws.ec2
52
53'''
54
55EXAMPLES = '''
56# Note: These examples do not set authentication details, see the AWS Guide for details.
57
58# Gather information about all KMS keys
59- community.aws.aws_kms_info:
60
61# Gather information about all keys with a Name tag
62- community.aws.aws_kms_info:
63    filters:
64      tag-key: Name
65
66# Gather information about all keys with a specific name
67- community.aws.aws_kms_info:
68    filters:
69      "tag:Name": Example
70'''
71
72RETURN = '''
73keys:
74  description: list of keys
75  type: complex
76  returned: always
77  contains:
78    key_id:
79      description: ID of key
80      type: str
81      returned: always
82      sample: abcd1234-abcd-1234-5678-ef1234567890
83    key_arn:
84      description: ARN of key
85      type: str
86      returned: always
87      sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890
88    key_state:
89      description: The state of the key
90      type: str
91      returned: always
92      sample: PendingDeletion
93    key_usage:
94      description: The cryptographic operations for which you can use the key.
95      type: str
96      returned: always
97      sample: ENCRYPT_DECRYPT
98    origin:
99      description:
100        The source of the key's key material. When this value is C(AWS_KMS),
101        AWS KMS created the key material. When this value is C(EXTERNAL), the
102        key material was imported or the CMK lacks key material.
103      type: str
104      returned: always
105      sample: AWS_KMS
106    aws_account_id:
107      description: The AWS Account ID that the key belongs to
108      type: str
109      returned: always
110      sample: 1234567890123
111    creation_date:
112      description: Date of creation of the key
113      type: str
114      returned: always
115      sample: "2017-04-18T15:12:08.551000+10:00"
116    description:
117      description: Description of the key
118      type: str
119      returned: always
120      sample: "My Key for Protecting important stuff"
121    enabled:
122      description: Whether the key is enabled. True if C(KeyState) is true.
123      type: str
124      returned: always
125      sample: false
126    enable_key_rotation:
127      description: Whether the automatically key rotation every year is enabled. Returns None if key rotation status can't be determined.
128      type: bool
129      returned: always
130      sample: false
131    aliases:
132      description: list of aliases associated with the key
133      type: list
134      returned: always
135      sample:
136        - aws/acm
137        - aws/ebs
138    tags:
139      description: dictionary of tags applied to the key. Empty when access is denied even if there are tags.
140      type: dict
141      returned: always
142      sample:
143        Name: myKey
144        Purpose: protecting_stuff
145    policies:
146      description: list of policy documents for the keys. Empty when access is denied even if there are policies.
147      type: list
148      returned: always
149      sample:
150        Version: "2012-10-17"
151        Id: "auto-ebs-2"
152        Statement:
153        - Sid: "Allow access through EBS for all principals in the account that are authorized to use EBS"
154          Effect: "Allow"
155          Principal:
156            AWS: "*"
157          Action:
158          - "kms:Encrypt"
159          - "kms:Decrypt"
160          - "kms:ReEncrypt*"
161          - "kms:GenerateDataKey*"
162          - "kms:CreateGrant"
163          - "kms:DescribeKey"
164          Resource: "*"
165          Condition:
166            StringEquals:
167              kms:CallerAccount: "111111111111"
168              kms:ViaService: "ec2.ap-southeast-2.amazonaws.com"
169        - Sid: "Allow direct access to key metadata to the account"
170          Effect: "Allow"
171          Principal:
172            AWS: "arn:aws:iam::111111111111:root"
173          Action:
174          - "kms:Describe*"
175          - "kms:Get*"
176          - "kms:List*"
177          - "kms:RevokeGrant"
178          Resource: "*"
179    grants:
180      description: list of grants associated with a key
181      type: complex
182      returned: always
183      contains:
184        constraints:
185          description: Constraints on the encryption context that the grant allows.
186            See U(https://docs.aws.amazon.com/kms/latest/APIReference/API_GrantConstraints.html) for further details
187          type: dict
188          returned: always
189          sample:
190            encryption_context_equals:
191               "aws:lambda:_function_arn": "arn:aws:lambda:ap-southeast-2:012345678912:function:xyz"
192        creation_date:
193          description: Date of creation of the grant
194          type: str
195          returned: always
196          sample: "2017-04-18T15:12:08+10:00"
197        grant_id:
198          description: The unique ID for the grant
199          type: str
200          returned: always
201          sample: abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234abcd1234
202        grantee_principal:
203          description: The principal that receives the grant's permissions
204          type: str
205          returned: always
206          sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz
207        issuing_account:
208          description: The AWS account under which the grant was issued
209          type: str
210          returned: always
211          sample: arn:aws:iam::01234567890:root
212        key_id:
213          description: The key ARN to which the grant applies.
214          type: str
215          returned: always
216          sample: arn:aws:kms:ap-southeast-2:123456789012:key/abcd1234-abcd-1234-5678-ef1234567890
217        name:
218          description: The friendly name that identifies the grant
219          type: str
220          returned: always
221          sample: xyz
222        operations:
223          description: The list of operations permitted by the grant
224          type: list
225          returned: always
226          sample:
227            - Decrypt
228            - RetireGrant
229        retiring_principal:
230          description: The principal that can retire the grant
231          type: str
232          returned: always
233          sample: arn:aws:sts::0123456789012:assumed-role/lambda_xyz/xyz
234'''
235
236
237try:
238    import botocore
239except ImportError:
240    pass  # Handled by AnsibleAWSModule
241
242from ansible.module_utils.common.dict_transformations import camel_dict_to_snake_dict
243
244from ansible_collections.amazon.aws.plugins.module_utils.core import AnsibleAWSModule
245from ansible_collections.amazon.aws.plugins.module_utils.core import is_boto3_error_code
246from ansible_collections.amazon.aws.plugins.module_utils.ec2 import AWSRetry
247from ansible_collections.amazon.aws.plugins.module_utils.ec2 import boto3_tag_list_to_ansible_dict
248
249# Caching lookup for aliases
250_aliases = dict()
251
252
253@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
254def get_kms_keys_with_backoff(connection):
255    paginator = connection.get_paginator('list_keys')
256    return paginator.paginate().build_full_result()
257
258
259@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
260def get_kms_aliases_with_backoff(connection):
261    paginator = connection.get_paginator('list_aliases')
262    return paginator.paginate().build_full_result()
263
264
265def get_kms_aliases_lookup(connection):
266    if not _aliases:
267        for alias in get_kms_aliases_with_backoff(connection)['Aliases']:
268            # Not all aliases are actually associated with a key
269            if 'TargetKeyId' in alias:
270                # strip off leading 'alias/' and add it to key's aliases
271                if alias['TargetKeyId'] in _aliases:
272                    _aliases[alias['TargetKeyId']].append(alias['AliasName'][6:])
273                else:
274                    _aliases[alias['TargetKeyId']] = [alias['AliasName'][6:]]
275    return _aliases
276
277
278@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
279def get_kms_tags_with_backoff(connection, key_id, **kwargs):
280    return connection.list_resource_tags(KeyId=key_id, **kwargs)
281
282
283@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
284def get_kms_grants_with_backoff(connection, key_id, **kwargs):
285    params = dict(KeyId=key_id)
286    if kwargs.get('tokens'):
287        params['GrantTokens'] = kwargs['tokens']
288    paginator = connection.get_paginator('list_grants')
289    return paginator.paginate(**params).build_full_result()
290
291
292@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
293def get_kms_metadata_with_backoff(connection, key_id):
294    return connection.describe_key(KeyId=key_id)
295
296
297@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
298def list_key_policies_with_backoff(connection, key_id):
299    paginator = connection.get_paginator('list_key_policies')
300    return paginator.paginate(KeyId=key_id).build_full_result()
301
302
303@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
304def get_key_policy_with_backoff(connection, key_id, policy_name):
305    return connection.get_key_policy(KeyId=key_id, PolicyName=policy_name)
306
307
308@AWSRetry.backoff(tries=5, delay=5, backoff=2.0)
309def get_enable_key_rotation_with_backoff(connection, key_id):
310    try:
311        current_rotation_status = connection.get_key_rotation_status(KeyId=key_id)
312    except is_boto3_error_code(['AccessDeniedException', 'UnsupportedOperationException']) as e:
313        return None
314
315    return current_rotation_status.get('KeyRotationEnabled')
316
317
318def canonicalize_alias_name(alias):
319    if alias is None:
320        return None
321    if alias.startswith('alias/'):
322        return alias
323    return 'alias/' + alias
324
325
326def get_kms_tags(connection, module, key_id):
327    # Handle pagination here as list_resource_tags does not have
328    # a paginator
329    kwargs = {}
330    tags = []
331    more = True
332    while more:
333        try:
334            tag_response = get_kms_tags_with_backoff(connection, key_id, **kwargs)
335            tags.extend(tag_response['Tags'])
336        except is_boto3_error_code('AccessDeniedException'):
337            tag_response = {}
338        except botocore.exceptions.ClientError as e:  # pylint: disable=duplicate-except
339            module.fail_json_aws(e, msg="Failed to obtain key tags")
340        if tag_response.get('NextMarker'):
341            kwargs['Marker'] = tag_response['NextMarker']
342        else:
343            more = False
344    return tags
345
346
347def get_kms_policies(connection, module, key_id):
348    try:
349        policies = list_key_policies_with_backoff(connection, key_id)['PolicyNames']
350        return [get_key_policy_with_backoff(connection, key_id, policy)['Policy'] for
351                policy in policies]
352    except is_boto3_error_code('AccessDeniedException'):
353        return []
354    except botocore.exceptions.ClientError as e:  # pylint: disable=duplicate-except
355        module.fail_json_aws(e, msg="Failed to obtain key policies")
356
357
358def key_matches_filter(key, filtr):
359    if filtr[0] == 'key-id':
360        return filtr[1] == key['key_id']
361    if filtr[0] == 'tag-key':
362        return filtr[1] in key['tags']
363    if filtr[0] == 'tag-value':
364        return filtr[1] in key['tags'].values()
365    if filtr[0] == 'alias':
366        return filtr[1] in key['aliases']
367    if filtr[0].startswith('tag:'):
368        tag_key = filtr[0][4:]
369        if tag_key not in key['tags']:
370            return False
371        return key['tags'].get(tag_key) == filtr[1]
372
373
374def key_matches_filters(key, filters):
375    if not filters:
376        return True
377    else:
378        return all([key_matches_filter(key, filtr) for filtr in filters.items()])
379
380
381def get_key_details(connection, module, key_id, tokens=None):
382    if not tokens:
383        tokens = []
384    try:
385        result = get_kms_metadata_with_backoff(connection, key_id)['KeyMetadata']
386        # Make sure we have the canonical ARN, we might have been passed an alias
387        key_id = result['Arn']
388    except is_boto3_error_code('NotFoundException'):
389        return None
390    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:  # pylint: disable=duplicate-except
391        module.fail_json_aws(e, msg="Failed to obtain key metadata")
392    result['KeyArn'] = result.pop('Arn')
393
394    try:
395        aliases = get_kms_aliases_lookup(connection)
396    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
397        module.fail_json_aws(e, msg="Failed to obtain aliases")
398    # We can only get aliases for our own account, so we don't need the full ARN
399    result['aliases'] = aliases.get(result['KeyId'], [])
400    result['enable_key_rotation'] = get_enable_key_rotation_with_backoff(connection, key_id)
401
402    if module.params.get('pending_deletion'):
403        return camel_dict_to_snake_dict(result)
404
405    try:
406        result['grants'] = get_kms_grants_with_backoff(connection, key_id, tokens=tokens)['Grants']
407    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
408        module.fail_json_aws(e, msg="Failed to obtain key grants")
409    tags = get_kms_tags(connection, module, key_id)
410
411    result = camel_dict_to_snake_dict(result)
412    result['tags'] = boto3_tag_list_to_ansible_dict(tags, 'TagKey', 'TagValue')
413    result['policies'] = get_kms_policies(connection, module, key_id)
414    return result
415
416
417def get_kms_info(connection, module):
418    if module.params.get('key_id'):
419        key_id = module.params.get('key_id')
420        details = get_key_details(connection, module, key_id)
421        if details:
422            return [details]
423        return []
424    elif module.params.get('alias'):
425        alias = canonicalize_alias_name(module.params.get('alias'))
426        details = get_key_details(connection, module, alias)
427        if details:
428            return [details]
429        return []
430    else:
431        try:
432            keys = get_kms_keys_with_backoff(connection)['Keys']
433        except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
434            module.fail_json_aws(e, msg="Failed to obtain keys")
435        return [get_key_details(connection, module, key['KeyId']) for key in keys]
436
437
438def main():
439    argument_spec = dict(
440        alias=dict(aliases=['key_alias']),
441        key_id=dict(aliases=['key_arn']),
442        filters=dict(type='dict'),
443        pending_deletion=dict(type='bool', default=False),
444    )
445
446    module = AnsibleAWSModule(argument_spec=argument_spec,
447                              mutually_exclusive=[['alias', 'filters', 'key_id']],
448                              supports_check_mode=True)
449    if module._name == 'aws_kms_facts':
450        module.deprecate("The 'aws_kms_facts' module has been renamed to 'aws_kms_info'", date='2021-12-01', collection_name='community.aws')
451
452    try:
453        connection = module.client('kms')
454    except (botocore.exceptions.ClientError, botocore.exceptions.BotoCoreError) as e:
455        module.fail_json_aws(e, msg='Failed to connect to AWS')
456
457    all_keys = get_kms_info(connection, module)
458    module.exit_json(keys=[key for key in all_keys if key_matches_filters(key, module.params['filters'])])
459
460
461if __name__ == '__main__':
462    main()
463