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