Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial support for CORS #133

Merged
merged 10 commits into from
Oct 10, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 45 additions & 1 deletion README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -635,6 +635,50 @@ the raw body bytes:
you will need to use ``app.current_request.raw_body`` and parse
the request body as needed.

Tutorial: CORS Support
======================

You can specify whether a view supports CORS by adding the
``cors=True`` parameter to your ``@app.route()`` call. By
default this value is false:

.. code-block:: python

@app.route('/supports-cors', methods=['PUT'], cors=True)
def supports_cors():
return {}


Settings ``cors=True`` has similar behavior to enabling CORS
using the AWS Console. This includes:

* Injecting the ``Access-Control-Allow-Origin: *`` header to your
responses, including all error responses you can return.
* Automatically adding an ``OPTIONS`` method so support preflighting
requests.

The preflight request will return a response that includes:

* ``Access-Control-Allow-Origin: *``
* The ``Access-Control-Allow-Methods`` header will return a list of all HTTP
methods you've called out in your view function. In the example above,
this will be ``PUT,OPTIONS``.
* ``Access-Control-Allow-Headers: Content-Type,X-Amz-Date,Authorization,
X-Api-Key,X-Amz-Security-Token``.

There's a couple of things to keep in mind when enabling cors for a view:

* An ``OPTIONS`` method for preflighting is always injected. Ensure that
you don't have ``OPTIONS`` in the ``methods=[...]`` list of your
view function.
* Every view function must explicitly enable CORS support.
* There's no support for customizing the CORS configuration.

The last two points will change in the future. See
`this issue
<https://github.com/awslabs/chalice/issues/70#issuecomment-248787037>`_
for more information.


Tutorial: Policy Generation
===========================
Expand Down Expand Up @@ -785,7 +829,7 @@ auto policy generator detects actions that it would like to add or remove::
.. quick-start-end

Tutorial: Using Custom Authentication
===========================
=====================================

AWS API Gateway routes can be authenticated in multiple ways:
- API Key
Expand Down
19 changes: 8 additions & 11 deletions chalice/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,8 @@ class RouteEntry(object):

def __init__(self, view_function, view_name, path, methods,
authorization_type=None, authorizer_id=None,
api_key_required=None, content_types=None):
api_key_required=None, content_types=None,
cors=False):
self.view_function = view_function
self.view_name = view_name
self.uri_pattern = path
Expand All @@ -110,6 +111,7 @@ def __init__(self, view_function, view_name, path, methods,
#: e.g, '/foo/{bar}/{baz}/qux -> ['bar', 'baz']
self.view_args = self._parse_view_args()
self.content_types = content_types
self.cors = cors

def _parse_view_args(self):
if '{' not in self.uri_pattern:
Expand Down Expand Up @@ -144,6 +146,7 @@ def _add_route(self, path, view_func, **kwargs):
authorizer_id = kwargs.get('authorizer_id', None)
api_key_required = kwargs.get('api_key_required', None)
content_types = kwargs.get('content_types', ['application/json'])
cors = kwargs.get('cors', False)
if not isinstance(content_types, list):
raise ValueError('In view function "%s", the content_types '
'value must be a list, not %s: %s'
Expand All @@ -153,16 +156,10 @@ def _add_route(self, path, view_func, **kwargs):
raise ValueError(
"Duplicate route detected: '%s'\n"
"URL paths must be unique." % path)
self.routes[path] = RouteEntry(
view_func,
name,
path,
methods,
authorization_type,
authorizer_id,
api_key_required,
content_types,
)
entry = RouteEntry(view_func, name, path, methods, authorization_type,
authorizer_id, api_key_required,
content_types, cors)
self.routes[path] = entry

def __call__(self, event, context):
# This is what's invoked via lambda.
Expand Down
11 changes: 9 additions & 2 deletions chalice/app.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -41,15 +41,22 @@ class RouteEntry(object):
# TODO: How so I specify *args, where args is a tuple of strings.
view_function = ... # type: Callable[..., Any]
view_name = ... # type: str
uri_pattern = ... # type: str
methods = ... # type: List[str]
uri_pattern = ... # type: str
authorization_type = ... # type: str
authorizer_id = ... # type: str
api_key_required = ... # type: bool
content_types = ... # type: List[str]
view_args = ... # type: List[str]
cors = ... # type: bool

def __init__(self, view_function: Callable[..., Any],
view_name: str, path: str, methods: List[str],
authorization_type: str=None,
authorizer_id: str=None,
api_key_required: bool=None,
content_types: List[str]=None) -> None: ...
content_types: List[str]=None,
cors: bool=False) -> None: ...

def _parse_view_args(self) -> List[str]: ...

Expand Down
1 change: 1 addition & 0 deletions chalice/cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,7 @@ def deploy(ctx, project_dir, autogen_policy, profile, stage):
e.exit_code = 2
raise e
except Exception as e:
raise
e = click.ClickException("Error when deploying: %s" % e)
e.exit_code = 1
raise e
Expand Down
132 changes: 100 additions & 32 deletions chalice/deployer.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@
from chalice.awsclient import TypedAWSClient
from chalice import compat


LAMBDA_TRUST_POLICY = {
"Version": "2012-10-17",
"Statement": [{
Expand All @@ -38,7 +37,6 @@
]
}


CLOUDWATCH_LOGS = {
"Effect": "Allow",
"Action": [
Expand All @@ -49,7 +47,6 @@
"Resource": "arn:aws:logs:*:*:*"
}


FULL_PASSTHROUGH = """
#set($allParams = $input.params())
{
Expand Down Expand Up @@ -97,7 +94,6 @@
}
"""


ERROR_MAPPING = (
"#set($inputRoot = $input.path('$'))"
"{"
Expand Down Expand Up @@ -219,14 +215,12 @@ def node(name, uri_path, is_route=False):


class NoPrompt(object):

def confirm(self, text, default=False, abort=False):
# type: (str, bool, bool) -> bool
return default


class Deployer(object):

def __init__(self, apigateway_deploy, lambda_deploy):
# type: (APIGatewayDeployer, LambdaDeployer) -> None
self._apigateway_deploy = apigateway_deploy
Expand Down Expand Up @@ -299,6 +293,9 @@ def build_resources(self, chalice_trie):
assert current['route_entry'] is not None, current
for http_method in current['route_entry'].methods:
self._configure_resource_route(current, http_method)
if current['route_entry'].cors:
self._add_options_preflight_request(
current, current['route_entry'].methods)
for child in current['children']:
stack.append(current['children'][child])
# Add a catch all auth that says anything in this rest API can call
Expand Down Expand Up @@ -358,37 +355,111 @@ def _configure_resource_route(self, node, http_method):
uri=self._lambda_uri()
)
# Success case.
c.put_integration_response(
integration_response_args = {
'restApiId': self.rest_api_id,
'resourceId': node['resource_id'],
'httpMethod': http_method,
'statusCode': '200',
'responseTemplates': {'application/json': ''},
}
method_response_args = {
'restApiId': self.rest_api_id,
'resourceId': node['resource_id'],
'httpMethod': http_method,
'statusCode': '200',
'responseModels': {'application/json': 'Empty'},
}
if route_entry.cors:
method_response_args['responseParameters'] = {
'method.response.header.Access-Control-Allow-Origin': False}
c.put_method_response(**method_response_args)
if route_entry.cors:
integration_response_args['responseParameters'] = {
'method.response.header.Access-Control-Allow-Origin': "'*'"}
c.put_integration_response(**integration_response_args)
self._add_error_responses(http_method, node, route_entry, c)

def _add_error_responses(self, http_method, node, route_entry, client):
# type: (str, Dict[str, Any], app.RouteEntry, Any) -> None
for error_cls in app.ALL_ERRORS:
method_response_args = {
'restApiId': self.rest_api_id,
'resourceId': node['resource_id'],
'httpMethod': http_method,
'statusCode': str(error_cls.STATUS_CODE),
'responseModels': {'application/json': 'Empty'},
}
if route_entry.cors:
method_response_args['responseParameters'] = {
'method.response.header.Access-Control-Allow-Origin':
False}
client.put_method_response(**method_response_args)
integration_response_args = {
'restApiId': self.rest_api_id,
'resourceId': node['resource_id'],
'httpMethod': http_method,
'statusCode': str(error_cls.STATUS_CODE),
'selectionPattern': error_cls.__name__ + '.*',
'responseTemplates': {'application/json': ERROR_MAPPING},
}
if route_entry.cors:
integration_response_args['responseParameters'] = {
'method.response.header.Access-Control-Allow-Origin':
"'*'"}
client.put_integration_response(**integration_response_args)

def _add_options_preflight_request(self, node, http_methods):
# type: (Dict[str, Any], List[str]) -> None
# If CORs is configured we also need to set up
# an OPTIONS method for them for preflight requests.
# TODO: We should probably warn/error if they've also configured
# the view function to support an OPTIONs method.
c = self.client
c.put_method(
restApiId=self.rest_api_id,
resourceId=node['resource_id'],
httpMethod=http_method,
statusCode='200',
responseTemplates={'application/json': ''},
httpMethod='OPTIONS',
authorizationType='NONE',
)
c.put_integration(
restApiId=self.rest_api_id,
resourceId=node['resource_id'],
httpMethod='OPTIONS',
type='MOCK',
requestTemplates={
'application/json': '{"statusCode": 200}',
},
)
c.put_method_response(
restApiId=self.rest_api_id,
resourceId=node['resource_id'],
httpMethod=http_method,
httpMethod='OPTIONS',
statusCode='200',
responseModels={'application/json': 'Empty'},
responseParameters={
"method.response.header.Access-Control-Allow-Origin": False,
"method.response.header.Access-Control-Allow-Methods": False,
"method.response.header.Access-Control-Allow-Headers": False,
},
)
if 'OPTIONS' not in http_methods:
http_methods.append('OPTIONS')
allowed_methods = ','.join(http_methods)
c.put_integration_response(
restApiId=self.rest_api_id,
resourceId=node['resource_id'],
httpMethod='OPTIONS',
statusCode='200',
responseTemplates={'application/json': ''},
responseParameters={
"method.response.header.Access-Control-Allow-Origin": "'*'",
"method.response.header.Access-Control-Allow-Methods": (
"'%s'" % allowed_methods),
"method.response.header.Access-Control-Allow-Headers": (
"'Content-Type,X-Amz-Date,Authorization,X-Api-Key"
",X-Amz-Security-Token'")
},
)
# And we have to create a pair for each error type.
for error_cls in app.ALL_ERRORS:
c.put_integration_response(
restApiId=self.rest_api_id,
resourceId=node['resource_id'],
httpMethod=http_method,
statusCode=str(error_cls.STATUS_CODE),
selectionPattern=error_cls.__name__ + '.*',
responseTemplates={'application/json': ERROR_MAPPING},
)
c.put_method_response(
restApiId=self.rest_api_id,
resourceId=node['resource_id'],
httpMethod=http_method,
statusCode=str(error_cls.STATUS_CODE),
responseModels={'application/json': 'Empty'},
)

def _lambda_uri(self):
# type: () -> str
Expand All @@ -404,7 +475,6 @@ def _lambda_uri(self):


class LambdaDeploymentPackager(object):

def _create_virtualenv(self, venv_dir):
# type: (str) -> None
# The original implementation used Popen(['virtualenv', ...])
Expand Down Expand Up @@ -505,7 +575,7 @@ def _add_app_files(self, zip, project_dir):
chalice_init = inspect.getfile(chalice)
if chalice_init.endswith('.pyc'):
chalice_init = chalice_init[:-1]
zip.write(chalice_router, 'chalice/__init__.py')
zip.write(chalice_init, 'chalice/__init__.py')

zip.write(os.path.join(project_dir, 'app.py'),
'app.py')
Expand Down Expand Up @@ -559,7 +629,6 @@ def inject_latest_app(self, deployment_package_filename, project_dir):


class LambdaDeployer(object):

def __init__(self,
aws_client, # type: TypedAWSClient
packager, # type: LambdaDeploymentPackager
Expand Down Expand Up @@ -718,7 +787,6 @@ def _create_role_from_source_code(self, config):


class APIGatewayDeployer(object):

def __init__(self,
aws_client, # type: TypedAWSClient
api_gateway_client, # type: Any
Expand Down
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ tox==2.2.1
wheel==0.26.0
doc8==0.7.0
pylint==1.5.5
pytest-cov==2.3.0
pytest-cov==2.3.1
pydocstyle==1.0.0
-rrequirements-test.txt
2 changes: 1 addition & 1 deletion requirements-test.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pytest==3.0.2
pytest==3.0.3
py==1.4.31
pygments==2.1.3
mock==2.0.0
Expand Down
Loading