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