Skip to content

Commit

Permalink
Add Access Control, CORS (Cross Origin Request Sharing) header methods
Browse files Browse the repository at this point in the history
This should make it a little easier to get and set access control
headers as it ensures the types and naming is correct. It is also
intentionally very minimal like the other header accessors.
  • Loading branch information
pgjones authored and davidism committed Jan 14, 2020
1 parent 1c4837a commit 73358a8
Show file tree
Hide file tree
Showing 5 changed files with 140 additions and 2 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
methods to the Request and Response wrappers. :pr:`1699`


Version 0.16.1
Expand Down
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:`BaseRequest` subclasses. ``Request`` classes
that subclass this will get 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:`BaseResponse` subclasses. ``Response``
classes that subclass this will get 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.",
)
5 changes: 5 additions & 0 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,6 +13,7 @@ class Request(
ETagRequestMixin,
UserAgentMixin,
AuthorizationMixin,
CORSRequestMixin,
CommonRequestDescriptorsMixin,
):
"""Full featured request object implementing the following mixins:
Expand All @@ -20,7 +22,10 @@ class Request(
- :class:`ETagRequestMixin` for etag and cache control handling
- :class:`UserAgentMixin` for user agent introspection
- :class:`AuthorizationMixin` for http auth handling
- :class:`RequestCORSMixin` for Cross Origin Resource Sharing
headers
- :class:`CommonRequestDescriptorsMixin` for common headers
"""


Expand Down
8 changes: 6 additions & 2 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,17 @@ 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:`WWWAuthenticateMixin` for HTTP authentication support
- :class:`ResponseCORSMixin` for Cross Origin Resource Sharing
headers
- :class:`ResponseStreamMixin` to add support for the `stream` property
- :class:`CommonResponseDescriptorsMixin` for various HTTP descriptors
- :class:`WWWAuthenticateMixin` for HTTP authentication support
"""
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 73358a8

Please sign in to comment.