Skip to content
Commits on Source (4)
Tue 16 Jan 2018 Yasuhiro Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Bump version v0.1.1
* Add csp_coverage (HTTP CSP Header support) tween
Tue 09 Jan 2018 Yasuhiro Asaka <yasuhiro.asaka@grauwoelfchen.net>
* Bump version v0.1.0
* Make configuration as separated by tween
......
......@@ -14,14 +14,15 @@ Pyramid Secure Response
:alt: Version
`pyramid_secure_response`_ handles insecure request to provide secure response.
`pyramid_secure_response`_ handles request to provide secure response.
* redirects http as https
* sets HSTS Header to response
* sets CSP Header to response
For response headers (hsts), the tweens do not set anything if that
already exist in your response. This means you can set by yourself as you need
(e.g. at view action)
For response headers (HSTS, CSP), the tweens do not set anything if that
already exist in your response. This means you can set these values by yourself
as you need (e.g. at view action)
Repository
......@@ -41,14 +42,16 @@ Install
Features
--------
``pyramid_secure_response`` has 2 tweens:
``pyramid_secure_response`` has 3 tweens:
* HTTP Redirecton (`ssl_redirect`_, +http+ request will be redirected as
+https+ on same host)
* HSTS Support (`hsts_support`_, The ``HTTP Strict Transport Security`` will be
set in response)
* CSP Coverage ( `csp_coverage`_, The ``Content Security Policy`` will be set
in response)
With an additional feature.
With few additional features.
* Ignore path filter (matched paths with ``str.startswith()`` will be ignored)
......@@ -69,8 +72,8 @@ like `PasteDeploy`_ config file.
pyramid.includes =
pyramid_secure_response
Python
~~~~~~
Code
~~~~
Or you can include in python code.
......@@ -84,6 +87,7 @@ It's also available to add tween(s) directly, as you need.
config.add_tween('pyramid_secure_response.ssl_redirect.tween')
config.add_tween('pyramid_secure_response.hsts_support.tween')
config.add_tween('pyramid_secure_response.csp_coverage.tween')
You may want to add also kwargs ``under`` or ``over``. (
See `pyramid.config.Configurator.add_tween`_.)
......@@ -96,6 +100,8 @@ By default, *ssl_redirect* tween will be handled before another tweens.
over=tweens.MAIN)
config.add_tween('pyramid_secure_response.hsts_support.tween',
over=tweens.MAIN, under='pyramid_secure_response.ssl_redirect.tween')
config.add_tween('pyramid_secure_response.csp_coverage.tween',
over=tweens.MAIN, under='pyramid_secure_response.ssl_redirect.tween')
Configuration
*************
......@@ -111,6 +117,10 @@ For example:
pyramid_secure_response.hsts_support.include_subdomains = True
pyramid_secure_response.hsts_support.preload = True
pyramid_secure_response.csp_coverage.enabled = True
pyramid_secure_response.csp_coverage.script_src = https://example.com
pyramid_secure_response.csp_coverage.default_src = self
# fallback (global)
pyramid_secure_response.proto_header = X-Forwarded-Proto
pyramid_secure_response.ignore_paths =
......@@ -120,7 +130,8 @@ For example:
Default values
**************
(ssl_redirect)
ssl_redirect
~~~~~~~~~~~~
+--------------+----------------+--------+-------------------------+
| Key | Value (INI) | Type | Note |
......@@ -129,16 +140,17 @@ Default values
| | | | tween |
+--------------+----------------+--------+-------------------------+
| proto_header | ``''`` | *str* | An header like |
| | | | *X-Forwarded-Proto*. |
| | | | *X-Forwarded-Proto* |
| | | | Checked in criteria as |
| | | | ``'https'``, if exists. |
| | | | ``'https'`` if exists |
+--------------+----------------+--------+-------------------------+
| ignore_paths | ``''`` | *list* | Splittable string like |
| | | | *\n/path\n/path\n*. |
| | | | Skiped, if matched. |
| | | | *\n/path\n/path\n* |
| | | | Skipped if matched |
+--------------+----------------+--------+-------------------------+
(hsts_support)
hsts_support
~~~~~~~~~~~~
+--------------------+----------------+--------+-------------------------+
| Key | Value (INI) | Type | Note |
......@@ -156,15 +168,115 @@ Default values
| | | | HSTS Header |
+--------------------+----------------+--------+-------------------------+
| proto_header | ``''`` | *str* | An header like |
| | | | *X-Forwarded-Proto*. |
| | | | *X-Forwarded-Proto* |
| | | | Checked in criteria as |
| | | | ``'https'``, if exists. |
| | | | ``'https'`` if exists |
+--------------------+----------------+--------+-------------------------+
| ignore_paths | ``''`` | *list* | Splittable string like |
| | | | *\n/path\n/path\n*. |
| | | | Skiped, if matched. |
| | | | *\n/path\n/path\n* |
| | | | Skipped if matched |
+--------------------+----------------+--------+-------------------------+
csp_coverage
~~~~~~~~~~~~
+---------------------------+-------------+--------+----------------------------------+
| Key | Value (INI) | Type | Note |
+===========================+=============+========+==================================+
| enabled | ``'True'`` | *bool* | Enable ``csp_coverage`` tween |
+---------------------------+-------------+--------+----------------------------------+
| child_src | ``''`` | *str* | ``child-src`` fetch directive |
| | | | <source> (deprecated) |
+---------------------------+-------------+--------+----------------------------------+
| connect_src | ``''`` | *str* | ``connect-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| default_src | ``''`` | *str* | ``default-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| font_src | ``''`` | *str* | ``font-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| frame_src | ``''`` | *str* | ``frame-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| img_src | ``''`` | *str* | ``img-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| manifest_src | ``''`` | *str* | ``manifest-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| media_src | ``''`` | *str* | ``media-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| object_src | ``''`` | *str* | ``object-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| script_src | ``''`` | *str* | ``script-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| style_src | ``''`` | *str* | ``style-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| worker_src | ``''`` | *str* | ``worker-src`` fetch directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| base_uri | ``''`` | *str* | ``base-uri`` document directive |
| | | | <source> |
+---------------------------+-------------+--------+----------------------------------+
| plugin_types | ``''`` | *str* | ``plugin-types`` document |
| | | | directive <type>/<subtype> |
+---------------------------+-------------+--------+----------------------------------+
| sandbox | ``''`` | *str* | ``sandbox`` document directive |
| | | | <value> |
+---------------------------+-------------+--------+----------------------------------+
| form_action | ``''`` | *str* | ``form-action`` navigation |
| | | | directive <source> |
+---------------------------+-------------+--------+----------------------------------+
| frame_ancestors | ``''`` | *str* | ``frame-ancestors`` navigation |
| | | | directive <source> |
+---------------------------+-------------+--------+----------------------------------+
| report_uri | ``''`` | *str* | ``report_uri`` reporting |
| | | | directive <uri> |
+---------------------------+-------------+--------+----------------------------------+
| report_to | ``''`` | *str* | ``report_to`` reporting |
| | | | directive <uri> |
+---------------------------+-------------+--------+----------------------------------+
| block_all_mixed_content | ``'False'`` | *bool* | ``block_all_mixed_content`` |
| | | | directive value |
+---------------------------+-------------+--------+----------------------------------+
| referrer | ``''`` | *str* | ``referrer`` directive value |
| | | | such as ``"origin"`` (obsolete) |
+---------------------------+-------------+--------+----------------------------------+
| require_sri_for | ``''`` | *str* | ``require_sri_for`` directive |
| | | | value |
+---------------------------+-------------+--------+----------------------------------+
| upgrade_insecure_requests | ``'False'`` | *bool* | ``upgrade_insecure_requests`` |
| | | | directive value |
+---------------------------+-------------+--------+----------------------------------+
| ignore_paths | ``''`` | *list* | Splittable string like |
| | | | *\n/path\n/path\n*. Skipped, if |
| | | | matched |
+---------------------------+-------------+--------+----------------------------------+
Note
****
Format
~~~~~~
For some policy values `<source>`, `<type>/<subtype>`, `<value>` and `<uri>`,
you don't need single quote for values. See above example section.
Syntax
~~~~~~
pyramid_secure_response moment does not validate value which is set by you, if
its syntax is valid or not, for now. Please check that yourself ;)
Fallback (global)
~~~~~~~~~~~~~~~~~
These values are like a global variables. If exist, its are also applied
to all tweens as fallback (If same keys are already exist for the tweens, it
will be taken priority, over these values).
......@@ -173,15 +285,21 @@ will be taken priority, over these values).
| Key | Value (INI) | Type | Note |
+===============+================+========+=========================+
| proto_header | ``''`` | *str* | An header like |
| | | | *X-Forwarded-Proto*. |
| | | | *X-Forwarded-Proto* |
| | | | Checked in criteria as |
| | | | ``'https'``, if exists. |
| | | | ``'https'`` if exists |
+---------------+----------------+--------+-------------------------+
| ignore_paths | ``''`` | *list* | Splittable string like |
| | | | *\n/path\n/path\n*. |
| | | | Skiped, if matched. |
| | | | *\n/path\n/path\n* |
| | | | Skipped if matched |
+---------------+----------------+--------+-------------------------+
Links
~~~~~
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy
- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
Development
......
......@@ -5,7 +5,7 @@ __all__ = (
'__name__', # pylint: disable=undefined-all-variable
)
__version__ = '0.1.0'
__version__ = '0.1.1'
def includeme(config):
......
import re
from pyramid_secure_response.util import (
logger,
apply_path_filter,
get_config,
)
HEADER_KEY = 'Content-Security-Policy'
# MIME types
# * https://developer.mozilla.org/en-US/docs/Web/HTTP/\
# Basics_of_HTTP/MIME_types/Complete_list_of_MIME_types
# * https://www.iana.org/assignments/media-types/media-types.xhtml
MIME_TYPE_PATTERN = re.compile(r'\A[-\w]+\/[-\w]+\Z')
# SCHEME
# colon at the end is required
SCHEME_VALUES = (
'blob:',
'data:',
'filesystem:',
'http:',
'https:',
'mediastream:',
)
# sandbox
# this directive is not supported in <meta> element and
# 'Content-Security-Policy-Report-Only'.
SANDBOX_VALUES = (
'allow-forms',
'allow-modals',
'allow-orientation-lock',
'allow-pointer-lock',
'allow-popups',
'allow-popups-to-escape-sandbox',
'allow-presentation',
'allow-same-origin',
'allow-scripts',
'allow-top-navigation',
)
# directives
NO_QUOTE_DIRECTIVES = (
'report-uri', 'report-to',
'plugin-types'
)
BOOLEAN_DIRECTIVES = (
'block-all-mixed-content', 'upgrade-insecure-requests'
)
def _build_csp_header_value(directive, texts):
"""Creates CSP Header values for directive from texts.
Returns empty string if does not valid directive or texts is given.
"""
if texts:
if directive in BOOLEAN_DIRECTIVES:
# bool value
if texts.lower() == 'true':
return '{0:s}'.format(directive)
else:
values = []
for text in texts.split(' '):
# schemes <scheme-source>, mime type <type>/<subtype> and
# sandbox <value> shouldn't be surrounded with single
# quotes
if not text.startswith(SCHEME_VALUES) and \
text not in SANDBOX_VALUES and \
(directive not in NO_QUOTE_DIRECTIVES and
not MIME_TYPE_PATTERN.match(text)) and \
'\'' not in text:
text = "'{:s}'".format(text)
values.append(text)
return '{0:s} {1:s}'.format(directive, ' '.join(values))
return ''
def build_csp_header(config): # type: (Union[namedtuple, dict]) -> str
"""Returns CSP Header values."""
policy_text = ''
config_dict = config
# namedtuple
if isinstance(config, tuple) and hasattr(config, '_asdict'):
config_dict = config._asdict()
if not isinstance(config_dict, dict):
raise ValueError
for name in config_dict:
if name in ('enabled', 'ignore_paths'): # not directive
continue
value = _build_csp_header_value(
name.replace('_', '-'), str(config_dict[name]))
if value:
policy_text += '; {:s}'.format(value)
return policy_text[2:]
def tween(handler, registry):
r"""Sets Content Security Policy Header as configured.
About details of CSP, see below:
* https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
* https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/\
Content-Security-Policy
"""
config = get_config(registry)
csp_coverage = config.csp_coverage
ignore_paths = config.ignore_paths
if csp_coverage.ignore_paths:
ignore_paths = csp_coverage.ignore_paths
tween_name = 'csp_coverage'
def _csp_coverage_tween(req):
if not config.csp_coverage.enabled:
return handler(req)
if apply_path_filter(req, ignore_paths):
logger.info('(%s) Ignore path %s', tween_name, req.path)
return handler(req)
res = handler(req)
if HEADER_KEY not in res.headers:
# ignore if already exists
header_value = build_csp_header(csp_coverage)
if header_value:
res.headers[HEADER_KEY] = header_value
return res
return _csp_coverage_tween
......@@ -64,6 +64,44 @@ def get_config(registry): # type: (Registry) -> namedtuple
('preload', True),
), registry=registry)
# Content Security Policy (csp_coverage.xxx)
csp_coverage = _build_config(prefix='csp_coverage', defaults=(
('enabled', True),
('ignore_paths', tuple()),
# NOTE:
# These directives are appended into header as alphabetical
# order by directive sections.
# [fetch]
('child_src', ''), # (deprecated)
('connect_src', ''),
('default_src', ''),
('font_src', ''),
('frame_src', ''),
('img_src', ''),
('manifest_src', ''),
('media_src', ''),
('object_src', ''),
('script_src', ''),
('style_src', ''),
('worker_src', ''),
# [document]
('base_uri', ''),
('plugin_types', ''),
('sandbox', ''),
# [navigation]
('form_action', ''),
('frame_ancestors', ''),
# [reporting]
('report_uri', ''), # (deprecated)
('report_to', ''),
# [other]
('block_all_mixed_content', False),
('referrer', ''), # (obsolete)
('require_sri_for', ''),
('upgrade_insecure_requests', False),
), registry=registry)
# Shared
return _build_config(prefix='', defaults=(
('proto_header', ''), # e.g. X-Forwarded-Proto
......@@ -71,6 +109,7 @@ def get_config(registry): # type: (Registry) -> namedtuple
('ssl_redirect', ssl_redirect),
('hsts_support', hsts_support),
('csp_coverage', csp_coverage),
), registry=registry)
......
import pytest
from pyramid_secure_response.csp_coverage import tween
@pytest.fixture(autouse=True)
def setup():
import logging
from pyramid_secure_response.csp_coverage import logger
logger.setLevel(logging.ERROR)
@pytest.mark.parametrize('directives,header', [
# [fetch directive]
({},
""),
({'child_src': 'self', 'default_src': 'none'},
"child-src 'self'; default-src 'none'"),
({'connect_src': 'self', 'default_src': 'none'},
"connect-src 'self'; default-src 'none'"),
({'default_src': ''},
""),
({'default_src': 'https:'},
"default-src https:"),
({'default_src': "self https://example.org/"},
"default-src 'self' https://example.org/"),
({'default_src': 'none'},
"default-src 'none'"),
({'font_src': 'unsafe-inline'},
"font-src 'unsafe-inline'"),
({'frame_src': 'unsafe-eval'},
"frame-src 'unsafe-eval'"),
({'img_src': 'strict-dynamic'},
"img-src 'strict-dynamic'"),
({'manifest_src': 'nonce-2726c7f26c'},
"manifest-src 'nonce-2726c7f26c'"),
({'media_src': (
'sha256-076c8f1ca6979ef156b510a121b69b626'
'5011597557ca2971db5ad5a2743545f')},
"media-src '{:s}'".format(
'sha256-076c8f1ca6979ef156b510a121b69b626'
'5011597557ca2971db5ad5a2743545f')),
({'object_src': '*'},
"object-src '*'"),
({'script_src': 'https://example.org/'},
"script-src https://example.org/"),
({'style_src': 'self'},
"style-src 'self'"),
({'worker_src': 'self https://example.org/ https://example.com/'},
"worker-src 'self' https://example.org/ https://example.com/"),
# [document directive]
({'base_uri': 'self', 'default_src': 'none'},
"default-src 'none'; base-uri 'self'"),
({'plugin_types': 'application/x-schockwave-flash', 'object_src': ''},
"plugin-types application/x-schockwave-flash"),
({'plugin_types': 'application/xhtml+xml', 'object_src': ''},
"plugin-types application/xhtml+xml"),
({'plugin_types': 'application/vnd.mozilla.xul+xml', 'object_src': ''},
"plugin-types application/vnd.mozilla.xul+xml"),
({'plugin_types': 'video/3gpp2', 'object_src': ''},
"plugin-types video/3gpp2"),
({'sandbox': 'allow-forms'},
"sandbox allow-forms"),
# [navigation directive]
({'form_action': 'self'},
"form-action 'self'"),
({'frame_ancestors': 'self'},
"frame-ancestors 'self'"),
({'default_src': 'https:',
'report_uri': '/csp-violation-report-endpoint/'},
"default-src https:; report-uri /csp-violation-report-endpoint/"),
({'default_src': 'https:',
'report_to': '/csp-violation-report-endpoint/'},
"default-src https:; report-to /csp-violation-report-endpoint/"),
# - block_all_mixed_content
# - referrer
# - require_sri_for
# - upgrade_insecure_requests
])
def test_build_csp_header(directives, header, dummy_request):
from pyramid_secure_response.util import get_config
from pyramid_secure_response.csp_coverage import build_csp_header
dummy_request.url = 'http://example.org/'
settings = {
'pyramid_secure_response.csp_coverage.enabled': 'True',
}
for key, value in directives.items():
settings['pyramid_secure_response.csp_coverage.{:s}'.format(
key)] = value
dummy_request.registry.settings = settings
config = get_config(dummy_request.registry)
# NOTE:
# [fetch]
# - connect_src
# - default_src
# - font_src
# - frame_src
# - img_src
# - manifest_src
# - media_src
# - object_src
# - script_src
# - style_src
# - worker_src
# [document]
# - base_uri
# - plugin_types
# - sandbox
# [navigation]
# - form_action
# - frame_ancestors
# [reporting]
# - report_uri
# - report_to
# [other]
# - block_all_mixed_content
# - referrer
# - require_sri_for
# - upgrade_insecure_requests
assert header == build_csp_header(config.csp_coverage)
def test_build_csp_header_config_argument():
from collections import namedtuple
from pyramid_secure_response.csp_coverage import build_csp_header
with pytest.raises(ValueError):
build_csp_header('not dict or namedtuple')
with pytest.raises(ValueError):
build_csp_header(tuple())
assert '' == build_csp_header({})
# pylint: disable=invalid-name
Config = namedtuple('Config', 'enabled')
assert '' == build_csp_header(Config(True))
def test_csp_coverage_tween_with_disabled(mocker, dummy_request):
mocker.patch('pyramid_secure_response.csp_coverage.apply_path_filter',
return_value=True)
from pyramid.response import Response
from pyramid_secure_response.csp_coverage import (
apply_path_filter,
)
dummy_request.registry.settings = {
'pyramid_secure_response.csp_coverage.enabled': 'False'
}
handler_stub = mocker.stub(name='handler_stub')
handler_stub.return_value = Response(status=200)
csp_coverage_tween = tween(handler_stub, dummy_request.registry)
res = csp_coverage_tween(dummy_request)
# pylint: disable=no-member
assert 1 == handler_stub.call_count
assert 0 == apply_path_filter.call_count
assert 'Content-Security-Policy' not in res.headers
def test_csp_coverage_tween_with_ignored_path(mocker, dummy_request):
mocker.patch('pyramid_secure_response.csp_coverage.apply_path_filter',
return_value=True)
from pyramid.response import Response
from pyramid_secure_response.csp_coverage import (
apply_path_filter,
)
dummy_request.path = '/humans.txt'
dummy_request.registry.settings = {
'pyramid_secure_response.csp_coverage.enabled': 'True',
'pyramid_secure_response.csp_coverage.ignore_paths': '\n/humans.txt\n'
}
handler_stub = mocker.stub(name='handler_stub')
handler_stub.return_value = Response(status=200)
csp_coverage_tween = tween(handler_stub, dummy_request.registry)
res = csp_coverage_tween(dummy_request)
# pylint: disable=no-member
assert 1 == handler_stub.call_count
assert 1 == apply_path_filter.call_count
apply_path_filter.assert_called_once_with(
dummy_request, ('/humans.txt',))
assert 'Content-Security-Policy' not in res.headers
def test_csp_coverage_with_default_values(mocker, dummy_request):
from pyramid_secure_response import csp_coverage
mocker.spy(csp_coverage, 'apply_path_filter')
from pyramid.response import Response
from pyramid_secure_response.csp_coverage import (
apply_path_filter,
)
dummy_request.url = 'http://example.org/'
dummy_request.registry.settings = {
'pyramid_secure_response.csp_coverage.enabled': 'True',
'pyramid_secure_response.csp_coverage.ignore_paths': '\n',
}
handler_stub = mocker.stub(name='handler_stub')
handler_stub.return_value = Response(status=200)
csp_coverage_tween = tween(handler_stub, dummy_request.registry)
res = csp_coverage_tween(dummy_request)
# pylint: disable=no-member
assert 1 == handler_stub.call_count
assert 1 == apply_path_filter.call_count
apply_path_filter.assert_called_once_with(dummy_request, tuple())
# does not set if header is empty
assert 'Content-Security-Policy' not in res.headers
def test_csp_coverage_tween_default_src_with_host_source(
mocker, dummy_request):
from pyramid_secure_response import csp_coverage
mocker.spy(csp_coverage, 'apply_path_filter')
from pyramid.response import Response
from pyramid_secure_response.csp_coverage import (
apply_path_filter,
)
dummy_request.url = 'https://example.org/'
dummy_request.registry.settings = {
'pyramid_secure_response.csp_coverage.enabled': 'True',
'pyramid_secure_response.csp_coverage.ignore_paths': '\n',
'pyramid_secure_response.csp_coverage.default_src':
'https://example.org/',
}
handler_stub = mocker.stub(name='handler_stub')
handler_stub.return_value = Response(status=200)
csp_coverage_tween = tween(handler_stub, dummy_request.registry)
res = csp_coverage_tween(dummy_request)
# pylint: disable=no-member
assert 1 == handler_stub.call_count
assert 1 == apply_path_filter.call_count
apply_path_filter.assert_called_once_with(dummy_request, tuple())
assert 'Content-Security-Policy' in res.headers
assert 'default-src https://example.org/' == \
res.headers['Content-Security-Policy']
def test_csp_coverage_tween_default_src_with_scheme_source(
mocker, dummy_request):
from pyramid_secure_response import csp_coverage
mocker.spy(csp_coverage, 'apply_path_filter')
from pyramid.response import Response
from pyramid_secure_response.csp_coverage import (
apply_path_filter,
)
dummy_request.url = 'https://example.org/'
dummy_request.registry.settings = {
'pyramid_secure_response.csp_coverage.enabled': 'True',
'pyramid_secure_response.csp_coverage.ignore_paths': '\n',
'pyramid_secure_response.csp_coverage.default_src': 'https:',
}
handler_stub = mocker.stub(name='handler_stub')
handler_stub.return_value = Response(status=200)
csp_coverage_tween = tween(handler_stub, dummy_request.registry)
res = csp_coverage_tween(dummy_request)
# pylint: disable=no-member
assert 1 == handler_stub.call_count
assert 1 == apply_path_filter.call_count
apply_path_filter.assert_called_once_with(dummy_request, tuple())
assert 'Content-Security-Policy' in res.headers
assert 'default-src https:' == \
res.headers['Content-Security-Policy']
......@@ -12,8 +12,10 @@ def test_get_config_keys(dummy_request):
expected_keys = (
'proto_header',
'ignore_paths',
# tweens
'ssl_redirect',
'hsts_support',
'csp_coverage',
)
assert expected_keys == tuple(config._asdict().keys())
......