Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.11/site-packages/c7n/resources/apigw.py: 49%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

585 statements  

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 )