Skip to content

Commit

Permalink
Merge pull request #1699 from pgjones/access_control
Browse files Browse the repository at this point in the history
Add Access Control, CORS (Cross Origin Request Sharing) header methods
  • Loading branch information
davidism committed Jan 14, 2020
2 parents 1a02200 + a4836ed commit 2b2663b
Show file tree
Hide file tree
Showing 6 changed files with 193 additions and 21 deletions.
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

0 comments on commit 2b2663b

Please sign in to comment.