# Copyright 2016-2017 Capital One Services, LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from __future__ import absolute_import, division, print_function, unicode_literals
from botocore.exceptions import ClientError
from concurrent.futures import as_completed
from c7n.actions import ActionRegistry, BaseAction
from c7n.filters import FilterRegistry, ValueFilter
from c7n.filters.iamaccess import CrossAccountAccessFilter
from c7n.manager import resources, ResourceManager
from c7n import query, utils
ANNOTATION_KEY_MATCHED_METHODS = 'c7n:matched-resource-methods'
ANNOTATION_KEY_MATCHED_INTEGRATIONS = 'c7n:matched-method-integrations'
[docs]@resources.register('rest-account')
class RestAccount(ResourceManager):
filter_registry = FilterRegistry('rest-account.filters')
action_registry = ActionRegistry('rest-account.actions')
[docs] class resource_type(object):
service = 'apigateway'
name = id = 'account_id'
dimensions = None
arn = False
[docs] @classmethod
def get_permissions(cls):
return ('apigateway:GET',)
[docs] @classmethod
def has_arn(self):
return False
[docs] def get_model(self):
return self.resource_type
def _get_account(self):
client = utils.local_session(self.session_factory).client('apigateway')
try:
account = client.get_account()
except ClientError as e:
if e.response['Error']['Code'] == 'NotFoundException':
return []
account.pop('ResponseMetadata', None)
account['account_id'] = 'apigw-settings'
return [account]
[docs] def resources(self):
return self.filter_resources(self._get_account())
[docs] def get_resources(self, resource_ids):
return self._get_account()
OP_SCHEMA = {
'type': 'object',
'required': ['op', 'path'],
'additonalProperties': False,
'properties': {
'op': {'enum': ['add', 'remove', 'update', 'copy', 'replace', 'test']},
'path': {'type': 'string'},
'value': {'type': 'string'},
'from': {'type': 'string'}
}
}
[docs]@RestAccount.action_registry.register('update')
class UpdateAccount(BaseAction):
"""Update the cloudwatch role associated to a rest account
:example:
.. code-block:: yaml
policies:
- name: correct-rest-account-log-role
resource: rest-account
filters:
- cloudwatchRoleArn: arn:aws:iam::000000000000:role/GatewayLogger
actions:
- type: update
patch:
- op: replace
path: /cloudwatchRoleArn
value: arn:aws:iam::000000000000:role/BetterGatewayLogger
"""
permissions = ('apigateway:PATCH',)
schema = utils.type_schema(
'update',
patch={'type': 'array', 'items': OP_SCHEMA},
required=['patch'])
[docs] def process(self, resources):
client = utils.local_session(
self.manager.session_factory).client('apigateway')
client.update_account(patchOperations=self.data['patch'])
[docs]@resources.register('rest-api')
class RestApi(query.QueryResourceManager):
[docs] class resource_type(object):
service = 'apigateway'
type = 'restapis'
enum_spec = ('get_rest_apis', 'items', None)
id = 'id'
filter_name = None
name = 'name'
date = 'createdDate'
dimension = 'GatewayName'
config_type = "AWS::ApiGateway::RestApi"
[docs]@RestApi.filter_registry.register('cross-account')
class RestApiCrossAccount(CrossAccountAccessFilter):
policy_attribute = 'policy'
permissions = ('apigateway:GET',)
[docs]@RestApi.action_registry.register('update')
class UpdateApi(BaseAction):
"""Update configuration of a REST API.
Non-exhaustive list of updateable attributes.
https://docs.aws.amazon.com/apigateway/api-reference/link-relation/restapi-update/#remarks
:example:
contrived example to update description on api gateways
.. code-block:: yaml
policies:
- name: apigw-description
resource: rest-api
filters:
- description: empty
actions:
- type: update
patch:
- op: replace
path: /description
value: "not empty :-)"
"""
permissions = ('apigateway:PATCH',)
schema = utils.type_schema(
'update',
patch={'type': 'array', 'items': OP_SCHEMA},
required=['patch'])
[docs] def process(self, resources):
client = utils.local_session(
self.manager.session_factory).client('apigateway')
for r in resources:
client.update_rest_api(
restApiId=r['id'],
patchOperations=self.data['patch'])
[docs]@resources.register('rest-stage')
class RestStage(query.ChildResourceManager):
child_source = 'describe-rest-stage'
[docs] class resource_type(object):
service = 'apigateway'
parent_spec = ('rest-api', 'restApiId', None)
enum_spec = ('get_stages', 'item', None)
name = id = 'stageName'
date = 'createdDate'
dimension = None
universal_taggable = True
type = None
config_type = "AWS::ApiGateway::Stage"
[docs] def get_source(self, source_type):
if source_type == 'describe-rest-stage':
return DescribeRestStage(self)
return super(RestStage, self).get_source(source_type)
[docs]@query.sources.register('describe-rest-stage')
class DescribeRestStage(query.ChildDescribeSource):
[docs] def get_query(self):
query = super(DescribeRestStage, self).get_query()
query.capture_parent_id = True
return query
[docs] def augment(self, resources):
results = []
# Using capture parent, changes the protocol
for parent_id, r in resources:
r['restApiId'] = parent_id
tags = r.setdefault('Tags', [])
for k, v in r.pop('tags', {}).items():
tags.append({
'Key': k,
'Value': v})
results.append(r)
return results
[docs]@RestStage.action_registry.register('update')
class UpdateStage(BaseAction):
"""Update/remove values of an api stage
:example:
.. code-block:: yaml
policies:
- name: disable-stage-caching
resource: rest-stage
filters:
- methodSettings."*/*".cachingEnabled: true
actions:
- type: update
patch:
- op: replace
path: /*/*/caching/enabled
value: 'false'
"""
permissions = ('apigateway:PATCH',)
schema = utils.type_schema(
'update',
patch={'type': 'array', 'items': OP_SCHEMA},
required=['patch'])
[docs] def process(self, resources):
client = utils.local_session(
self.manager.session_factory).client('apigateway')
for r in resources:
self.manager.retry(
client.update_stage,
restApiId=r['restApiId'],
stageName=r['stageName'],
patchOperations=self.data['patch'])
[docs]@RestStage.action_registry.register('delete')
class DeleteStage(BaseAction):
"""Delete an api stage
:example:
.. code-block: yaml
policies:
- name: delete-rest-stage
resource: rest-stage
filters:
- methodSettings."*/*".cachingEnabled: true
actions:
- type: delete
"""
permissions = ('apigateway:Delete',)
schema = utils.type_schema('delete')
[docs] def process(self, resources):
client = utils.local_session(self.manager.session_factory).client('apigateway')
for r in resources:
try:
self.manager.retry(
client.delete_stage,
restApiId=r['restApiId'],
stageName=r['stageName'])
except client.exceptions.NotFoundException:
pass
[docs]@resources.register('rest-resource')
class RestResource(query.ChildResourceManager):
child_source = 'describe-rest-resource'
[docs] class resource_type(object):
service = 'apigateway'
parent_spec = ('rest-api', 'restApiId', None)
enum_spec = ('get_resources', 'items', None)
id = 'id'
name = 'path'
dimension = None
[docs]@query.sources.register('describe-rest-resource')
class DescribeRestResource(query.ChildDescribeSource):
[docs] def get_query(self):
query = super(DescribeRestResource, self).get_query()
query.capture_parent_id = True
return query
[docs] def augment(self, resources):
results = []
# Using capture parent id, changes the protocol
for parent_id, r in resources:
r['restApiId'] = parent_id
results.append(r)
return results
[docs]@resources.register('rest-vpclink')
class RestApiVpcLink(query.QueryResourceManager):
[docs] class resource_type(object):
service = 'apigateway'
type = None
dimension = None
enum_spec = ('get_vpc_links', 'items', None)
id = 'id'
filter_name = None
name = 'name'
[docs]@RestResource.filter_registry.register('rest-integration')
class FilterRestIntegration(ValueFilter):
"""Filter rest resources based on a key value for the rest method integration of the api
:example:
.. code-block:: yaml
policies:
- name: api-method-integrations-with-type-aws
resource: rest-resource
filters:
- type: rest-integration
key: type
value: AWS
"""
schema = utils.type_schema(
'rest-integration',
method={'type': 'string', 'enum': [
'all', 'ANY', 'PUT', 'GET', "POST",
"DELETE", "OPTIONS", "HEAD", "PATCH"]},
rinherit=ValueFilter.schema)
permissions = ('apigateway:GET',)
[docs] def process(self, resources, event=None):
method_set = self.data.get('method', 'all')
# 10 req/s with burst to 40
client = utils.local_session(
self.manager.session_factory).client('apigateway')
# uniqueness constraint validity across apis?
resource_map = {r['id']: r for r in resources}
futures = {}
results = set()
with self.executor_factory(max_workers=2) as w:
tasks = []
for r in resources:
r_method_set = method_set
if method_set == 'all':
r_method_set = r.get('resourceMethods', {}).keys()
for m in r_method_set:
tasks.append((r, m))
for task_set in utils.chunks(tasks, 20):
futures[w.submit(
self.process_task_set, client, task_set)] = task_set
for f in as_completed(futures):
task_set = futures[f]
if f.exception():
self.manager.log.warning(
"Error retrieving integrations on resources %s",
["%s:%s" % (r['restApiId'], r['path'])
for r, mt in task_set])
continue
for i in f.result():
if self.match(i):
results.add(i['resourceId'])
resource_map[i['resourceId']].setdefault(
ANNOTATION_KEY_MATCHED_INTEGRATIONS, []).append(i)
return [resource_map[rid] for rid in results]
[docs] def process_task_set(self, client, task_set):
results = []
for r, m in task_set:
try:
integration = client.get_integration(
restApiId=r['restApiId'],
resourceId=r['id'],
httpMethod=m)
integration.pop('ResponseMetadata', None)
integration['restApiId'] = r['restApiId']
integration['resourceId'] = r['id']
integration['resourceHttpMethod'] = m
results.append(integration)
except ClientError as e:
if e.response['Error']['Code'] == 'NotFoundException':
pass
return results
[docs]@RestResource.action_registry.register('update-integration')
class UpdateRestIntegration(BaseAction):
"""Change or remove api integration properties based on key value
:example:
.. code-block: yaml
policies:
- name: enforce-timeout-on-api-integration
resource: rest-resource
filters:
- type: rest-integration
key: timeoutInMillis
value: 29000
actions:
- type: update-integration
patch:
- op: replace
path: /timeoutInMillis
value: "3000"
"""
schema = utils.type_schema(
'update-integration',
patch={'type': 'array', 'items': OP_SCHEMA},
required=['patch'])
permissions = ('apigateway:PATCH',)
[docs] def validate(self):
found = False
for f in self.manager.iter_filters():
if isinstance(f, FilterRestIntegration):
found = True
break
if not found:
raise ValueError(
("update-integration action requires ",
"rest-integration filter usage in policy"))
return self
[docs] def process(self, resources):
client = utils.local_session(
self.manager.session_factory).client('apigateway')
ops = self.data['patch']
for r in resources:
for i in r.get(ANNOTATION_KEY_MATCHED_INTEGRATIONS, []):
client.update_integration(
restApiId=i['restApiId'],
resourceId=i['resourceId'],
httpMethod=i['resourceHttpMethod'],
patchOperations=ops)
[docs]@RestResource.action_registry.register('delete-integration')
class DeleteRestIntegration(BaseAction):
"""Delete an api integration. Useful if the integration type is a security risk.
:example:
.. code-block: yaml
policies:
- name: enforce-no-resource-integration-with-type-aws
resource: rest-resource
filters:
- type: rest-integration
key: type
value: AWS
actions:
- type: delete-integration
"""
permissions = ('apigateway:Delete',)
schema = utils.type_schema('delete-integration')
[docs] def process(self, resources):
client = utils.local_session(self.manager.session_factory).client('apigateway')
for r in resources:
for i in r.get(ANNOTATION_KEY_MATCHED_INTEGRATIONS, []):
try:
client.delete_integration(
restApiId=i['restApiId'],
resourceId=i['resourceId'],
httpMethod=i['resourceHttpMethod'])
except client.exceptions.NotFoundException:
continue
[docs]@RestResource.filter_registry.register('rest-method')
class FilterRestMethod(ValueFilter):
"""Filter rest resources based on a key value for the rest method of the api
:example:
.. code-block:: yaml
policies:
- name: api-without-key-required
resource: rest-resource
filters:
- type: rest-method
key: apiKeyRequired
value: false
"""
schema = utils.type_schema(
'rest-method',
method={'type': 'string', 'enum': [
'all', 'ANY', 'PUT', 'GET', "POST",
"DELETE", "OPTIONS", "HEAD", "PATCH"]},
rinherit=ValueFilter.schema)
permissions = ('apigateway:GET',)
[docs] def process(self, resources, event=None):
method_set = self.data.get('method', 'all')
# 10 req/s with burst to 40
client = utils.local_session(
self.manager.session_factory).client('apigateway')
# uniqueness constraint validity across apis?
resource_map = {r['id']: r for r in resources}
futures = {}
results = set()
with self.executor_factory(max_workers=2) as w:
tasks = []
for r in resources:
r_method_set = method_set
if method_set == 'all':
r_method_set = r.get('resourceMethods', {}).keys()
for m in r_method_set:
tasks.append((r, m))
for task_set in utils.chunks(tasks, 20):
futures[w.submit(
self.process_task_set, client, task_set)] = task_set
for f in as_completed(futures):
task_set = futures[f]
if f.exception():
self.manager.log.warning(
"Error retrieving methods on resources %s",
["%s:%s" % (r['restApiId'], r['path'])
for r, mt in task_set])
continue
for m in f.result():
if self.match(m):
results.add(m['resourceId'])
resource_map[m['resourceId']].setdefault(
ANNOTATION_KEY_MATCHED_METHODS, []).append(m)
return [resource_map[rid] for rid in results]
[docs] def process_task_set(self, client, task_set):
results = []
for r, m in task_set:
method = client.get_method(
restApiId=r['restApiId'],
resourceId=r['id'],
httpMethod=m)
method.pop('ResponseMetadata', None)
method['restApiId'] = r['restApiId']
method['resourceId'] = r['id']
results.append(method)
return results
[docs]@RestResource.action_registry.register('update-method')
class UpdateRestMethod(BaseAction):
"""Change or remove api method behaviors based on key value
:example:
.. code-block: yaml
policies:
- name: enforce-iam-permissions-on-api
resource: rest-resource
filters:
- type: rest-method
key: authorizationType
value: NONE
op: eq
actions:
- type: update-method
patch:
- op: replace
path: /authorizationType
value: AWS_IAM
"""
schema = utils.type_schema(
'update-method',
patch={'type': 'array', 'items': OP_SCHEMA},
required=['patch'])
permissions = ('apigateway:GET',)
[docs] def validate(self):
found = False
for f in self.manager.iter_filters():
if isinstance(f, FilterRestMethod):
found = True
break
if not found:
raise ValueError(
("update-method action requires ",
"rest-method filter usage in policy"))
return self
[docs] def process(self, resources):
client = utils.local_session(
self.manager.session_factory).client('apigateway')
ops = self.data['patch']
for r in resources:
for m in r.get(ANNOTATION_KEY_MATCHED_METHODS, []):
client.update_method(
restApiId=m['restApiId'],
resourceId=m['resourceId'],
httpMethod=m['httpMethod'],
patchOperations=ops)