From a626c25c51c845938d5bf0e8c65313bc11489b65 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Sun, 30 Oct 2022 21:09:01 +0000 Subject: [PATCH 01/12] Update supported Django and Python versions * Update tox matrix * Update Circle CI config * Update setup.py with newer Python versions --- .circleci/config.yml | 27 +++++++++--- setup.py | 100 ++++++++++++++++++++++--------------------- tox.ini | 12 ++++-- 3 files changed, 80 insertions(+), 59 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 93080aa..008cd02 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -11,18 +11,33 @@ workflows: - "python:3.7-slim" - "python:3.8-slim" - "python:3.9-slim" + - "python:3.10-slim" + - "python:3.11-slim" - "pypy:3-slim-buster" django_version: - - "2.2.x" # 2.2 supports python 3.5 to 3.9 - "3.2.x" # 3.0 supports python 3.6 to 3.9 - - "main" # 4.0 supports 3.8 to 3.9 + - "4.0.x" # 4.0 supports python 3.8 to 3.10 + - "4.1.x" # 4.1 supports python 3.8 to 3.11 + - "main" # 4.1 supports 3.8 to 3.11 exclude: - python_image: "python:3.6-slim" django_version: "main" + - python_image: "python:3.6-slim" + django_version: "4.0.x" + - python_image: "python:3.6-slim" + django_version: "4.1.x" - python_image: "python:3.7-slim" django_version: "main" - - python_image: "pypy:3-slim-buster" # on 3.7 as of 2021-07-26 - django_version: "main" + - python_image: "python:3.7-slim" + django_version: "4.0.x" + - python_image: "python:3.7-slim" + django_version: "4.1.x" + - python_image: "python:3.10-slim" + django_version: "3.2.x" + - python_image: "python:3.11-slim" + django_version: "3.2.x" + - python_image: "python:3.11-slim" + django_version: "4.0.x" jobs: test: @@ -48,7 +63,7 @@ jobs: condition: not: or: - - equal: [ "pypy:3-slim-buster", << parameters.python_image >> ] + - equal: ["pypy:3-slim-buster", << parameters.python_image >>] steps: - run: name: test cpython @@ -59,7 +74,7 @@ jobs: - when: condition: or: - - equal: [ "pypy:3-slim-buster", << parameters.python_image >> ] + - equal: ["pypy:3-slim-buster", << parameters.python_image >>] steps: - run: name: test pypy diff --git a/setup.py b/setup.py index 95cae78..f410b65 100644 --- a/setup.py +++ b/setup.py @@ -1,60 +1,60 @@ -import sys -import os import codecs -from setuptools import setup, find_packages +import os +import sys +from setuptools import find_packages, setup -version = '3.7' +version = "3.7" -if sys.argv[-1] == 'publish': - os.system('python setup.py sdist upload') - os.system('python setup.py bdist_wheel upload') - print('You probably want to also tag the version now:') +if sys.argv[-1] == "publish": + os.system("python setup.py sdist upload") + os.system("python setup.py bdist_wheel upload") + print("You probably want to also tag the version now:") print(' git tag -a %s -m "version %s"' % (version, version)) - print(' git push --tags') + print(" git push --tags") sys.exit() def read(*parts): filename = os.path.join(os.path.dirname(__file__), *parts) - with codecs.open(filename, encoding='utf-8') as fp: + with codecs.open(filename, encoding="utf-8") as fp: return fp.read() install_requires = [ - 'Django>=2.2', + "Django>=2.2", ] jinja2_requires = [ - 'jinja2>=2.9.6', + "jinja2>=2.9.6", ] test_requires = [ - 'pytest<4.0', - 'pytest-cov', - 'pytest-django', - 'pytest-flakes==1.0.1', - 'pytest-pep8==1.0.6', - 'pep8==1.4.6', - 'mock==1.0.1', - 'six==1.12.0', + "pytest<4.0", + "pytest-cov", + "pytest-django", + "pytest-flakes==1.0.1", + "pytest-pep8==1.0.6", + "pep8==1.4.6", + "mock==1.0.1", + "six==1.12.0", ] test_requires += jinja2_requires setup( - name='django_csp', + name="django_csp", version=version, - description='Django Content Security Policy support.', - long_description=read('README.rst'), - author='James Socol', - author_email='me@jamessocol.com', - maintainer='Christopher Grebs', - maintainer_email='cg@webshox.org', - url='http://github.com/mozilla/django-csp', - license='BSD', + description="Django Content Security Policy support.", + long_description=read("README.rst"), + author="James Socol", + author_email="me@jamessocol.com", + maintainer="Christopher Grebs", + maintainer_email="cg@webshox.org", + url="http://github.com/mozilla/django-csp", + license="BSD", packages=find_packages(), project_urls={ "Documentation": "http://django-csp.readthedocs.org/", @@ -64,28 +64,30 @@ def read(*parts): }, install_requires=install_requires, extras_require={ - 'tests': test_requires, - 'jinja2': jinja2_requires, + "tests": test_requires, + "jinja2": jinja2_requires, }, include_package_data=True, zip_safe=False, classifiers=[ - 'Development Status :: 5 - Production/Stable', - 'Environment :: Web Environment', - 'Environment :: Web Environment :: Mozilla', - 'Programming Language :: Python', - 'License :: OSI Approved :: BSD License', - 'Operating System :: OS Independent', - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Libraries :: Python Modules', - 'Programming Language :: Python', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.6', - 'Programming Language :: Python :: 3.7', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: Implementation :: PyPy', - 'Programming Language :: Python :: Implementation :: CPython', - 'Framework :: Django', - ] + "Development Status :: 5 - Production/Stable", + "Environment :: Web Environment", + "Environment :: Web Environment :: Mozilla", + "Programming Language :: Python", + "License :: OSI Approved :: BSD License", + "Operating System :: OS Independent", + "Intended Audience :: Developers", + "Topic :: Software Development :: Libraries :: Python Modules", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.6", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: Implementation :: PyPy", + "Programming Language :: Python :: Implementation :: CPython", + "Framework :: Django", + ], ) diff --git a/tox.ini b/tox.ini index 65c4dbd..e2c50cc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,9 @@ [tox] envlist = - {3.6,3.7,3.8,3.9,pypy3}-main - {3.6,3.7,3.8,3.9,pypy3}-3.0.x - {3.6,3,7,3.8,3.9,pypy3}-2.2.x + {3.8,3.9,3.10,3.11,pypy3}-main + {3.8,3.9,3.10,3.11,pypy3}-4.1.x + {3.8,3.9,3.10,pypy3}-4.0.x + {3.6,3.7,3.8,3.9,3.10,pypy3}-3.2.x [testenv] setenv = @@ -17,9 +18,12 @@ basepython = 3.7: python3.7 3.8: python3.8 3.9: python3.9 + 3.10: python3.10 + 3.11: python3.11 pypy3: pypy3 deps= pytest - 2.2.x: Django>=2.2,<2.3 + 4.0.x: Django>=4.0,<4.1 + 4.1.x: Django>=4.1,<4.2 3.2.x: Django>=3.2,<3.3 main: https://github.com/django/django/archive/main.tar.gz From f72304ae3f5ca429905c019b1d64f28ddc2588b0 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Tue, 23 Jan 2024 17:35:21 +0000 Subject: [PATCH 02/12] Swap pytest-pep8 and pytest-flakes for ruff + reformat accordingly --- csp/context_processors.py | 6 +- csp/contrib/rate_limiting.py | 15 ++-- csp/decorators.py | 17 ++-- csp/extensions/__init__.py | 21 ++--- csp/middleware.py | 33 ++++--- csp/templatetags/csp.py | 14 +-- csp/tests/settings.py | 47 +++++----- csp/tests/test_context_processors.py | 11 ++- csp/tests/test_contrib.py | 9 +- csp/tests/test_decorators.py | 56 ++++++------ csp/tests/test_jinja_extension.py | 23 ++--- csp/tests/test_middleware.py | 57 ++++++------ csp/tests/test_templatetags.py | 25 ++---- csp/tests/test_utils.py | 121 ++++++++++++------------- csp/tests/utils.py | 21 +++-- csp/utils.py | 122 ++++++++++++------------- docs/conf.py | 129 +++++++++++++-------------- docs/contributing.rst | 10 +-- ruff.toml | 60 +++++++++++++ setup.cfg | 2 +- setup.py | 8 +- 21 files changed, 411 insertions(+), 396 deletions(-) create mode 100644 ruff.toml diff --git a/csp/context_processors.py b/csp/context_processors.py index d91c56e..666da2c 100644 --- a/csp/context_processors.py +++ b/csp/context_processors.py @@ -1,6 +1,4 @@ def nonce(request): - nonce = request.csp_nonce if hasattr(request, 'csp_nonce') else '' + nonce = request.csp_nonce if hasattr(request, "csp_nonce") else "" - return { - 'CSP_NONCE': nonce - } + return {"CSP_NONCE": nonce} diff --git a/csp/contrib/rate_limiting.py b/csp/contrib/rate_limiting.py index 8a4d087..2419c67 100644 --- a/csp/contrib/rate_limiting.py +++ b/csp/contrib/rate_limiting.py @@ -11,15 +11,14 @@ class RateLimitedCSPMiddleware(CSPMiddleware): to report-uri by excluding it from some requests.""" def build_policy(self, request, response): - config = getattr(response, '_csp_config', None) - update = getattr(response, '_csp_update', None) - replace = getattr(response, '_csp_replace', {}) - nonce = getattr(request, '_csp_nonce', None) + config = getattr(response, "_csp_config", None) + update = getattr(response, "_csp_update", None) + replace = getattr(response, "_csp_replace", {}) + nonce = getattr(request, "_csp_nonce", None) - report_percentage = getattr(settings, 'CSP_REPORT_PERCENTAGE') + report_percentage = getattr(settings, "CSP_REPORT_PERCENTAGE") include_report_uri = random.random() < report_percentage if not include_report_uri: - replace['report-uri'] = None + replace["report-uri"] = None - return build_policy(config=config, update=update, replace=replace, - nonce=nonce) + return build_policy(config=config, update=update, replace=replace, nonce=nonce) diff --git a/csp/decorators.py b/csp/decorators.py index bce3352..e6489a2 100644 --- a/csp/decorators.py +++ b/csp/decorators.py @@ -7,11 +7,12 @@ def _wrapped(*a, **kw): r = f(*a, **kw) r._csp_exempt = True return r + return _wrapped def csp_update(**kwargs): - update = dict((k.lower().replace('_', '-'), v) for k, v in kwargs.items()) + update = dict((k.lower().replace("_", "-"), v) for k, v in kwargs.items()) def decorator(f): @wraps(f) @@ -19,12 +20,14 @@ def _wrapped(*a, **kw): r = f(*a, **kw) r._csp_update = update return r + return _wrapped + return decorator def csp_replace(**kwargs): - replace = dict((k.lower().replace('_', '-'), v) for k, v in kwargs.items()) + replace = dict((k.lower().replace("_", "-"), v) for k, v in kwargs.items()) def decorator(f): @wraps(f) @@ -32,16 +35,14 @@ def _wrapped(*a, **kw): r = f(*a, **kw) r._csp_replace = replace return r + return _wrapped + return decorator def csp(**kwargs): - config = dict( - (k.lower().replace('_', '-'), [v] if isinstance(v, str) else v) - for k, v - in kwargs.items() - ) + config = dict((k.lower().replace("_", "-"), [v] if isinstance(v, str) else v) for k, v in kwargs.items()) def decorator(f): @wraps(f) @@ -49,5 +50,7 @@ def _wrapped(*a, **kw): r = f(*a, **kw) r._csp_config = config return r + return _wrapped + return decorator diff --git a/csp/extensions/__init__.py b/csp/extensions/__init__.py index c84a419..227289b 100644 --- a/csp/extensions/__init__.py +++ b/csp/extensions/__init__.py @@ -8,7 +8,7 @@ class NoncedScript(Extension): # a set of names that trigger the extension. - tags = set(['script']) + tags = set(["script"]) def parse(self, parser): # the first token is the token that started the tag. In our case @@ -18,29 +18,26 @@ def parse(self, parser): lineno = next(parser.stream).lineno # Get the current context and pass along - kwargs = [nodes.Keyword('ctx', nodes.ContextReference())] + kwargs = [nodes.Keyword("ctx", nodes.ContextReference())] # Parse until we are done with optional script tag attributes while parser.stream.current.value in SCRIPT_ATTRS: attr_name = parser.stream.current.value parser.stream.skip(2) - kwargs.append( - nodes.Keyword(attr_name, parser.parse_expression())) + kwargs.append(nodes.Keyword(attr_name, parser.parse_expression())) # now we parse the body of the script block up to `endscript` and # drop the needle (which would always be `endscript` in that case) - body = parser.parse_statements(['name:endscript'], drop_needle=True) + body = parser.parse_statements(["name:endscript"], drop_needle=True) # now return a `CallBlock` node that calls our _render_script # helper method on this extension. - return nodes.CallBlock( - self.call_method('_render_script', kwargs=kwargs), - [], [], body).set_lineno(lineno) + return nodes.CallBlock(self.call_method("_render_script", kwargs=kwargs), [], [], body).set_lineno(lineno) def _render_script(self, caller, **kwargs): - ctx = kwargs.pop('ctx') - request = ctx.get('request') - kwargs['nonce'] = request.csp_nonce - kwargs['content'] = caller().strip() + ctx = kwargs.pop("ctx") + request = ctx.get("request") + kwargs["nonce"] = request.csp_nonce + kwargs["content"] = caller().strip() return build_script_tag(**kwargs) diff --git a/csp/middleware.py b/csp/middleware.py index 73397e1..49168ba 100644 --- a/csp/middleware.py +++ b/csp/middleware.py @@ -16,13 +16,16 @@ try: from django.utils.deprecation import MiddlewareMixin except ImportError: + class MiddlewareMixin(object): """ If this middleware doesn't exist, this is an older version of django and we don't need it. """ + pass + from csp.utils import build_policy @@ -35,15 +38,12 @@ class CSPMiddleware(MiddlewareMixin): See http://www.w3.org/TR/CSP/ """ + def _make_nonce(self, request): # Ensure that any subsequent calls to request.csp_nonce return the # same value - if not getattr(request, '_csp_nonce', None): - request._csp_nonce = ( - base64 - .b64encode(os.urandom(16)) - .decode("ascii") - ) + if not getattr(request, "_csp_nonce", None): + request._csp_nonce = base64.b64encode(os.urandom(16)).decode("ascii") return request._csp_nonce def process_request(self, request): @@ -51,11 +51,11 @@ def process_request(self, request): request.csp_nonce = SimpleLazyObject(nonce) def process_response(self, request, response): - if getattr(response, '_csp_exempt', False): + if getattr(response, "_csp_exempt", False): return response # Check for ignored path prefix. - prefixes = getattr(settings, 'CSP_EXCLUDE_URL_PREFIXES', ()) + prefixes = getattr(settings, "CSP_EXCLUDE_URL_PREFIXES", ()) if request.path_info.startswith(prefixes): return response @@ -68,9 +68,9 @@ def process_response(self, request, response): if status_code in exempted_debug_codes and settings.DEBUG: return response - header = 'Content-Security-Policy' - if getattr(settings, 'CSP_REPORT_ONLY', False): - header += '-Report-Only' + header = "Content-Security-Policy" + if getattr(settings, "CSP_REPORT_ONLY", False): + header += "-Report-Only" if header in response: # Don't overwrite existing headers. @@ -81,9 +81,8 @@ def process_response(self, request, response): return response def build_policy(self, request, response): - config = getattr(response, '_csp_config', None) - update = getattr(response, '_csp_update', None) - replace = getattr(response, '_csp_replace', None) - nonce = getattr(request, '_csp_nonce', None) - return build_policy(config=config, update=update, replace=replace, - nonce=nonce) + config = getattr(response, "_csp_config", None) + update = getattr(response, "_csp_update", None) + replace = getattr(response, "_csp_replace", None) + nonce = getattr(request, "_csp_nonce", None) + return build_policy(config=config, update=update, replace=replace, nonce=nonce) diff --git a/csp/templatetags/csp.py b/csp/templatetags/csp.py index e5737df..572bfe7 100644 --- a/csp/templatetags/csp.py +++ b/csp/templatetags/csp.py @@ -10,16 +10,16 @@ def _unquote(s): """Helper func that strips single and double quotes from inside strings""" - return s.replace('"', '').replace("'", "") + return s.replace('"', "").replace("'", "") -@register.tag(name='script') +@register.tag(name="script") def script(parser, token): # Parse out any keyword args token_args = token.split_contents() kwargs = token_kwargs(token_args[1:], parser) - nodelist = parser.parse(('endscript',)) + nodelist = parser.parse(("endscript",)) parser.delete_first_token() return NonceScriptNode(nodelist, **kwargs) @@ -33,12 +33,12 @@ def __init__(self, nodelist, **kwargs): self.script_attrs[k] = self._get_token_value(v) def _get_token_value(self, t): - return _unquote(t.token) if getattr(t, 'token', None) else None + return _unquote(t.token) if getattr(t, "token", None) else None def render(self, context): output = self.nodelist.render(context).strip() - request = context.get('request') - nonce = request.csp_nonce if hasattr(request, 'csp_nonce') else '' - self.script_attrs.update({'nonce': nonce, 'content': output}) + request = context.get("request") + nonce = request.csp_nonce if hasattr(request, "csp_nonce") else "" + self.script_attrs.update({"nonce": nonce, "content": output}) return build_script_tag(**self.script_attrs) diff --git a/csp/tests/settings.py b/csp/tests/settings.py index dc74ff1..7c97a1d 100644 --- a/csp/tests/settings.py +++ b/csp/tests/settings.py @@ -1,47 +1,46 @@ import django - CSP_REPORT_ONLY = False -CSP_INCLUDE_NONCE_IN = ['default-src'] +CSP_INCLUDE_NONCE_IN = ["default-src"] DATABASES = { - 'default': { - 'NAME': 'test.db', - 'ENGINE': 'django.db.backends.sqlite3', + "default": { + "NAME": "test.db", + "ENGINE": "django.db.backends.sqlite3", } } INSTALLED_APPS = ( - 'django.contrib.admin', - 'django.contrib.auth', - 'django.contrib.contenttypes', - 'django.contrib.sessions', - 'django.contrib.sites', - 'csp', + "django.contrib.admin", + "django.contrib.auth", + "django.contrib.contenttypes", + "django.contrib.sessions", + "django.contrib.sites", + "csp", ) -SECRET_KEY = 'csp-test-key' +SECRET_KEY = "csp-test-key" TEMPLATES = [ { - 'BACKEND': 'django.template.backends.jinja2.Jinja2', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': { - 'environment': 'csp.tests.environment.environment', - 'extensions': ['csp.extensions.NoncedScript'] - }, + "BACKEND": "django.template.backends.jinja2.Jinja2", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": { + "environment": "csp.tests.environment.environment", + "extensions": ["csp.extensions.NoncedScript"], + }, }, { - 'BACKEND': 'django.template.backends.django.DjangoTemplates', - 'DIRS': [], - 'APP_DIRS': True, - 'OPTIONS': {}, + "BACKEND": "django.template.backends.django.DjangoTemplates", + "DIRS": [], + "APP_DIRS": True, + "OPTIONS": {}, }, ] # Django >1.6 requires `setup` call to initialise apps framework -if hasattr(django, 'setup'): +if hasattr(django, "setup"): django.setup() diff --git a/csp/tests/test_context_processors.py b/csp/tests/test_context_processors.py index df980a4..666f53f 100644 --- a/csp/tests/test_context_processors.py +++ b/csp/tests/test_context_processors.py @@ -1,28 +1,27 @@ from django.http import HttpResponse from django.test import RequestFactory -from csp.middleware import CSPMiddleware from csp.context_processors import nonce +from csp.middleware import CSPMiddleware from csp.tests.utils import response - rf = RequestFactory() mw = CSPMiddleware(response()) def test_nonce_context_processor(): - request = rf.get('/') + request = rf.get("/") mw.process_request(request) context = nonce(request) response = HttpResponse() mw.process_response(request, response) - assert context['CSP_NONCE'] == request.csp_nonce + assert context["CSP_NONCE"] == request.csp_nonce def test_nonce_context_processor_with_middleware_disabled(): - request = rf.get('/') + request = rf.get("/") context = nonce(request) - assert context['CSP_NONCE'] == '' + assert context["CSP_NONCE"] == "" diff --git a/csp/tests/test_contrib.py b/csp/tests/test_contrib.py index 98ccded..6b7bbc0 100644 --- a/csp/tests/test_contrib.py +++ b/csp/tests/test_contrib.py @@ -5,20 +5,19 @@ from csp.contrib.rate_limiting import RateLimitedCSPMiddleware from csp.tests.utils import response - -HEADER = 'Content-Security-Policy' +HEADER = "Content-Security-Policy" mw = RateLimitedCSPMiddleware(response()) rf = RequestFactory() -@override_settings(CSP_REPORT_PERCENTAGE=0.1, CSP_REPORT_URI='x') +@override_settings(CSP_REPORT_PERCENTAGE=0.1, CSP_REPORT_URI="x") def test_report_percentage(): times_seen = 0 for _ in range(5000): - request = rf.get('/') + request = rf.get("/") response = HttpResponse() mw.process_response(request, response) - if 'report-uri' in response[HEADER]: + if "report-uri" in response[HEADER]: times_seen += 1 # Roughly 10% assert 400 <= times_seen <= 600 diff --git a/csp/tests/test_decorators.py b/csp/tests/test_decorators.py index a4bd733..a877bc4 100644 --- a/csp/tests/test_decorators.py +++ b/csp/tests/test_decorators.py @@ -2,12 +2,11 @@ from django.test import RequestFactory from django.test.utils import override_settings -from csp.decorators import csp, csp_replace, csp_update, csp_exempt +from csp.decorators import csp, csp_exempt, csp_replace, csp_update from csp.middleware import CSPMiddleware from csp.tests.utils import response - -REQUEST = RequestFactory().get('/') +REQUEST = RequestFactory().get("/") mw = CSPMiddleware(response()) @@ -15,60 +14,66 @@ def test_csp_exempt(): @csp_exempt def view(request): return HttpResponse() + response = view(REQUEST) assert response._csp_exempt -@override_settings(CSP_IMG_SRC=['foo.com']) +@override_settings(CSP_IMG_SRC=["foo.com"]) def test_csp_update(): def view_without_decorator(request): return HttpResponse() + response = view_without_decorator(REQUEST) mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] - @csp_update(IMG_SRC='bar.com') + @csp_update(IMG_SRC="bar.com") def view_with_decorator(request): return HttpResponse() + response = view_with_decorator(REQUEST) - assert response._csp_update == {'img-src': 'bar.com'} + assert response._csp_update == {"img-src": "bar.com"} mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com bar.com"] response = view_without_decorator(REQUEST) mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] -@override_settings(CSP_IMG_SRC=['foo.com']) +@override_settings(CSP_IMG_SRC=["foo.com"]) def test_csp_replace(): def view_without_decorator(request): return HttpResponse() + response = view_without_decorator(REQUEST) mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] - @csp_replace(IMG_SRC='bar.com') + @csp_replace(IMG_SRC="bar.com") def view_with_decorator(request): return HttpResponse() + response = view_with_decorator(REQUEST) - assert response._csp_replace == {'img-src': 'bar.com'} + assert response._csp_replace == {"img-src": "bar.com"} mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["default-src 'self'", "img-src bar.com"] response = view_without_decorator(REQUEST) mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["default-src 'self'", "img-src foo.com"] @csp_replace(IMG_SRC=None) def view_removing_directive(request): return HttpResponse() + response = view_removing_directive(REQUEST) mw.process_response(REQUEST, response) policy_list = sorted(response["Content-Security-Policy"].split("; ")) @@ -78,35 +83,36 @@ def view_removing_directive(request): def test_csp(): def view_without_decorator(request): return HttpResponse() + response = view_without_decorator(REQUEST) mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["default-src 'self'"] - @csp(IMG_SRC=['foo.com'], FONT_SRC=['bar.com']) + @csp(IMG_SRC=["foo.com"], FONT_SRC=["bar.com"]) def view_with_decorator(request): return HttpResponse() + response = view_with_decorator(REQUEST) - assert response._csp_config == \ - {'img-src': ['foo.com'], 'font-src': ['bar.com']} + assert response._csp_config == {"img-src": ["foo.com"], "font-src": ["bar.com"]} mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["font-src bar.com", "img-src foo.com"] response = view_without_decorator(REQUEST) mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["default-src 'self'"] def test_csp_string_values(): # Test backwards compatibility where values were strings - @csp(IMG_SRC='foo.com', FONT_SRC='bar.com') + @csp(IMG_SRC="foo.com", FONT_SRC="bar.com") def view_with_decorator(request): return HttpResponse() + response = view_with_decorator(REQUEST) - assert response._csp_config == \ - {'img-src': ['foo.com'], 'font-src': ['bar.com']} + assert response._csp_config == {"img-src": ["foo.com"], "font-src": ["bar.com"]} mw.process_response(REQUEST, response) - policy_list = sorted(response['Content-Security-Policy'].split("; ")) + policy_list = sorted(response["Content-Security-Policy"].split("; ")) assert policy_list == ["font-src bar.com", "img-src foo.com"] diff --git a/csp/tests/test_jinja_extension.py b/csp/tests/test_jinja_extension.py index 339ddb5..f6e573f 100644 --- a/csp/tests/test_jinja_extension.py +++ b/csp/tests/test_jinja_extension.py @@ -2,7 +2,6 @@ class TestJinjaExtension(ScriptExtensionTestBase): - def test_script_tag_injects_nonce(self): tpl = """ {% script %} @@ -10,7 +9,7 @@ def test_script_tag_injects_nonce(self): {% endscript %} """ - expected = ("""""") + expected = """""" self.assert_template_eq(*self.process_templates(tpl, expected)) def test_script_with_src_ignores_body(self): @@ -43,9 +42,7 @@ def test_async_attribute_with_falsey(self): var hello='world'; {% endscript %}""" - expected = ('') + expected = '" self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -55,15 +52,13 @@ def test_async_attribute_with_truthy(self): var hello='world'; {% endscript %}""" - expected = ('') + expected = '" self.assert_template_eq(*self.process_templates(tpl, expected)) def test_nested_script_tags_are_removed(self): """Let users wrap their code in script tags for the sake of their - development environment""" + development environment""" tpl = """ {% script type="application/javascript" id="jeff" defer=True%} {% endscript %}""" - expected = ( - '' - 'var hello=\'world\';') + expected = "' "var hello='world';" self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -90,9 +82,6 @@ def test_regex_captures_script_content_including_brackets(self): {% endscript %} """ - expected = ( - '"' - '') + expected = '"' "" self.assert_template_eq(*self.process_templates(tpl, expected)) diff --git a/csp/tests/test_middleware.py b/csp/tests/test_middleware.py index ce06b24..742ad1c 100644 --- a/csp/tests/test_middleware.py +++ b/csp/tests/test_middleware.py @@ -1,7 +1,7 @@ from django.http import ( HttpResponse, - HttpResponseServerError, HttpResponseNotFound, + HttpResponseServerError, ) from django.test import RequestFactory from django.test.utils import override_settings @@ -9,30 +9,29 @@ from csp.middleware import CSPMiddleware from csp.tests.utils import response - -HEADER = 'Content-Security-Policy' +HEADER = "Content-Security-Policy" mw = CSPMiddleware(response()) rf = RequestFactory() def test_add_header(): - request = rf.get('/') + request = rf.get("/") response = HttpResponse() mw.process_response(request, response) assert HEADER in response def test_exempt(): - request = rf.get('/') + request = rf.get("/") response = HttpResponse() response._csp_exempt = True mw.process_response(request, response) assert HEADER not in response -@override_settings(CSP_EXCLUDE_URL_PREFIXES=('/inlines-r-us')) +@override_settings(CSP_EXCLUDE_URL_PREFIXES=("/inlines-r-us")) def text_exclude(): - request = rf.get('/inlines-r-us/foo') + request = rf.get("/inlines-r-us/foo") response = HttpResponse() mw.process_response(request, response) assert HEADER not in response @@ -40,50 +39,50 @@ def text_exclude(): @override_settings(CSP_REPORT_ONLY=True) def test_report_only(): - request = rf.get('/') + request = rf.get("/") response = HttpResponse() mw.process_response(request, response) assert HEADER not in response - assert HEADER + '-Report-Only' in response + assert HEADER + "-Report-Only" in response def test_dont_replace(): - request = rf.get('/') + request = rf.get("/") response = HttpResponse() - response[HEADER] = 'default-src example.com' + response[HEADER] = "default-src example.com" mw.process_response(request, response) - assert response[HEADER] == 'default-src example.com' + assert response[HEADER] == "default-src example.com" def test_use_config(): - request = rf.get('/') + request = rf.get("/") response = HttpResponse() - response._csp_config = {'default-src': ['example.com']} + response._csp_config = {"default-src": ["example.com"]} mw.process_response(request, response) - assert response[HEADER] == 'default-src example.com' + assert response[HEADER] == "default-src example.com" def test_use_update(): - request = rf.get('/') + request = rf.get("/") response = HttpResponse() - response._csp_update = {'default-src': ['example.com']} + response._csp_update = {"default-src": ["example.com"]} mw.process_response(request, response) assert response[HEADER] == "default-src 'self' example.com" -@override_settings(CSP_IMG_SRC=['foo.com']) +@override_settings(CSP_IMG_SRC=["foo.com"]) def test_use_replace(): - request = rf.get('/') + request = rf.get("/") response = HttpResponse() - response._csp_replace = {'img-src': ['bar.com']} + response._csp_replace = {"img-src": ["bar.com"]} mw.process_response(request, response) - policy_list = sorted(response[HEADER].split('; ')) + policy_list = sorted(response[HEADER].split("; ")) assert policy_list == ["default-src 'self'", "img-src bar.com"] @override_settings(DEBUG=True) def test_debug_errors_exempt(): - request = rf.get('/') + request = rf.get("/") response = HttpResponseServerError() mw.process_response(request, response) assert HEADER not in response @@ -91,14 +90,14 @@ def test_debug_errors_exempt(): @override_settings(DEBUG=True) def test_debug_notfound_exempt(): - request = rf.get('/') + request = rf.get("/") response = HttpResponseNotFound() mw.process_response(request, response) assert HEADER not in response def test_nonce_created_when_accessed(): - request = rf.get('/') + request = rf.get("/") mw.process_request(request) nonce = str(request.csp_nonce) response = HttpResponse() @@ -107,16 +106,16 @@ def test_nonce_created_when_accessed(): def test_no_nonce_when_not_accessed(): - request = rf.get('/') + request = rf.get("/") mw.process_request(request) response = HttpResponse() mw.process_response(request, response) - assert 'nonce-' not in response[HEADER] + assert "nonce-" not in response[HEADER] def test_nonce_regenerated_on_new_request(): - request1 = rf.get('/') - request2 = rf.get('/') + request1 = rf.get("/") + request2 = rf.get("/") mw.process_request(request1) mw.process_request(request2) nonce1 = str(request1.csp_nonce) @@ -133,7 +132,7 @@ def test_nonce_regenerated_on_new_request(): @override_settings(CSP_INCLUDE_NONCE_IN=[]) def test_no_nonce_when_disabled_by_settings(): - request = rf.get('/') + request = rf.get("/") mw.process_request(request) nonce = str(request.csp_nonce) response = HttpResponse() diff --git a/csp/tests/test_templatetags.py b/csp/tests/test_templatetags.py index a7083e2..03e5953 100644 --- a/csp/tests/test_templatetags.py +++ b/csp/tests/test_templatetags.py @@ -2,7 +2,6 @@ class TestDjangoTemplateTag(ScriptTagTestBase): - def test_script_tag_injects_nonce(self): tpl = """ {% load csp %} @@ -13,11 +12,11 @@ def test_script_tag_injects_nonce(self): self.assert_template_eq(*self.process_templates(tpl, expected)) def test_script_with_src_ignores_body(self): - tpl = (""" + tpl = """ {% load csp %} {% script src="foo" %} var hello='world'; - {% endscript %}""") + {% endscript %}""" expected = """""" @@ -30,10 +29,7 @@ def test_script_tag_sets_attrs_correctly(self): var hello='world'; {% endscript %}""" - expected = ( - '' - 'var hello=\'world\';') + expected = "' "var hello='world';" self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -43,8 +39,7 @@ def test_async_attribute_with_falsey(self): {% script src="foo.com/bar.js" async=False %} {% endscript %}""" - expected = ('') + expected = '" self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -61,7 +56,7 @@ def test_async_attribute_with_truthy(self): def test_nested_script_tags_are_removed(self): """Lets end users wrap their code in script tags for the sake of their - development environment""" + development environment""" tpl = """ {% load csp %} {% script type="application/javascript" id="jeff" defer=True%} @@ -70,10 +65,7 @@ def test_nested_script_tags_are_removed(self): {% endscript %}""" - expected = ( - '' - 'var hello=\'world\';') + expected = "' "var hello='world';" self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -90,9 +82,6 @@ def test_regex_captures_script_content_including_brackets(self): {% endscript %} """ - expected = ( - '"' - '') + expected = '"' "" self.assert_template_eq(*self.process_templates(tpl, expected)) diff --git a/csp/tests/test_utils.py b/csp/tests/test_utils.py index 5a4afd8..dcdc597 100644 --- a/csp/tests/test_utils.py +++ b/csp/tests/test_utils.py @@ -8,9 +8,9 @@ from csp.utils import build_policy -def policy_eq(a, b, msg='%r != %r'): - parts_a = sorted(a.split('; ')) - parts_b = sorted(b.split('; ')) +def policy_eq(a, b, msg="%r != %r"): + parts_a = sorted(a.split("; ")) + parts_b = sorted(b.split("; ")) assert parts_a == parts_b, msg % (a, b) @@ -26,91 +26,91 @@ def literal(s): lazy_literal = lazy(literal, six.text_type) -@override_settings(CSP_DEFAULT_SRC=['example.com', 'example2.com']) +@override_settings(CSP_DEFAULT_SRC=["example.com", "example2.com"]) def test_default_src(): policy = build_policy() - assert 'default-src example.com example2.com' == policy + assert "default-src example.com example2.com" == policy -@override_settings(CSP_SCRIPT_SRC=['example.com']) +@override_settings(CSP_SCRIPT_SRC=["example.com"]) def test_script_src(): policy = build_policy() policy_eq("default-src 'self'; script-src example.com", policy) -@override_settings(CSP_SCRIPT_SRC_ATTR=['example.com']) +@override_settings(CSP_SCRIPT_SRC_ATTR=["example.com"]) def test_script_src_attr(): policy = build_policy() policy_eq("default-src 'self'; script-src-attr example.com", policy) -@override_settings(CSP_SCRIPT_SRC_ELEM=['example.com']) +@override_settings(CSP_SCRIPT_SRC_ELEM=["example.com"]) def test_script_src_elem(): policy = build_policy() policy_eq("default-src 'self'; script-src-elem example.com", policy) -@override_settings(CSP_OBJECT_SRC=['example.com']) +@override_settings(CSP_OBJECT_SRC=["example.com"]) def test_object_src(): policy = build_policy() policy_eq("default-src 'self'; object-src example.com", policy) -@override_settings(CSP_PREFETCH_SRC=['example.com']) +@override_settings(CSP_PREFETCH_SRC=["example.com"]) def test_prefetch_src(): policy = build_policy() policy_eq("default-src 'self'; prefetch-src example.com", policy) -@override_settings(CSP_STYLE_SRC=['example.com']) +@override_settings(CSP_STYLE_SRC=["example.com"]) def test_style_src(): policy = build_policy() policy_eq("default-src 'self'; style-src example.com", policy) -@override_settings(CSP_STYLE_SRC_ATTR=['example.com']) +@override_settings(CSP_STYLE_SRC_ATTR=["example.com"]) def test_style_src_attr(): policy = build_policy() policy_eq("default-src 'self'; style-src-attr example.com", policy) -@override_settings(CSP_STYLE_SRC_ELEM=['example.com']) +@override_settings(CSP_STYLE_SRC_ELEM=["example.com"]) def test_style_src_elem(): policy = build_policy() policy_eq("default-src 'self'; style-src-elem example.com", policy) -@override_settings(CSP_IMG_SRC=['example.com']) +@override_settings(CSP_IMG_SRC=["example.com"]) def test_img_src(): policy = build_policy() policy_eq("default-src 'self'; img-src example.com", policy) -@override_settings(CSP_MEDIA_SRC=['example.com']) +@override_settings(CSP_MEDIA_SRC=["example.com"]) def test_media_src(): policy = build_policy() policy_eq("default-src 'self'; media-src example.com", policy) -@override_settings(CSP_FRAME_SRC=['example.com']) +@override_settings(CSP_FRAME_SRC=["example.com"]) def test_frame_src(): policy = build_policy() policy_eq("default-src 'self'; frame-src example.com", policy) -@override_settings(CSP_FONT_SRC=['example.com']) +@override_settings(CSP_FONT_SRC=["example.com"]) def test_font_src(): policy = build_policy() policy_eq("default-src 'self'; font-src example.com", policy) -@override_settings(CSP_CONNECT_SRC=['example.com']) +@override_settings(CSP_CONNECT_SRC=["example.com"]) def test_connect_src(): policy = build_policy() policy_eq("default-src 'self'; connect-src example.com", policy) -@override_settings(CSP_SANDBOX=['allow-scripts']) +@override_settings(CSP_SANDBOX=["allow-scripts"]) def test_sandbox(): policy = build_policy() policy_eq("default-src 'self'; sandbox allow-scripts", policy) @@ -122,126 +122,121 @@ def test_sandbox_empty(): policy_eq("default-src 'self'; sandbox", policy) -@override_settings(CSP_REPORT_URI='/foo') +@override_settings(CSP_REPORT_URI="/foo") def test_report_uri(): policy = build_policy() policy_eq("default-src 'self'; report-uri /foo", policy) -@override_settings(CSP_REPORT_URI=lazy_literal('/foo')) +@override_settings(CSP_REPORT_URI=lazy_literal("/foo")) def test_report_uri_lazy(): policy = build_policy() policy_eq("default-src 'self'; report-uri /foo", policy) -@override_settings(CSP_REPORT_TO='some_endpoint') +@override_settings(CSP_REPORT_TO="some_endpoint") def test_report_to(): policy = build_policy() - policy_eq("default-src 'self'; report-to some_endpoint", - policy) + policy_eq("default-src 'self'; report-to some_endpoint", policy) -@override_settings(CSP_IMG_SRC=['example.com']) +@override_settings(CSP_IMG_SRC=["example.com"]) def test_update_img(): - policy = build_policy(update={'img-src': 'example2.com'}) - policy_eq("default-src 'self'; img-src example.com example2.com", - policy) + policy = build_policy(update={"img-src": "example2.com"}) + policy_eq("default-src 'self'; img-src example.com example2.com", policy) def test_update_missing_setting(): """update should work even if the setting is not defined.""" - policy = build_policy(update={'img-src': 'example.com'}) + policy = build_policy(update={"img-src": "example.com"}) policy_eq("default-src 'self'; img-src example.com", policy) -@override_settings(CSP_IMG_SRC=['example.com']) +@override_settings(CSP_IMG_SRC=["example.com"]) def test_replace_img(): - policy = build_policy(replace={'img-src': 'example2.com'}) + policy = build_policy(replace={"img-src": "example2.com"}) policy_eq("default-src 'self'; img-src example2.com", policy) def test_replace_missing_setting(): """replace should work even if the setting is not defined.""" - policy = build_policy(replace={'img-src': 'example.com'}) + policy = build_policy(replace={"img-src": "example.com"}) policy_eq("default-src 'self'; img-src example.com", policy) def test_config(): - policy = build_policy( - config={'default-src': ["'none'"], 'img-src': ["'self'"]}) + policy = build_policy(config={"default-src": ["'none'"], "img-src": ["'self'"]}) policy_eq("default-src 'none'; img-src 'self'", policy) -@override_settings(CSP_IMG_SRC=('example.com',)) +@override_settings(CSP_IMG_SRC=("example.com",)) def test_update_string(): """ GitHub issue #40 - given project settings as a tuple, and an update/replace with a string, concatenate correctly. """ - policy = build_policy(update={'img-src': 'example2.com'}) - policy_eq("default-src 'self'; img-src example.com example2.com", - policy) + policy = build_policy(update={"img-src": "example2.com"}) + policy_eq("default-src 'self'; img-src example.com example2.com", policy) -@override_settings(CSP_IMG_SRC=('example.com',)) +@override_settings(CSP_IMG_SRC=("example.com",)) def test_replace_string(): """ Demonstrate that GitHub issue #40 doesn't affect replacements """ - policy = build_policy(replace={'img-src': 'example2.com'}) - policy_eq("default-src 'self'; img-src example2.com", - policy) + policy = build_policy(replace={"img-src": "example2.com"}) + policy_eq("default-src 'self'; img-src example2.com", policy) -@override_settings(CSP_FORM_ACTION=['example.com']) +@override_settings(CSP_FORM_ACTION=["example.com"]) def test_form_action(): policy = build_policy() policy_eq("default-src 'self'; form-action example.com", policy) -@override_settings(CSP_BASE_URI=['example.com']) +@override_settings(CSP_BASE_URI=["example.com"]) def test_base_uri(): policy = build_policy() policy_eq("default-src 'self'; base-uri example.com", policy) -@override_settings(CSP_CHILD_SRC=['example.com']) +@override_settings(CSP_CHILD_SRC=["example.com"]) def test_child_src(): policy = build_policy() policy_eq("default-src 'self'; child-src example.com", policy) -@override_settings(CSP_FRAME_ANCESTORS=['example.com']) +@override_settings(CSP_FRAME_ANCESTORS=["example.com"]) def test_frame_ancestors(): policy = build_policy() policy_eq("default-src 'self'; frame-ancestors example.com", policy) -@override_settings(CSP_NAVIGATE_TO=['example.com']) +@override_settings(CSP_NAVIGATE_TO=["example.com"]) def test_navigate_to(): policy = build_policy() policy_eq("default-src 'self'; navigate-to example.com", policy) -@override_settings(CSP_MANIFEST_SRC=['example.com']) +@override_settings(CSP_MANIFEST_SRC=["example.com"]) def test_manifest_src(): policy = build_policy() policy_eq("default-src 'self'; manifest-src example.com", policy) -@override_settings(CSP_WORKER_SRC=['example.com']) +@override_settings(CSP_WORKER_SRC=["example.com"]) def test_worker_src(): policy = build_policy() policy_eq("default-src 'self'; worker-src example.com", policy) -@override_settings(CSP_PLUGIN_TYPES=['application/pdf']) +@override_settings(CSP_PLUGIN_TYPES=["application/pdf"]) def test_plugin_types(): policy = build_policy() policy_eq("default-src 'self'; plugin-types application/pdf", policy) -@override_settings(CSP_REQUIRE_SRI_FOR=['script']) +@override_settings(CSP_REQUIRE_SRI_FOR=["script"]) def test_require_sri_for(): policy = build_policy() policy_eq("default-src 'self'; require-sri-for script", policy) @@ -253,12 +248,13 @@ def test_require_trusted_types_for(): policy_eq("default-src 'self'; require-trusted-types-for 'script'", policy) -@override_settings(CSP_TRUSTED_TYPES=["strictPolicy", "laxPolicy", - "default", "'allow-duplicates'"]) +@override_settings(CSP_TRUSTED_TYPES=["strictPolicy", "laxPolicy", "default", "'allow-duplicates'"]) def test_trusted_types(): policy = build_policy() - policy_eq("default-src 'self'; trusted-types strictPolicy laxPolicy " - + "default 'allow-duplicates'", policy) + policy_eq( + "default-src 'self'; trusted-types strictPolicy laxPolicy " + "default 'allow-duplicates'", + policy, + ) @override_settings(CSP_UPGRADE_INSECURE_REQUESTS=True) @@ -274,20 +270,21 @@ def test_block_all_mixed_content(): def test_nonce(): - policy = build_policy(nonce='abc123') + policy = build_policy(nonce="abc123") policy_eq("default-src 'self' 'nonce-abc123'", policy) -@override_settings(CSP_INCLUDE_NONCE_IN=['script-src', 'style-src']) +@override_settings(CSP_INCLUDE_NONCE_IN=["script-src", "style-src"]) def test_nonce_include_in(): - policy = build_policy(nonce='abc123') - policy_eq(("default-src 'self'; " - "script-src 'nonce-abc123'; " - "style-src 'nonce-abc123'"), policy) + policy = build_policy(nonce="abc123") + policy_eq( + ("default-src 'self'; " "script-src 'nonce-abc123'; " "style-src 'nonce-abc123'"), + policy, + ) @override_settings() def test_nonce_include_in_absent(): del settings.CSP_INCLUDE_NONCE_IN - policy = build_policy(nonce='abc123') + policy = build_policy(nonce="abc123") policy_eq("default-src 'self' 'nonce-abc123'", policy) diff --git a/csp/tests/utils.py b/csp/tests/utils.py index e51ae10..ba60bc7 100644 --- a/csp/tests/utils.py +++ b/csp/tests/utils.py @@ -1,5 +1,5 @@ from django.http import HttpResponse -from django.template import engines, Template, Context +from django.template import Context, Template, engines from django.test import RequestFactory from csp.middleware import CSPMiddleware @@ -12,31 +12,34 @@ def get_response(req): for k, v in headers.items(): response.headers[k] = v return response + return get_response -JINJA_ENV = engines['jinja2'] +JINJA_ENV = engines["jinja2"] mw = CSPMiddleware(response()) rf = RequestFactory() class ScriptTestBase(object): def assert_template_eq(self, tpl1, tpl2): - aaa = tpl1.replace('\n', '').replace(' ', '') - bbb = tpl2.replace('\n', '').replace(' ', '') + aaa = tpl1.replace("\n", "").replace(" ", "") + bbb = tpl2.replace("\n", "").replace(" ", "") assert aaa == bbb, "{} != {}".format(aaa, bbb) def process_templates(self, tpl, expected): - request = rf.get('/') + request = rf.get("/") mw.process_request(request) ctx = self.make_context(request) - return (self.make_template(tpl).render(ctx).strip(), - expected.format(request.csp_nonce)) + return ( + self.make_template(tpl).render(ctx).strip(), + expected.format(request.csp_nonce), + ) class ScriptTagTestBase(ScriptTestBase): def make_context(self, request): - return Context({'request': request}) + return Context({"request": request}) def make_template(self, tpl): return Template(tpl) @@ -44,7 +47,7 @@ def make_template(self, tpl): class ScriptExtensionTestBase(ScriptTestBase): def make_context(self, request): - return {'request': request} + return {"request": request} def make_template(self, tpl): return JINJA_ENV.from_string(tpl) diff --git a/csp/utils.py b/csp/utils.py index 35a73be..a213fcf 100644 --- a/csp/utils.py +++ b/csp/utils.py @@ -1,6 +1,5 @@ import copy import re - from collections import OrderedDict from itertools import chain @@ -11,45 +10,41 @@ def from_settings(): return { # Fetch Directives - 'child-src': getattr(settings, 'CSP_CHILD_SRC', None), - 'connect-src': getattr(settings, 'CSP_CONNECT_SRC', None), - 'default-src': getattr(settings, 'CSP_DEFAULT_SRC', ["'self'"]), - 'script-src': getattr(settings, 'CSP_SCRIPT_SRC', None), - 'script-src-attr': getattr(settings, 'CSP_SCRIPT_SRC_ATTR', None), - 'script-src-elem': getattr(settings, 'CSP_SCRIPT_SRC_ELEM', None), - 'object-src': getattr(settings, 'CSP_OBJECT_SRC', None), - 'style-src': getattr(settings, 'CSP_STYLE_SRC', None), - 'style-src-attr': getattr(settings, 'CSP_STYLE_SRC_ATTR', None), - 'style-src-elem': getattr(settings, 'CSP_STYLE_SRC_ELEM', None), - 'font-src': getattr(settings, 'CSP_FONT_SRC', None), - 'frame-src': getattr(settings, 'CSP_FRAME_SRC', None), - 'img-src': getattr(settings, 'CSP_IMG_SRC', None), - 'manifest-src': getattr(settings, 'CSP_MANIFEST_SRC', None), - 'media-src': getattr(settings, 'CSP_MEDIA_SRC', None), - 'prefetch-src': getattr(settings, 'CSP_PREFETCH_SRC', None), - 'worker-src': getattr(settings, 'CSP_WORKER_SRC', None), + "child-src": getattr(settings, "CSP_CHILD_SRC", None), + "connect-src": getattr(settings, "CSP_CONNECT_SRC", None), + "default-src": getattr(settings, "CSP_DEFAULT_SRC", ["'self'"]), + "script-src": getattr(settings, "CSP_SCRIPT_SRC", None), + "script-src-attr": getattr(settings, "CSP_SCRIPT_SRC_ATTR", None), + "script-src-elem": getattr(settings, "CSP_SCRIPT_SRC_ELEM", None), + "object-src": getattr(settings, "CSP_OBJECT_SRC", None), + "style-src": getattr(settings, "CSP_STYLE_SRC", None), + "style-src-attr": getattr(settings, "CSP_STYLE_SRC_ATTR", None), + "style-src-elem": getattr(settings, "CSP_STYLE_SRC_ELEM", None), + "font-src": getattr(settings, "CSP_FONT_SRC", None), + "frame-src": getattr(settings, "CSP_FRAME_SRC", None), + "img-src": getattr(settings, "CSP_IMG_SRC", None), + "manifest-src": getattr(settings, "CSP_MANIFEST_SRC", None), + "media-src": getattr(settings, "CSP_MEDIA_SRC", None), + "prefetch-src": getattr(settings, "CSP_PREFETCH_SRC", None), + "worker-src": getattr(settings, "CSP_WORKER_SRC", None), # Document Directives - 'base-uri': getattr(settings, 'CSP_BASE_URI', None), - 'plugin-types': getattr(settings, 'CSP_PLUGIN_TYPES', None), - 'sandbox': getattr(settings, 'CSP_SANDBOX', None), + "base-uri": getattr(settings, "CSP_BASE_URI", None), + "plugin-types": getattr(settings, "CSP_PLUGIN_TYPES", None), + "sandbox": getattr(settings, "CSP_SANDBOX", None), # Navigation Directives - 'form-action': getattr(settings, 'CSP_FORM_ACTION', None), - 'frame-ancestors': getattr(settings, 'CSP_FRAME_ANCESTORS', None), - 'navigate-to': getattr(settings, 'CSP_NAVIGATE_TO', None), + "form-action": getattr(settings, "CSP_FORM_ACTION", None), + "frame-ancestors": getattr(settings, "CSP_FRAME_ANCESTORS", None), + "navigate-to": getattr(settings, "CSP_NAVIGATE_TO", None), # Reporting Directives - 'report-uri': getattr(settings, 'CSP_REPORT_URI', None), - 'report-to': getattr(settings, 'CSP_REPORT_TO', None), - 'require-sri-for': getattr(settings, 'CSP_REQUIRE_SRI_FOR', None), - #trusted Types Directives - 'require-trusted-types-for': getattr( - settings, - 'CSP_REQUIRE_TRUSTED_TYPES_FOR', None), - 'trusted-types': getattr(settings, 'CSP_TRUSTED_TYPES', None), + "report-uri": getattr(settings, "CSP_REPORT_URI", None), + "report-to": getattr(settings, "CSP_REPORT_TO", None), + "require-sri-for": getattr(settings, "CSP_REQUIRE_SRI_FOR", None), + # trusted Types Directives + "require-trusted-types-for": getattr(settings, "CSP_REQUIRE_TRUSTED_TYPES_FOR", None), + "trusted-types": getattr(settings, "CSP_TRUSTED_TYPES", None), # Other Directives - 'upgrade-insecure-requests': getattr( - settings, 'CSP_UPGRADE_INSECURE_REQUESTS', False), - 'block-all-mixed-content': getattr( - settings, 'CSP_BLOCK_ALL_MIXED_CONTENT', False), + "upgrade-insecure-requests": getattr(settings, "CSP_UPGRADE_INSECURE_REQUESTS", False), + "block-all-mixed-content": getattr(settings, "CSP_BLOCK_ALL_MIXED_CONTENT", False), } @@ -84,75 +79,72 @@ def build_policy(config=None, update=None, replace=None, nonce=None): else: csp[k] += tuple(v) - report_uri = csp.pop('report-uri', None) + report_uri = csp.pop("report-uri", None) policy_parts = {} for key, value in csp.items(): # flag directives with an empty directive value if len(value) and value[0] is True: - policy_parts[key] = '' + policy_parts[key] = "" elif len(value) and value[0] is False: pass else: # directives with many values like src lists - policy_parts[key] = ' '.join(value) + policy_parts[key] = " ".join(value) if report_uri: report_uri = map(force_str, report_uri) - policy_parts['report-uri'] = ' '.join(report_uri) + policy_parts["report-uri"] = " ".join(report_uri) if nonce: - include_nonce_in = getattr(settings, 'CSP_INCLUDE_NONCE_IN', - ['default-src']) + include_nonce_in = getattr(settings, "CSP_INCLUDE_NONCE_IN", ["default-src"]) for section in include_nonce_in: - policy = policy_parts.get(section, '') - policy_parts[section] = ("%s %s" % - (policy, "'nonce-%s'" % nonce)).strip() + policy = policy_parts.get(section, "") + policy_parts[section] = ("%s %s" % (policy, "'nonce-%s'" % nonce)).strip() - return '; '.join(['{} {}'.format(k, val).strip() - for k, val in policy_parts.items()]) + return "; ".join(["{} {}".format(k, val).strip() for k, val in policy_parts.items()]) def _default_attr_mapper(attr_name, val): if val: return ' {}="{}"'.format(attr_name, val) else: - return '' + return "" def _bool_attr_mapper(attr_name, val): # Only return the bare word if the value is truthy # ie - defer=False should actually return an empty string if val: - return ' {}'.format(attr_name) + return " {}".format(attr_name) else: - return '' + return "" def _async_attr_mapper(attr_name, val): """The `async` attribute works slightly different than the other bool attributes. It can be set explicitly to `false` with no surrounding quotes according to the spec.""" - if val in [False, 'False']: - return ' {}=false'.format(attr_name) + if val in [False, "False"]: + return " {}=false".format(attr_name) elif val: - return ' {}'.format(attr_name) + return " {}".format(attr_name) else: - return '' + return "" # Allow per-attribute customization of returned string template SCRIPT_ATTRS = OrderedDict() -SCRIPT_ATTRS['nonce'] = _default_attr_mapper -SCRIPT_ATTRS['id'] = _default_attr_mapper -SCRIPT_ATTRS['src'] = _default_attr_mapper -SCRIPT_ATTRS['type'] = _default_attr_mapper -SCRIPT_ATTRS['async'] = _async_attr_mapper -SCRIPT_ATTRS['defer'] = _bool_attr_mapper -SCRIPT_ATTRS['integrity'] = _default_attr_mapper -SCRIPT_ATTRS['nomodule'] = _bool_attr_mapper +SCRIPT_ATTRS["nonce"] = _default_attr_mapper +SCRIPT_ATTRS["id"] = _default_attr_mapper +SCRIPT_ATTRS["src"] = _default_attr_mapper +SCRIPT_ATTRS["type"] = _default_attr_mapper +SCRIPT_ATTRS["async"] = _async_attr_mapper +SCRIPT_ATTRS["defer"] = _bool_attr_mapper +SCRIPT_ATTRS["integrity"] = _default_attr_mapper +SCRIPT_ATTRS["nomodule"] = _bool_attr_mapper # Generates an interpolatable string of valid attrs eg - '{nonce}{id}...' -ATTR_FORMAT_STR = ''.join(['{{{}}}'.format(a) for a in SCRIPT_ATTRS]) +ATTR_FORMAT_STR = "".join(["{{{}}}".format(a) for a in SCRIPT_ATTRS]) _script_tag_contents_re = re.compile( @@ -182,6 +174,6 @@ def build_script_tag(content=None, **kwargs): data[attr_name] = mapper(attr_name, kwargs.get(attr_name)) # Don't render block contents if the script has a 'src' attribute - c = _unwrap_script(content) if content and not kwargs.get('src') else '' + c = _unwrap_script(content) if content and not kwargs.get("src") else "" attrs = ATTR_FORMAT_STR.format(**data).rstrip() - return ('{}'.format(attrs, c).strip()) + return "{}".format(attrs, c).strip() diff --git a/docs/conf.py b/docs/conf.py index b9969f7..602892b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -11,215 +11,208 @@ # All configuration values have a default; values that are commented out # serve to show the default. -import sys, os import pkg_resources # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. -#sys.path.insert(0, os.path.abspath('.')) +# sys.path.insert(0, os.path.abspath('.')) # -- General configuration ----------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. -#needs_sphinx = '1.0' +# needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc'] +extensions = ["sphinx.ext.autodoc"] # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. -master_doc = 'index' +master_doc = "index" # General information about the project. -project = u'Django-CSP' -copyright = u'2016 Mozilla Foundation' +project = "Django-CSP" +copyright = "2016 Mozilla Foundation" # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the # built documents. # # The short X.Y version. -version = pkg_resources.get_distribution('django_csp').version +version = pkg_resources.get_distribution("django_csp").version # The full version, including alpha/beta/rc tags. release = version # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. -#language = None +# language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: -#today = '' +# today = '' # Else, today_fmt is used as the format for a strftime call. -#today_fmt = '%B %d, %Y' +# today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['_build'] +exclude_patterns = ["_build"] # The reST default role (used for this markup: `text`) to use for all documents. -#default_role = None +# default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. -#add_function_parentheses = True +# add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). -#add_module_names = True +# add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. -#show_authors = False +# show_authors = False # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # A list of ignored prefixes for module index sorting. -#modindex_common_prefix = [] +# modindex_common_prefix = [] # -- Options for HTML output --------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. -html_theme = 'default' +html_theme = "default" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -#html_theme_options = {} +# html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = [] +# html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". -#html_title = None +# html_title = None # A shorter title for the navigation bar. Default is the same as html_title. -#html_short_title = None +# html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. -#html_logo = None +# html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. -#html_favicon = None +# html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. -#html_last_updated_fmt = '%b %d, %Y' +# html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. -#html_use_smartypants = True +# html_use_smartypants = True # Custom sidebar templates, maps document names to template names. -#html_sidebars = {} +# html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. -#html_additional_pages = {} +# html_additional_pages = {} # If false, no module index is generated. -#html_domain_indices = True +# html_domain_indices = True # If false, no index is generated. -#html_use_index = True +# html_use_index = True # If true, the index is split into individual pages for each letter. -#html_split_index = False +# html_split_index = False # If true, links to the reST sources are added to the pages. -#html_show_sourcelink = True +# html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. -#html_show_sphinx = True +# html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. -#html_show_copyright = True +# html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. -#html_use_opensearch = '' +# html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). -#html_file_suffix = None +# html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'Django-CSPdoc' +htmlhelp_basename = "Django-CSPdoc" # -- Options for LaTeX output -------------------------------------------------- latex_elements = { -# The paper size ('letterpaper' or 'a4paper'). -#'papersize': 'letterpaper', - -# The font size ('10pt', '11pt' or '12pt'). -#'pointsize': '10pt', - -# Additional stuff for the LaTeX preamble. -#'preamble': '', + # The paper size ('letterpaper' or 'a4paper'). + #'papersize': 'letterpaper', + # The font size ('10pt', '11pt' or '12pt'). + #'pointsize': '10pt', + # Additional stuff for the LaTeX preamble. + #'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'Django-CSP.tex', u'Django-CSP Documentation', - u'James Socol, Mozilla', 'manual'), + ("index", "Django-CSP.tex", "Django-CSP Documentation", "James Socol, Mozilla", "manual"), ] # The name of an image file (relative to this directory) to place at the top of # the title page. -#latex_logo = None +# latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. -#latex_use_parts = False +# latex_use_parts = False # If true, show page references after internal links. -#latex_show_pagerefs = False +# latex_show_pagerefs = False # If true, show URL addresses after external links. -#latex_show_urls = False +# latex_show_urls = False # Documents to append as an appendix to all manuals. -#latex_appendices = [] +# latex_appendices = [] # If false, no module index is generated. -#latex_domain_indices = True +# latex_domain_indices = True # -- Options for manual page output -------------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). -man_pages = [ - ('index', 'django-csp', u'Django-CSP Documentation', - [u'James Socol, Mozilla'], 1) -] +man_pages = [("index", "django-csp", "Django-CSP Documentation", ["James Socol, Mozilla"], 1)] # If true, show URL addresses after external links. -#man_show_urls = False +# man_show_urls = False # -- Options for Texinfo output ------------------------------------------------ @@ -228,16 +221,14 @@ # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ - ('index', 'Django-CSP', u'Django-CSP Documentation', - u'James Socol, Mozilla', 'Django-CSP', 'One line description of project.', - 'Miscellaneous'), + ("index", "Django-CSP", "Django-CSP Documentation", "James Socol, Mozilla", "Django-CSP", "One line description of project.", "Miscellaneous"), ] # Documents to append as an appendix to all manuals. -#texinfo_appendices = [] +# texinfo_appendices = [] # If false, no module index is generated. -#texinfo_domain_indices = True +# texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. -#texinfo_show_urls = 'footnote' +# texinfo_show_urls = 'footnote' diff --git a/docs/contributing.rst b/docs/contributing.rst index ae316d8..5ca400f 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -13,7 +13,7 @@ Style ===== Patches should follow PEP8_ and should not introduce any new violations -as detected by the flake8_ tool. +as detected by the ruff_ tool. Tests @@ -28,12 +28,12 @@ To run the tests, install the requirements (probably into a virtualenv_):: pip install -e . pip install -e ".[tests]" -Then just `py.test`_ to run the tests:: +Then just `pytest`_ to run the tests:: - py.test + pytest .. _PEP8: http://www.python.org/dev/peps/pep-0008/ -.. _flake8: https://pypi.python.org/pypi/flake8 +.. _ruff: https://pypi.org/project/ruff/ .. _virtualenv: http://www.virtualenv.org/ -.. _py.test: https://pytest.org/latest/usage.html +.. _pytest: https://pytest.org/latest/usage.html diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..532f2d0 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,60 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".eggs", + ".git", + ".pyenv", + ".pytest_cache", + ".ruff_cache", + ".tox", + ".vscode", + "build", + "dist", + "node_modules", +] + +line-length = 150 +indent-width = 4 + +# Assume Python 3.11 +target-version = "py311" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = ["E4", "E7", "E9", "F"] +ignore = [] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" \ No newline at end of file diff --git a/setup.cfg b/setup.cfg index b92ce01..6476415 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,4 @@ [tool:pytest] -addopts = -vs --tb=short --pep8 --flakes +addopts = -vs --tb=short --ruff --ruff-format DJANGO_SETTINGS_MODULE = csp.tests.settings diff --git a/setup.py b/setup.py index f410b65..f3e1299 100644 --- a/setup.py +++ b/setup.py @@ -31,14 +31,10 @@ def read(*parts): ] test_requires = [ - "pytest<4.0", + "pytest", "pytest-cov", "pytest-django", - "pytest-flakes==1.0.1", - "pytest-pep8==1.0.6", - "pep8==1.4.6", - "mock==1.0.1", - "six==1.12.0", + "pytest-ruff", ] test_requires += jinja2_requires From 26c39e40888f3d184df85e33be26b39324a07b06 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Tue, 23 Jan 2024 17:37:13 +0000 Subject: [PATCH 03/12] Stop using six --- csp/middleware.py | 9 ++------- csp/tests/test_utils.py | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/csp/middleware.py b/csp/middleware.py index 49168ba..0e58024 100644 --- a/csp/middleware.py +++ b/csp/middleware.py @@ -1,18 +1,13 @@ from __future__ import absolute_import -import os import base64 +import http.client as http_client +import os from functools import partial from django.conf import settings from django.utils.functional import SimpleLazyObject -try: - from django.utils.six.moves import http_client -except ImportError: - # django 3.x removed six - import http.client as http_client - try: from django.utils.deprecation import MiddlewareMixin except ImportError: diff --git a/csp/tests/test_utils.py b/csp/tests/test_utils.py index dcdc597..00760d5 100644 --- a/csp/tests/test_utils.py +++ b/csp/tests/test_utils.py @@ -1,6 +1,5 @@ from __future__ import absolute_import -import six from django.conf import settings from django.test.utils import override_settings from django.utils.functional import lazy @@ -23,7 +22,7 @@ def literal(s): return s -lazy_literal = lazy(literal, six.text_type) +lazy_literal = lazy(literal) @override_settings(CSP_DEFAULT_SRC=["example.com", "example2.com"]) From 0f321ac2f694fa094f1d85e050023fe129495df4 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 11:08:07 +0000 Subject: [PATCH 04/12] Drop support for EOL Python <3.8 and Django <2.2 versions --- CHANGES | 26 ++++++++++++++++---------- setup.py | 13 +++++++------ tox.ini | 17 ++++++++--------- 3 files changed, 31 insertions(+), 25 deletions(-) diff --git a/CHANGES b/CHANGES index 90b5406..5b3c2eb 100644 --- a/CHANGES +++ b/CHANGES @@ -6,10 +6,16 @@ Next ==== - Remove deprecation warning for child-src -- Add project urls to setup.py -- Drop support for EOL Python <3.6 and Django <2.2 versions -- Rename default branch to main - Fix capturing brackets in script template tags +- Move to hatch +- Move to pyproject.toml + - Add project urls +- Rename default branch to main + +Unreleased +========== + +- Drop support for EOL Python <3.8 and Django <2.2 versions 3.7 === @@ -50,12 +56,12 @@ Next 3.2 === -- Add manifest-src fetch directive - https://w3c.github.io/webappsec-csp/#directive-manifest-src -- Add worker-src fetch directive - https://w3c.github.io/webappsec-csp/#directive-worker-src -- Add plugin-types document directive - https://w3c.github.io/webappsec-csp/#directive-plugin-types -- Add require-sri-for https://www.w3.org/TR/CSP/#directives-elsewhere - https://w3c.github.io/webappsec-subresource-integrity/#request-verification-algorithms -- Add upgrade-insecure-requests - https://w3c.github.io/webappsec-upgrade-insecure-requests/#delivery -- Add block-all-mixed-content - https://w3c.github.io/webappsec-mixed-content/ +- Add manifest-src fetch directive - +- Add worker-src fetch directive - +- Add plugin-types document directive - +- Add require-sri-for - +- Add upgrade-insecure-requests - +- Add block-all-mixed-content - - Add deprecation warning for child-src (#80) 3.1 @@ -76,7 +82,7 @@ v3.0 Please note that this is a big release that touches quite a few parts so please make sure you're testing thoroughly and report any issues to -https://github.com/mozilla/django-csp/issues + v2.0.3 ====== diff --git a/setup.py b/setup.py index f3e1299..1c10c07 100644 --- a/setup.py +++ b/setup.py @@ -23,7 +23,7 @@ def read(*parts): install_requires = [ - "Django>=2.2", + "Django>=3.2", ] jinja2_requires = [ @@ -47,8 +47,8 @@ def read(*parts): long_description=read("README.rst"), author="James Socol", author_email="me@jamessocol.com", - maintainer="Christopher Grebs", - maintainer_email="cg@webshox.org", + maintainer="Mozilla MEAO team", + maintainer_email="meao-backend@mozilla.com", url="http://github.com/mozilla/django-csp", license="BSD", packages=find_packages(), @@ -76,14 +76,15 @@ def read(*parts): "Topic :: Software Development :: Libraries :: Python Modules", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.6", - "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", "Programming Language :: Python :: Implementation :: PyPy", "Programming Language :: Python :: Implementation :: CPython", - "Framework :: Django", + "Framework :: Django :: 3.2", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", ], ) diff --git a/tox.ini b/tox.ini index e2c50cc..656e6f7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,29 +1,28 @@ [tox] envlist = - {3.8,3.9,3.10,3.11,pypy3}-main - {3.8,3.9,3.10,3.11,pypy3}-4.1.x - {3.8,3.9,3.10,pypy3}-4.0.x - {3.6,3.7,3.8,3.9,3.10,pypy3}-3.2.x + {3.10,3.11,3.12,pypy3}-main + {3.10,3.11,3.12pypy3}-5.0.x + {3.8,3.9,3.10,3.11,3.12,pypy3}-4.2.x + {3.8,3.9,3.10,pypy3}-3.2.x [testenv] setenv = PYTHONPATH={toxinidir} PYTHONDONTWRITEBYTECODE=1 commands = - pip install --upgrade pip setuptools wheel + pip install --upgrade pip pip install -e .[tests] pytest --cov={toxinidir}/csp {toxinidir}/csp basepython = - 3.6: python3.6 - 3.7: python3.7 3.8: python3.8 3.9: python3.9 3.10: python3.10 3.11: python3.11 + 3.12: python3.12 pypy3: pypy3 deps= pytest - 4.0.x: Django>=4.0,<4.1 - 4.1.x: Django>=4.1,<4.2 3.2.x: Django>=3.2,<3.3 + 4.2.x: Django>=4.2,<4.3 + 5.0.x: Django>=5.0.1,<5.1 main: https://github.com/django/django/archive/main.tar.gz From cdad9abfe23c7736074250a54ffb126d54d0285f Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 13:46:34 +0000 Subject: [PATCH 05/12] Update changelog --- CHANGES | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGES b/CHANGES index 5b3c2eb..defadfa 100644 --- a/CHANGES +++ b/CHANGES @@ -10,12 +10,12 @@ Next - Move to hatch - Move to pyproject.toml - Add project urls -- Rename default branch to main Unreleased ========== - Drop support for EOL Python <3.8 and Django <2.2 versions +- Switch to ruff instead of pep8 and flake8 3.7 === From 111c78f1292c9d15255a7bafdb15f6b535410c03 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 13:46:54 +0000 Subject: [PATCH 06/12] Swap CircleCI for GH Actions for CI: test and relase publishing --- .circleci/config.yml | 87 --------------------------------------- .github/workflows/ci.yaml | 57 +++++++++++++++++++++++++ CHANGES | 1 + README.rst | 4 +- tox.ini | 38 ++++++++++++++--- 5 files changed, 93 insertions(+), 94 deletions(-) delete mode 100644 .circleci/config.yml create mode 100644 .github/workflows/ci.yaml diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 008cd02..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,87 +0,0 @@ -version: 2.1 - -workflows: - test: - jobs: - - test: - matrix: - parameters: - python_image: - - "python:3.6-slim" - - "python:3.7-slim" - - "python:3.8-slim" - - "python:3.9-slim" - - "python:3.10-slim" - - "python:3.11-slim" - - "pypy:3-slim-buster" - django_version: - - "3.2.x" # 3.0 supports python 3.6 to 3.9 - - "4.0.x" # 4.0 supports python 3.8 to 3.10 - - "4.1.x" # 4.1 supports python 3.8 to 3.11 - - "main" # 4.1 supports 3.8 to 3.11 - exclude: - - python_image: "python:3.6-slim" - django_version: "main" - - python_image: "python:3.6-slim" - django_version: "4.0.x" - - python_image: "python:3.6-slim" - django_version: "4.1.x" - - python_image: "python:3.7-slim" - django_version: "main" - - python_image: "python:3.7-slim" - django_version: "4.0.x" - - python_image: "python:3.7-slim" - django_version: "4.1.x" - - python_image: "python:3.10-slim" - django_version: "3.2.x" - - python_image: "python:3.11-slim" - django_version: "3.2.x" - - python_image: "python:3.11-slim" - django_version: "4.0.x" - -jobs: - test: - parameters: - python_image: - type: string - django_version: - type: string - docker: - - image: << parameters.python_image >> - # auth: - # username: $DOCKER_USER - # password: $DOCKER_PASS - environment: - DJANGO_VERSION: << parameters.django_version >> - PYTHON_IMAGE: << parameters.python_image >> - steps: - - checkout - - run: - name: install - command: pip install tox coveralls - - when: - condition: - not: - or: - - equal: ["pypy:3-slim-buster", << parameters.python_image >>] - steps: - - run: - name: test cpython - command: | - PYTHON_VERSION="$(python --version 2>&1 | cut -d ' ' -f 2 | cut -d '.' -f 1,2)" - echo "$PYTHON_VERSION-$DJANGO_VERSION" - tox -e "$PYTHON_VERSION-$DJANGO_VERSION" - - when: - condition: - or: - - equal: ["pypy:3-slim-buster", << parameters.python_image >>] - steps: - - run: - name: test pypy - command: | - PYTHON_VERSION="pypy$(pypy --version 2>&1 | head -n 1 | cut -d ' ' -f 2 | cut -d '.' -f 1)" - echo "$PYTHON_VERSION-$DJANGO_VERSION" - tox -e "$PYTHON_VERSION-$DJANGO_VERSION" - - run: - name: report coverage - command: coveralls diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..32a810c --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,57 @@ +name: "CI" # Note that this name appears in the README's badge +on: + push: + branches: + - main + workflow_dispatch: + pull_request: + release: + types: [published] +jobs: + run-tests: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: + - '3.8' + - '3.9' + - '3.10' + - '3.11' + - '3.12' + - 'pypy-3.8' + - 'pypy-3.9' + - 'pypy-3.10' + + steps: + - uses: actions/checkout@v3 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + - name: Install dependencies + run: | + python -m pip install --upgrade pip + python -m pip install tox tox-gh-actions + - name: Test with tox + run: tox + + release: + name: Release django-csp + if: github.event_name == 'release' && github.event.action == 'published' + needs: + - run-tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - uses: actions/setup-python@v4 + with: + python-version: 3.12 + - name: Install dependencies for package building only + run: pip install build + - name: Build package for upload to PyPI + run: python -m build . + - name: Upload the distribution to PyPI + uses: pypa/gh-action-pypi-publish@v1.4.2 + with: + user: __token__ + password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file diff --git a/CHANGES b/CHANGES index defadfa..0afeea4 100644 --- a/CHANGES +++ b/CHANGES @@ -16,6 +16,7 @@ Unreleased - Drop support for EOL Python <3.8 and Django <2.2 versions - Switch to ruff instead of pep8 and flake8 +- Move from CircleCI to Github Actions for CI 3.7 === diff --git a/README.rst b/README.rst index 5c7b02a..d4e13d9 100644 --- a/README.rst +++ b/README.rst @@ -1,8 +1,8 @@ .. image:: https://badge.fury.io/py/django-csp.svg :target: https://pypi.python.org/pypi/django_csp -.. image:: https://circleci.com/gh/mozilla/django-csp/tree/main.svg?style=shield - :target: https://circleci.com/gh/mozilla/django-csp/?branch=main +.. image:: https://github.com/mozilla/django-csp/actions/workflows/run-tests.yaml/badge.svg + :target: https://github.com/mozilla/django-csp/actions/workflows/run-tests.yaml .. image:: https://coveralls.io/repos/github/mozilla/django-csp/badge.svg?branch=main :target: https://coveralls.io/github/mozilla/django-csp?branch=main diff --git a/tox.ini b/tox.ini index 656e6f7..e9ec469 100644 --- a/tox.ini +++ b/tox.ini @@ -1,18 +1,30 @@ [tox] envlist = - {3.10,3.11,3.12,pypy3}-main - {3.10,3.11,3.12pypy3}-5.0.x - {3.8,3.9,3.10,3.11,3.12,pypy3}-4.2.x - {3.8,3.9,3.10,pypy3}-3.2.x + {3.10,3.11,3.12,pypy310}-main + {3.10,3.11,3.12,pypy310}-5.0.x + {3.8,3.9,3.10,3.11,3.12,pypy38,pypy39,pypy310}-4.2.x + {3.8,3.9,3.10,pypy38,pypy39,pypy310}-3.2.x + + +# Don't run coverage when testing with pypy: +# see https://github.com/nedbat/coveragepy/issues/1382 +[testenv:pypy310-main,pypy310-5.0.x,{pypy38,pypy39,pypy310}-4.2.x,{pypy38,pypy39,pypy310}-3.2.x] +commands = + pip install --upgrade pip + pip install -e .[tests] + pytest {toxinidir}/csp + [testenv] setenv = PYTHONPATH={toxinidir} PYTHONDONTWRITEBYTECODE=1 + commands = pip install --upgrade pip pip install -e .[tests] pytest --cov={toxinidir}/csp {toxinidir}/csp + basepython = 3.8: python3.8 3.9: python3.9 @@ -20,9 +32,25 @@ basepython = 3.11: python3.11 3.12: python3.12 pypy3: pypy3 -deps= + +deps = pytest 3.2.x: Django>=3.2,<3.3 4.2.x: Django>=4.2,<4.3 5.0.x: Django>=5.0.1,<5.1 main: https://github.com/django/django/archive/main.tar.gz + + +[gh-actions] +# Running tox in GHA without redefining it all in a GHA matrix: +# https://github.com/ymyzk/tox-gh-actions +python = + 3.7: py37 + 3.8: py38 + 3.9: py39 + 3.10: py310 + 3.11: py311 + 3.12: py312 + pypy-3.8: pypy38 + pypy-3.9: pypy39 + pypy-3.10: pypy310 From ef195a1a23ddb305f4ef7b1863202faec3824eb7 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 16:36:16 +0000 Subject: [PATCH 07/12] Add pre-commit config with sensible hooks; apply those hooks --- .github/workflows/ci.yaml | 2 +- .pre-commit-config.yaml | 24 ++++++++++++++++++++++++ CHANGES | 1 + CODE_OF_CONDUCT.md | 4 ++-- LICENSE | 1 - README.rst | 2 -- docs/contributing.rst | 3 +++ docs/trusted_types.rst | 2 +- ruff.toml | 2 +- 9 files changed, 33 insertions(+), 8 deletions(-) create mode 100644 .pre-commit-config.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 32a810c..66de6e5 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -54,4 +54,4 @@ jobs: uses: pypa/gh-action-pypi-publish@v1.4.2 with: user: __token__ - password: ${{ secrets.PYPI_API_TOKEN }} \ No newline at end of file + password: ${{ secrets.PYPI_API_TOKEN }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..198e4af --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,24 @@ +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at https://mozilla.org/MPL/2.0/. + +# Global excludes, override per repo below if different excludes required. +# exclude: > +# (?x)^( +# DIRNAME_OR_FILENAME_HERE +# | DIRNAME_OR_FILENAME_HERE +# | DIRNAME_OR_FILENAME_HERE +# ) +repos: + # Note: hooks that add content must run before ones which check formatting, lint, etc + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 # Use the ref you want to point at + hooks: + - id: check-yaml + - id: end-of-file-fixer + - id: trailing-whitespace + - repo: https://github.com/charliermarsh/ruff-pre-commit + rev: v0.1.14 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] diff --git a/CHANGES b/CHANGES index 0afeea4..d132b6c 100644 --- a/CHANGES +++ b/CHANGES @@ -17,6 +17,7 @@ Unreleased - Drop support for EOL Python <3.8 and Django <2.2 versions - Switch to ruff instead of pep8 and flake8 - Move from CircleCI to Github Actions for CI +- Add support for using pre-commit with the project 3.7 === diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md index 498baa3..041fbb6 100644 --- a/CODE_OF_CONDUCT.md +++ b/CODE_OF_CONDUCT.md @@ -1,8 +1,8 @@ # Community Participation Guidelines -This repository is governed by Mozilla's code of conduct and etiquette guidelines. +This repository is governed by Mozilla's code of conduct and etiquette guidelines. For more details, please read the -[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). +[Mozilla Community Participation Guidelines](https://www.mozilla.org/about/governance/policies/participation/). ## How to Report For more information on how to report violations of the Community Participation Guidelines, please read our '[How to Report](https://www.mozilla.org/about/governance/policies/participation/reporting/)' page. diff --git a/LICENSE b/LICENSE index 9776754..8651744 100644 --- a/LICENSE +++ b/LICENSE @@ -25,4 +25,3 @@ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - diff --git a/README.rst b/README.rst index d4e13d9..b2f88a6 100644 --- a/README.rst +++ b/README.rst @@ -16,8 +16,6 @@ Django-CSP adds Content-Security-Policy_ headers to Django. The code lives on GitHub_, where you can report Issues_. The full documentation is available on ReadTheDocs_. - - .. _Content-Security-Policy: http://www.w3.org/TR/CSP/ .. _GitHub: https://github.com/mozilla/django-csp .. _Issues: https://github.com/mozilla/django-csp/issues diff --git a/docs/contributing.rst b/docs/contributing.rst index 5ca400f..4fc170e 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -15,6 +15,8 @@ Style Patches should follow PEP8_ and should not introduce any new violations as detected by the ruff_ tool. +To help stay on top of this, install pre-commit_, and then run ``pre-commit install-hooks``. Now you'll be set up +to auto-format your code according to our style and check for errors for every commit. Tests ===== @@ -37,3 +39,4 @@ Then just `pytest`_ to run the tests:: .. _ruff: https://pypi.org/project/ruff/ .. _virtualenv: http://www.virtualenv.org/ .. _pytest: https://pytest.org/latest/usage.html +.. _pre-commit: https://pre-commit.com/#install diff --git a/docs/trusted_types.rst b/docs/trusted_types.rst index 638536c..58d1bee 100644 --- a/docs/trusted_types.rst +++ b/docs/trusted_types.rst @@ -119,7 +119,7 @@ dangerous sink that requires Trusted Types. ``Step 3: Enforce Trusted Types`` ================================= Once you have addressed all of the Trusted Types violations present in your -application, you can begin enforcing Trusted Types to prevent DOM XSS. +application, you can begin enforcing Trusted Types to prevent DOM XSS. Configure django-csp so that ``CSP_REPORT_ONLY`` is set to *False*. diff --git a/ruff.toml b/ruff.toml index 532f2d0..42cf5a4 100644 --- a/ruff.toml +++ b/ruff.toml @@ -57,4 +57,4 @@ docstring-code-format = false # # This only has an effect when the `docstring-code-format` setting is # enabled. -docstring-code-line-length = "dynamic" \ No newline at end of file +docstring-code-line-length = "dynamic" From b00c829f977e5ae69d60736ef62afb3856a44277 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 16:55:05 +0000 Subject: [PATCH 08/12] Remove inert Coveralls badge --- CHANGES | 5 +++-- README.rst | 3 --- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/CHANGES b/CHANGES index d132b6c..afbc0ee 100644 --- a/CHANGES +++ b/CHANGES @@ -7,9 +7,10 @@ Next - Remove deprecation warning for child-src - Fix capturing brackets in script template tags -- Move to hatch -- Move to pyproject.toml +- Move to Hatch for packaging +- Move project to pyproject.toml - Add project urls +- Set up coverage badge Unreleased ========== diff --git a/README.rst b/README.rst index b2f88a6..e70615d 100644 --- a/README.rst +++ b/README.rst @@ -4,9 +4,6 @@ .. image:: https://github.com/mozilla/django-csp/actions/workflows/run-tests.yaml/badge.svg :target: https://github.com/mozilla/django-csp/actions/workflows/run-tests.yaml -.. image:: https://coveralls.io/repos/github/mozilla/django-csp/badge.svg?branch=main - :target: https://coveralls.io/github/mozilla/django-csp?branch=main - ========== Django-CSP ========== From 4eddacc112652e2ee334b553cd9e90ec8ed6ca15 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 17:17:14 +0000 Subject: [PATCH 09/12] Fold in code suggestion from PR#199 --- csp/tests/test_utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/csp/tests/test_utils.py b/csp/tests/test_utils.py index 00760d5..39c687d 100644 --- a/csp/tests/test_utils.py +++ b/csp/tests/test_utils.py @@ -22,7 +22,7 @@ def literal(s): return s -lazy_literal = lazy(literal) +lazy_literal = lazy(literal, str) @override_settings(CSP_DEFAULT_SRC=["example.com", "example2.com"]) From f38dd2f6615a455fdfba1a1630feb8f9423b7262 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 17:59:19 +0000 Subject: [PATCH 10/12] Remove unused Python spec Co-authored-by: Tim Schilling --- tox.ini | 1 - 1 file changed, 1 deletion(-) diff --git a/tox.ini b/tox.ini index e9ec469..4f82062 100644 --- a/tox.ini +++ b/tox.ini @@ -45,7 +45,6 @@ deps = # Running tox in GHA without redefining it all in a GHA matrix: # https://github.com/ymyzk/tox-gh-actions python = - 3.7: py37 3.8: py38 3.9: py39 3.10: py310 From 3d3fac4a5f23054f2633b7607541ecd93beb8dfc Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 21:06:56 +0000 Subject: [PATCH 11/12] Fix up unnecessarily split strings, which were a side-effect of reformatting --- csp/tests/test_jinja_extension.py | 8 ++++---- csp/tests/test_templatetags.py | 6 +++--- csp/tests/test_utils.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/csp/tests/test_jinja_extension.py b/csp/tests/test_jinja_extension.py index f6e573f..227feb3 100644 --- a/csp/tests/test_jinja_extension.py +++ b/csp/tests/test_jinja_extension.py @@ -42,7 +42,7 @@ def test_async_attribute_with_falsey(self): var hello='world'; {% endscript %}""" - expected = '" + expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -52,7 +52,7 @@ def test_async_attribute_with_truthy(self): var hello='world'; {% endscript %}""" - expected = '" + expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -66,7 +66,7 @@ def test_nested_script_tags_are_removed(self): {% endscript %}""" - expected = "' "var hello='world';" + expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -82,6 +82,6 @@ def test_regex_captures_script_content_including_brackets(self): {% endscript %} """ - expected = '"' "" + expected = '"' self.assert_template_eq(*self.process_templates(tpl, expected)) diff --git a/csp/tests/test_templatetags.py b/csp/tests/test_templatetags.py index 03e5953..45e96d5 100644 --- a/csp/tests/test_templatetags.py +++ b/csp/tests/test_templatetags.py @@ -29,7 +29,7 @@ def test_script_tag_sets_attrs_correctly(self): var hello='world'; {% endscript %}""" - expected = "' "var hello='world';" + expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -65,7 +65,7 @@ def test_nested_script_tags_are_removed(self): {% endscript %}""" - expected = "' "var hello='world';" + expected = '' self.assert_template_eq(*self.process_templates(tpl, expected)) @@ -82,6 +82,6 @@ def test_regex_captures_script_content_including_brackets(self): {% endscript %} """ - expected = '"' "" + expected = '"' self.assert_template_eq(*self.process_templates(tpl, expected)) diff --git a/csp/tests/test_utils.py b/csp/tests/test_utils.py index 39c687d..b5f6a60 100644 --- a/csp/tests/test_utils.py +++ b/csp/tests/test_utils.py @@ -251,7 +251,7 @@ def test_require_trusted_types_for(): def test_trusted_types(): policy = build_policy() policy_eq( - "default-src 'self'; trusted-types strictPolicy laxPolicy " + "default 'allow-duplicates'", + "default-src 'self'; trusted-types strictPolicy laxPolicy default 'allow-duplicates'", policy, ) @@ -277,7 +277,7 @@ def test_nonce(): def test_nonce_include_in(): policy = build_policy(nonce="abc123") policy_eq( - ("default-src 'self'; " "script-src 'nonce-abc123'; " "style-src 'nonce-abc123'"), + "default-src 'self'; script-src 'nonce-abc123'; style-src 'nonce-abc123'", policy, ) From a5bf0b391ae211136ed3376d27537182811d3216 Mon Sep 17 00:00:00 2001 From: Steve Jalim Date: Wed, 24 Jan 2024 21:11:23 +0000 Subject: [PATCH 12/12] Ensure pre-commit also runs ruff-format, to match test runs --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 198e4af..41be4c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,5 +20,8 @@ repos: - repo: https://github.com/charliermarsh/ruff-pre-commit rev: v0.1.14 hooks: + # Run the linter - id: ruff args: [--fix, --exit-non-zero-on-fix] + # Run the formatter + - id: ruff-format