1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3import functools
4import re
5from botocore.exceptions import ClientError
6
7from concurrent.futures import as_completed
8from contextlib import suppress
9
10from c7n.actions import ActionRegistry, BaseAction
11from c7n.exceptions import PolicyValidationError
12from c7n.filters import (
13 FilterRegistry, ValueFilter, MetricsFilter, WafV2FilterBase,
14 WafClassicRegionalFilterBase)
15from c7n.filters.iamaccess import CrossAccountAccessFilter
16from c7n.filters.related import RelatedResourceFilter
17from c7n.manager import resources, ResourceManager
18from c7n import query, utils
19from c7n.utils import generate_arn, type_schema, get_retry, jmespath_search, get_partition
20
21
22ANNOTATION_KEY_MATCHED_METHODS = 'c7n:matched-resource-methods'
23ANNOTATION_KEY_MATCHED_INTEGRATIONS = 'c7n:matched-method-integrations'
24
25
26@resources.register('rest-account')
27class RestAccount(ResourceManager):
28 # note this is not using a regular resource manager or type info
29 # its a pseudo resource, like an aws account
30
31 filter_registry = FilterRegistry('rest-account.filters')
32 action_registry = ActionRegistry('rest-account.actions')
33
34 class resource_type(query.TypeInfo):
35 service = 'apigateway'
36 name = id = 'account_id'
37 dimension = None
38 arn = False
39
40 @classmethod
41 def get_permissions(cls):
42 # this resource is not query manager based as its a pseudo
43 # resource. in that it always exists, it represents the
44 # service's account settings.
45 return ('apigateway:GET',)
46
47 @classmethod
48 def has_arn(self):
49 return False
50
51 def get_model(self):
52 return self.resource_type
53
54 def _get_account(self):
55 client = utils.local_session(self.session_factory).client('apigateway')
56 try:
57 account = client.get_account()
58 except ClientError as e:
59 if e.response['Error']['Code'] == 'NotFoundException':
60 return []
61 account.pop('ResponseMetadata', None)
62 account['account_id'] = 'apigw-settings'
63 return [account]
64
65 def resources(self):
66 return self.filter_resources(self._get_account())
67
68 def get_resources(self, resource_ids):
69 return self._get_account()
70
71
72OP_SCHEMA = {
73 'type': 'object',
74 'required': ['op', 'path'],
75 'additonalProperties': False,
76 'properties': {
77 'op': {'enum': ['add', 'remove', 'update', 'copy', 'replace', 'test']},
78 'path': {'type': 'string'},
79 'value': {'type': 'string'},
80 'from': {'type': 'string'}
81 }
82}
83
84
85@RestAccount.action_registry.register('update')
86class UpdateAccount(BaseAction):
87 """Update the cloudwatch role associated to a rest account
88
89 :example:
90
91 .. code-block:: yaml
92
93 policies:
94 - name: correct-rest-account-log-role
95 resource: rest-account
96 filters:
97 - cloudwatchRoleArn: arn:aws:iam::000000000000:role/GatewayLogger
98 actions:
99 - type: update
100 patch:
101 - op: replace
102 path: /cloudwatchRoleArn
103 value: arn:aws:iam::000000000000:role/BetterGatewayLogger
104 """
105
106 permissions = ('apigateway:PATCH',)
107 schema = utils.type_schema(
108 'update',
109 patch={'type': 'array', 'items': OP_SCHEMA},
110 required=['patch'])
111
112 def process(self, resources):
113 client = utils.local_session(
114 self.manager.session_factory).client('apigateway')
115 client.update_account(patchOperations=self.data['patch'])
116
117
118class ApiDescribeSource(query.DescribeSource):
119
120 def augment(self, resources):
121 for r in resources:
122 tags = r.setdefault('Tags', [])
123 for k, v in r.pop('tags', {}).items():
124 tags.append({
125 'Key': k,
126 'Value': v})
127 return resources
128
129
130@resources.register('rest-api')
131class RestApi(query.QueryResourceManager):
132
133 class resource_type(query.TypeInfo):
134 service = 'apigateway'
135 arn_type = '/restapis'
136 enum_spec = ('get_rest_apis', 'items', None)
137 id = 'id'
138 name = 'name'
139 date = 'createdDate'
140 dimension = 'GatewayName'
141 cfn_type = config_type = "AWS::ApiGateway::RestApi"
142 universal_taggable = object()
143 permissions_enum = ('apigateway:GET',)
144
145 source_mapping = {
146 'config': query.ConfigSource,
147 'describe': ApiDescribeSource
148 }
149
150 @property
151 def generate_arn(self):
152 """
153 Sample arn: arn:aws:apigateway:us-east-1::/restapis/rest-api-id
154 This method overrides c7n.utils.generate_arn and drops
155 account id from the generic arn.
156 """
157 if self._generate_arn is None:
158 self._generate_arn = functools.partial(
159 generate_arn,
160 self.resource_type.service,
161 region=self.config.region,
162 resource_type=self.resource_type.arn_type)
163 return self._generate_arn
164
165
166@RestApi.filter_registry.register('metrics')
167class Metrics(MetricsFilter):
168
169 def get_dimensions(self, resource):
170 return [{'Name': 'ApiName',
171 'Value': resource['name']}]
172
173
174@RestApi.filter_registry.register('cross-account')
175class RestApiCrossAccount(CrossAccountAccessFilter):
176
177 policy_attribute = 'policy'
178 permissions = ('apigateway:GET',)
179
180 def get_resource_policy(self, r):
181 policy = super().get_resource_policy(r)
182 if policy:
183 policy = policy.replace('\\', '')
184 else:
185 # api gateway default iam policy is public
186 # authorizers and app code may mitigate but
187 # the iam policy intent here is clear.
188 policy = {'Statement': [{
189 'Action': 'execute-api:Invoke',
190 'Effect': 'Allow',
191 'Principal': '*'}]}
192 return policy
193
194
195@RestApi.action_registry.register('update')
196class UpdateApi(BaseAction):
197 """Update configuration of a REST API.
198
199 Non-exhaustive list of updateable attributes.
200 https://docs.aws.amazon.com/apigateway/api-reference/link-relation/restapi-update/#remarks
201
202 :example:
203
204 contrived example to update description on api gateways
205
206 .. code-block:: yaml
207
208 policies:
209 - name: apigw-description
210 resource: rest-api
211 filters:
212 - description: empty
213 actions:
214 - type: update
215 patch:
216 - op: replace
217 path: /description
218 value: "not empty :-)"
219 """
220 permissions = ('apigateway:PATCH',)
221 schema = utils.type_schema(
222 'update',
223 patch={'type': 'array', 'items': OP_SCHEMA},
224 required=['patch'])
225
226 def process(self, resources):
227 client = utils.local_session(
228 self.manager.session_factory).client('apigateway')
229 for r in resources:
230 client.update_rest_api(
231 restApiId=r['id'],
232 patchOperations=self.data['patch'])
233
234
235@RestApi.action_registry.register('delete')
236class DeleteApi(BaseAction):
237 """Delete a REST API.
238
239 :example:
240
241 contrived example to delete rest api
242
243 .. code-block:: yaml
244
245 policies:
246 - name: apigw-delete
247 resource: rest-api
248 filters:
249 - description: empty
250 actions:
251 - type: delete
252 """
253 permissions = ('apigateway:DELETE',)
254 schema = type_schema('delete')
255
256 def process(self, resources):
257 client = utils.local_session(
258 self.manager.session_factory).client('apigateway')
259 retry = get_retry(('TooManyRequestsException',))
260
261 for r in resources:
262 try:
263 retry(client.delete_rest_api, restApiId=r['id'])
264 except client.exceptions.NotFoundException:
265 continue
266
267
268@query.sources.register('describe-rest-stage')
269class DescribeRestStage(query.ChildDescribeSource):
270
271 def __init__(self, manager):
272 self.manager = manager
273 self.query = query.ChildResourceQuery(
274 self.manager.session_factory, self.manager, capture_parent_id=True)
275
276 def get_query(self):
277 return super(DescribeRestStage, self).get_query(capture_parent_id=True)
278
279 def augment(self, resources):
280 results = []
281 rest_apis = self.manager.get_resource_manager(
282 'rest-api').resources()
283 # Using capture parent, changes the protocol
284 for parent_id, r in resources:
285 r['restApiId'] = parent_id
286 for rest_api in rest_apis:
287 if rest_api['id'] == parent_id:
288 r['restApiType'] = rest_api['endpointConfiguration']['types']
289 r['stageArn'] = "arn:aws:{service}:{region}::" \
290 "/restapis/{rest_api_id}/stages/" \
291 "{stage_name}".format(
292 service="apigateway",
293 region=self.manager.config.region,
294 rest_api_id=parent_id,
295 stage_name=r['stageName'])
296 tags = r.setdefault('Tags', [])
297 for k, v in r.pop('tags', {}).items():
298 tags.append({
299 'Key': k,
300 'Value': v})
301 results.append(r)
302 return results
303
304 def get_resources(self, ids, cache=True):
305 deployment_ids = []
306 client = utils.local_session(
307 self.manager.session_factory).client('apigateway')
308 for id in ids:
309 # if we get stage arn, we pick rest_api_id and stageName to get deploymentId
310 if id.startswith('arn:aws:apigateway'):
311 _, ident = id.rsplit(':', 1)
312 parts = ident.split('/', 4)
313 # if we get stage name in arn, use stage_name to get stage information
314 # from stage information, pick deploymentId
315 if len(parts) > 3:
316 response = self.manager.retry(
317 client.get_stage,
318 restApiId=parts[2],
319 stageName=parts[4])
320 deployment_ids.append(response[self.manager.resource_type.id])
321 else:
322 deployment_ids.append(id)
323 return super(DescribeRestStage, self).get_resources(deployment_ids, cache)
324
325
326@resources.register('rest-stage')
327class RestStage(query.ChildResourceManager):
328
329 class resource_type(query.TypeInfo):
330 service = 'apigateway'
331 parent_spec = ('rest-api', 'restApiId', None)
332 enum_spec = ('get_stages', 'item', None)
333 name = 'stageName'
334 id = 'deploymentId'
335 config_id = 'stageArn'
336 date = 'createdDate'
337 universal_taggable = True
338 cfn_type = config_type = "AWS::ApiGateway::Stage"
339 arn_type = 'stages'
340 permissions_enum = ('apigateway:GET',)
341 supports_trailevents = True
342
343 child_source = 'describe'
344 source_mapping = {
345 'describe': DescribeRestStage,
346 'config': query.ConfigSource
347 }
348
349 @property
350 def generate_arn(self):
351 self._generate_arn = functools.partial(
352 generate_arn,
353 self.resource_type.service,
354 region=self.config.region)
355 return self._generate_arn
356
357 def get_arns(self, resources):
358 arns = []
359 for r in resources:
360 arns.append(self.generate_arn('/restapis/' + r['restApiId'] +
361 '/stages/' + r[self.get_model().name]))
362 return arns
363
364
365@RestStage.action_registry.register('update')
366class UpdateStage(BaseAction):
367 """Update/remove values of an api stage
368
369 :example:
370
371 .. code-block:: yaml
372
373 policies:
374 - name: disable-stage-caching
375 resource: rest-stage
376 filters:
377 - methodSettings."*/*".cachingEnabled: true
378 actions:
379 - type: update
380 patch:
381 - op: replace
382 path: /*/*/caching/enabled
383 value: 'false'
384 """
385
386 permissions = ('apigateway:PATCH',)
387 schema = utils.type_schema(
388 'update',
389 patch={'type': 'array', 'items': OP_SCHEMA},
390 required=['patch'])
391
392 def process(self, resources):
393 client = utils.local_session(
394 self.manager.session_factory).client('apigateway')
395 for r in resources:
396 self.manager.retry(
397 client.update_stage,
398 restApiId=r['restApiId'],
399 stageName=r['stageName'],
400 patchOperations=self.data['patch'])
401
402
403@RestStage.action_registry.register('delete')
404class DeleteStage(BaseAction):
405 """Delete an api stage
406
407 :example:
408
409 .. code-block:: yaml
410
411 policies:
412 - name: delete-rest-stage
413 resource: rest-stage
414 filters:
415 - methodSettings."*/*".cachingEnabled: true
416 actions:
417 - type: delete
418 """
419 permissions = ('apigateway:DELETE',)
420 schema = utils.type_schema('delete')
421
422 def process(self, resources):
423 client = utils.local_session(self.manager.session_factory).client('apigateway')
424 for r in resources:
425 try:
426 self.manager.retry(
427 client.delete_stage,
428 restApiId=r['restApiId'],
429 stageName=r['stageName'])
430 except client.exceptions.NotFoundException:
431 pass
432
433
434@resources.register('rest-resource')
435class RestResource(query.ChildResourceManager):
436
437 child_source = 'describe-rest-resource'
438
439 class resource_type(query.TypeInfo):
440 service = 'apigateway'
441 parent_spec = ('rest-api', 'restApiId', None)
442 enum_spec = ('get_resources', 'items', None)
443 id = 'id'
444 name = 'path'
445 permissions_enum = ('apigateway:GET',)
446 cfn_type = 'AWS::ApiGateway::Resource'
447
448
449@query.sources.register('describe-rest-resource')
450class DescribeRestResource(query.ChildDescribeSource):
451
452 def get_query(self):
453 return super(DescribeRestResource, self).get_query(capture_parent_id=True)
454
455 def augment(self, resources):
456 results = []
457 # Using capture parent id, changes the protocol
458 for parent_id, r in resources:
459 r['restApiId'] = parent_id
460 results.append(r)
461 return results
462
463
464@resources.register('rest-vpclink')
465class RestApiVpcLink(query.QueryResourceManager):
466
467 class resource_type(query.TypeInfo):
468 service = 'apigateway'
469 enum_spec = ('get_vpc_links', 'items', None)
470 id = 'id'
471 name = 'name'
472 permissions_enum = ('apigateway:GET',)
473 cfn_type = 'AWS::ApiGateway::VpcLink'
474
475
476@resources.register('rest-client-certificate')
477class RestClientCertificate(query.QueryResourceManager):
478 """TLS client certificates generated by API Gateway
479
480 :example:
481
482 .. code-block:: yaml
483
484 policies:
485 - name: old-client-certificates
486 resource: rest-client-certificate
487 filters:
488 - key: createdDate
489 value_type: age
490 value: 90
491 op: greater-than
492 """
493 class resource_type(query.TypeInfo):
494 service = 'apigateway'
495 enum_spec = ('get_client_certificates', 'items', None)
496 id = 'clientCertificateId'
497 name = 'client_certificate_id'
498 permissions_enum = ('apigateway:GET',)
499 cfn_type = 'AWS::ApiGateway::ClientCertificate'
500
501
502@RestStage.filter_registry.register('client-certificate')
503class StageClientCertificateFilter(RelatedResourceFilter):
504 """Filter API stages by a client certificate
505
506 :example:
507
508 .. code-block:: yaml
509
510 policies:
511 - name: rest-stages-old-certificate
512 resource: rest-stage
513 filters:
514 - type: client-certificate
515 key: createdDate
516 value_type: age
517 value: 90
518 op: greater-than
519 """
520 schema = type_schema('client-certificate', rinherit=ValueFilter.schema)
521 RelatedResource = "c7n.resources.apigw.RestClientCertificate"
522 RelatedIdsExpression = 'clientCertificateId'
523 annotation_key = "c7n:matched-client-certificate"
524
525 def process(self, resources, event=None):
526 related = self.get_related(resources)
527 matched = []
528 for r in resources:
529 if self.process_resource(r, related):
530 # Add the full certificate details rather than just the ID
531 self.augment(related, r)
532 matched.append(r)
533 return matched
534
535 def augment(self, related, resource):
536 rid = resource[self.RelatedIdsExpression]
537 with suppress(KeyError):
538 resource[self.annotation_key] = {
539 self.data['key']: jmespath_search(self.data['key'], related[rid])
540 }
541
542
543@RestStage.filter_registry.register('waf-enabled')
544class WafEnabled(WafClassicRegionalFilterBase):
545 """Filter API Gateway stage by waf-regional web-acl
546
547 :example:
548
549 .. code-block:: yaml
550
551 policies:
552 - name: filter-apigw-waf-regional
553 resource: rest-stage
554 filters:
555 - type: waf-enabled
556 state: false
557 web-acl: test
558 """
559
560 def get_associated_web_acl(self, resource):
561 return self.get_web_acl_by_arn(resource.get('webAclArn'))
562
563
564@RestStage.action_registry.register('set-waf')
565class SetWaf(BaseAction):
566 """Enable waf protection on API Gateway stage.
567
568 :example:
569
570 .. code-block:: yaml
571
572 policies:
573 - name: set-waf-for-stage
574 resource: rest-stage
575 filters:
576 - type: waf-enabled
577 state: false
578 web-acl: test
579 actions:
580 - type: set-waf
581 state: true
582 web-acl: test
583
584 - name: disassociate-wafv2-associate-waf-regional-apigw
585 resource: rest-stage
586 filters:
587 - type: wafv2-enabled
588 state: true
589 actions:
590 - type: set-waf
591 state: true
592 web-acl: test
593
594 """
595 permissions = ('waf-regional:AssociateWebACL', 'waf-regional:ListWebACLs')
596
597 schema = type_schema(
598 'set-waf', required=['web-acl'], **{
599 'web-acl': {'type': 'string'},
600 # 'force': {'type': 'boolean'},
601 'state': {'type': 'boolean'}})
602
603 def validate(self):
604 found = False
605 for f in self.manager.iter_filters():
606 if isinstance(f, WafEnabled) or isinstance(f, WafV2Enabled):
607 found = True
608 break
609 if not found:
610 # try to ensure idempotent usage
611 raise PolicyValidationError(
612 "set-waf should be used in conjunction with waf-enabled or wafv2-enabled \
613 filter on %s" % (self.manager.data,))
614 return self
615
616 def process(self, resources):
617 wafs = self.manager.get_resource_manager('waf-regional').resources(augment=False)
618 name_id_map = {w['Name']: w['WebACLId'] for w in wafs}
619 target_acl = self.data.get('web-acl', '')
620 target_acl_id = name_id_map.get(target_acl, target_acl)
621 state = self.data.get('state', True)
622 if state and target_acl_id not in name_id_map.values():
623 raise ValueError("invalid web acl: %s" % (target_acl))
624
625 client = utils.local_session(
626 self.manager.session_factory).client('waf-regional')
627
628 for r in resources:
629 r_arn = self.manager.get_arns([r])[0]
630 if state:
631 client.associate_web_acl(WebACLId=target_acl_id, ResourceArn=r_arn)
632 else:
633 client.disassociate_web_acl(WebACLId=target_acl_id, ResourceArn=r_arn)
634
635
636@RestStage.filter_registry.register('wafv2-enabled')
637class WafV2Enabled(WafV2FilterBase):
638 """Filter API Gateway stage by wafv2 web-acl
639
640 :example:
641
642 .. code-block:: yaml
643
644 policies:
645 - name: filter-wafv2-apigw
646 resource: rest-stage
647 filters:
648 - type: wafv2-enabled
649 state: false
650 web-acl: testv2
651 """
652
653 def get_associated_web_acl(self, resource):
654 return self.get_web_acl_by_arn(resource.get('webAclArn'))
655
656
657@RestStage.action_registry.register('set-wafv2')
658class SetWafv2(BaseAction):
659 """Enable wafv2 protection on API Gateway stage.
660
661 :example:
662
663 .. code-block:: yaml
664
665 policies:
666 - name: set-wafv2-for-stage
667 resource: rest-stage
668 filters:
669 - type: wafv2-enabled
670 state: false
671 web-acl: testv2
672 actions:
673 - type: set-wafv2
674 state: true
675 web-acl: testv2
676
677 - name: disassociate-waf-regional-associate-wafv2-apigw
678 resource: rest-stage
679 filters:
680 - type: waf-enabled
681 state: true
682 actions:
683 - type: set-wafv2
684 state: true
685 web-acl: testv2
686
687 """
688 permissions = ('wafv2:AssociateWebACL', 'wafv2:ListWebACLs')
689
690 schema = type_schema(
691 'set-wafv2', **{
692 'web-acl': {'type': 'string'},
693 'state': {'type': 'boolean'}})
694
695 retry = staticmethod(get_retry((
696 'ThrottlingException',
697 'RequestLimitExceeded',
698 'Throttled',
699 'ThrottledException',
700 'Throttling',
701 'Client.RequestLimitExceeded')))
702
703 def validate(self):
704 found = False
705 for f in self.manager.iter_filters():
706 if isinstance(f, WafV2Enabled) or isinstance(f, WafEnabled):
707 found = True
708 break
709 if not found:
710 # try to ensure idempotent usage
711 raise PolicyValidationError(
712 "set-wafv2 should be used in conjunction with wafv2-enabled or waf-enabled \
713 filter on %s" % (self.manager.data,))
714 if self.data.get('state'):
715 if 'web-acl' not in self.data:
716 raise PolicyValidationError((
717 "set-wafv2 filter parameter state is true, "
718 "requires `web-acl` on %s" % (self.manager.data,)))
719
720 return self
721
722 def process(self, resources):
723 wafs = self.manager.get_resource_manager('wafv2').resources(augment=False)
724 name_id_map = {w['Name']: w['ARN'] for w in wafs}
725 state = self.data.get('state', True)
726 target_acl_arn = ''
727
728 if state:
729 target_acl = self.data.get('web-acl', '')
730 target_acl_ids = [v for k, v in name_id_map.items() if
731 re.match(target_acl, k)]
732 if len(target_acl_ids) != 1:
733 raise ValueError(f'{target_acl} matching to none or the '
734 f'multiple web-acls')
735 target_acl_arn = target_acl_ids[0]
736
737 if state and target_acl_arn not in name_id_map.values():
738 raise ValueError("invalid web acl: %s" % target_acl_arn)
739
740 client = utils.local_session(self.manager.session_factory).client('wafv2')
741
742 for r in resources:
743 r_arn = self.manager.get_arns([r])[0]
744 if state:
745 self.retry(client.associate_web_acl,
746 WebACLArn=target_acl_arn,
747 ResourceArn=r_arn)
748 else:
749 self.retry(client.disassociate_web_acl,
750 ResourceArn=r_arn)
751
752
753@RestResource.filter_registry.register('rest-integration')
754class FilterRestIntegration(ValueFilter):
755 """Filter rest resources based on a key value for the rest method integration of the api
756
757 :example:
758
759 .. code-block:: yaml
760
761 policies:
762 - name: api-method-integrations-with-type-aws
763 resource: rest-resource
764 filters:
765 - type: rest-integration
766 key: type
767 value: AWS
768 """
769
770 schema = utils.type_schema(
771 'rest-integration',
772 method={'type': 'string', 'enum': [
773 'all', 'ANY', 'PUT', 'GET', "POST",
774 "DELETE", "OPTIONS", "HEAD", "PATCH"]},
775 rinherit=ValueFilter.schema)
776 schema_alias = False
777 permissions = ('apigateway:GET',)
778
779 def process(self, resources, event=None):
780 method_set = self.data.get('method', 'all')
781 # 10 req/s with burst to 40
782 client = utils.local_session(
783 self.manager.session_factory).client('apigateway')
784
785 # uniqueness constraint validity across apis?
786 resource_map = {r['id']: r for r in resources}
787
788 futures = {}
789 results = set()
790
791 with self.executor_factory(max_workers=2) as w:
792 tasks = []
793 for r in resources:
794 r_method_set = method_set
795 if method_set == 'all':
796 r_method_set = r.get('resourceMethods', {}).keys()
797 for m in r_method_set:
798 tasks.append((r, m))
799 for task_set in utils.chunks(tasks, 20):
800 futures[w.submit(
801 self.process_task_set, client, task_set)] = task_set
802
803 for f in as_completed(futures):
804 task_set = futures[f]
805
806 if f.exception():
807 self.manager.log.warning(
808 "Error retrieving integrations on resources %s",
809 ["%s:%s" % (r['restApiId'], r['path'])
810 for r, mt in task_set])
811 continue
812
813 for i in f.result():
814 if self.match(i):
815 results.add(i['resourceId'])
816 resource_map[i['resourceId']].setdefault(
817 ANNOTATION_KEY_MATCHED_INTEGRATIONS, []).append(i)
818
819 return [resource_map[rid] for rid in results]
820
821 def process_task_set(self, client, task_set):
822 results = []
823 for r, m in task_set:
824 try:
825 integration = client.get_integration(
826 restApiId=r['restApiId'],
827 resourceId=r['id'],
828 httpMethod=m)
829 integration.pop('ResponseMetadata', None)
830 integration['restApiId'] = r['restApiId']
831 integration['resourceId'] = r['id']
832 integration['resourceHttpMethod'] = m
833 results.append(integration)
834 except ClientError as e:
835 if e.response['Error']['Code'] == 'NotFoundException':
836 pass
837
838 return results
839
840
841@RestResource.action_registry.register('update-integration')
842class UpdateRestIntegration(BaseAction):
843 """Change or remove api integration properties based on key value
844
845 :example:
846
847 .. code-block:: yaml
848
849 policies:
850 - name: enforce-timeout-on-api-integration
851 resource: rest-resource
852 filters:
853 - type: rest-integration
854 key: timeoutInMillis
855 value: 29000
856 actions:
857 - type: update-integration
858 patch:
859 - op: replace
860 path: /timeoutInMillis
861 value: "3000"
862 """
863
864 schema = utils.type_schema(
865 'update-integration',
866 patch={'type': 'array', 'items': OP_SCHEMA},
867 required=['patch'])
868 permissions = ('apigateway:PATCH',)
869
870 def validate(self):
871 found = False
872 for f in self.manager.iter_filters():
873 if isinstance(f, FilterRestIntegration):
874 found = True
875 break
876 if not found:
877 raise ValueError(
878 ("update-integration action requires ",
879 "rest-integration filter usage in policy"))
880 return self
881
882 def process(self, resources):
883 client = utils.local_session(
884 self.manager.session_factory).client('apigateway')
885 ops = self.data['patch']
886 for r in resources:
887 for i in r.get(ANNOTATION_KEY_MATCHED_INTEGRATIONS, []):
888 client.update_integration(
889 restApiId=i['restApiId'],
890 resourceId=i['resourceId'],
891 httpMethod=i['resourceHttpMethod'],
892 patchOperations=ops)
893
894
895@RestResource.action_registry.register('delete-integration')
896class DeleteRestIntegration(BaseAction):
897 """Delete an api integration. Useful if the integration type is a security risk.
898
899 :example:
900
901 .. code-block:: yaml
902
903 policies:
904 - name: enforce-no-resource-integration-with-type-aws
905 resource: rest-resource
906 filters:
907 - type: rest-integration
908 key: type
909 value: AWS
910 actions:
911 - type: delete-integration
912 """
913 permissions = ('apigateway:DELETE',)
914 schema = utils.type_schema('delete-integration')
915
916 def process(self, resources):
917 client = utils.local_session(self.manager.session_factory).client('apigateway')
918
919 for r in resources:
920 for i in r.get(ANNOTATION_KEY_MATCHED_INTEGRATIONS, []):
921 try:
922 client.delete_integration(
923 restApiId=i['restApiId'],
924 resourceId=i['resourceId'],
925 httpMethod=i['resourceHttpMethod'])
926 except client.exceptions.NotFoundException:
927 continue
928
929
930@RestResource.filter_registry.register('rest-method')
931class FilterRestMethod(ValueFilter):
932 """Filter rest resources based on a key value for the rest method of the api
933
934 :example:
935
936 .. code-block:: yaml
937
938 policies:
939 - name: api-without-key-required
940 resource: rest-resource
941 filters:
942 - type: rest-method
943 key: apiKeyRequired
944 value: false
945 """
946
947 schema = utils.type_schema(
948 'rest-method',
949 method={'type': 'string', 'enum': [
950 'all', 'ANY', 'PUT', 'GET', "POST",
951 "DELETE", "OPTIONS", "HEAD", "PATCH"]},
952 rinherit=ValueFilter.schema)
953 schema_alias = False
954 permissions = ('apigateway:GET',)
955
956 def process(self, resources, event=None):
957 method_set = self.data.get('method', 'all')
958 # 10 req/s with burst to 40
959 client = utils.local_session(
960 self.manager.session_factory).client('apigateway')
961
962 # uniqueness constraint validity across apis?
963 resource_map = {r['id']: r for r in resources}
964
965 futures = {}
966 results = set()
967
968 with self.executor_factory(max_workers=2) as w:
969 tasks = []
970 for r in resources:
971 r_method_set = method_set
972 if method_set == 'all':
973 r_method_set = r.get('resourceMethods', {}).keys()
974 for m in r_method_set:
975 tasks.append((r, m))
976 for task_set in utils.chunks(tasks, 20):
977 futures[w.submit(
978 self.process_task_set, client, task_set)] = task_set
979
980 for f in as_completed(futures):
981 task_set = futures[f]
982 if f.exception():
983 self.manager.log.warning(
984 "Error retrieving methods on resources %s",
985 ["%s:%s" % (r['restApiId'], r['path'])
986 for r, mt in task_set])
987 continue
988 for m in f.result():
989 if self.match(m):
990 results.add(m['resourceId'])
991 resource_map[m['resourceId']].setdefault(
992 ANNOTATION_KEY_MATCHED_METHODS, []).append(m)
993 return [resource_map[rid] for rid in results]
994
995 def process_task_set(self, client, task_set):
996 results = []
997 for r, m in task_set:
998 method = client.get_method(
999 restApiId=r['restApiId'],
1000 resourceId=r['id'],
1001 httpMethod=m)
1002 method.pop('ResponseMetadata', None)
1003 method['restApiId'] = r['restApiId']
1004 method['resourceId'] = r['id']
1005 results.append(method)
1006 return results
1007
1008
1009@RestResource.action_registry.register('update-method')
1010class UpdateRestMethod(BaseAction):
1011 """Change or remove api method behaviors based on key value
1012
1013 :example:
1014
1015 .. code-block:: yaml
1016
1017 policies:
1018 - name: enforce-iam-permissions-on-api
1019 resource: rest-resource
1020 filters:
1021 - type: rest-method
1022 key: authorizationType
1023 value: NONE
1024 op: eq
1025 actions:
1026 - type: update-method
1027 patch:
1028 - op: replace
1029 path: /authorizationType
1030 value: AWS_IAM
1031 """
1032
1033 schema = utils.type_schema(
1034 'update-method',
1035 patch={'type': 'array', 'items': OP_SCHEMA},
1036 required=['patch'])
1037 permissions = ('apigateway:GET',)
1038
1039 def validate(self):
1040 found = False
1041 for f in self.manager.iter_filters():
1042 if isinstance(f, FilterRestMethod):
1043 found = True
1044 break
1045 if not found:
1046 raise ValueError(
1047 ("update-method action requires ",
1048 "rest-method filter usage in policy"))
1049 return self
1050
1051 def process(self, resources):
1052 client = utils.local_session(
1053 self.manager.session_factory).client('apigateway')
1054 ops = self.data['patch']
1055 for r in resources:
1056 for m in r.get(ANNOTATION_KEY_MATCHED_METHODS, []):
1057 client.update_method(
1058 restApiId=m['restApiId'],
1059 resourceId=m['resourceId'],
1060 httpMethod=m['httpMethod'],
1061 patchOperations=ops)
1062
1063
1064@resources.register('apigw-domain-name')
1065class CustomDomainName(query.QueryResourceManager):
1066
1067 class resource_type(query.TypeInfo):
1068 enum_spec = ('get_domain_names', 'items', None)
1069 arn_type = '/domainnames'
1070 id = name = 'domainName'
1071 service = 'apigateway'
1072 universal_taggable = True
1073 cfn_type = 'AWS::ApiGateway::DomainName'
1074 date = 'createdDate'
1075
1076 @classmethod
1077 def get_permissions(cls):
1078 return ('apigateway:GET',)
1079
1080 @property
1081 def generate_arn(self):
1082 """
1083 Sample arn: arn:aws:apigateway:us-east-1::/restapis/rest-api-id
1084 This method overrides c7n.utils.generate_arn and drops
1085 account id from the generic arn.
1086 """
1087 if self._generate_arn is None:
1088 self._generate_arn = functools.partial(
1089 generate_arn,
1090 self.resource_type.service,
1091 region=self.config.region,
1092 resource_type=self.resource_type.arn_type)
1093 return self._generate_arn
1094
1095
1096@CustomDomainName.action_registry.register('update-security')
1097class DomainNameRemediateTls(BaseAction):
1098
1099 schema = type_schema(
1100 'update-security',
1101 securityPolicy={'type': 'string', 'enum': [
1102 'TLS_1_0', 'TLS_1_2']},
1103 required=['securityPolicy'])
1104
1105 permissions = ('apigateway:PATCH',)
1106
1107 def process(self, resources, event=None):
1108 client = utils.local_session(
1109 self.manager.session_factory).client('apigateway')
1110 retryable = ('TooManyRequestsException', 'ConflictException')
1111 retry = utils.get_retry(retryable, max_attempts=8)
1112
1113 for r in resources:
1114 try:
1115 retry(client.update_domain_name,
1116 domainName=r['domainName'],
1117 patchOperations=[
1118 {
1119 'op': 'replace',
1120 'path': '/securityPolicy',
1121 'value': self.data.get('securityPolicy')
1122 },
1123 ]
1124 )
1125 except ClientError as e:
1126 if e.response['Error']['Code'] in retryable:
1127 continue
1128
1129
1130class ApiGwV2DescribeSource(query.DescribeSource):
1131
1132 def augment(self, resources):
1133 # convert tags from {'Key': 'Value'} to standard aws format
1134 for r in resources:
1135 r['Tags'] = [
1136 {'Key': k, 'Value': v} for k, v in r.pop('Tags', {}).items()]
1137 return resources
1138
1139
1140@resources.register('apigwv2')
1141class ApiGwV2(query.QueryResourceManager):
1142
1143 class resource_type(query.TypeInfo):
1144 service = 'apigatewayv2'
1145 arn_type = '/apis'
1146 enum_spec = ('get_apis', 'Items', None)
1147 id = 'ApiId'
1148 name = 'name'
1149 date = 'createdDate'
1150 dimension = 'ApiId'
1151 cfn_type = config_type = "AWS::ApiGatewayV2::Api"
1152 permission_prefix = 'apigateway'
1153 permissions_enum = ('apigateway:GET',)
1154 universal_taggable = object()
1155
1156 source_mapping = {
1157 'config': query.ConfigSource,
1158 'describe': ApiGwV2DescribeSource
1159 }
1160
1161 @property
1162 def generate_arn(self):
1163 """
1164 Sample arn: arn:aws:apigateway:us-east-1::/apis/api-id
1165 This method overrides c7n.utils.generate_arn and drops
1166 account id from the generic arn.
1167 """
1168 if self._generate_arn is None:
1169 self._generate_arn = functools.partial(
1170 generate_arn,
1171 "apigateway",
1172 region=self.config.region,
1173 resource_type=self.resource_type.arn_type,
1174 )
1175
1176 return self._generate_arn
1177
1178
1179class StageDescribe(query.ChildDescribeSource):
1180
1181 def augment(self, resources):
1182 # convert tags from {'Key': 'Value'} to standard aws format
1183 for r in resources:
1184 r['Tags'] = [
1185 {'Key': k, 'Value': v} for k, v in r.pop('Tags', {}).items()]
1186 return resources
1187
1188
1189@resources.register("apigwv2-stage")
1190class ApiGatewayV2Stage(query.ChildResourceManager):
1191 class resource_type(query.TypeInfo):
1192 service = "apigatewayv2"
1193 enum_spec = ('get_stages', 'Items', None)
1194 parent_spec = ('aws.apigwv2', 'ApiId', True)
1195 arn_type = "/apis"
1196 id = name = "StageName"
1197 cfn_type = config_type = "AWS::ApiGatewayV2::Stage"
1198 universal_taggable = object()
1199 permission_prefix = 'apigateway'
1200 permissions_enum = ('apigateway:GET',)
1201
1202 source_mapping = {
1203 "describe-child": StageDescribe,
1204 "config": query.ConfigSource
1205 }
1206
1207 def get_arns(self, resources):
1208 partition = get_partition(self.config.region)
1209 return [
1210 "arn:{}:apigateway:{}::/apis/{}/stages/{}".format(
1211 partition, self.config.region, r['c7n:parent-id'], r['StageName']
1212 )
1213 for r in resources]