diff --git a/.circleci/config.yml b/.circleci/config.yml deleted file mode 100644 index 93080aa..0000000 --- a/.circleci/config.yml +++ /dev/null @@ -1,72 +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" - - "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 - exclude: - - python_image: "python:3.6-slim" - django_version: "main" - - 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" - -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..66de6e5 --- /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 }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..41be4c1 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,27 @@ +# 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: + # Run the linter + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + # Run the formatter + - id: ruff-format diff --git a/CHANGES b/CHANGES index 90b5406..afbc0ee 100644 --- a/CHANGES +++ b/CHANGES @@ -6,10 +6,19 @@ 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 for packaging +- Move project to pyproject.toml + - Add project urls +- Set up coverage badge + +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 === @@ -50,12 +59,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 +85,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/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 5c7b02a..e70615d 100644 --- a/README.rst +++ b/README.rst @@ -1,11 +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://coveralls.io/repos/github/mozilla/django-csp/badge.svg?branch=main - :target: https://coveralls.io/github/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 ========== Django-CSP @@ -16,8 +13,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/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..0e58024 100644 --- a/csp/middleware.py +++ b/csp/middleware.py @@ -1,28 +1,26 @@ 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: + 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 +33,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 +46,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 +63,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 +76,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..227feb3 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 = '' 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..45e96d5 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 = '' 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 = '' 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..b5f6a60 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 @@ -8,9 +7,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) @@ -23,94 +22,94 @@ def literal(s): return s -lazy_literal = lazy(literal, six.text_type) +lazy_literal = lazy(literal, str) -@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 +121,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 +247,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 +269,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..4fc170e 100644 --- a/docs/contributing.rst +++ b/docs/contributing.rst @@ -13,8 +13,10 @@ Style ===== Patches should follow PEP8_ and should not introduce any new violations -as detected by the flake8_ tool. +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 ===== @@ -28,12 +30,13 @@ 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 +.. _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 new file mode 100644 index 0000000..42cf5a4 --- /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" 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 95cae78..1c10c07 100644 --- a/setup.py +++ b/setup.py @@ -1,60 +1,56 @@ -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>=3.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", + "pytest-cov", + "pytest-django", + "pytest-ruff", ] 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="Mozilla MEAO team", + maintainer_email="meao-backend@mozilla.com", + url="http://github.com/mozilla/django-csp", + license="BSD", packages=find_packages(), project_urls={ "Documentation": "http://django-csp.readthedocs.org/", @@ -64,28 +60,31 @@ 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.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 :: 3.2", + "Framework :: Django :: 4.2", + "Framework :: Django :: 5.0", + ], ) diff --git a/tox.ini b/tox.ini index 65c4dbd..4f82062 100644 --- a/tox.ini +++ b/tox.ini @@ -1,25 +1,55 @@ [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.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 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= + +deps = pytest - 2.2.x: Django>=2.2,<2.3 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.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