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

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

533 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.related import RelatedResourceFilter 

17from c7n.manager import resources, ResourceManager 

18from c7n import query, utils 

19from c7n.utils import generate_arn, type_schema, get_retry, jmespath_search, get_partition 

20 

21 

22ANNOTATION_KEY_MATCHED_METHODS = 'c7n:matched-resource-methods' 

23ANNOTATION_KEY_MATCHED_INTEGRATIONS = 'c7n:matched-method-integrations' 

24 

25 

26@resources.register('rest-account') 

27class RestAccount(ResourceManager): 

28 # note this is not using a regular resource manager or type info 

29 # its a pseudo resource, like an aws account 

30 

31 filter_registry = FilterRegistry('rest-account.filters') 

32 action_registry = ActionRegistry('rest-account.actions') 

33 

34 class resource_type(query.TypeInfo): 

35 service = 'apigateway' 

36 name = id = 'account_id' 

37 dimension = None 

38 arn = False 

39 

40 @classmethod 

41 def get_permissions(cls): 

42 # this resource is not query manager based as its a pseudo 

43 # resource. in that it always exists, it represents the 

44 # service's account settings. 

45 return ('apigateway:GET',) 

46 

47 @classmethod 

48 def has_arn(self): 

49 return False 

50 

51 def get_model(self): 

52 return self.resource_type 

53 

54 def _get_account(self): 

55 client = utils.local_session(self.session_factory).client('apigateway') 

56 try: 

57 account = client.get_account() 

58 except ClientError as e: 

59 if e.response['Error']['Code'] == 'NotFoundException': 

60 return [] 

61 account.pop('ResponseMetadata', None) 

62 account['account_id'] = 'apigw-settings' 

63 return [account] 

64 

65 def resources(self): 

66 return self.filter_resources(self._get_account()) 

67 

68 def get_resources(self, resource_ids): 

69 return self._get_account() 

70 

71 

72OP_SCHEMA = { 

73 'type': 'object', 

74 'required': ['op', 'path'], 

75 'additonalProperties': False, 

76 'properties': { 

77 'op': {'enum': ['add', 'remove', 'update', 'copy', 'replace', 'test']}, 

78 'path': {'type': 'string'}, 

79 'value': {'type': 'string'}, 

80 'from': {'type': 'string'} 

81 } 

82} 

83 

84 

85@RestAccount.action_registry.register('update') 

86class UpdateAccount(BaseAction): 

87 """Update the cloudwatch role associated to a rest account 

88 

89 :example: 

90 

91 .. code-block:: yaml 

92 

93 policies: 

94 - name: correct-rest-account-log-role 

95 resource: rest-account 

96 filters: 

97 - cloudwatchRoleArn: arn:aws:iam::000000000000:role/GatewayLogger 

98 actions: 

99 - type: update 

100 patch: 

101 - op: replace 

102 path: /cloudwatchRoleArn 

103 value: arn:aws:iam::000000000000:role/BetterGatewayLogger 

104 """ 

105 

106 permissions = ('apigateway:PATCH',) 

107 schema = utils.type_schema( 

108 'update', 

109 patch={'type': 'array', 'items': OP_SCHEMA}, 

110 required=['patch']) 

111 

112 def process(self, resources): 

113 client = utils.local_session( 

114 self.manager.session_factory).client('apigateway') 

115 client.update_account(patchOperations=self.data['patch']) 

116 

117 

118class ApiDescribeSource(query.DescribeSource): 

119 

120 def augment(self, resources): 

121 for r in resources: 

122 tags = r.setdefault('Tags', []) 

123 for k, v in r.pop('tags', {}).items(): 

124 tags.append({ 

125 'Key': k, 

126 'Value': v}) 

127 return resources 

128 

129 

130@resources.register('rest-api') 

131class RestApi(query.QueryResourceManager): 

132 

133 class resource_type(query.TypeInfo): 

134 service = 'apigateway' 

135 arn_type = '/restapis' 

136 enum_spec = ('get_rest_apis', 'items', None) 

137 id = 'id' 

138 name = 'name' 

139 date = 'createdDate' 

140 dimension = 'GatewayName' 

141 cfn_type = config_type = "AWS::ApiGateway::RestApi" 

142 universal_taggable = object() 

143 permissions_enum = ('apigateway:GET',) 

144 

145 source_mapping = { 

146 'config': query.ConfigSource, 

147 'describe': ApiDescribeSource 

148 } 

149 

150 @property 

151 def generate_arn(self): 

152 """ 

153 Sample arn: arn:aws:apigateway:us-east-1::/restapis/rest-api-id 

154 This method overrides c7n.utils.generate_arn and drops 

155 account id from the generic arn. 

156 """ 

157 if self._generate_arn is None: 

158 self._generate_arn = functools.partial( 

159 generate_arn, 

160 self.resource_type.service, 

161 region=self.config.region, 

162 resource_type=self.resource_type.arn_type) 

163 return self._generate_arn 

164 

165 

166@RestApi.filter_registry.register('metrics') 

167class Metrics(MetricsFilter): 

168 

169 def get_dimensions(self, resource): 

170 return [{'Name': 'ApiName', 

171 'Value': resource['name']}] 

172 

173 

174@RestApi.filter_registry.register('cross-account') 

175class RestApiCrossAccount(CrossAccountAccessFilter): 

176 

177 policy_attribute = 'policy' 

178 permissions = ('apigateway:GET',) 

179 

180 def get_resource_policy(self, r): 

181 policy = super().get_resource_policy(r) 

182 if policy: 

183 policy = policy.replace('\\', '') 

184 else: 

185 # api gateway default iam policy is public 

186 # authorizers and app code may mitigate but 

187 # the iam policy intent here is clear. 

188 policy = {'Statement': [{ 

189 'Action': 'execute-api:Invoke', 

190 'Effect': 'Allow', 

191 'Principal': '*'}]} 

192 return policy 

193 

194 

195@RestApi.action_registry.register('update') 

196class UpdateApi(BaseAction): 

197 """Update configuration of a REST API. 

198 

199 Non-exhaustive list of updateable attributes. 

200 https://docs.aws.amazon.com/apigateway/api-reference/link-relation/restapi-update/#remarks 

201 

202 :example: 

203 

204 contrived example to update description on api gateways 

205 

206 .. code-block:: yaml 

207 

208 policies: 

209 - name: apigw-description 

210 resource: rest-api 

211 filters: 

212 - description: empty 

213 actions: 

214 - type: update 

215 patch: 

216 - op: replace 

217 path: /description 

218 value: "not empty :-)" 

219 """ 

220 permissions = ('apigateway:PATCH',) 

221 schema = utils.type_schema( 

222 'update', 

223 patch={'type': 'array', 'items': OP_SCHEMA}, 

224 required=['patch']) 

225 

226 def process(self, resources): 

227 client = utils.local_session( 

228 self.manager.session_factory).client('apigateway') 

229 for r in resources: 

230 client.update_rest_api( 

231 restApiId=r['id'], 

232 patchOperations=self.data['patch']) 

233 

234 

235@RestApi.action_registry.register('delete') 

236class DeleteApi(BaseAction): 

237 """Delete a REST API. 

238 

239 :example: 

240 

241 contrived example to delete rest api 

242 

243 .. code-block:: yaml 

244 

245 policies: 

246 - name: apigw-delete 

247 resource: rest-api 

248 filters: 

249 - description: empty 

250 actions: 

251 - type: delete 

252 """ 

253 permissions = ('apigateway:DELETE',) 

254 schema = type_schema('delete') 

255 

256 def process(self, resources): 

257 client = utils.local_session( 

258 self.manager.session_factory).client('apigateway') 

259 retry = get_retry(('TooManyRequestsException',)) 

260 

261 for r in resources: 

262 try: 

263 retry(client.delete_rest_api, restApiId=r['id']) 

264 except client.exceptions.NotFoundException: 

265 continue 

266 

267 

268@query.sources.register('describe-rest-stage') 

269class DescribeRestStage(query.ChildDescribeSource): 

270 

271 def __init__(self, manager): 

272 self.manager = manager 

273 self.query = query.ChildResourceQuery( 

274 self.manager.session_factory, self.manager, capture_parent_id=True) 

275 

276 def get_query(self): 

277 return super(DescribeRestStage, self).get_query(capture_parent_id=True) 

278 

279 def augment(self, resources): 

280 results = [] 

281 rest_apis = self.manager.get_resource_manager( 

282 'rest-api').resources() 

283 # Using capture parent, changes the protocol 

284 for parent_id, r in resources: 

285 r['restApiId'] = parent_id 

286 for rest_api in rest_apis: 

287 if rest_api['id'] == parent_id: 

288 r['restApiType'] = rest_api['endpointConfiguration']['types'] 

289 r['stageArn'] = "arn:aws:{service}:{region}::" \ 

290 "/restapis/{rest_api_id}/stages/" \ 

291 "{stage_name}".format( 

292 service="apigateway", 

293 region=self.manager.config.region, 

294 rest_api_id=parent_id, 

295 stage_name=r['stageName']) 

296 tags = r.setdefault('Tags', []) 

297 for k, v in r.pop('tags', {}).items(): 

298 tags.append({ 

299 'Key': k, 

300 'Value': v}) 

301 results.append(r) 

302 return results 

303 

304 def get_resources(self, ids, cache=True): 

305 deployment_ids = [] 

306 client = utils.local_session( 

307 self.manager.session_factory).client('apigateway') 

308 for id in ids: 

309 # if we get stage arn, we pick rest_api_id and stageName to get deploymentId 

310 if id.startswith('arn:aws:apigateway'): 

311 _, ident = id.rsplit(':', 1) 

312 parts = ident.split('/', 4) 

313 # if we get stage name in arn, use stage_name to get stage information 

314 # from stage information, pick deploymentId 

315 if len(parts) > 3: 

316 response = self.manager.retry( 

317 client.get_stage, 

318 restApiId=parts[2], 

319 stageName=parts[4]) 

320 deployment_ids.append(response[self.manager.resource_type.id]) 

321 else: 

322 deployment_ids.append(id) 

323 return super(DescribeRestStage, self).get_resources(deployment_ids, cache) 

324 

325 

326@resources.register('rest-stage') 

327class RestStage(query.ChildResourceManager): 

328 

329 class resource_type(query.TypeInfo): 

330 service = 'apigateway' 

331 parent_spec = ('rest-api', 'restApiId', None) 

332 enum_spec = ('get_stages', 'item', None) 

333 name = 'stageName' 

334 id = 'deploymentId' 

335 config_id = 'stageArn' 

336 date = 'createdDate' 

337 universal_taggable = True 

338 cfn_type = config_type = "AWS::ApiGateway::Stage" 

339 arn_type = 'stages' 

340 permissions_enum = ('apigateway:GET',) 

341 supports_trailevents = True 

342 

343 child_source = 'describe' 

344 source_mapping = { 

345 'describe': DescribeRestStage, 

346 'config': query.ConfigSource 

347 } 

348 

349 @property 

350 def generate_arn(self): 

351 self._generate_arn = functools.partial( 

352 generate_arn, 

353 self.resource_type.service, 

354 region=self.config.region) 

355 return self._generate_arn 

356 

357 def get_arns(self, resources): 

358 arns = [] 

359 for r in resources: 

360 arns.append(self.generate_arn('/restapis/' + r['restApiId'] + 

361 '/stages/' + r[self.get_model().name])) 

362 return arns 

363 

364 

365@RestStage.action_registry.register('update') 

366class UpdateStage(BaseAction): 

367 """Update/remove values of an api stage 

368 

369 :example: 

370 

371 .. code-block:: yaml 

372 

373 policies: 

374 - name: disable-stage-caching 

375 resource: rest-stage 

376 filters: 

377 - methodSettings."*/*".cachingEnabled: true 

378 actions: 

379 - type: update 

380 patch: 

381 - op: replace 

382 path: /*/*/caching/enabled 

383 value: 'false' 

384 """ 

385 

386 permissions = ('apigateway:PATCH',) 

387 schema = utils.type_schema( 

388 'update', 

389 patch={'type': 'array', 'items': OP_SCHEMA}, 

390 required=['patch']) 

391 

392 def process(self, resources): 

393 client = utils.local_session( 

394 self.manager.session_factory).client('apigateway') 

395 for r in resources: 

396 self.manager.retry( 

397 client.update_stage, 

398 restApiId=r['restApiId'], 

399 stageName=r['stageName'], 

400 patchOperations=self.data['patch']) 

401 

402 

403@RestStage.action_registry.register('delete') 

404class DeleteStage(BaseAction): 

405 """Delete an api stage 

406 

407 :example: 

408 

409 .. code-block:: yaml 

410 

411 policies: 

412 - name: delete-rest-stage 

413 resource: rest-stage 

414 filters: 

415 - methodSettings."*/*".cachingEnabled: true 

416 actions: 

417 - type: delete 

418 """ 

419 permissions = ('apigateway:DELETE',) 

420 schema = utils.type_schema('delete') 

421 

422 def process(self, resources): 

423 client = utils.local_session(self.manager.session_factory).client('apigateway') 

424 for r in resources: 

425 try: 

426 self.manager.retry( 

427 client.delete_stage, 

428 restApiId=r['restApiId'], 

429 stageName=r['stageName']) 

430 except client.exceptions.NotFoundException: 

431 pass 

432 

433 

434@resources.register('rest-resource') 

435class RestResource(query.ChildResourceManager): 

436 

437 child_source = 'describe-rest-resource' 

438 

439 class resource_type(query.TypeInfo): 

440 service = 'apigateway' 

441 parent_spec = ('rest-api', 'restApiId', None) 

442 enum_spec = ('get_resources', 'items', None) 

443 id = 'id' 

444 name = 'path' 

445 permissions_enum = ('apigateway:GET',) 

446 cfn_type = 'AWS::ApiGateway::Resource' 

447 

448 

449@query.sources.register('describe-rest-resource') 

450class DescribeRestResource(query.ChildDescribeSource): 

451 

452 def get_query(self): 

453 return super(DescribeRestResource, self).get_query(capture_parent_id=True) 

454 

455 def augment(self, resources): 

456 results = [] 

457 # Using capture parent id, changes the protocol 

458 for parent_id, r in resources: 

459 r['restApiId'] = parent_id 

460 results.append(r) 

461 return results 

462 

463 

464@resources.register('rest-vpclink') 

465class RestApiVpcLink(query.QueryResourceManager): 

466 

467 class resource_type(query.TypeInfo): 

468 service = 'apigateway' 

469 enum_spec = ('get_vpc_links', 'items', None) 

470 id = 'id' 

471 name = 'name' 

472 permissions_enum = ('apigateway:GET',) 

473 cfn_type = 'AWS::ApiGateway::VpcLink' 

474 

475 

476@resources.register('rest-client-certificate') 

477class RestClientCertificate(query.QueryResourceManager): 

478 """TLS client certificates generated by API Gateway 

479 

480 :example: 

481 

482 .. code-block:: yaml 

483 

484 policies: 

485 - name: old-client-certificates 

486 resource: rest-client-certificate 

487 filters: 

488 - key: createdDate 

489 value_type: age 

490 value: 90 

491 op: greater-than 

492 """ 

493 class resource_type(query.TypeInfo): 

494 service = 'apigateway' 

495 enum_spec = ('get_client_certificates', 'items', None) 

496 id = 'clientCertificateId' 

497 name = 'client_certificate_id' 

498 permissions_enum = ('apigateway:GET',) 

499 cfn_type = 'AWS::ApiGateway::ClientCertificate' 

500 

501 

502@RestStage.filter_registry.register('client-certificate') 

503class StageClientCertificateFilter(RelatedResourceFilter): 

504 """Filter API stages by a client certificate 

505 

506 :example: 

507 

508 .. code-block:: yaml 

509 

510 policies: 

511 - name: rest-stages-old-certificate 

512 resource: rest-stage 

513 filters: 

514 - type: client-certificate 

515 key: createdDate 

516 value_type: age 

517 value: 90 

518 op: greater-than 

519 """ 

520 schema = type_schema('client-certificate', rinherit=ValueFilter.schema) 

521 RelatedResource = "c7n.resources.apigw.RestClientCertificate" 

522 RelatedIdsExpression = 'clientCertificateId' 

523 annotation_key = "c7n:matched-client-certificate" 

524 

525 def process(self, resources, event=None): 

526 related = self.get_related(resources) 

527 matched = [] 

528 for r in resources: 

529 if self.process_resource(r, related): 

530 # Add the full certificate details rather than just the ID 

531 self.augment(related, r) 

532 matched.append(r) 

533 return matched 

534 

535 def augment(self, related, resource): 

536 rid = resource[self.RelatedIdsExpression] 

537 with suppress(KeyError): 

538 resource[self.annotation_key] = { 

539 self.data['key']: jmespath_search(self.data['key'], related[rid]) 

540 } 

541 

542 

543@RestStage.filter_registry.register('waf-enabled') 

544class WafEnabled(WafClassicRegionalFilterBase): 

545 """Filter API Gateway stage by waf-regional web-acl 

546 

547 :example: 

548 

549 .. code-block:: yaml 

550 

551 policies: 

552 - name: filter-apigw-waf-regional 

553 resource: rest-stage 

554 filters: 

555 - type: waf-enabled 

556 state: false 

557 web-acl: test 

558 """ 

559 

560 def get_associated_web_acl(self, resource): 

561 return self.get_web_acl_by_arn(resource.get('webAclArn')) 

562 

563 

564@RestStage.action_registry.register('set-waf') 

565class SetWaf(BaseAction): 

566 """Enable waf protection on API Gateway stage. 

567 

568 :example: 

569 

570 .. code-block:: yaml 

571 

572 policies: 

573 - name: set-waf-for-stage 

574 resource: rest-stage 

575 filters: 

576 - type: waf-enabled 

577 state: false 

578 web-acl: test 

579 actions: 

580 - type: set-waf 

581 state: true 

582 web-acl: test 

583 

584 - name: disassociate-wafv2-associate-waf-regional-apigw 

585 resource: rest-stage 

586 filters: 

587 - type: wafv2-enabled 

588 state: true 

589 actions: 

590 - type: set-waf 

591 state: true 

592 web-acl: test 

593 

594 """ 

595 permissions = ('waf-regional:AssociateWebACL', 'waf-regional:ListWebACLs') 

596 

597 schema = type_schema( 

598 'set-waf', required=['web-acl'], **{ 

599 'web-acl': {'type': 'string'}, 

600 # 'force': {'type': 'boolean'}, 

601 'state': {'type': 'boolean'}}) 

602 

603 def validate(self): 

604 found = False 

605 for f in self.manager.iter_filters(): 

606 if isinstance(f, WafEnabled) or isinstance(f, WafV2Enabled): 

607 found = True 

608 break 

609 if not found: 

610 # try to ensure idempotent usage 

611 raise PolicyValidationError( 

612 "set-waf should be used in conjunction with waf-enabled or wafv2-enabled \ 

613 filter on %s" % (self.manager.data,)) 

614 return self 

615 

616 def process(self, resources): 

617 wafs = self.manager.get_resource_manager('waf-regional').resources(augment=False) 

618 name_id_map = {w['Name']: w['WebACLId'] for w in wafs} 

619 target_acl = self.data.get('web-acl', '') 

620 target_acl_id = name_id_map.get(target_acl, target_acl) 

621 state = self.data.get('state', True) 

622 if state and target_acl_id not in name_id_map.values(): 

623 raise ValueError("invalid web acl: %s" % (target_acl)) 

624 

625 client = utils.local_session( 

626 self.manager.session_factory).client('waf-regional') 

627 

628 for r in resources: 

629 r_arn = self.manager.get_arns([r])[0] 

630 if state: 

631 client.associate_web_acl(WebACLId=target_acl_id, ResourceArn=r_arn) 

632 else: 

633 client.disassociate_web_acl(WebACLId=target_acl_id, ResourceArn=r_arn) 

634 

635 

636@RestStage.filter_registry.register('wafv2-enabled') 

637class WafV2Enabled(WafV2FilterBase): 

638 """Filter API Gateway stage by wafv2 web-acl 

639 

640 :example: 

641 

642 .. code-block:: yaml 

643 

644 policies: 

645 - name: filter-wafv2-apigw 

646 resource: rest-stage 

647 filters: 

648 - type: wafv2-enabled 

649 state: false 

650 web-acl: testv2 

651 """ 

652 

653 def get_associated_web_acl(self, resource): 

654 return self.get_web_acl_by_arn(resource.get('webAclArn')) 

655 

656 

657@RestStage.action_registry.register('set-wafv2') 

658class SetWafv2(BaseAction): 

659 """Enable wafv2 protection on API Gateway stage. 

660 

661 :example: 

662 

663 .. code-block:: yaml 

664 

665 policies: 

666 - name: set-wafv2-for-stage 

667 resource: rest-stage 

668 filters: 

669 - type: wafv2-enabled 

670 state: false 

671 web-acl: testv2 

672 actions: 

673 - type: set-wafv2 

674 state: true 

675 web-acl: testv2 

676 

677 - name: disassociate-waf-regional-associate-wafv2-apigw 

678 resource: rest-stage 

679 filters: 

680 - type: waf-enabled 

681 state: true 

682 actions: 

683 - type: set-wafv2 

684 state: true 

685 web-acl: testv2 

686 

687 """ 

688 permissions = ('wafv2:AssociateWebACL', 'wafv2:ListWebACLs') 

689 

690 schema = type_schema( 

691 'set-wafv2', **{ 

692 'web-acl': {'type': 'string'}, 

693 'state': {'type': 'boolean'}}) 

694 

695 retry = staticmethod(get_retry(( 

696 'ThrottlingException', 

697 'RequestLimitExceeded', 

698 'Throttled', 

699 'ThrottledException', 

700 'Throttling', 

701 'Client.RequestLimitExceeded'))) 

702 

703 def validate(self): 

704 found = False 

705 for f in self.manager.iter_filters(): 

706 if isinstance(f, WafV2Enabled) or isinstance(f, WafEnabled): 

707 found = True 

708 break 

709 if not found: 

710 # try to ensure idempotent usage 

711 raise PolicyValidationError( 

712 "set-wafv2 should be used in conjunction with wafv2-enabled or waf-enabled \ 

713 filter on %s" % (self.manager.data,)) 

714 if self.data.get('state'): 

715 if 'web-acl' not in self.data: 

716 raise PolicyValidationError(( 

717 "set-wafv2 filter parameter state is true, " 

718 "requires `web-acl` on %s" % (self.manager.data,))) 

719 

720 return self 

721 

722 def process(self, resources): 

723 wafs = self.manager.get_resource_manager('wafv2').resources(augment=False) 

724 name_id_map = {w['Name']: w['ARN'] for w in wafs} 

725 state = self.data.get('state', True) 

726 target_acl_arn = '' 

727 

728 if state: 

729 target_acl = self.data.get('web-acl', '') 

730 target_acl_ids = [v for k, v in name_id_map.items() if 

731 re.match(target_acl, k)] 

732 if len(target_acl_ids) != 1: 

733 raise ValueError(f'{target_acl} matching to none or the ' 

734 f'multiple web-acls') 

735 target_acl_arn = target_acl_ids[0] 

736 

737 if state and target_acl_arn not in name_id_map.values(): 

738 raise ValueError("invalid web acl: %s" % target_acl_arn) 

739 

740 client = utils.local_session(self.manager.session_factory).client('wafv2') 

741 

742 for r in resources: 

743 r_arn = self.manager.get_arns([r])[0] 

744 if state: 

745 self.retry(client.associate_web_acl, 

746 WebACLArn=target_acl_arn, 

747 ResourceArn=r_arn) 

748 else: 

749 self.retry(client.disassociate_web_acl, 

750 ResourceArn=r_arn) 

751 

752 

753@RestResource.filter_registry.register('rest-integration') 

754class FilterRestIntegration(ValueFilter): 

755 """Filter rest resources based on a key value for the rest method integration of the api 

756 

757 :example: 

758 

759 .. code-block:: yaml 

760 

761 policies: 

762 - name: api-method-integrations-with-type-aws 

763 resource: rest-resource 

764 filters: 

765 - type: rest-integration 

766 key: type 

767 value: AWS 

768 """ 

769 

770 schema = utils.type_schema( 

771 'rest-integration', 

772 method={'type': 'string', 'enum': [ 

773 'all', 'ANY', 'PUT', 'GET', "POST", 

774 "DELETE", "OPTIONS", "HEAD", "PATCH"]}, 

775 rinherit=ValueFilter.schema) 

776 schema_alias = False 

777 permissions = ('apigateway:GET',) 

778 

779 def process(self, resources, event=None): 

780 method_set = self.data.get('method', 'all') 

781 # 10 req/s with burst to 40 

782 client = utils.local_session( 

783 self.manager.session_factory).client('apigateway') 

784 

785 # uniqueness constraint validity across apis? 

786 resource_map = {r['id']: r for r in resources} 

787 

788 futures = {} 

789 results = set() 

790 

791 with self.executor_factory(max_workers=2) as w: 

792 tasks = [] 

793 for r in resources: 

794 r_method_set = method_set 

795 if method_set == 'all': 

796 r_method_set = r.get('resourceMethods', {}).keys() 

797 for m in r_method_set: 

798 tasks.append((r, m)) 

799 for task_set in utils.chunks(tasks, 20): 

800 futures[w.submit( 

801 self.process_task_set, client, task_set)] = task_set 

802 

803 for f in as_completed(futures): 

804 task_set = futures[f] 

805 

806 if f.exception(): 

807 self.manager.log.warning( 

808 "Error retrieving integrations on resources %s", 

809 ["%s:%s" % (r['restApiId'], r['path']) 

810 for r, mt in task_set]) 

811 continue 

812 

813 for i in f.result(): 

814 if self.match(i): 

815 results.add(i['resourceId']) 

816 resource_map[i['resourceId']].setdefault( 

817 ANNOTATION_KEY_MATCHED_INTEGRATIONS, []).append(i) 

818 

819 return [resource_map[rid] for rid in results] 

820 

821 def process_task_set(self, client, task_set): 

822 results = [] 

823 for r, m in task_set: 

824 try: 

825 integration = client.get_integration( 

826 restApiId=r['restApiId'], 

827 resourceId=r['id'], 

828 httpMethod=m) 

829 integration.pop('ResponseMetadata', None) 

830 integration['restApiId'] = r['restApiId'] 

831 integration['resourceId'] = r['id'] 

832 integration['resourceHttpMethod'] = m 

833 results.append(integration) 

834 except ClientError as e: 

835 if e.response['Error']['Code'] == 'NotFoundException': 

836 pass 

837 

838 return results 

839 

840 

841@RestResource.action_registry.register('update-integration') 

842class UpdateRestIntegration(BaseAction): 

843 """Change or remove api integration properties based on key value 

844 

845 :example: 

846 

847 .. code-block:: yaml 

848 

849 policies: 

850 - name: enforce-timeout-on-api-integration 

851 resource: rest-resource 

852 filters: 

853 - type: rest-integration 

854 key: timeoutInMillis 

855 value: 29000 

856 actions: 

857 - type: update-integration 

858 patch: 

859 - op: replace 

860 path: /timeoutInMillis 

861 value: "3000" 

862 """ 

863 

864 schema = utils.type_schema( 

865 'update-integration', 

866 patch={'type': 'array', 'items': OP_SCHEMA}, 

867 required=['patch']) 

868 permissions = ('apigateway:PATCH',) 

869 

870 def validate(self): 

871 found = False 

872 for f in self.manager.iter_filters(): 

873 if isinstance(f, FilterRestIntegration): 

874 found = True 

875 break 

876 if not found: 

877 raise ValueError( 

878 ("update-integration action requires ", 

879 "rest-integration filter usage in policy")) 

880 return self 

881 

882 def process(self, resources): 

883 client = utils.local_session( 

884 self.manager.session_factory).client('apigateway') 

885 ops = self.data['patch'] 

886 for r in resources: 

887 for i in r.get(ANNOTATION_KEY_MATCHED_INTEGRATIONS, []): 

888 client.update_integration( 

889 restApiId=i['restApiId'], 

890 resourceId=i['resourceId'], 

891 httpMethod=i['resourceHttpMethod'], 

892 patchOperations=ops) 

893 

894 

895@RestResource.action_registry.register('delete-integration') 

896class DeleteRestIntegration(BaseAction): 

897 """Delete an api integration. Useful if the integration type is a security risk. 

898 

899 :example: 

900 

901 .. code-block:: yaml 

902 

903 policies: 

904 - name: enforce-no-resource-integration-with-type-aws 

905 resource: rest-resource 

906 filters: 

907 - type: rest-integration 

908 key: type 

909 value: AWS 

910 actions: 

911 - type: delete-integration 

912 """ 

913 permissions = ('apigateway:DELETE',) 

914 schema = utils.type_schema('delete-integration') 

915 

916 def process(self, resources): 

917 client = utils.local_session(self.manager.session_factory).client('apigateway') 

918 

919 for r in resources: 

920 for i in r.get(ANNOTATION_KEY_MATCHED_INTEGRATIONS, []): 

921 try: 

922 client.delete_integration( 

923 restApiId=i['restApiId'], 

924 resourceId=i['resourceId'], 

925 httpMethod=i['resourceHttpMethod']) 

926 except client.exceptions.NotFoundException: 

927 continue 

928 

929 

930@RestResource.filter_registry.register('rest-method') 

931class FilterRestMethod(ValueFilter): 

932 """Filter rest resources based on a key value for the rest method of the api 

933 

934 :example: 

935 

936 .. code-block:: yaml 

937 

938 policies: 

939 - name: api-without-key-required 

940 resource: rest-resource 

941 filters: 

942 - type: rest-method 

943 key: apiKeyRequired 

944 value: false 

945 """ 

946 

947 schema = utils.type_schema( 

948 'rest-method', 

949 method={'type': 'string', 'enum': [ 

950 'all', 'ANY', 'PUT', 'GET', "POST", 

951 "DELETE", "OPTIONS", "HEAD", "PATCH"]}, 

952 rinherit=ValueFilter.schema) 

953 schema_alias = False 

954 permissions = ('apigateway:GET',) 

955 

956 def process(self, resources, event=None): 

957 method_set = self.data.get('method', 'all') 

958 # 10 req/s with burst to 40 

959 client = utils.local_session( 

960 self.manager.session_factory).client('apigateway') 

961 

962 # uniqueness constraint validity across apis? 

963 resource_map = {r['id']: r for r in resources} 

964 

965 futures = {} 

966 results = set() 

967 

968 with self.executor_factory(max_workers=2) as w: 

969 tasks = [] 

970 for r in resources: 

971 r_method_set = method_set 

972 if method_set == 'all': 

973 r_method_set = r.get('resourceMethods', {}).keys() 

974 for m in r_method_set: 

975 tasks.append((r, m)) 

976 for task_set in utils.chunks(tasks, 20): 

977 futures[w.submit( 

978 self.process_task_set, client, task_set)] = task_set 

979 

980 for f in as_completed(futures): 

981 task_set = futures[f] 

982 if f.exception(): 

983 self.manager.log.warning( 

984 "Error retrieving methods on resources %s", 

985 ["%s:%s" % (r['restApiId'], r['path']) 

986 for r, mt in task_set]) 

987 continue 

988 for m in f.result(): 

989 if self.match(m): 

990 results.add(m['resourceId']) 

991 resource_map[m['resourceId']].setdefault( 

992 ANNOTATION_KEY_MATCHED_METHODS, []).append(m) 

993 return [resource_map[rid] for rid in results] 

994 

995 def process_task_set(self, client, task_set): 

996 results = [] 

997 for r, m in task_set: 

998 method = client.get_method( 

999 restApiId=r['restApiId'], 

1000 resourceId=r['id'], 

1001 httpMethod=m) 

1002 method.pop('ResponseMetadata', None) 

1003 method['restApiId'] = r['restApiId'] 

1004 method['resourceId'] = r['id'] 

1005 results.append(method) 

1006 return results 

1007 

1008 

1009@RestResource.action_registry.register('update-method') 

1010class UpdateRestMethod(BaseAction): 

1011 """Change or remove api method behaviors based on key value 

1012 

1013 :example: 

1014 

1015 .. code-block:: yaml 

1016 

1017 policies: 

1018 - name: enforce-iam-permissions-on-api 

1019 resource: rest-resource 

1020 filters: 

1021 - type: rest-method 

1022 key: authorizationType 

1023 value: NONE 

1024 op: eq 

1025 actions: 

1026 - type: update-method 

1027 patch: 

1028 - op: replace 

1029 path: /authorizationType 

1030 value: AWS_IAM 

1031 """ 

1032 

1033 schema = utils.type_schema( 

1034 'update-method', 

1035 patch={'type': 'array', 'items': OP_SCHEMA}, 

1036 required=['patch']) 

1037 permissions = ('apigateway:GET',) 

1038 

1039 def validate(self): 

1040 found = False 

1041 for f in self.manager.iter_filters(): 

1042 if isinstance(f, FilterRestMethod): 

1043 found = True 

1044 break 

1045 if not found: 

1046 raise ValueError( 

1047 ("update-method action requires ", 

1048 "rest-method filter usage in policy")) 

1049 return self 

1050 

1051 def process(self, resources): 

1052 client = utils.local_session( 

1053 self.manager.session_factory).client('apigateway') 

1054 ops = self.data['patch'] 

1055 for r in resources: 

1056 for m in r.get(ANNOTATION_KEY_MATCHED_METHODS, []): 

1057 client.update_method( 

1058 restApiId=m['restApiId'], 

1059 resourceId=m['resourceId'], 

1060 httpMethod=m['httpMethod'], 

1061 patchOperations=ops) 

1062 

1063 

1064@resources.register('apigw-domain-name') 

1065class CustomDomainName(query.QueryResourceManager): 

1066 

1067 class resource_type(query.TypeInfo): 

1068 enum_spec = ('get_domain_names', 'items', None) 

1069 arn_type = '/domainnames' 

1070 id = name = 'domainName' 

1071 service = 'apigateway' 

1072 universal_taggable = True 

1073 cfn_type = 'AWS::ApiGateway::DomainName' 

1074 date = 'createdDate' 

1075 

1076 @classmethod 

1077 def get_permissions(cls): 

1078 return ('apigateway:GET',) 

1079 

1080 @property 

1081 def generate_arn(self): 

1082 """ 

1083 Sample arn: arn:aws:apigateway:us-east-1::/restapis/rest-api-id 

1084 This method overrides c7n.utils.generate_arn and drops 

1085 account id from the generic arn. 

1086 """ 

1087 if self._generate_arn is None: 

1088 self._generate_arn = functools.partial( 

1089 generate_arn, 

1090 self.resource_type.service, 

1091 region=self.config.region, 

1092 resource_type=self.resource_type.arn_type) 

1093 return self._generate_arn 

1094 

1095 

1096@CustomDomainName.action_registry.register('update-security') 

1097class DomainNameRemediateTls(BaseAction): 

1098 

1099 schema = type_schema( 

1100 'update-security', 

1101 securityPolicy={'type': 'string', 'enum': [ 

1102 'TLS_1_0', 'TLS_1_2']}, 

1103 required=['securityPolicy']) 

1104 

1105 permissions = ('apigateway:PATCH',) 

1106 

1107 def process(self, resources, event=None): 

1108 client = utils.local_session( 

1109 self.manager.session_factory).client('apigateway') 

1110 retryable = ('TooManyRequestsException', 'ConflictException') 

1111 retry = utils.get_retry(retryable, max_attempts=8) 

1112 

1113 for r in resources: 

1114 try: 

1115 retry(client.update_domain_name, 

1116 domainName=r['domainName'], 

1117 patchOperations=[ 

1118 { 

1119 'op': 'replace', 

1120 'path': '/securityPolicy', 

1121 'value': self.data.get('securityPolicy') 

1122 }, 

1123 ] 

1124 ) 

1125 except ClientError as e: 

1126 if e.response['Error']['Code'] in retryable: 

1127 continue 

1128 

1129 

1130class ApiGwV2DescribeSource(query.DescribeSource): 

1131 

1132 def augment(self, resources): 

1133 # convert tags from {'Key': 'Value'} to standard aws format 

1134 for r in resources: 

1135 r['Tags'] = [ 

1136 {'Key': k, 'Value': v} for k, v in r.pop('Tags', {}).items()] 

1137 return resources 

1138 

1139 

1140@resources.register('apigwv2') 

1141class ApiGwV2(query.QueryResourceManager): 

1142 

1143 class resource_type(query.TypeInfo): 

1144 service = 'apigatewayv2' 

1145 arn_type = '/apis' 

1146 enum_spec = ('get_apis', 'Items', None) 

1147 id = 'ApiId' 

1148 name = 'name' 

1149 date = 'createdDate' 

1150 dimension = 'ApiId' 

1151 cfn_type = config_type = "AWS::ApiGatewayV2::Api" 

1152 permission_prefix = 'apigateway' 

1153 permissions_enum = ('apigateway:GET',) 

1154 universal_taggable = object() 

1155 

1156 source_mapping = { 

1157 'config': query.ConfigSource, 

1158 'describe': ApiGwV2DescribeSource 

1159 } 

1160 

1161 @property 

1162 def generate_arn(self): 

1163 """ 

1164 Sample arn: arn:aws:apigateway:us-east-1::/apis/api-id 

1165 This method overrides c7n.utils.generate_arn and drops 

1166 account id from the generic arn. 

1167 """ 

1168 if self._generate_arn is None: 

1169 self._generate_arn = functools.partial( 

1170 generate_arn, 

1171 "apigateway", 

1172 region=self.config.region, 

1173 resource_type=self.resource_type.arn_type, 

1174 ) 

1175 

1176 return self._generate_arn 

1177 

1178 

1179class StageDescribe(query.ChildDescribeSource): 

1180 

1181 def augment(self, resources): 

1182 # convert tags from {'Key': 'Value'} to standard aws format 

1183 for r in resources: 

1184 r['Tags'] = [ 

1185 {'Key': k, 'Value': v} for k, v in r.pop('Tags', {}).items()] 

1186 return resources 

1187 

1188 

1189@resources.register("apigwv2-stage") 

1190class ApiGatewayV2Stage(query.ChildResourceManager): 

1191 class resource_type(query.TypeInfo): 

1192 service = "apigatewayv2" 

1193 enum_spec = ('get_stages', 'Items', None) 

1194 parent_spec = ('aws.apigwv2', 'ApiId', True) 

1195 arn_type = "/apis" 

1196 id = name = "StageName" 

1197 cfn_type = config_type = "AWS::ApiGatewayV2::Stage" 

1198 universal_taggable = object() 

1199 permission_prefix = 'apigateway' 

1200 permissions_enum = ('apigateway:GET',) 

1201 

1202 source_mapping = { 

1203 "describe-child": StageDescribe, 

1204 "config": query.ConfigSource 

1205 } 

1206 

1207 def get_arns(self, resources): 

1208 partition = get_partition(self.config.region) 

1209 return [ 

1210 "arn:{}:apigateway:{}::/apis/{}/stages/{}".format( 

1211 partition, self.config.region, r['c7n:parent-id'], r['StageName'] 

1212 ) 

1213 for r in resources]