Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.8/site-packages/c7n/resources/apigw.py: 47%
538 statements
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
« prev ^ index » next coverage.py v7.3.2, created at 2023-12-08 06:51 +0000
1# Copyright The Cloud Custodian Authors.
2# SPDX-License-Identifier: Apache-2.0
3import functools
4import re
5from botocore.exceptions import ClientError
7from concurrent.futures import as_completed
8from contextlib import suppress
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
22ANNOTATION_KEY_MATCHED_METHODS = 'c7n:matched-resource-methods'
23ANNOTATION_KEY_MATCHED_INTEGRATIONS = 'c7n:matched-method-integrations'
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
31 filter_registry = FilterRegistry('rest-account.filters')
32 action_registry = ActionRegistry('rest-account.actions')
34 class resource_type(query.TypeInfo):
35 service = 'apigateway'
36 name = id = 'account_id'
37 dimension = None
38 arn = False
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',)
47 @classmethod
48 def has_arn(self):
49 return False
51 def get_model(self):
52 return self.resource_type
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]
65 def resources(self):
66 return self.filter_resources(self._get_account())
68 def get_resources(self, resource_ids):
69 return self._get_account()
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}
85@RestAccount.action_registry.register('update')
86class UpdateAccount(BaseAction):
87 """Update the cloudwatch role associated to a rest account
89 :example:
91 .. code-block:: yaml
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 """
106 permissions = ('apigateway:PATCH',)
107 schema = utils.type_schema(
108 'update',
109 patch={'type': 'array', 'items': OP_SCHEMA},
110 required=['patch'])
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'])
118class ApiDescribeSource(query.DescribeSource):
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
130@resources.register('rest-api')
131class RestApi(query.QueryResourceManager):
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',)
145 source_mapping = {
146 'config': query.ConfigSource,
147 'describe': ApiDescribeSource
148 }
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
166@RestApi.filter_registry.register('metrics')
167class Metrics(MetricsFilter):
169 def get_dimensions(self, resource):
170 return [{'Name': 'ApiName',
171 'Value': resource['name']}]
174@RestApi.filter_registry.register('cross-account')
175class RestApiCrossAccount(CrossAccountAccessFilter):
177 policy_attribute = 'policy'
178 permissions = ('apigateway:GET',)
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
195@RestApi.action_registry.register('update')
196class UpdateApi(BaseAction):
197 """Update configuration of a REST API.
199 Non-exhaustive list of updateable attributes.
200 https://docs.aws.amazon.com/apigateway/api-reference/link-relation/restapi-update/#remarks
202 :example:
204 contrived example to update description on api gateways
206 .. code-block:: yaml
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'])
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'])
235@RestApi.action_registry.register('delete')
236class DeleteApi(BaseAction):
237 """Delete a REST API.
239 :example:
241 contrived example to delete rest api
243 .. code-block:: yaml
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')
256 def process(self, resources):
257 client = utils.local_session(
258 self.manager.session_factory).client('apigateway')
259 retry = get_retry(('TooManyRequestsException',))
261 for r in resources:
262 try:
263 retry(client.delete_rest_api, restApiId=r['id'])
264 except client.exceptions.NotFoundException:
265 continue
268@query.sources.register('describe-rest-stage')
269class DescribeRestStage(query.ChildDescribeSource):
271 def __init__(self, manager):
272 self.manager = manager
273 self.query = query.ChildResourceQuery(
274 self.manager.session_factory, self.manager)
275 self.query.capture_parent_id = True
277 def get_query(self):
278 query = super(DescribeRestStage, self).get_query()
279 query.capture_parent_id = True
280 return query
282 def augment(self, resources):
283 results = []
284 rest_apis = self.manager.get_resource_manager(
285 'rest-api').resources()
286 # Using capture parent, changes the protocol
287 for parent_id, r in resources:
288 r['restApiId'] = parent_id
289 for rest_api in rest_apis:
290 if rest_api['id'] == parent_id:
291 r['restApiType'] = rest_api['endpointConfiguration']['types']
292 r['stageArn'] = "arn:aws:{service}:{region}::" \
293 "/restapis/{rest_api_id}/stages/" \
294 "{stage_name}".format(
295 service="apigateway",
296 region=self.manager.config.region,
297 rest_api_id=parent_id,
298 stage_name=r['stageName'])
299 tags = r.setdefault('Tags', [])
300 for k, v in r.pop('tags', {}).items():
301 tags.append({
302 'Key': k,
303 'Value': v})
304 results.append(r)
305 return results
307 def get_resources(self, ids, cache=True):
308 deployment_ids = []
309 client = utils.local_session(
310 self.manager.session_factory).client('apigateway')
311 for id in ids:
312 # if we get stage arn, we pick rest_api_id and stageName to get deploymentId
313 if id.startswith('arn:aws:apigateway'):
314 _, ident = id.rsplit(':', 1)
315 parts = ident.split('/', 4)
316 # if we get stage name in arn, use stage_name to get stage information
317 # from stage information, pick deploymentId
318 if len(parts) > 3:
319 response = self.manager.retry(
320 client.get_stage,
321 restApiId=parts[2],
322 stageName=parts[4])
323 deployment_ids.append(response[self.manager.resource_type.id])
324 else:
325 deployment_ids.append(id)
326 return super(DescribeRestStage, self).get_resources(deployment_ids, cache)
329@resources.register('rest-stage')
330class RestStage(query.ChildResourceManager):
332 class resource_type(query.TypeInfo):
333 service = 'apigateway'
334 parent_spec = ('rest-api', 'restApiId', None)
335 enum_spec = ('get_stages', 'item', None)
336 name = 'stageName'
337 id = 'deploymentId'
338 config_id = 'stageArn'
339 date = 'createdDate'
340 universal_taggable = True
341 cfn_type = config_type = "AWS::ApiGateway::Stage"
342 arn_type = 'stages'
343 permissions_enum = ('apigateway:GET',)
344 supports_trailevents = True
346 child_source = 'describe'
347 source_mapping = {
348 'describe': DescribeRestStage,
349 'config': query.ConfigSource
350 }
352 @property
353 def generate_arn(self):
354 self._generate_arn = functools.partial(
355 generate_arn,
356 self.resource_type.service,
357 region=self.config.region)
358 return self._generate_arn
360 def get_arns(self, resources):
361 arns = []
362 for r in resources:
363 arns.append(self.generate_arn('/restapis/' + r['restApiId'] +
364 '/stages/' + r[self.get_model().name]))
365 return arns
368@RestStage.action_registry.register('update')
369class UpdateStage(BaseAction):
370 """Update/remove values of an api stage
372 :example:
374 .. code-block:: yaml
376 policies:
377 - name: disable-stage-caching
378 resource: rest-stage
379 filters:
380 - methodSettings."*/*".cachingEnabled: true
381 actions:
382 - type: update
383 patch:
384 - op: replace
385 path: /*/*/caching/enabled
386 value: 'false'
387 """
389 permissions = ('apigateway:PATCH',)
390 schema = utils.type_schema(
391 'update',
392 patch={'type': 'array', 'items': OP_SCHEMA},
393 required=['patch'])
395 def process(self, resources):
396 client = utils.local_session(
397 self.manager.session_factory).client('apigateway')
398 for r in resources:
399 self.manager.retry(
400 client.update_stage,
401 restApiId=r['restApiId'],
402 stageName=r['stageName'],
403 patchOperations=self.data['patch'])
406@RestStage.action_registry.register('delete')
407class DeleteStage(BaseAction):
408 """Delete an api stage
410 :example:
412 .. code-block:: yaml
414 policies:
415 - name: delete-rest-stage
416 resource: rest-stage
417 filters:
418 - methodSettings."*/*".cachingEnabled: true
419 actions:
420 - type: delete
421 """
422 permissions = ('apigateway:DELETE',)
423 schema = utils.type_schema('delete')
425 def process(self, resources):
426 client = utils.local_session(self.manager.session_factory).client('apigateway')
427 for r in resources:
428 try:
429 self.manager.retry(
430 client.delete_stage,
431 restApiId=r['restApiId'],
432 stageName=r['stageName'])
433 except client.exceptions.NotFoundException:
434 pass
437@resources.register('rest-resource')
438class RestResource(query.ChildResourceManager):
440 child_source = 'describe-rest-resource'
442 class resource_type(query.TypeInfo):
443 service = 'apigateway'
444 parent_spec = ('rest-api', 'restApiId', None)
445 enum_spec = ('get_resources', 'items', None)
446 id = 'id'
447 name = 'path'
448 permissions_enum = ('apigateway:GET',)
449 cfn_type = 'AWS::ApiGateway::Resource'
452@query.sources.register('describe-rest-resource')
453class DescribeRestResource(query.ChildDescribeSource):
455 def get_query(self):
456 query = super(DescribeRestResource, self).get_query()
457 query.capture_parent_id = True
458 return query
460 def augment(self, resources):
461 results = []
462 # Using capture parent id, changes the protocol
463 for parent_id, r in resources:
464 r['restApiId'] = parent_id
465 results.append(r)
466 return results
469@resources.register('rest-vpclink')
470class RestApiVpcLink(query.QueryResourceManager):
472 class resource_type(query.TypeInfo):
473 service = 'apigateway'
474 enum_spec = ('get_vpc_links', 'items', None)
475 id = 'id'
476 name = 'name'
477 permissions_enum = ('apigateway:GET',)
478 cfn_type = 'AWS::ApiGateway::VpcLink'
481@resources.register('rest-client-certificate')
482class RestClientCertificate(query.QueryResourceManager):
483 """TLS client certificates generated by API Gateway
485 :example:
487 .. code-block:: yaml
489 policies:
490 - name: old-client-certificates
491 resource: rest-client-certificate
492 filters:
493 - key: createdDate
494 value_type: age
495 value: 90
496 op: greater-than
497 """
498 class resource_type(query.TypeInfo):
499 service = 'apigateway'
500 enum_spec = ('get_client_certificates', 'items', None)
501 id = 'clientCertificateId'
502 name = 'client_certificate_id'
503 permissions_enum = ('apigateway:GET',)
504 cfn_type = 'AWS::ApiGateway::ClientCertificate'
507@RestStage.filter_registry.register('client-certificate')
508class StageClientCertificateFilter(RelatedResourceFilter):
509 """Filter API stages by a client certificate
511 :example:
513 .. code-block:: yaml
515 policies:
516 - name: rest-stages-old-certificate
517 resource: rest-stage
518 filters:
519 - type: client-certificate
520 key: createdDate
521 value_type: age
522 value: 90
523 op: greater-than
524 """
525 schema = type_schema('client-certificate', rinherit=ValueFilter.schema)
526 RelatedResource = "c7n.resources.apigw.RestClientCertificate"
527 RelatedIdsExpression = 'clientCertificateId'
528 annotation_key = "c7n:matched-client-certificate"
530 def process(self, resources, event=None):
531 related = self.get_related(resources)
532 matched = []
533 for r in resources:
534 if self.process_resource(r, related):
535 # Add the full certificate details rather than just the ID
536 self.augment(related, r)
537 matched.append(r)
538 return matched
540 def augment(self, related, resource):
541 rid = resource[self.RelatedIdsExpression]
542 with suppress(KeyError):
543 resource[self.annotation_key] = {
544 self.data['key']: jmespath_search(self.data['key'], related[rid])
545 }
548@RestStage.filter_registry.register('waf-enabled')
549class WafEnabled(WafClassicRegionalFilterBase):
550 """Filter API Gateway stage by waf-regional web-acl
552 :example:
554 .. code-block:: yaml
556 policies:
557 - name: filter-apigw-waf-regional
558 resource: rest-stage
559 filters:
560 - type: waf-enabled
561 state: false
562 web-acl: test
563 """
565 def get_associated_web_acl(self, resource):
566 return self.get_web_acl_by_arn(resource.get('webAclArn'))
569@RestStage.action_registry.register('set-waf')
570class SetWaf(BaseAction):
571 """Enable waf protection on API Gateway stage.
573 :example:
575 .. code-block:: yaml
577 policies:
578 - name: set-waf-for-stage
579 resource: rest-stage
580 filters:
581 - type: waf-enabled
582 state: false
583 web-acl: test
584 actions:
585 - type: set-waf
586 state: true
587 web-acl: test
589 - name: disassociate-wafv2-associate-waf-regional-apigw
590 resource: rest-stage
591 filters:
592 - type: wafv2-enabled
593 state: true
594 actions:
595 - type: set-waf
596 state: true
597 web-acl: test
599 """
600 permissions = ('waf-regional:AssociateWebACL', 'waf-regional:ListWebACLs')
602 schema = type_schema(
603 'set-waf', required=['web-acl'], **{
604 'web-acl': {'type': 'string'},
605 # 'force': {'type': 'boolean'},
606 'state': {'type': 'boolean'}})
608 def validate(self):
609 found = False
610 for f in self.manager.iter_filters():
611 if isinstance(f, WafEnabled) or isinstance(f, WafV2Enabled):
612 found = True
613 break
614 if not found:
615 # try to ensure idempotent usage
616 raise PolicyValidationError(
617 "set-waf should be used in conjunction with waf-enabled or wafv2-enabled \
618 filter on %s" % (self.manager.data,))
619 return self
621 def process(self, resources):
622 wafs = self.manager.get_resource_manager('waf-regional').resources(augment=False)
623 name_id_map = {w['Name']: w['WebACLId'] for w in wafs}
624 target_acl = self.data.get('web-acl', '')
625 target_acl_id = name_id_map.get(target_acl, target_acl)
626 state = self.data.get('state', True)
627 if state and target_acl_id not in name_id_map.values():
628 raise ValueError("invalid web acl: %s" % (target_acl))
630 client = utils.local_session(
631 self.manager.session_factory).client('waf-regional')
633 for r in resources:
634 r_arn = self.manager.get_arns([r])[0]
635 if state:
636 client.associate_web_acl(WebACLId=target_acl_id, ResourceArn=r_arn)
637 else:
638 client.disassociate_web_acl(WebACLId=target_acl_id, ResourceArn=r_arn)
641@RestStage.filter_registry.register('wafv2-enabled')
642class WafV2Enabled(WafV2FilterBase):
643 """Filter API Gateway stage by wafv2 web-acl
645 :example:
647 .. code-block:: yaml
649 policies:
650 - name: filter-wafv2-apigw
651 resource: rest-stage
652 filters:
653 - type: wafv2-enabled
654 state: false
655 web-acl: testv2
656 """
658 def get_associated_web_acl(self, resource):
659 return self.get_web_acl_by_arn(resource.get('webAclArn'))
662@RestStage.action_registry.register('set-wafv2')
663class SetWafv2(BaseAction):
664 """Enable wafv2 protection on API Gateway stage.
666 :example:
668 .. code-block:: yaml
670 policies:
671 - name: set-wafv2-for-stage
672 resource: rest-stage
673 filters:
674 - type: wafv2-enabled
675 state: false
676 web-acl: testv2
677 actions:
678 - type: set-wafv2
679 state: true
680 web-acl: testv2
682 - name: disassociate-waf-regional-associate-wafv2-apigw
683 resource: rest-stage
684 filters:
685 - type: waf-enabled
686 state: true
687 actions:
688 - type: set-wafv2
689 state: true
690 web-acl: testv2
692 """
693 permissions = ('wafv2:AssociateWebACL', 'wafv2:ListWebACLs')
695 schema = type_schema(
696 'set-wafv2', **{
697 'web-acl': {'type': 'string'},
698 'state': {'type': 'boolean'}})
700 retry = staticmethod(get_retry((
701 'ThrottlingException',
702 'RequestLimitExceeded',
703 'Throttled',
704 'ThrottledException',
705 'Throttling',
706 'Client.RequestLimitExceeded')))
708 def validate(self):
709 found = False
710 for f in self.manager.iter_filters():
711 if isinstance(f, WafV2Enabled) or isinstance(f, WafEnabled):
712 found = True
713 break
714 if not found:
715 # try to ensure idempotent usage
716 raise PolicyValidationError(
717 "set-wafv2 should be used in conjunction with wafv2-enabled or waf-enabled \
718 filter on %s" % (self.manager.data,))
719 if self.data.get('state'):
720 if 'web-acl' not in self.data:
721 raise PolicyValidationError((
722 "set-wafv2 filter parameter state is true, "
723 "requires `web-acl` on %s" % (self.manager.data,)))
725 return self
727 def process(self, resources):
728 wafs = self.manager.get_resource_manager('wafv2').resources(augment=False)
729 name_id_map = {w['Name']: w['ARN'] for w in wafs}
730 state = self.data.get('state', True)
731 target_acl_arn = ''
733 if state:
734 target_acl = self.data.get('web-acl', '')
735 target_acl_ids = [v for k, v in name_id_map.items() if
736 re.match(target_acl, k)]
737 if len(target_acl_ids) != 1:
738 raise ValueError(f'{target_acl} matching to none or the '
739 f'multiple web-acls')
740 target_acl_arn = target_acl_ids[0]
742 if state and target_acl_arn not in name_id_map.values():
743 raise ValueError("invalid web acl: %s" % target_acl_arn)
745 client = utils.local_session(self.manager.session_factory).client('wafv2')
747 for r in resources:
748 r_arn = self.manager.get_arns([r])[0]
749 if state:
750 self.retry(client.associate_web_acl,
751 WebACLArn=target_acl_arn,
752 ResourceArn=r_arn)
753 else:
754 self.retry(client.disassociate_web_acl,
755 ResourceArn=r_arn)
758@RestResource.filter_registry.register('rest-integration')
759class FilterRestIntegration(ValueFilter):
760 """Filter rest resources based on a key value for the rest method integration of the api
762 :example:
764 .. code-block:: yaml
766 policies:
767 - name: api-method-integrations-with-type-aws
768 resource: rest-resource
769 filters:
770 - type: rest-integration
771 key: type
772 value: AWS
773 """
775 schema = utils.type_schema(
776 'rest-integration',
777 method={'type': 'string', 'enum': [
778 'all', 'ANY', 'PUT', 'GET', "POST",
779 "DELETE", "OPTIONS", "HEAD", "PATCH"]},
780 rinherit=ValueFilter.schema)
781 schema_alias = False
782 permissions = ('apigateway:GET',)
784 def process(self, resources, event=None):
785 method_set = self.data.get('method', 'all')
786 # 10 req/s with burst to 40
787 client = utils.local_session(
788 self.manager.session_factory).client('apigateway')
790 # uniqueness constraint validity across apis?
791 resource_map = {r['id']: r for r in resources}
793 futures = {}
794 results = set()
796 with self.executor_factory(max_workers=2) as w:
797 tasks = []
798 for r in resources:
799 r_method_set = method_set
800 if method_set == 'all':
801 r_method_set = r.get('resourceMethods', {}).keys()
802 for m in r_method_set:
803 tasks.append((r, m))
804 for task_set in utils.chunks(tasks, 20):
805 futures[w.submit(
806 self.process_task_set, client, task_set)] = task_set
808 for f in as_completed(futures):
809 task_set = futures[f]
811 if f.exception():
812 self.manager.log.warning(
813 "Error retrieving integrations on resources %s",
814 ["%s:%s" % (r['restApiId'], r['path'])
815 for r, mt in task_set])
816 continue
818 for i in f.result():
819 if self.match(i):
820 results.add(i['resourceId'])
821 resource_map[i['resourceId']].setdefault(
822 ANNOTATION_KEY_MATCHED_INTEGRATIONS, []).append(i)
824 return [resource_map[rid] for rid in results]
826 def process_task_set(self, client, task_set):
827 results = []
828 for r, m in task_set:
829 try:
830 integration = client.get_integration(
831 restApiId=r['restApiId'],
832 resourceId=r['id'],
833 httpMethod=m)
834 integration.pop('ResponseMetadata', None)
835 integration['restApiId'] = r['restApiId']
836 integration['resourceId'] = r['id']
837 integration['resourceHttpMethod'] = m
838 results.append(integration)
839 except ClientError as e:
840 if e.response['Error']['Code'] == 'NotFoundException':
841 pass
843 return results
846@RestResource.action_registry.register('update-integration')
847class UpdateRestIntegration(BaseAction):
848 """Change or remove api integration properties based on key value
850 :example:
852 .. code-block:: yaml
854 policies:
855 - name: enforce-timeout-on-api-integration
856 resource: rest-resource
857 filters:
858 - type: rest-integration
859 key: timeoutInMillis
860 value: 29000
861 actions:
862 - type: update-integration
863 patch:
864 - op: replace
865 path: /timeoutInMillis
866 value: "3000"
867 """
869 schema = utils.type_schema(
870 'update-integration',
871 patch={'type': 'array', 'items': OP_SCHEMA},
872 required=['patch'])
873 permissions = ('apigateway:PATCH',)
875 def validate(self):
876 found = False
877 for f in self.manager.iter_filters():
878 if isinstance(f, FilterRestIntegration):
879 found = True
880 break
881 if not found:
882 raise ValueError(
883 ("update-integration action requires ",
884 "rest-integration filter usage in policy"))
885 return self
887 def process(self, resources):
888 client = utils.local_session(
889 self.manager.session_factory).client('apigateway')
890 ops = self.data['patch']
891 for r in resources:
892 for i in r.get(ANNOTATION_KEY_MATCHED_INTEGRATIONS, []):
893 client.update_integration(
894 restApiId=i['restApiId'],
895 resourceId=i['resourceId'],
896 httpMethod=i['resourceHttpMethod'],
897 patchOperations=ops)
900@RestResource.action_registry.register('delete-integration')
901class DeleteRestIntegration(BaseAction):
902 """Delete an api integration. Useful if the integration type is a security risk.
904 :example:
906 .. code-block:: yaml
908 policies:
909 - name: enforce-no-resource-integration-with-type-aws
910 resource: rest-resource
911 filters:
912 - type: rest-integration
913 key: type
914 value: AWS
915 actions:
916 - type: delete-integration
917 """
918 permissions = ('apigateway:DELETE',)
919 schema = utils.type_schema('delete-integration')
921 def process(self, resources):
922 client = utils.local_session(self.manager.session_factory).client('apigateway')
924 for r in resources:
925 for i in r.get(ANNOTATION_KEY_MATCHED_INTEGRATIONS, []):
926 try:
927 client.delete_integration(
928 restApiId=i['restApiId'],
929 resourceId=i['resourceId'],
930 httpMethod=i['resourceHttpMethod'])
931 except client.exceptions.NotFoundException:
932 continue
935@RestResource.filter_registry.register('rest-method')
936class FilterRestMethod(ValueFilter):
937 """Filter rest resources based on a key value for the rest method of the api
939 :example:
941 .. code-block:: yaml
943 policies:
944 - name: api-without-key-required
945 resource: rest-resource
946 filters:
947 - type: rest-method
948 key: apiKeyRequired
949 value: false
950 """
952 schema = utils.type_schema(
953 'rest-method',
954 method={'type': 'string', 'enum': [
955 'all', 'ANY', 'PUT', 'GET', "POST",
956 "DELETE", "OPTIONS", "HEAD", "PATCH"]},
957 rinherit=ValueFilter.schema)
958 schema_alias = False
959 permissions = ('apigateway:GET',)
961 def process(self, resources, event=None):
962 method_set = self.data.get('method', 'all')
963 # 10 req/s with burst to 40
964 client = utils.local_session(
965 self.manager.session_factory).client('apigateway')
967 # uniqueness constraint validity across apis?
968 resource_map = {r['id']: r for r in resources}
970 futures = {}
971 results = set()
973 with self.executor_factory(max_workers=2) as w:
974 tasks = []
975 for r in resources:
976 r_method_set = method_set
977 if method_set == 'all':
978 r_method_set = r.get('resourceMethods', {}).keys()
979 for m in r_method_set:
980 tasks.append((r, m))
981 for task_set in utils.chunks(tasks, 20):
982 futures[w.submit(
983 self.process_task_set, client, task_set)] = task_set
985 for f in as_completed(futures):
986 task_set = futures[f]
987 if f.exception():
988 self.manager.log.warning(
989 "Error retrieving methods on resources %s",
990 ["%s:%s" % (r['restApiId'], r['path'])
991 for r, mt in task_set])
992 continue
993 for m in f.result():
994 if self.match(m):
995 results.add(m['resourceId'])
996 resource_map[m['resourceId']].setdefault(
997 ANNOTATION_KEY_MATCHED_METHODS, []).append(m)
998 return [resource_map[rid] for rid in results]
1000 def process_task_set(self, client, task_set):
1001 results = []
1002 for r, m in task_set:
1003 method = client.get_method(
1004 restApiId=r['restApiId'],
1005 resourceId=r['id'],
1006 httpMethod=m)
1007 method.pop('ResponseMetadata', None)
1008 method['restApiId'] = r['restApiId']
1009 method['resourceId'] = r['id']
1010 results.append(method)
1011 return results
1014@RestResource.action_registry.register('update-method')
1015class UpdateRestMethod(BaseAction):
1016 """Change or remove api method behaviors based on key value
1018 :example:
1020 .. code-block:: yaml
1022 policies:
1023 - name: enforce-iam-permissions-on-api
1024 resource: rest-resource
1025 filters:
1026 - type: rest-method
1027 key: authorizationType
1028 value: NONE
1029 op: eq
1030 actions:
1031 - type: update-method
1032 patch:
1033 - op: replace
1034 path: /authorizationType
1035 value: AWS_IAM
1036 """
1038 schema = utils.type_schema(
1039 'update-method',
1040 patch={'type': 'array', 'items': OP_SCHEMA},
1041 required=['patch'])
1042 permissions = ('apigateway:GET',)
1044 def validate(self):
1045 found = False
1046 for f in self.manager.iter_filters():
1047 if isinstance(f, FilterRestMethod):
1048 found = True
1049 break
1050 if not found:
1051 raise ValueError(
1052 ("update-method action requires ",
1053 "rest-method filter usage in policy"))
1054 return self
1056 def process(self, resources):
1057 client = utils.local_session(
1058 self.manager.session_factory).client('apigateway')
1059 ops = self.data['patch']
1060 for r in resources:
1061 for m in r.get(ANNOTATION_KEY_MATCHED_METHODS, []):
1062 client.update_method(
1063 restApiId=m['restApiId'],
1064 resourceId=m['resourceId'],
1065 httpMethod=m['httpMethod'],
1066 patchOperations=ops)
1069@resources.register('apigw-domain-name')
1070class CustomDomainName(query.QueryResourceManager):
1072 class resource_type(query.TypeInfo):
1073 enum_spec = ('get_domain_names', 'items', None)
1074 arn_type = '/domainnames'
1075 id = name = 'domainName'
1076 service = 'apigateway'
1077 universal_taggable = True
1078 cfn_type = 'AWS::ApiGateway::DomainName'
1079 date = 'createdDate'
1081 @classmethod
1082 def get_permissions(cls):
1083 return ('apigateway:GET',)
1085 @property
1086 def generate_arn(self):
1087 """
1088 Sample arn: arn:aws:apigateway:us-east-1::/restapis/rest-api-id
1089 This method overrides c7n.utils.generate_arn and drops
1090 account id from the generic arn.
1091 """
1092 if self._generate_arn is None:
1093 self._generate_arn = functools.partial(
1094 generate_arn,
1095 self.resource_type.service,
1096 region=self.config.region,
1097 resource_type=self.resource_type.arn_type)
1098 return self._generate_arn
1101@CustomDomainName.action_registry.register('update-security')
1102class DomainNameRemediateTls(BaseAction):
1104 schema = type_schema(
1105 'update-security',
1106 securityPolicy={'type': 'string', 'enum': [
1107 'TLS_1_0', 'TLS_1_2']},
1108 required=['securityPolicy'])
1110 permissions = ('apigateway:PATCH',)
1112 def process(self, resources, event=None):
1113 client = utils.local_session(
1114 self.manager.session_factory).client('apigateway')
1115 retryable = ('TooManyRequestsException', 'ConflictException')
1116 retry = utils.get_retry(retryable, max_attempts=8)
1118 for r in resources:
1119 try:
1120 retry(client.update_domain_name,
1121 domainName=r['domainName'],
1122 patchOperations=[
1123 {
1124 'op': 'replace',
1125 'path': '/securityPolicy',
1126 'value': self.data.get('securityPolicy')
1127 },
1128 ]
1129 )
1130 except ClientError as e:
1131 if e.response['Error']['Code'] in retryable:
1132 continue
1135class ApiGwV2DescribeSource(query.DescribeSource):
1137 def augment(self, resources):
1138 # convert tags from {'Key': 'Value'} to standard aws format
1139 for r in resources:
1140 r['Tags'] = [
1141 {'Key': k, 'Value': v} for k, v in r.pop('Tags', {}).items()]
1142 return resources
1145@resources.register('apigwv2')
1146class ApiGwV2(query.QueryResourceManager):
1148 class resource_type(query.TypeInfo):
1149 service = 'apigatewayv2'
1150 arn_type = '/apis'
1151 enum_spec = ('get_apis', 'Items', None)
1152 id = 'ApiId'
1153 name = 'name'
1154 date = 'createdDate'
1155 dimension = 'ApiId'
1156 cfn_type = config_type = "AWS::ApiGatewayV2::Api"
1157 permission_prefix = 'apigateway'
1158 permissions_enum = ('apigateway:GET',)
1159 universal_taggable = object()
1161 source_mapping = {
1162 'config': query.ConfigSource,
1163 'describe': ApiGwV2DescribeSource
1164 }
1166 @property
1167 def generate_arn(self):
1168 """
1169 Sample arn: arn:aws:apigateway:us-east-1::/apis/api-id
1170 This method overrides c7n.utils.generate_arn and drops
1171 account id from the generic arn.
1172 """
1173 if self._generate_arn is None:
1174 self._generate_arn = functools.partial(
1175 generate_arn,
1176 "apigateway",
1177 region=self.config.region,
1178 resource_type=self.resource_type.arn_type,
1179 )
1181 return self._generate_arn
1184class StageDescribe(query.ChildDescribeSource):
1186 def augment(self, resources):
1187 # convert tags from {'Key': 'Value'} to standard aws format
1188 for r in resources:
1189 r['Tags'] = [
1190 {'Key': k, 'Value': v} for k, v in r.pop('Tags', {}).items()]
1191 return resources
1194@resources.register("apigwv2-stage")
1195class ApiGatewayV2Stage(query.ChildResourceManager):
1196 class resource_type(query.TypeInfo):
1197 service = "apigatewayv2"
1198 enum_spec = ('get_stages', 'Items', None)
1199 parent_spec = ('aws.apigwv2', 'ApiId', True)
1200 arn_type = "/apis"
1201 id = name = "StageName"
1202 cfn_type = config_type = "AWS::ApiGatewayV2::Stage"
1203 universal_taggable = object()
1204 permission_prefix = 'apigateway'
1205 permissions_enum = ('apigateway:GET',)
1207 source_mapping = {
1208 "describe-child": StageDescribe,
1209 "config": query.ConfigSource
1210 }
1212 def get_arns(self, resources):
1213 partition = get_partition(self.config.region)
1214 return [
1215 "arn:{}:apigateway:{}::/apis/{}/stages/{}".format(
1216 partition, self.config.region, r['c7n:parent-id'], r['StageName']
1217 )
1218 for r in resources]