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

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) 

275 self.query.capture_parent_id = True 

276 

277 def get_query(self): 

278 query = super(DescribeRestStage, self).get_query() 

279 query.capture_parent_id = True 

280 return query 

281 

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 

306 

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) 

327 

328 

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

330class RestStage(query.ChildResourceManager): 

331 

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 

345 

346 child_source = 'describe' 

347 source_mapping = { 

348 'describe': DescribeRestStage, 

349 'config': query.ConfigSource 

350 } 

351 

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 

359 

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 

366 

367 

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

369class UpdateStage(BaseAction): 

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

371 

372 :example: 

373 

374 .. code-block:: yaml 

375 

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 """ 

388 

389 permissions = ('apigateway:PATCH',) 

390 schema = utils.type_schema( 

391 'update', 

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

393 required=['patch']) 

394 

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']) 

404 

405 

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

407class DeleteStage(BaseAction): 

408 """Delete an api stage 

409 

410 :example: 

411 

412 .. code-block:: yaml 

413 

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') 

424 

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 

435 

436 

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

438class RestResource(query.ChildResourceManager): 

439 

440 child_source = 'describe-rest-resource' 

441 

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' 

450 

451 

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

453class DescribeRestResource(query.ChildDescribeSource): 

454 

455 def get_query(self): 

456 query = super(DescribeRestResource, self).get_query() 

457 query.capture_parent_id = True 

458 return query 

459 

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 

467 

468 

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

470class RestApiVpcLink(query.QueryResourceManager): 

471 

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' 

479 

480 

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

482class RestClientCertificate(query.QueryResourceManager): 

483 """TLS client certificates generated by API Gateway 

484 

485 :example: 

486 

487 .. code-block:: yaml 

488 

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' 

505 

506 

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

508class StageClientCertificateFilter(RelatedResourceFilter): 

509 """Filter API stages by a client certificate 

510 

511 :example: 

512 

513 .. code-block:: yaml 

514 

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" 

529 

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 

539 

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 } 

546 

547 

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

549class WafEnabled(WafClassicRegionalFilterBase): 

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

551 

552 :example: 

553 

554 .. code-block:: yaml 

555 

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 """ 

564 

565 def get_associated_web_acl(self, resource): 

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

567 

568 

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

570class SetWaf(BaseAction): 

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

572 

573 :example: 

574 

575 .. code-block:: yaml 

576 

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 

588 

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 

598 

599 """ 

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

601 

602 schema = type_schema( 

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

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

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

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

607 

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 

620 

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)) 

629 

630 client = utils.local_session( 

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

632 

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) 

639 

640 

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

642class WafV2Enabled(WafV2FilterBase): 

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

644 

645 :example: 

646 

647 .. code-block:: yaml 

648 

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 """ 

657 

658 def get_associated_web_acl(self, resource): 

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

660 

661 

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

663class SetWafv2(BaseAction): 

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

665 

666 :example: 

667 

668 .. code-block:: yaml 

669 

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 

681 

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 

691 

692 """ 

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

694 

695 schema = type_schema( 

696 'set-wafv2', **{ 

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

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

699 

700 retry = staticmethod(get_retry(( 

701 'ThrottlingException', 

702 'RequestLimitExceeded', 

703 'Throttled', 

704 'ThrottledException', 

705 'Throttling', 

706 'Client.RequestLimitExceeded'))) 

707 

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,))) 

724 

725 return self 

726 

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 = '' 

732 

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] 

741 

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

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

744 

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

746 

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) 

756 

757 

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 

761 

762 :example: 

763 

764 .. code-block:: yaml 

765 

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 """ 

774 

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',) 

783 

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') 

789 

790 # uniqueness constraint validity across apis? 

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

792 

793 futures = {} 

794 results = set() 

795 

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 

807 

808 for f in as_completed(futures): 

809 task_set = futures[f] 

810 

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 

817 

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) 

823 

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

825 

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 

842 

843 return results 

844 

845 

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

847class UpdateRestIntegration(BaseAction): 

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

849 

850 :example: 

851 

852 .. code-block:: yaml 

853 

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 """ 

868 

869 schema = utils.type_schema( 

870 'update-integration', 

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

872 required=['patch']) 

873 permissions = ('apigateway:PATCH',) 

874 

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 

886 

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) 

898 

899 

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

901class DeleteRestIntegration(BaseAction): 

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

903 

904 :example: 

905 

906 .. code-block:: yaml 

907 

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') 

920 

921 def process(self, resources): 

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

923 

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 

933 

934 

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 

938 

939 :example: 

940 

941 .. code-block:: yaml 

942 

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 """ 

951 

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',) 

960 

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') 

966 

967 # uniqueness constraint validity across apis? 

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

969 

970 futures = {} 

971 results = set() 

972 

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 

984 

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] 

999 

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 

1012 

1013 

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

1015class UpdateRestMethod(BaseAction): 

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

1017 

1018 :example: 

1019 

1020 .. code-block:: yaml 

1021 

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 """ 

1037 

1038 schema = utils.type_schema( 

1039 'update-method', 

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

1041 required=['patch']) 

1042 permissions = ('apigateway:GET',) 

1043 

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 

1055 

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) 

1067 

1068 

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

1070class CustomDomainName(query.QueryResourceManager): 

1071 

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' 

1080 

1081 @classmethod 

1082 def get_permissions(cls): 

1083 return ('apigateway:GET',) 

1084 

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 

1099 

1100 

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

1102class DomainNameRemediateTls(BaseAction): 

1103 

1104 schema = type_schema( 

1105 'update-security', 

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

1107 'TLS_1_0', 'TLS_1_2']}, 

1108 required=['securityPolicy']) 

1109 

1110 permissions = ('apigateway:PATCH',) 

1111 

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) 

1117 

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 

1133 

1134 

1135class ApiGwV2DescribeSource(query.DescribeSource): 

1136 

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 

1143 

1144 

1145@resources.register('apigwv2') 

1146class ApiGwV2(query.QueryResourceManager): 

1147 

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() 

1160 

1161 source_mapping = { 

1162 'config': query.ConfigSource, 

1163 'describe': ApiGwV2DescribeSource 

1164 } 

1165 

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 ) 

1180 

1181 return self._generate_arn 

1182 

1183 

1184class StageDescribe(query.ChildDescribeSource): 

1185 

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 

1192 

1193 

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',) 

1206 

1207 source_mapping = { 

1208 "describe-child": StageDescribe, 

1209 "config": query.ConfigSource 

1210 } 

1211 

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] 

1219 

1220