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 Access Control, CORS (Cross Origin Request Sharing) header methods #1699

Merged
merged 2 commits into from Jan 14, 2020
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.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGES.rst
Expand Up @@ -88,6 +88,8 @@ Unreleased
and kwargs. :pr:`1687, 1697`
- The development server accepts paths that start with two slashes,
rather than stripping off the first path segment. :issue:`491`
- Add access control (Cross Origin Request Sharing, CORS) header
properties to the ``Request`` and ``Response`` wrappers. :pr:`1699`


Version 0.16.1
Expand Down
54 changes: 43 additions & 11 deletions docs/wrappers.rst
Expand Up @@ -150,30 +150,64 @@ and :class:`BaseResponse` classes and implement all the mixins Werkzeug provides

.. autoclass:: Response

.. autoclass:: AcceptMixin
:members:

.. autoclass:: AuthorizationMixin
:members:
Common Descriptors
------------------

.. autoclass:: ETagRequestMixin
.. autoclass:: CommonRequestDescriptorsMixin
:members:

.. autoclass:: ETagResponseMixin
.. autoclass:: CommonResponseDescriptorsMixin
:members:


Response Stream
---------------

.. autoclass:: ResponseStreamMixin
:members:

.. autoclass:: CommonRequestDescriptorsMixin

Accept
------

.. autoclass:: AcceptMixin
:members:

.. autoclass:: CommonResponseDescriptorsMixin

Authentication
--------------

.. autoclass:: AuthorizationMixin
:members:

.. autoclass:: WWWAuthenticateMixin
:members:


CORS
----

.. autoclass:: werkzeug.wrappers.cors.CORSRequestMixin
:members:

.. autoclass:: werkzeug.wrappers.cors.CORSResponseMixin
:members:


ETag
----

.. autoclass:: ETagRequestMixin
:members:

.. autoclass:: ETagResponseMixin
:members:


User Agent
----------

.. autoclass:: UserAgentMixin
:members:

Expand All @@ -189,10 +223,8 @@ opted into by creating your own subclasses::
pass


.. module:: werkzeug.wrappers.json

JSON
----

.. autoclass:: JSONMixin
.. autoclass:: werkzeug.wrappers.json.JSONMixin
:members:
102 changes: 102 additions & 0 deletions src/werkzeug/wrappers/cors.py
@@ -0,0 +1,102 @@
from ..http import dump_header
from ..http import parse_set_header
from ..utils import environ_property
from ..utils import header_property


class CORSRequestMixin(object):
"""A mixin for :class:`~werkzeug.wrappers.BaseRequest` subclasses
that adds descriptors for Cross Origin Resource Sharing (CORS)
headers.

.. versionadded:: 1.0
"""

origin = environ_property(
"HTTP_ORIGIN",
doc=(
"The host that the request originated from. Set"
" :attr:`~CORSResponseMixin.access_control_allow_origin` on"
" the response to indicate which origins are allowed."
),
)

access_control_request_headers = environ_property(
"HTTP_ACCESS_CONTROL_REQUEST_HEADERS",
load_func=parse_set_header,
doc=(
"Sent with a preflight request to indicate which headers"
" will be sent with the cross origin request. Set"
" :attr:`~CORSResponseMixin.access_control_allow_headers`"
" on the response to indicate which headers are allowed."
),
)

access_control_request_method = environ_property(
"HTTP_ACCESS_CONTROL_REQUEST_METHOD",
doc=(
"Sent with a preflight request to indicate which method"
" will be used for the cross origin request. Set"
" :attr:`~CORSResponseMixin.access_control_allow_methods`"
" on the response to indicate which methods are allowed."
),
)


class CORSResponseMixin(object):
"""A mixin for :class:`~werkzeug.wrappers.BaseResponse` subclasses
that adds descriptors for Cross Origin Resource Sharing (CORS)
headers.

.. versionadded:: 1.0
"""

@property
def access_control_allow_credentials(self):
"""Whether credentials can be shared by the browser to
JavaScript code. As part of the preflight request it indicates
whether credentials can be used on the cross origin request.
"""
return "Access-Control-Allow-Credentials" in self.headers

@access_control_allow_credentials.setter
def access_control_allow_credentials(self, value):
if value is True:
self.headers["Access-Control-Allow-Credentials"] = "true"
else:
self.headers.pop("Access-Control-Allow-Credentials", None)

access_control_allow_headers = header_property(
"Access-Control-Allow-Headers",
load_func=parse_set_header,
dump_func=dump_header,
doc="Which headers can be sent with the cross origin request.",
)

access_control_allow_methods = header_property(
"Access-Control-Allow-Methods",
load_func=parse_set_header,
dump_func=dump_header,
doc="Which methods can be used for the cross origin request.",
)

access_control_allow_origin = header_property(
"Access-Control-Allow-Origin",
load_func=parse_set_header,
dump_func=dump_header,
doc="The origins that may make cross origin requests.",
)

access_control_expose_headers = header_property(
"Access-Control-Expose-Headers",
load_func=parse_set_header,
dump_func=dump_header,
doc="Which headers can be shared by the browser to JavaScript code.",
)

access_control_max_age = header_property(
"Access-Control-Max-Age",
load_func=int,
dump_func=str,
doc="The maximum age in seconds the access control settings can be cached for.",
)
15 changes: 10 additions & 5 deletions src/werkzeug/wrappers/request.py
Expand Up @@ -2,6 +2,7 @@
from .auth import AuthorizationMixin
from .base_request import BaseRequest
from .common_descriptors import CommonRequestDescriptorsMixin
from .cors import CORSRequestMixin
from .etag import ETagRequestMixin
from .user_agent import UserAgentMixin

Expand All @@ -12,15 +13,19 @@ class Request(
ETagRequestMixin,
UserAgentMixin,
AuthorizationMixin,
CORSRequestMixin,
CommonRequestDescriptorsMixin,
):
"""Full featured request object implementing the following mixins:

- :class:`AcceptMixin` for accept header parsing
- :class:`ETagRequestMixin` for etag and cache control handling
- :class:`UserAgentMixin` for user agent introspection
- :class:`AuthorizationMixin` for http auth handling
- :class:`CommonRequestDescriptorsMixin` for common headers
- :class:`AcceptMixin` for accept header parsing
- :class:`ETagRequestMixin` for etag and cache control handling
- :class:`UserAgentMixin` for user agent introspection
- :class:`AuthorizationMixin` for http auth handling
- :class:`~werkzeug.wrappers.cors.CORSRequestMixin` for Cross
Origin Resource Sharing headers
- :class:`CommonRequestDescriptorsMixin` for common headers

"""


Expand Down
16 changes: 11 additions & 5 deletions src/werkzeug/wrappers/response.py
Expand Up @@ -2,6 +2,7 @@
from .auth import WWWAuthenticateMixin
from .base_response import BaseResponse
from .common_descriptors import CommonResponseDescriptorsMixin
from .cors import CORSResponseMixin
from .etag import ETagResponseMixin


Expand Down Expand Up @@ -65,14 +66,19 @@ def stream(self):
class Response(
BaseResponse,
ETagResponseMixin,
WWWAuthenticateMixin,
CORSResponseMixin,
ResponseStreamMixin,
CommonResponseDescriptorsMixin,
WWWAuthenticateMixin,
):
"""Full featured response object implementing the following mixins:

- :class:`ETagResponseMixin` for etag and cache control handling
- :class:`ResponseStreamMixin` to add support for the `stream` property
- :class:`CommonResponseDescriptorsMixin` for various HTTP descriptors
- :class:`WWWAuthenticateMixin` for HTTP authentication support
- :class:`ETagResponseMixin` for etag and cache control handling
- :class:`WWWAuthenticateMixin` for HTTP authentication support
- :class:`~werkzeug.wrappers.cors.CORSResponseMixin` for Cross
Origin Resource Sharing headers
- :class:`ResponseStreamMixin` to add support for the ``stream``
property
- :class:`CommonResponseDescriptorsMixin` for various HTTP
descriptors
"""
25 changes: 25 additions & 0 deletions tests/test_wrappers.py
Expand Up @@ -270,6 +270,31 @@ def failing_application(request):
assert resp.status_code == 400


def test_request_access_control():
request = wrappers.Request.from_values(
headers={
"Origin": "https://palletsprojects.com",
"Access-Control-Request-Headers": "X-A, X-B",
"Access-Control-Request-Method": "PUT",
},
)
assert request.origin == "https://palletsprojects.com"
assert request.access_control_request_headers == {"X-A", "X-B"}
assert request.access_control_request_method == "PUT"


def test_response_access_control():
response = wrappers.Response("Hello World")
assert response.access_control_allow_credentials is False
response.access_control_allow_credentials = True
response.access_control_allow_headers = ["X-A", "X-B"]
assert response.headers["Access-Control-Allow-Credentials"] == "true"
assert set(response.headers["Access-Control-Allow-Headers"].split(", ")) == {
"X-A",
"X-B",
}


def test_base_response():
# unicode
response = wrappers.BaseResponse(u"öäü")
Expand Down