diff --git a/.editorconfig b/.editorconfig
new file mode 100644
index 0000000..1714ae0
--- /dev/null
+++ b/.editorconfig
@@ -0,0 +1,28 @@
+# Defines the coding style for different editors and IDEs.
+# http://editorconfig.org
+
+# top-most EditorConfig file
+root = true
+
+# Rules for source code.
+[*]
+charset = utf-8
+end_of_line = lf
+insert_final_newline = true
+indent_size = 2
+indent_style = space
+trim_trailing_whitespace = true
+
+# Rules for Python code.
+[*.py]
+indent_size = 4
+
+# Rules for markdown documents.
+[*.md]
+indent_size = 4
+trim_trailing_whitespace = false
+
+# Rules for makefile
+[Makefile]
+indent_style = tab
+indent_size = 4
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..9d866e3
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,11 @@
+# To get started with Dependabot version updates, you'll need to specify which
+# package ecosystems to update and where the package manifests are located.
+# Please see the documentation for all configuration options:
+# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file
+
+version: 2
+updates:
+ - package-ecosystem: "pip" # See documentation for possible values
+ directory: "/" # Location of package manifests
+ schedule:
+ interval: "weekly"
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
new file mode 100644
index 0000000..d9296f6
--- /dev/null
+++ b/.github/pull_request_template.md
@@ -0,0 +1,11 @@
+### Summary
+- Write a quick summary of what this PR is for
+
+
+##### Related Links
+- Paste link to ticket or any other related sites here
+
+##### Ready for QA Checklist
+- [ ] Code Review
+- [ ] Dev QA
+- [ ] Rebase and Squash
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..4005830
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,32 @@
+name: Release
+
+on:
+ push:
+ tags:
+ - '*'
+ workflow_dispatch:
+
+jobs:
+ publish:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.12"]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip setuptools poetry tox-gh-actions
+ poetry install
+ - name: Build wheels and source tarball
+ run: poetry build
+ - name: publish to PyPi
+ uses: pypa/gh-action-pypi-publish@release/v1
+ with:
+ user: __token__
+ password: ${{ secrets.PYPI_API_TOKEN }}
+ skip_existing: true
diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
new file mode 100644
index 0000000..0a59d99
--- /dev/null
+++ b/.github/workflows/validate.yml
@@ -0,0 +1,37 @@
+name: Validate
+
+on:
+ push:
+ branches:
+ - develop
+ - master
+ - main
+ - 'release/**'
+ pull_request:
+ branches:
+ - '*'
+ workflow_dispatch:
+
+jobs:
+ validate:
+ runs-on: ubuntu-latest
+ strategy:
+ matrix:
+ python-version: ["3.12"]
+ steps:
+ - uses: actions/checkout@v4
+ - name: Set up Python ${{ matrix.python-version }}
+ uses: actions/setup-python@v5
+ with:
+ python-version: ${{ matrix.python-version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install --upgrade pip setuptools poetry
+ poetry install
+ - name: Linting
+ run: |
+ make lint
+ - name: Security
+ run: make bandit
+ - name: Testing
+ run: make tests
diff --git a/.gitignore b/.gitignore
index 8b7f3a5..54f234e 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,24 +1,58 @@
-# python compiled files
-*.pyc
-*.pyo
-*.egg-info
-
-# back up files from VIM / Emacs
-*~
+*.py[co]
*.swp
+*.bak
-# ignore setup.py build dir
-build/
+# docs
+_build
-# ignore sphinx built documentation
-_build/
+# Packages
+*.egg
+*.egg-info
+dist
+build
+eggs
+parts
+bin
+var
+sdist
+develop-eggs
+.installed.cfg
-# ignore tox files
-.tox/
+# Installer logs
+pip-log.txt
-# ignore coverage files
+# Unit test / coverage reports
.coverage
+.tox
+
+.idea
+
+.DS_Store
+
+#Translations
+*.mo
+
+#Mr Developer
+.mr.developer.cfg
+
+# Configuration
+sdelint.cnf
+
+#generated data
+usecases/output.csv
+
+# Test files
+info.log
htmlcov/
-# development tools
-.vscode/
+#ides
+.idea
+.vscode
+
+#custom cert bundle
+my_root_certs.crt
+
+# symbolic links
+.flake8
+
+conf/
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 0000000..5a073f0
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,38 @@
+repos:
+- repo: https://github.com/Lucas-C/pre-commit-hooks
+ rev: v1.1.13
+ hooks:
+ - id: forbid-crlf
+ - id: remove-crlf
+ - id: forbid-tabs
+ exclude_types: [csv]
+ - id: remove-tabs
+ exclude_types: [csv]
+
+- repo: https://github.com/pre-commit/pre-commit-hooks
+ rev: v4.1.0
+ hooks:
+ - id: trailing-whitespace
+ - id: end-of-file-fixer
+ - id: check-merge-conflict
+ - id: check-yaml
+ args: [--unsafe]
+
+- repo: https://github.com/pre-commit/mirrors-isort
+ rev: v5.10.1
+ hooks:
+ - id: isort
+
+- repo: https://github.com/ambv/black
+ rev: 22.3.0
+ hooks:
+ - id: black
+ language_version: python3.12
+
+- repo: https://github.com/pycqa/flake8
+ rev: 3.9.2
+ hooks:
+ - id: flake8
+ additional_dependencies: [flake8-typing-imports==1.10.0]
+ exclude: ^tests
+
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 806f9b5..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,19 +0,0 @@
-language: python
-matrix:
- include:
- - python: 3.6
- env: DJANGO=1.11 TOXENV=py36-django111
- - python: 3.6
- env: DJANGO=2.2 TOXENV=py36-django22
- - python: 3.6
- env: DJANGO=3.0 TOXENV=py36-django30
- - python: 3.6
- env: DJANGO=3.0 TOXENV=docs
-
-install:
- - pip install tox
- - pip install coveralls
-script:
- - tox
-after_success:
- - coveralls
diff --git a/LICENSE.txt b/LICENSE.md
similarity index 100%
rename from LICENSE.txt
rename to LICENSE.md
diff --git a/MANIFEST.in b/MANIFEST.in
deleted file mode 100644
index bb3ec5f..0000000
--- a/MANIFEST.in
+++ /dev/null
@@ -1 +0,0 @@
-include README.md
diff --git a/Makefile b/Makefile
index 6ef5ac5..da3ad5f 100644
--- a/Makefile
+++ b/Makefile
@@ -87,3 +87,14 @@ doctest:
$(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
@echo "Testing of doctests in the sources finished, look at the " \
"results in $(BUILDDIR)/doctest/output.txt."
+
+lint:
+ poetry run black .
+ poetry run isort .
+ poetry run flake8 . --extend-ignore=D,E501,W601 --extend-exclude=docs/,**/migrations/*,tests/ --statistics --count
+
+bandit:
+ poetry run bandit -c pyproject.toml -r .
+
+test:
+ poetry run python ./runtests.py
diff --git a/README.md b/README.md
index 81b673a..dfc6528 100644
--- a/README.md
+++ b/README.md
@@ -12,13 +12,8 @@ Automatically generated documentation of `django-security` is available on Read
# Requirements
-* Python >= 3.6
-* Django >= 1.11
-
-For Django < 1.8 use django-security==0.9.4. For Django < 1.11 use django-security==0.11.3.
-
-Note: For versions prior to 0.10.0, `datetime` objects were being added to the session and required Django's PickleSerializer for (de)serializing. This has now been changed so that the strings of these `datetime`s are being stored instead. If you are still using PickleSerializer for this reason, we suggest switching to Django's default JSONSerializer (default since Django 1.6) for better security.
-
+* Python >=3.12
+* Django ~4.2
# Installation
@@ -31,7 +26,7 @@ If you prefer the latest development version, install from
git clone https://github.com/sdelements/django-security.git
cd django-security
- sudo python setup.py install
+ poetry install
Adding to Django application's `settings.py` file:
@@ -41,28 +36,14 @@ Adding to Django application's `settings.py` file:
...
)
-Pre-Django 1.10, middleware modules can be added to `MIDDLEWARE_CLASSES` list in settings file:
-
- MIDDLEWARE_CLASSES = (
- ...
- 'security.middleware.DoNotTrackMiddleware',
- 'security.middleware.ContentNoSniff',
- 'security.middleware.XssProtectMiddleware',
- 'security.middleware.XFrameOptionsMiddleware',
- )
-
-After Django 1.10, middleware modules can be added to `MIDDLEWARE` list in settings file:
+Middleware modules can be added to `MIDDLEWARE` list in settings file:
MIDDLEWARE = (
...
- 'security.middleware.DoNotTrackMiddleware',
- 'security.middleware.ContentNoSniff',
- 'security.middleware.XssProtectMiddleware',
- 'security.middleware.XFrameOptionsMiddleware',
+ 'security.middleware.LoginRequiredMiddleware',
+ ...
)
-
-
Unlike the modules listed above, some other modules **require** configuration settings,
fully described in [django-security documentation](http://django-security.readthedocs.org/en/latest/).
Brief description is provided below.
@@ -73,76 +54,60 @@ Provided middleware modules will modify web application's output and input and i
or minimum configuration.
-
-Middleware
- Description
- Configuration
-
-ClearSiteDataMiddleware
-Send Clear-Site-Data header in HTTP response for any page that has been whitelisted. Recommended .
- Required.
-
-
-ContentNoSniff
-DEPRECATED: Will be removed in future releases, consider django.middleware.security.SecurityMiddleware via SECURE_CONTENT_TYPE_NOSNIFF setting. Disable possibly insecure autodetection of MIME types in browsers. Recommended.
-None.
-
-
-ContentSecurityPolicyMiddleware
-Send Content Security Policy (CSP) header in HTTP response. Recommended, requires careful tuning.
- Required.
-
-
-DoNotTrackMiddleware
-Read user browser's DoNotTrack preference and pass it to application. Recommended, requires implementation in views and templates.
- None.
-
-
-LoginRequiredMiddleware
-Requires a user to be authenticated to view any page on the site that hasn't been white listed.
- Required.
+ Middleware
+Description
+Configuration
+
-MandatoryPasswordChangeMiddleware
-Redirects any request from an authenticated user to the password change form if that user's password has expired.
- Required.
+ ClearSiteDataMiddleware
+Send Clear-Site-Data header in HTTP response for any page that has been whitelisted. Recommended .
+Required.
+
-NoConfidentialCachingMiddleware
-Adds No-Cache and No-Store headers to confidential pages.
- Required.
+ ContentSecurityPolicyMiddleware
+Send Content Security Policy (CSP) header in HTTP response. Recommended, requires careful tuning.
+Required.
+
-P3PPolicyMiddleware
-DEPRECATED: Will be removed in future releases. Adds the HTTP header attribute specifying compact P3P policy.
-Required.
+ LoginRequiredMiddleware
+Requires a user to be authenticated to view any page on the site that hasn't been white listed.
+Required.
+
-ReferrerPolicyMiddleware
-Specify when the browser will set a `Referer` header.
- Optional.
+ MandatoryPasswordChangeMiddleware
+Redirects any request from an authenticated user to the password change form if that user's password has expired.
+Required.
+
-SessionExpiryPolicyMiddleware
-Expire sessions on browser close, and on expiry times stored in the cookie itself.
- Required.
+ NoConfidentialCachingMiddleware
+Adds No-Cache and No-Store headers to confidential pages.
+Required.
+
-StrictTransportSecurityMiddleware
-DEPRECATED: Will be removed in future releases, consider django.middleware.security.SecurityMiddleware via SECURE_HSTS_SECONDS , SECURE_HSTS_INCLUDE_SUBDOMAINS and SECURE_HSTS_PRELOAD settings. Enforce SSL/TLS connection and disable plaintext fall-back. Recommended for SSL/TLS sites.
-Optional.
+ ReferrerPolicyMiddleware
+Specify when the browser will set a `Referer` header.
+Optional.
+
-XFrameOptionsMiddleware
-Disable framing of the website, mitigating Clickjacking attacks. Recommended.
- Optional.
+ SessionExpiryPolicyMiddleware
+Expire sessions on browser close, and on expiry times stored in the cookie itself.
+Required.
+
-XssProtectMiddleware
-DEPRECATED: Will be removed in future releases, consider django.middleware.security.SecurityMiddleware via SECURE_BROWSER_XSS_FILTER setting. Enforce browser's Cross Site Scripting protection. Recommended.
-None.
+ ProfilingMiddleware
+A simple middleware to capture useful profiling information in Django.
+Optional.
+
diff --git a/conf.py b/docs/conf.py
similarity index 78%
rename from conf.py
rename to docs/conf.py
index fe10b06..bd66d82 100644
--- a/conf.py
+++ b/docs/conf.py
@@ -11,112 +11,115 @@
# All configuration values have a default; values that are commented out
# serve to show the default.
-import sys, os
+import os
+import sys
# 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.append(os.path.dirname(__file__))
-os.environ.setdefault("DJANGO_SETTINGS_MODULE", "testing.settings")
+os.environ.setdefault("DJANGO_SETTINGS_MODULE", "test.settings")
# -- General configuration -----------------------------------------------------
# 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'
+source_encoding = "utf-8"
# The master toctree document.
-master_doc = 'index'
+master_doc = "index"
# General information about the project.
-project = u'django-security'
-copyright = u'2013, SD Elements'
+project = "django-security"
+copyright = "2013, SD Elements"
# 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 = '1.0'
+version = "1.0"
# The full version, including alpha/beta/rc tags.
-release = '1.0'
+release = "1.0"
# The language for content autogenerated by Sphinx. Refer to documentation
# for a list of supported languages.
-#language = Python
+# language = Python
# 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 documents that shouldn't be included in the build.
-#unused_docs = []
+# unused_docs = []
# List of directories, relative to source directory, that shouldn't be searched
# for source files.
-exclude_patterns = ['_build/*', '.tox/*']
+exclude_patterns = ["_build/*", ".tox/*"]
# 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. Major themes that come with
# Sphinx are currently 'default' and 'sphinxdoc'.
-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,
@@ -125,71 +128,76 @@
# 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_use_modindex = True
+# html_use_modindex = 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, 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 = ''
# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml").
-#html_file_suffix = ''
+# html_file_suffix = ''
# Output file base name for HTML help builder.
-htmlhelp_basename = 'django-securitydoc'
+htmlhelp_basename = "django-securitydoc"
# -- Options for LaTeX output --------------------------------------------------
# The paper size ('letter' or 'a4').
-#latex_paper_size = 'letter'
+# latex_paper_size = 'letter'
# The font size ('10pt', '11pt' or '12pt').
-#latex_font_size = '10pt'
+# latex_font_size = '10pt'
# Grouping the document tree into LaTeX files. List of tuples
# (source start file, target name, title, author, documentclass [howto/manual]).
latex_documents = [
- ('index', 'django-security.tex', u'django-security Documentation',
- u'Pawel Krawczyk', 'manual'),
+ (
+ "index",
+ "django-security.tex",
+ "django-security Documentation",
+ "Pawel Krawczyk",
+ "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
# Additional stuff for the LaTeX preamble.
-#latex_preamble = ''
+# latex_preamble = ''
# Documents to append as an appendix to all manuals.
-#latex_appendices = []
+# latex_appendices = []
# If false, no module index is generated.
-#latex_use_modindex = True
+# latex_use_modindex = True
diff --git a/index.rst b/docs/index.rst
similarity index 100%
rename from index.rst
rename to docs/index.rst
diff --git a/poetry.lock b/poetry.lock
new file mode 100644
index 0000000..be001ae
--- /dev/null
+++ b/poetry.lock
@@ -0,0 +1,704 @@
+# This file is automatically @generated by Poetry 1.6.1 and should not be changed by hand.
+
+[[package]]
+name = "asgiref"
+version = "3.8.1"
+description = "ASGI specs, helper code, and adapters"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "asgiref-3.8.1-py3-none-any.whl", hash = "sha256:3e1e3ecc849832fe52ccf2cb6686b7a55f82bb1d6aee72a58826471390335e47"},
+ {file = "asgiref-3.8.1.tar.gz", hash = "sha256:c343bd80a0bec947a9860adb4c432ffa7db769836c64238fc34bdc3fec84d590"},
+]
+
+[package.extras]
+tests = ["mypy (>=0.800)", "pytest", "pytest-asyncio"]
+
+[[package]]
+name = "attrs"
+version = "23.2.0"
+description = "Classes Without Boilerplate"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "attrs-23.2.0-py3-none-any.whl", hash = "sha256:99b87a485a5820b23b879f04c2305b44b951b502fd64be915879d77a7e8fc6f1"},
+ {file = "attrs-23.2.0.tar.gz", hash = "sha256:935dc3b529c262f6cf76e50877d35a4bd3c1de194fd41f47a2b7ae8f19971f30"},
+]
+
+[package.extras]
+cov = ["attrs[tests]", "coverage[toml] (>=5.3)"]
+dev = ["attrs[tests]", "pre-commit"]
+docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier", "zope-interface"]
+tests = ["attrs[tests-no-zope]", "zope-interface"]
+tests-mypy = ["mypy (>=1.6)", "pytest-mypy-plugins"]
+tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "pytest (>=4.3.0)", "pytest-xdist[psutil]"]
+
+[[package]]
+name = "bandit"
+version = "1.7.8"
+description = "Security oriented static analyser for python code."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "bandit-1.7.8-py3-none-any.whl", hash = "sha256:509f7af645bc0cd8fd4587abc1a038fc795636671ee8204d502b933aee44f381"},
+ {file = "bandit-1.7.8.tar.gz", hash = "sha256:36de50f720856ab24a24dbaa5fee2c66050ed97c1477e0a1159deab1775eab6b"},
+]
+
+[package.dependencies]
+colorama = {version = ">=0.3.9", markers = "platform_system == \"Windows\""}
+PyYAML = ">=5.3.1"
+rich = "*"
+stevedore = ">=1.20.0"
+
+[package.extras]
+baseline = ["GitPython (>=3.1.30)"]
+sarif = ["jschema-to-python (>=1.2.3)", "sarif-om (>=1.0.4)"]
+test = ["beautifulsoup4 (>=4.8.0)", "coverage (>=4.5.4)", "fixtures (>=3.0.0)", "flake8 (>=4.0.0)", "pylint (==1.9.4)", "stestr (>=2.5.0)", "testscenarios (>=0.5.0)", "testtools (>=2.3.0)"]
+toml = ["tomli (>=1.1.0)"]
+yaml = ["PyYAML"]
+
+[[package]]
+name = "black"
+version = "24.4.2"
+description = "The uncompromising code formatter."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"},
+ {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"},
+ {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"},
+ {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"},
+ {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"},
+ {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"},
+ {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"},
+ {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"},
+ {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"},
+ {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"},
+ {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"},
+ {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"},
+ {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"},
+ {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"},
+ {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"},
+ {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"},
+ {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"},
+ {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"},
+ {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"},
+ {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"},
+ {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"},
+ {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"},
+]
+
+[package.dependencies]
+click = ">=8.0.0"
+mypy-extensions = ">=0.4.3"
+packaging = ">=22.0"
+pathspec = ">=0.9.0"
+platformdirs = ">=2"
+
+[package.extras]
+colorama = ["colorama (>=0.4.3)"]
+d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"]
+jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
+uvloop = ["uvloop (>=0.15.2)"]
+
+[[package]]
+name = "cfgv"
+version = "3.4.0"
+description = "Validate configuration and produce human readable error messages."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"},
+ {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"},
+]
+
+[[package]]
+name = "click"
+version = "8.1.7"
+description = "Composable command line interface toolkit"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"},
+ {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"},
+]
+
+[package.dependencies]
+colorama = {version = "*", markers = "platform_system == \"Windows\""}
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+description = "Cross-platform colored terminal text."
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
+ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
+]
+
+[[package]]
+name = "distlib"
+version = "0.3.8"
+description = "Distribution utilities"
+optional = false
+python-versions = "*"
+files = [
+ {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"},
+ {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"},
+]
+
+[[package]]
+name = "django"
+version = "4.2.13"
+description = "A high-level Python web framework that encourages rapid development and clean, pragmatic design."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "Django-4.2.13-py3-none-any.whl", hash = "sha256:a17fcba2aad3fc7d46fdb23215095dbbd64e6174bf4589171e732b18b07e426a"},
+ {file = "Django-4.2.13.tar.gz", hash = "sha256:837e3cf1f6c31347a1396a3f6b65688f2b4bb4a11c580dcb628b5afe527b68a5"},
+]
+
+[package.dependencies]
+asgiref = ">=3.6.0,<4"
+sqlparse = ">=0.3.1"
+tzdata = {version = "*", markers = "sys_platform == \"win32\""}
+
+[package.extras]
+argon2 = ["argon2-cffi (>=19.1.0)"]
+bcrypt = ["bcrypt"]
+
+[[package]]
+name = "django-upgrade"
+version = "1.18.0"
+description = "Automatically upgrade your Django project code."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "django_upgrade-1.18.0-py3-none-any.whl", hash = "sha256:bae6a466bb9dd63dd7e23b665499b84499b2661348f06b371b88d2e609aa0df9"},
+ {file = "django_upgrade-1.18.0.tar.gz", hash = "sha256:ae2a2de13e7804773201aef6af2245fa5d503b0a7c88b85b12cf1fdb84197065"},
+]
+
+[package.dependencies]
+tokenize-rt = ">=4.1"
+
+[[package]]
+name = "filelock"
+version = "3.14.0"
+description = "A platform independent file lock."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "filelock-3.14.0-py3-none-any.whl", hash = "sha256:43339835842f110ca7ae60f1e1c160714c5a6afd15a2873419ab185334975c0f"},
+ {file = "filelock-3.14.0.tar.gz", hash = "sha256:6ea72da3be9b8c82afd3edcf99f2fffbb5076335a5ae4d03248bb5b6c3eae78a"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"]
+typing = ["typing-extensions (>=4.8)"]
+
+[[package]]
+name = "flake8"
+version = "7.0.0"
+description = "the modular source code checker: pep8 pyflakes and co"
+optional = false
+python-versions = ">=3.8.1"
+files = [
+ {file = "flake8-7.0.0-py2.py3-none-any.whl", hash = "sha256:a6dfbb75e03252917f2473ea9653f7cd799c3064e54d4c8140044c5c065f53c3"},
+ {file = "flake8-7.0.0.tar.gz", hash = "sha256:33f96621059e65eec474169085dc92bf26e7b2d47366b70be2f67ab80dc25132"},
+]
+
+[package.dependencies]
+mccabe = ">=0.7.0,<0.8.0"
+pycodestyle = ">=2.11.0,<2.12.0"
+pyflakes = ">=3.2.0,<3.3.0"
+
+[[package]]
+name = "flake8-bandit"
+version = "4.1.1"
+description = "Automated security testing with bandit and flake8."
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "flake8_bandit-4.1.1-py3-none-any.whl", hash = "sha256:4c8a53eb48f23d4ef1e59293657181a3c989d0077c9952717e98a0eace43e06d"},
+ {file = "flake8_bandit-4.1.1.tar.gz", hash = "sha256:068e09287189cbfd7f986e92605adea2067630b75380c6b5733dab7d87f9a84e"},
+]
+
+[package.dependencies]
+bandit = ">=1.7.3"
+flake8 = ">=5.0.0"
+
+[[package]]
+name = "flake8-bugbear"
+version = "24.4.26"
+description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle."
+optional = false
+python-versions = ">=3.8.1"
+files = [
+ {file = "flake8_bugbear-24.4.26-py3-none-any.whl", hash = "sha256:cb430dd86bc821d79ccc0b030789a9c87a47a369667f12ba06e80f11305e8258"},
+ {file = "flake8_bugbear-24.4.26.tar.gz", hash = "sha256:ff8d4ba5719019ebf98e754624c30c05cef0dadcf18a65d91c7567300e52a130"},
+]
+
+[package.dependencies]
+attrs = ">=19.2.0"
+flake8 = ">=6.0.0"
+
+[package.extras]
+dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", "tox"]
+
+[[package]]
+name = "flake8-docstrings"
+version = "1.7.0"
+description = "Extension for flake8 which uses pydocstyle to check docstrings"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "flake8_docstrings-1.7.0-py2.py3-none-any.whl", hash = "sha256:51f2344026da083fc084166a9353f5082b01f72901df422f74b4d953ae88ac75"},
+ {file = "flake8_docstrings-1.7.0.tar.gz", hash = "sha256:4c8cc748dc16e6869728699e5d0d685da9a10b0ea718e090b1ba088e67a941af"},
+]
+
+[package.dependencies]
+flake8 = ">=3"
+pydocstyle = ">=2.1"
+
+[[package]]
+name = "flake8-polyfill"
+version = "1.0.2"
+description = "Polyfill package for Flake8 plugins"
+optional = false
+python-versions = "*"
+files = [
+ {file = "flake8-polyfill-1.0.2.tar.gz", hash = "sha256:e44b087597f6da52ec6393a709e7108b2905317d0c0b744cdca6208e670d8eda"},
+ {file = "flake8_polyfill-1.0.2-py2.py3-none-any.whl", hash = "sha256:12be6a34ee3ab795b19ca73505e7b55826d5f6ad7230d31b18e106400169b9e9"},
+]
+
+[package.dependencies]
+flake8 = "*"
+
+[[package]]
+name = "identify"
+version = "2.5.36"
+description = "File identification library for Python"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"},
+ {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"},
+]
+
+[package.extras]
+license = ["ukkonen"]
+
+[[package]]
+name = "isort"
+version = "5.13.2"
+description = "A Python utility / library to sort Python imports."
+optional = false
+python-versions = ">=3.8.0"
+files = [
+ {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"},
+ {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"},
+]
+
+[package.extras]
+colors = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "markdown-it-py"
+version = "3.0.0"
+description = "Python port of markdown-it. Markdown parsing, done right!"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
+ {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
+]
+
+[package.dependencies]
+mdurl = ">=0.1,<1.0"
+
+[package.extras]
+benchmarking = ["psutil", "pytest", "pytest-benchmark"]
+code-style = ["pre-commit (>=3.0,<4.0)"]
+compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"]
+linkify = ["linkify-it-py (>=1,<3)"]
+plugins = ["mdit-py-plugins"]
+profiling = ["gprof2dot"]
+rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"]
+testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"]
+
+[[package]]
+name = "mccabe"
+version = "0.7.0"
+description = "McCabe checker, plugin for flake8"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
+ {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
+]
+
+[[package]]
+name = "mdurl"
+version = "0.1.2"
+description = "Markdown URL utilities"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
+ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.0.0"
+description = "Type system extensions for programs checked with the mypy type checker."
+optional = false
+python-versions = ">=3.5"
+files = [
+ {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"},
+ {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"},
+]
+
+[[package]]
+name = "nodeenv"
+version = "1.9.1"
+description = "Node.js virtual environment builder"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+files = [
+ {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"},
+ {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"},
+]
+
+[[package]]
+name = "packaging"
+version = "24.0"
+description = "Core utilities for Python packages"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"},
+ {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"},
+]
+
+[[package]]
+name = "pathspec"
+version = "0.12.1"
+description = "Utility library for gitignore style pattern matching of file paths."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
+ {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
+]
+
+[[package]]
+name = "pbr"
+version = "6.0.0"
+description = "Python Build Reasonableness"
+optional = false
+python-versions = ">=2.6"
+files = [
+ {file = "pbr-6.0.0-py2.py3-none-any.whl", hash = "sha256:4a7317d5e3b17a3dccb6a8cfe67dab65b20551404c52c8ed41279fa4f0cb4cda"},
+ {file = "pbr-6.0.0.tar.gz", hash = "sha256:d1377122a5a00e2f940ee482999518efe16d745d423a670c27773dfbc3c9a7d9"},
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.2.2"
+description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"},
+ {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"},
+]
+
+[package.extras]
+docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"]
+test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"]
+type = ["mypy (>=1.8)"]
+
+[[package]]
+name = "pre-commit"
+version = "3.7.1"
+description = "A framework for managing and maintaining multi-language pre-commit hooks."
+optional = false
+python-versions = ">=3.9"
+files = [
+ {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"},
+ {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"},
+]
+
+[package.dependencies]
+cfgv = ">=2.0.0"
+identify = ">=1.0.0"
+nodeenv = ">=0.11.1"
+pyyaml = ">=5.1"
+virtualenv = ">=20.10.0"
+
+[[package]]
+name = "pycodestyle"
+version = "2.11.1"
+description = "Python style guide checker"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pycodestyle-2.11.1-py2.py3-none-any.whl", hash = "sha256:44fe31000b2d866f2e41841b18528a505fbd7fef9017b04eff4e2648a0fadc67"},
+ {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
+]
+
+[[package]]
+name = "pydocstyle"
+version = "6.3.0"
+description = "Python docstring style checker"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "pydocstyle-6.3.0-py3-none-any.whl", hash = "sha256:118762d452a49d6b05e194ef344a55822987a462831ade91ec5c06fd2169d019"},
+ {file = "pydocstyle-6.3.0.tar.gz", hash = "sha256:7ce43f0c0ac87b07494eb9c0b462c0b73e6ff276807f204d6b53edc72b7e44e1"},
+]
+
+[package.dependencies]
+snowballstemmer = ">=2.2.0"
+
+[package.extras]
+toml = ["tomli (>=1.2.3)"]
+
+[[package]]
+name = "pyflakes"
+version = "3.2.0"
+description = "passive checker of Python programs"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pyflakes-3.2.0-py2.py3-none-any.whl", hash = "sha256:84b5be138a2dfbb40689ca07e2152deb896a65c3a3e24c251c5c62489568074a"},
+ {file = "pyflakes-3.2.0.tar.gz", hash = "sha256:1c61603ff154621fb2a9172037d84dca3500def8c8b630657d1701f026f8af3f"},
+]
+
+[[package]]
+name = "pygments"
+version = "2.18.0"
+description = "Pygments is a syntax highlighting package written in Python."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"},
+ {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"},
+]
+
+[package.extras]
+windows-terminal = ["colorama (>=0.4.6)"]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+description = "Extensions to the standard Python datetime module"
+optional = false
+python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+files = [
+ {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
+ {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
+]
+
+[package.dependencies]
+six = ">=1.5"
+
+[[package]]
+name = "pyyaml"
+version = "6.0.1"
+description = "YAML parser and emitter for Python"
+optional = false
+python-versions = ">=3.6"
+files = [
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"},
+ {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"},
+ {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"},
+ {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"},
+ {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"},
+ {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"},
+ {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"},
+ {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"},
+ {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"},
+ {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"},
+ {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"},
+ {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"},
+ {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"},
+ {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"},
+ {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"},
+ {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"},
+ {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"},
+ {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"},
+ {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"},
+ {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"},
+ {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"},
+ {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"},
+ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"},
+]
+
+[[package]]
+name = "rich"
+version = "13.7.1"
+description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
+optional = false
+python-versions = ">=3.7.0"
+files = [
+ {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"},
+ {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"},
+]
+
+[package.dependencies]
+markdown-it-py = ">=2.2.0"
+pygments = ">=2.13.0,<3.0.0"
+
+[package.extras]
+jupyter = ["ipywidgets (>=7.5.1,<9)"]
+
+[[package]]
+name = "six"
+version = "1.16.0"
+description = "Python 2 and 3 compatibility utilities"
+optional = false
+python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*"
+files = [
+ {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"},
+ {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"},
+]
+
+[[package]]
+name = "snowballstemmer"
+version = "2.2.0"
+description = "This package provides 29 stemmers for 28 languages generated from Snowball algorithms."
+optional = false
+python-versions = "*"
+files = [
+ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
+ {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
+]
+
+[[package]]
+name = "south"
+version = "1.0.2"
+description = "South: Migrations for Django"
+optional = false
+python-versions = "*"
+files = [
+ {file = "South-1.0.2.tar.gz", hash = "sha256:d360bd31898f9df59f6faa786551065bba45b35e7ee3c39b381b4fbfef7392f4"},
+]
+
+[[package]]
+name = "sqlparse"
+version = "0.5.0"
+description = "A non-validating SQL parser."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "sqlparse-0.5.0-py3-none-any.whl", hash = "sha256:c204494cd97479d0e39f28c93d46c0b2d5959c7b9ab904762ea6c7af211c8663"},
+ {file = "sqlparse-0.5.0.tar.gz", hash = "sha256:714d0a4932c059d16189f58ef5411ec2287a4360f17cdd0edd2d09d4c5087c93"},
+]
+
+[package.extras]
+dev = ["build", "hatch"]
+doc = ["sphinx"]
+
+[[package]]
+name = "stevedore"
+version = "5.2.0"
+description = "Manage dynamic plugins for Python applications"
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "stevedore-5.2.0-py3-none-any.whl", hash = "sha256:1c15d95766ca0569cad14cb6272d4d31dae66b011a929d7c18219c176ea1b5c9"},
+ {file = "stevedore-5.2.0.tar.gz", hash = "sha256:46b93ca40e1114cea93d738a6c1e365396981bb6bb78c27045b7587c9473544d"},
+]
+
+[package.dependencies]
+pbr = ">=2.0.0,<2.1.0 || >2.1.0"
+
+[[package]]
+name = "tokenize-rt"
+version = "5.2.0"
+description = "A wrapper around the stdlib `tokenize` which roundtrips."
+optional = false
+python-versions = ">=3.8"
+files = [
+ {file = "tokenize_rt-5.2.0-py2.py3-none-any.whl", hash = "sha256:b79d41a65cfec71285433511b50271b05da3584a1da144a0752e9c621a285289"},
+ {file = "tokenize_rt-5.2.0.tar.gz", hash = "sha256:9fe80f8a5c1edad2d3ede0f37481cc0cc1538a2f442c9c2f9e4feacd2792d054"},
+]
+
+[[package]]
+name = "tzdata"
+version = "2024.1"
+description = "Provider of IANA time zone data"
+optional = false
+python-versions = ">=2"
+files = [
+ {file = "tzdata-2024.1-py2.py3-none-any.whl", hash = "sha256:9068bc196136463f5245e51efda838afa15aaeca9903f49050dfa2679db4d252"},
+ {file = "tzdata-2024.1.tar.gz", hash = "sha256:2674120f8d891909751c38abcdfd386ac0a5a1127954fbc332af6b5ceae07efd"},
+]
+
+[[package]]
+name = "ua-parser"
+version = "0.18.0"
+description = "Python port of Browserscope's user agent parser"
+optional = false
+python-versions = "*"
+files = [
+ {file = "ua-parser-0.18.0.tar.gz", hash = "sha256:db51f1b59bfaa82ed9e2a1d99a54d3e4153dddf99ac1435d51828165422e624e"},
+ {file = "ua_parser-0.18.0-py2.py3-none-any.whl", hash = "sha256:9d94ac3a80bcb0166823956a779186c746b50ea4c9fd9bf30fdb758553c38950"},
+]
+
+[[package]]
+name = "virtualenv"
+version = "20.26.2"
+description = "Virtual Python Environment builder"
+optional = false
+python-versions = ">=3.7"
+files = [
+ {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"},
+ {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"},
+]
+
+[package.dependencies]
+distlib = ">=0.3.7,<1"
+filelock = ">=3.12.2,<4"
+platformdirs = ">=3.9.1,<5"
+
+[package.extras]
+docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"]
+test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"]
+
+[metadata]
+lock-version = "2.0"
+python-versions = "~3.12"
+content-hash = "035ae5f00aa880116e169b83eda8bb722a1675fee3eeab783fe558f64bef5e43"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..3f6db1c
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,69 @@
+[tool.poetry]
+name = "django-security"
+version = "1.0.0"
+homepage = "https://github.com/sdelements/django-security"
+description = "Models, views, middlewares and forms to facilitate security hardening of Django applications."
+authors = ["Security Compass "]
+license = "BSD-3-Clause"
+readme = "README.md"
+# See https://pypi.python.org/pypi?%3Aaction=list_classifiers
+classifiers=[
+ # How mature is this project? Common values are
+ # 3 - Alpha
+ # 4 - Beta
+ # 5 - Production/Stable
+ 'Development Status :: 5 - Production/Stable',
+
+ # Indicate who your project is intended for
+ 'Intended Audience :: Developers',
+ 'Topic :: Software Development :: Libraries :: Python Modules',
+
+ # Pick your license as you wish (should match "license" above)
+ 'License :: OSI Approved :: BSD License',
+
+ # Supported Languages
+ 'Natural Language :: English',
+ 'Operating System :: OS Independent',
+ 'Programming Language :: Python :: 3',
+ 'Programming Language :: Python :: 3.12',
+ 'Framework :: Django',
+]
+packages = [
+ { include = "security" },
+ { include = "tests", format = "sdist" },
+]
+exclude = [
+ "security/**/tests",
+ "tests"
+]
+
+[tool.poetry.dependencies]
+python = "~3.12"
+django = "~4.2"
+python-dateutil = "2.9.0.post0"
+south = "1.0.2"
+ua_parser = "0.18.0"
+
+[tool.poetry.dev-dependencies]
+pre-commit = "3.7.1"
+# lint
+black = "24.4.2"
+flake8 = "7.0.0"
+flake8-bandit = "4.1.1"
+flake8-bugbear = "24.4.26"
+flake8-docstrings = "1.7.0"
+flake8-polyfill = "1.0.2"
+isort = "5.13.2"
+# security
+bandit = "1.7.8"
+# test
+django-upgrade = "1.18.0"
+
+[build-system]
+requires = ["poetry-core"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.bandit]
+exclude_dirs = [
+ './tests/',
+]
diff --git a/requirements b/requirements
deleted file mode 100644
index 92cd4c5..0000000
--- a/requirements
+++ /dev/null
@@ -1,3 +0,0 @@
-django>=1.11
-ua_parser>=0.7.1
-python-dateutil>=2.8.1
diff --git a/runtests.py b/runtests.py
new file mode 100644
index 0000000..cdf26b6
--- /dev/null
+++ b/runtests.py
@@ -0,0 +1,15 @@
+#!/usr/bin/env python
+import os
+import sys
+
+from django.core.management import execute_from_command_line
+
+
+def runtests():
+ os.environ.setdefault("DJANGO_SETTINGS_MODULE", "tests.settings")
+ argv = sys.argv[:1] + ["test"] + sys.argv[1:]
+ execute_from_command_line(argv)
+
+
+if __name__ == "__main__":
+ runtests()
diff --git a/security/admin.py b/security/admin.py
index 2315d11..4ebb677 100644
--- a/security/admin.py
+++ b/security/admin.py
@@ -1,6 +1,6 @@
from django.contrib import admin
-from security.models import PasswordExpiry, CspReport
+from security.models import CspReport, PasswordExpiry
admin.site.register(PasswordExpiry)
admin.site.register(CspReport)
diff --git a/security/auth.py b/security/auth.py
index 98ac89d..0b42bf1 100644
--- a/security/auth.py
+++ b/security/auth.py
@@ -11,10 +11,12 @@ def min_length(n):
because django.core.validators.MinLengthValidator doesn't take a message
argument.
"""
+
def validate(password):
if len(password) < n:
message = _("It must contain at least %d characters.") % n
raise ValidationError(message)
+
return validate
@@ -23,17 +25,17 @@ def validate(password):
lowercase = RegexValidator(
r"[a-z]",
_("It must contain at least one lowercase letter."),
- '',
+ "",
)
uppercase = RegexValidator(
r"[A-Z]",
_("It must contain at least one uppercase letter."),
- '',
+ "",
)
digit = RegexValidator(
r"[0-9]",
_("It must contain at least one decimal digit."),
- '',
+ "",
)
diff --git a/security/auth_throttling/__init__.py b/security/auth_throttling/__init__.py
index 8d79939..8a8869c 100644
--- a/security/auth_throttling/__init__.py
+++ b/security/auth_throttling/__init__.py
@@ -2,8 +2,8 @@
import hashlib
import logging
-from math import ceil
import time # Monkeypatched by the tests.
+from math import ceil
from django.contrib.auth import REDIRECT_FIELD_NAME
from django.contrib.auth.forms import AuthenticationForm
@@ -12,7 +12,7 @@
from django.core.exceptions import ImproperlyConfigured
from django.http import HttpResponse
from django.shortcuts import render
-from django.utils.translation import ugettext as _
+from django.utils.translation import gettext as _
from django.views.decorators.csrf import csrf_protect
from security.middleware import BaseMiddleware
@@ -55,7 +55,7 @@ def _key(counter_type, counter_name):
counter_type,
counter_name,
)
- return hashlib.sha1(key.encode('ascii')).hexdigest()
+ return hashlib.sha256(key.encode("ascii")).hexdigest()
def reset_counters(**counters):
@@ -185,8 +185,7 @@ class Middleware(BaseMiddleware):
REQUIRED_SETTINGS = ("AUTHENTICATION_THROTTLING",)
def load_setting(self, setting, value):
- """
- """
+ """ """
value = value or {}
try:
@@ -239,15 +238,15 @@ def process_request(self, request):
current_site = get_current_site(request)
# Template-compatible with 'django.contrib.auth.views.login'.
return csrf_protect(
- lambda request: render(
+ lambda request, template_name=template_name, form=form, redirect_url=redirect_url, current_site=current_site: render(
request,
template_name,
{
"form": form,
self.redirect_field_name: redirect_url,
"site": current_site,
- "site_name": current_site.name
- }
+ "site_name": current_site.name,
+ },
),
)(request)
@@ -258,6 +257,10 @@ def process_response(self, request, response):
__all__ = [
- delay_message, increment_counters, reset_counters, attempt_count,
- default_delay_function, throttling_delay
+ delay_message,
+ increment_counters,
+ reset_counters,
+ attempt_count,
+ default_delay_function,
+ throttling_delay,
]
diff --git a/security/forms.py b/security/forms.py
index 9f159c9..368afd7 100644
--- a/security/forms.py
+++ b/security/forms.py
@@ -1,12 +1,11 @@
# Copyright (c) 2011, SD Elements. See LICENSE.txt for details.
-from django import forms
import django.contrib.auth.forms
-
-from django.utils.translation import ugettext_lazy as _
+from django import forms
+from django.utils.translation import gettext_lazy as _
from . import auth
-from .password_expiry import password_is_expired, never_expire_password
+from .password_expiry import never_expire_password, password_is_expired
class PasswordChangeForm(django.contrib.auth.forms.PasswordChangeForm):
diff --git a/security/middleware.py b/security/middleware.py
index d441a0a..bf28218 100644
--- a/security/middleware.py
+++ b/security/middleware.py
@@ -1,32 +1,32 @@
# Copyright (c) 2011, SD Elements. See LICENSE.txt for details.
-import dateutil.parser
+import cProfile
import importlib
import json
import logging
+import pstats
import warnings
+from io import StringIO
from re import compile
+import dateutil.parser
import django.conf
+import django.views.static
+import sqlparse
from django.contrib.auth import logout
-from django.core.exceptions import ImproperlyConfigured
-from django.urls import reverse, resolve
-from django.http import HttpResponseRedirect, HttpResponse
+from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed
+from django.db import connection
+from django.http import HttpResponse, HttpResponseRedirect
from django.test.signals import setting_changed
+from django.urls import resolve, reverse
from django.utils import timezone
from django.utils.deprecation import MiddlewareMixin
-import django.views.static
-
from ua_parser import user_agent_parser
-
logger = logging.getLogger(__name__)
-DJANGO_SECURITY_MIDDLEWARE_URL = (
- "https://docs.djangoproject.com/en/1.11/ref"
- "/middleware/#django.middleware.security.SecurityMiddleware")
DJANGO_CLICKJACKING_MIDDLEWARE_URL = (
- "https://docs.djangoproject.com/en/1.11/"
- "ref/clickjacking/")
+ "https://docs.djangoproject.com/en/4.2/ref/clickjacking/"
+)
class CustomLogoutMixin(object):
@@ -36,20 +36,25 @@ class CustomLogoutMixin(object):
"""
class Messages(object):
- NOT_A_MODULE_PATH = (u"Invalid CUSTOM_LOGOUT_MODULE setting '{0}'. "
- u"Expected module path to a function")
- FAILED_TO_LOAD = (u"Invalid CUSTOM_LOGOUT_MODULE setting. "
- u"Failed to load module '{0}': {1}")
- MISSING_FUNCTION = (u"Invalid CUSTOM_LOGOUT_MODULE setting. "
- u"Could not find function '{0}' in module '{1}'")
+ NOT_A_MODULE_PATH = (
+ "Invalid CUSTOM_LOGOUT_MODULE setting '{0}'. "
+ "Expected module path to a function"
+ )
+ FAILED_TO_LOAD = (
+ "Invalid CUSTOM_LOGOUT_MODULE setting. " "Failed to load module '{0}': {1}"
+ )
+ MISSING_FUNCTION = (
+ "Invalid CUSTOM_LOGOUT_MODULE setting. "
+ "Could not find function '{0}' in module '{1}'"
+ )
def perform_logout(self, request):
- if not getattr(self, 'CUSTOM_LOGOUT_MODULE', None):
+ if not getattr(self, "CUSTOM_LOGOUT_MODULE", None):
logout(request)
return
try:
- module_path, func_name = self.CUSTOM_LOGOUT_MODULE.rsplit('.', 1)
+ module_path, func_name = self.CUSTOM_LOGOUT_MODULE.rsplit(".", 1)
except ValueError:
err = self.Messages.NOT_A_MODULE_PATH
raise Exception(err.format(self.CUSTOM_LOGOUT_MODULE))
@@ -113,9 +118,7 @@ def __init__(self, get_response=None):
)
for key in self.OPTIONAL_SETTINGS:
- self.load_setting(
- key, getattr(django.conf.settings, key, None)
- )
+ self.load_setting(key, getattr(django.conf.settings, key, None))
setting_changed.connect(self._on_setting_changed)
@@ -169,8 +172,8 @@ def process_request(self, request):
Read DNT header from browser request and create request attribute
"""
request.dnt = None
- if 'HTTP_DNT' in request.META:
- request.dnt = request.META['HTTP_DNT'] == '1'
+ if "HTTP_DNT" in request.META:
+ request.dnt = request.META["HTTP_DNT"] == "1"
# returns None in normal conditions
def process_response(self, request, response):
@@ -178,84 +181,8 @@ def process_response(self, request, response):
Echo DNT header in response per section 8.4 of draft-mayer-do-not-
track-00
"""
- if 'HTTP_DNT' in request.META:
- response['DNT'] = request.META['HTTP_DNT']
- return response
-
-
-class XssProtectMiddleware(BaseMiddleware):
- """
- DEPRECATED: Will be removed in future releases. Consider
- django.middleware.security.SecurityMiddleware as a replacement for this via
- SECURE_BROWSER_XSS_FILTER setting.
-
- Sends X-XSS-Protection HTTP header that controls Cross-Site Scripting
- filter on MSIE. Use XSS_PROTECT option in settings file with the following
- values:
-
- ``sanitize`` enable XSS filter that tries to sanitize requests instead
- of blocking (*default*)
-
- ``on`` enable full XSS filter blocking XSS requests (may `leak
- document.referrer `_)
-
- ``off`` completely disable XSS filter
-
- **Note:** As of 1.8, Django's `SECURE_BROWSER_XSS_FILTER
- `_
- controls the X-XSS-Protection header.
-
- Reference:
-
- - `Controlling the XSS Filter
- `_
- """
-
- OPTIONAL_SETTINGS = ("XSS_PROTECT",)
-
- OPTIONS = {
- 'on': '1; mode=block',
- 'off': '0',
- 'sanitize': '1',
- }
-
- DEFAULT = 'sanitize'
-
- def __init__(self, get_response=None):
- super().__init__(get_response)
- warnings.warn((
- 'DEPRECATED: The middleware "{name}" will no longer be '
- 'supported in future releases of this library. Refer to {url} for '
- 'an alternative approach with regards to the settings: {settings}'
- ).format(
- name=self.__class__.__name__,
- url=DJANGO_SECURITY_MIDDLEWARE_URL,
- settings="SECURE_BROWSER_XSS_FILTER"))
-
- def load_setting(self, setting, value):
- if not value:
- self.option = self.DEFAULT
- return
-
- value = value.lower()
-
- if value in self.OPTIONS.keys():
- self.option = value
- return
-
- raise ImproperlyConfigured(
- self.__class__.__name__ + " invalid option for XSS_PROTECT."
- )
-
- def process_response(self, request, response):
- """
- Add X-XSS-Protection to the response header.
- """
- header = self.OPTIONS[self.option]
- response['X-XSS-Protection'] = header
+ if "HTTP_DNT" in request.META:
+ response["DNT"] = request.META["HTTP_DNT"]
return response
@@ -274,22 +201,18 @@ class ClearSiteDataMiddleware(BaseMiddleware):
`_
"""
- REQUIRED_SETTINGS = ('CLEAR_SITE_DATA_URL_WHITELIST',)
- OPTIONAL_SETTINGS = ('CLEAR_SITE_DATA_DIRECTIVES')
+ REQUIRED_SETTINGS = ("CLEAR_SITE_DATA_URL_WHITELIST",)
+ OPTIONAL_SETTINGS = "CLEAR_SITE_DATA_DIRECTIVES"
- DEFAULT_DIRECTIVES = ['cookies', 'storage']
- ALLOWED_DIRECTIVES = (
- 'cache', 'cookies', 'storage', 'executionContexts', '*'
- )
+ DEFAULT_DIRECTIVES = ["cookies", "storage"]
+ ALLOWED_DIRECTIVES = ("cache", "cookies", "storage", "executionContexts", "*")
def load_setting(self, setting, value):
- if setting == 'CLEAR_SITE_DATA_URL_WHITELIST':
+ if setting == "CLEAR_SITE_DATA_URL_WHITELIST":
self.clear_site_urls = value
directives = getattr(
- django.conf.settings,
- 'CLEAR_SITE_DATA_DIRECTIVES',
- self.DEFAULT_DIRECTIVES
+ django.conf.settings, "CLEAR_SITE_DATA_DIRECTIVES", self.DEFAULT_DIRECTIVES
)
directives = [
@@ -298,9 +221,8 @@ def load_setting(self, setting, value):
if directive.strip() in self.ALLOWED_DIRECTIVES
]
- self.clear_site_directives = ', '.join(
- '"{0}"'.format(directive)
- for directive in directives
+ self.clear_site_directives = ", ".join(
+ '"{0}"'.format(directive) for directive in directives
)
def process_response(self, request, response):
@@ -310,50 +232,8 @@ def process_response(self, request, response):
"""
if request.path in self.clear_site_urls:
- response['Clear-Site-Data'] = self.clear_site_directives
-
- return response
-
-
-class ContentNoSniff(MiddlewareMixin):
- """
- DEPRECATED: Will be removed in future releases. Consider
- django.middleware.security.SecurityMiddleware as a replacement for this via
- SECURE_CONTENT_TYPE_NOSNIFF setting.
-
- Sends X-Content-Options HTTP header to disable autodetection of MIME type
- of files returned by the server in Microsoft Internet Explorer.
- Specifically if this flag is enabled, MSIE will not load external CSS and
- JavaScript files unless server correctly declares their MIME type. This
- mitigates attacks where web page would for example load a script that was
- disguised as an user- supplied image.
-
- **Note:** As of 1.8, Django's `SECURE_CONTENT_TYPE_NOSNIFF
- `_
- controls the X-Content-Type-Options header.
-
- Reference:
-
- - `MIME-Handling Change: X-Content-Type-Options: nosniff
- `_
- """
-
- def __init__(self, get_response=None):
- super().__init__(get_response)
- warnings.warn((
- 'DEPRECATED: The middleware "{name}" will no longer be '
- 'supported in future releases of this library. Refer to {url} for '
- 'an alternative approach with regards to the settings: {settings}'
- ).format(
- name=self.__class__.__name__,
- url=DJANGO_SECURITY_MIDDLEWARE_URL,
- settings="SECURE_CONTENT_TYPE_NOSNIFF"))
+ response["Clear-Site-Data"] = self.clear_site_directives
- def process_response(self, request, response):
- """
- Add ``X-Content-Options: nosniff`` to the response header.
- """
- response['X-Content-Options'] = 'nosniff'
return response
@@ -402,7 +282,7 @@ def process_view(self, request, view, *args, **kwargs):
# because the reason the URL is exempt may be because a special URL
# config is in use (i.e. during a test) that doesn't have URL_NAME.
- path = request.path_info.lstrip('/')
+ path = request.path_info.lstrip("/")
if any(m.match(path) for m in self.exempt_urls):
return
@@ -418,6 +298,7 @@ def process_view(self, request, view, *args, **kwargs):
return
from .password_expiry import password_is_expired
+
if password_is_expired(request.user):
return HttpResponseRedirect(password_change_url)
@@ -460,12 +341,14 @@ def load_setting(self, setting, value):
value = value or {}
self.whitelist = value.get("WHITELIST_ON", False)
if self.whitelist:
- self.whitelist_url_regexes = \
- [compile(x) for x in value['WHITELIST_REGEXES']]
+ self.whitelist_url_regexes = [
+ compile(x) for x in value["WHITELIST_REGEXES"]
+ ]
self.blacklist = value.get("BLACKLIST_ON", False)
if self.blacklist:
- self.blacklist_url_regexes = \
- [compile(x) for x in value['BLACKLIST_REGEXES']]
+ self.blacklist_url_regexes = [
+ compile(x) for x in value["BLACKLIST_REGEXES"]
+ ]
def process_response(self, request, response):
"""
@@ -473,7 +356,7 @@ def process_response(self, request, response):
whitelist non-confidential pages and treat all others as non-
confidential, or specifically blacklist pages as confidential
"""
- path = request.path.lstrip('/')
+ path = request.path.lstrip("/")
if self.whitelist:
if not any(re.match(path) for re in self.whitelist_url_regexes):
self._remove_response_caching(response)
@@ -487,10 +370,9 @@ def _remove_response_caching(self, response):
"""
Overwrites specific headers to make the HTTP response confidential.
"""
- response['Cache-control'] = \
- 'no-cache, no-store, max-age=0, must-revalidate'
- response['Pragma'] = "no-cache"
- response['Expires'] = -1
+ response["Cache-control"] = "no-cache, no-store, max-age=0, must-revalidate"
+ response["Pragma"] = "no-cache"
+ response["Expires"] = -1
# http://tools.ietf.org/html/draft-ietf-websec-x-frame-options-01
@@ -528,29 +410,32 @@ class XFrameOptionsMiddleware(BaseMiddleware):
`_
"""
- OPTIONAL_SETTINGS = ('X_FRAME_OPTIONS', 'X_FRAME_OPTIONS_EXCLUDE_URLS')
+ OPTIONAL_SETTINGS = ("X_FRAME_OPTIONS", "X_FRAME_OPTIONS_EXCLUDE_URLS")
- DEFAULT = 'deny'
+ DEFAULT = "deny"
def __init__(self, get_response=None):
super().__init__(get_response)
- warnings.warn((
- 'An official middleware "{name}" is supported by Django. '
- 'Refer to {url} to see if its approach fits the use case.'
- ).format(
- name="XFrameOptionsMiddleware",
- url=DJANGO_CLICKJACKING_MIDDLEWARE_URL))
+ warnings.warn(
+ (
+ 'An official middleware "{name}" is supported by Django. '
+ "Refer to {url} to see if its approach fits the use case."
+ ).format(
+ name="XFrameOptionsMiddleware", url=DJANGO_CLICKJACKING_MIDDLEWARE_URL
+ ),
+ stacklevel=2,
+ )
def load_setting(self, setting, value):
- if setting == 'X_FRAME_OPTIONS':
+ if setting == "X_FRAME_OPTIONS":
if not value:
self.option = XFrameOptionsMiddleware.DEFAULT
return
value = value.lower()
- options = ['sameorigin', 'deny']
+ options = ["sameorigin", "deny"]
- if value in options or value.startswith('allow-from:'):
+ if value in options or value.startswith("allow-from:"):
self.option = value
return
@@ -558,7 +443,7 @@ def load_setting(self, setting, value):
self.__class__.__name__ + " invalid option for X_FRAME_OPTIONS"
)
- elif setting == 'X_FRAME_OPTIONS_EXCLUDE_URLS':
+ elif setting == "X_FRAME_OPTIONS_EXCLUDE_URLS":
if not value:
self.exclude_urls = []
return
@@ -567,8 +452,10 @@ def load_setting(self, setting, value):
self.exclude_urls = [compile(url) for url in value]
except TypeError:
raise ImproperlyConfigured(
- "{0} invalid option for X_FRAME_OPTIONS_EXCLUDE_URLS"
- .format(self.__class__.__name__))
+ "{0} invalid option for X_FRAME_OPTIONS_EXCLUDE_URLS".format(
+ self.__class__.__name__
+ )
+ )
def process_response(self, request, response):
"""
@@ -578,7 +465,7 @@ def process_response(self, request, response):
if url.match(request.path):
break
else:
- response['X-Frame-Options'] = self.option
+ response["X-Frame-Options"] = self.option
return response
@@ -680,43 +567,44 @@ class ContentSecurityPolicyMiddleware(MiddlewareMixin):
- `HTML5.1 - Sandboxing `_
"""
+
# these types accept CSP locations as arguments
_CSP_LOC_TYPES = [
- 'default-src',
- 'connect-src',
- 'child-src',
- 'font-src',
- 'form-action',
- 'frame-ancestors',
- 'frame-src',
- 'img-src',
- 'media-src',
- 'object-src',
- 'script-src',
- 'style-src',
- 'plugin-types',
- 'worker-src'
+ "default-src",
+ "connect-src",
+ "child-src",
+ "font-src",
+ "form-action",
+ "frame-ancestors",
+ "frame-src",
+ "img-src",
+ "media-src",
+ "object-src",
+ "script-src",
+ "style-src",
+ "plugin-types",
+ "worker-src",
]
# arguments to location types
- _CSP_LOCATIONS = ['self', 'none', 'unsafe-eval', 'unsafe-inline']
+ _CSP_LOCATIONS = ["self", "none", "unsafe-eval", "unsafe-inline"]
# sandbox allowed arguments
# http://www.w3.org/html/wg/drafts/html/master/single-page.html#sandboxing
# https://www.w3.org/TR/CSP2/
_CSP_SANDBOX_ARGS = [
- '',
- 'allow-forms',
- 'allow-pointer-lock',
- 'allow-popups',
- 'allow-same-origin',
- 'allow-scripts',
- 'allow-top-navigation',
+ "",
+ "allow-forms",
+ "allow-pointer-lock",
+ "allow-popups",
+ "allow-same-origin",
+ "allow-scripts",
+ "allow-top-navigation",
]
# reflected-xss allowed arguments
# http://www.w3.org/TR/CSP11/#directive-reflected-xss
- _CSP_XSS_ARGS = ['allow', 'block', 'filter']
+ _CSP_XSS_ARGS = ["allow", "block", "filter"]
# referrer allowed arguments
# http://www.w3.org/TR/CSP11/#directive-referrer
@@ -734,24 +622,24 @@ class ContentSecurityPolicyMiddleware(MiddlewareMixin):
def _csp_loc_builder(self, key, value):
if not isinstance(value, (list, tuple)):
- logger.warn('Arguments to %s must be given as list or tuple', key)
+ logger.warn("Arguments to %s must be given as list or tuple", key)
raise django.core.exceptions.MiddlewareNotUsed
csp_loc_string = "{0}".format(key)
for loc in value:
if loc in self._CSP_LOCATIONS:
csp_loc_string += " '{0}'".format(loc) # quoted
- elif loc == '*':
- csp_loc_string += ' *' # not quoted
+ elif loc == "*":
+ csp_loc_string += " *" # not quoted
else:
# XXX: check for valid hostname or URL
- csp_loc_string += " {0}".format(loc) # not quoted
+ csp_loc_string += " {0}".format(loc) # not quoted
return csp_loc_string
def _csp_sandbox_builder(self, key, value):
if not isinstance(value, (list, tuple)):
- logger.warn('Arguments to %s must be given as list or tuple', key)
+ logger.warn("Arguments to %s must be given as list or tuple", key)
raise django.core.exceptions.MiddlewareNotUsed
csp_sandbox_string = "{0}".format(key)
@@ -759,25 +647,25 @@ def _csp_sandbox_builder(self, key, value):
if opt in self._CSP_SANDBOX_ARGS:
csp_sandbox_string += " {0}".format(opt)
else:
- logger.warn('Invalid CSP sandbox argument %s', opt)
+ logger.warn("Invalid CSP sandbox argument %s", opt)
raise django.core.exceptions.MiddlewareNotUsed
return csp_sandbox_string
def _csp_report_uri_builder(self, key, value):
# XXX: add valid URL check
- return '{0} {1}'.format(key, value)
+ return "{0} {1}".format(key, value)
def _csp_referrer_builder(self, key, value):
if value not in self._CSP_REF_ARGS:
- logger.warning('Invalid CSP %s value %s', key, value)
+ logger.warning("Invalid CSP %s value %s", key, value)
raise django.core.exceptions.MiddlewareNotUsed
return "{0} {1}".format(key, value)
def _csp_reflected_xss_builder(self, key, value):
if value not in self._CSP_XSS_ARGS:
- logger.warning('Invalid CSP %s value %s', key, value)
+ logger.warning("Invalid CSP %s value %s", key, value)
raise django.core.exceptions.MiddlewareNotUsed
return "{0} {1}".format(key, value)
@@ -790,48 +678,48 @@ def _csp_builder(self, csp_dict):
if key in self._CSP_LOC_TYPES:
csp_components.append(self._csp_loc_builder(key, value))
- elif key == 'sandbox':
+ elif key == "sandbox":
csp_components.append(self._csp_sandbox_builder(key, value))
- elif key == 'report-uri':
+ elif key == "report-uri":
csp_components.append(self._csp_report_uri_builder(key, value))
- elif key == 'referrer':
+ elif key == "referrer":
csp_components.append(self._csp_referrer_builder(key, value))
- elif key == 'reflected-xss':
+ elif key == "reflected-xss":
csp_components.append(
self._csp_reflected_xss_builder(key, value),
)
else:
- logger.warning('Invalid CSP type %s', key)
+ logger.warning("Invalid CSP type %s", key)
raise django.core.exceptions.MiddlewareNotUsed
- return '; '.join(csp_components)
+ return "; ".join(csp_components)
def __init__(self, get_response=None):
# sanity checks
self.get_response = get_response
- conf_csp_mode = getattr(django.conf.settings, 'CSP_MODE', None)
- self._csp_mode = conf_csp_mode or 'enforce'
- csp_string = getattr(django.conf.settings, 'CSP_STRING', None)
- csp_dict = getattr(django.conf.settings, 'CSP_DICT', None)
- csp_report_string = getattr(django.conf.settings, 'CSP_REPORT_STRING',
- None)
- csp_report_dict = getattr(django.conf.settings, 'CSP_REPORT_DICT',
- None)
-
- set_csp_str = self._csp_mode in ['enforce', 'enforce-and-report-only']
- set_csp_report_str = self._csp_mode in ['report-only',
- 'enforce-and-report-only']
+ conf_csp_mode = getattr(django.conf.settings, "CSP_MODE", None)
+ self._csp_mode = conf_csp_mode or "enforce"
+ csp_string = getattr(django.conf.settings, "CSP_STRING", None)
+ csp_dict = getattr(django.conf.settings, "CSP_DICT", None)
+ csp_report_string = getattr(django.conf.settings, "CSP_REPORT_STRING", None)
+ csp_report_dict = getattr(django.conf.settings, "CSP_REPORT_DICT", None)
+
+ set_csp_str = self._csp_mode in ["enforce", "enforce-and-report-only"]
+ set_csp_report_str = self._csp_mode in [
+ "report-only",
+ "enforce-and-report-only",
+ ]
if not (set_csp_str or set_csp_report_str):
logger.error(
'Invalid CSP_MODE %s, "enforce", "report-only" '
'or "enforce-and-report-only" allowed',
- self._csp_mode
+ self._csp_mode,
)
raise django.core.exceptions.MiddlewareNotUsed
@@ -842,20 +730,21 @@ def __init__(self, get_response=None):
self._set_csp_report_str(csp_report_dict, csp_report_string)
def _set_csp_str(self, csp_dict, csp_string):
- err_msg = 'Middleware requires either CSP_STRING or CSP_DICT setting'
+ err_msg = "Middleware requires either CSP_STRING or CSP_DICT setting"
if not (csp_dict or csp_string):
- logger.error('%s, none found', err_msg)
+ logger.error("%s, none found", err_msg)
raise django.core.exceptions.MiddlewareNotUsed
- self._csp_string = self._choose_csp_str(csp_dict, csp_string,
- err_msg + ', not both')
+ self._csp_string = self._choose_csp_str(
+ csp_dict, csp_string, err_msg + ", not both"
+ )
def _set_csp_report_str(self, csp_report_dict, csp_report_string):
report_err_msg = (
- 'Middleware requires either CSP_REPORT_STRING, '
- 'CSP_REPORT_DICT setting, or neither. If neither, '
- 'middleware requires CSP_STRING or CSP_DICT, '
- 'but not both.'
+ "Middleware requires either CSP_REPORT_STRING, "
+ "CSP_REPORT_DICT setting, or neither. If neither, "
+ "middleware requires CSP_STRING or CSP_DICT, "
+ "but not both."
)
# Default to the regular CSP string if report string not configured
@@ -863,9 +752,7 @@ def _set_csp_report_str(self, csp_report_dict, csp_report_string):
self._csp_report_string = self._csp_string
else:
self._csp_report_string = self._choose_csp_str(
- csp_report_dict,
- csp_report_string,
- report_err_msg
+ csp_report_dict, csp_report_string, report_err_msg
)
def _choose_csp_str(self, csp_dict, csp_str, err_msg):
@@ -883,7 +770,7 @@ def _choose_csp_str(self, csp_dict, csp_str, err_msg):
Log an error message if both are provided.
"""
if csp_dict and csp_str:
- logger.error('%s', err_msg)
+ logger.error("%s", err_msg)
raise django.core.exceptions.MiddlewareNotUsed
if csp_dict:
@@ -891,7 +778,7 @@ def _choose_csp_str(self, csp_dict, csp_str, err_msg):
elif csp_str:
return csp_str
else:
- return ''
+ return ""
def process_response(self, request, response):
"""
@@ -900,149 +787,29 @@ def process_response(self, request, response):
"""
# choose headers based enforcement mode
is_ie = False
- if 'HTTP_USER_AGENT' in request.META:
- parsed_ua = user_agent_parser.ParseUserAgent(request.META['HTTP_USER_AGENT'])
- is_ie = parsed_ua['family'] == 'IE'
+ if "HTTP_USER_AGENT" in request.META:
+ parsed_ua = user_agent_parser.ParseUserAgent(
+ request.META["HTTP_USER_AGENT"]
+ )
+ is_ie = parsed_ua["family"] == "IE"
- csp_header = 'Content-Security-Policy'
+ csp_header = "Content-Security-Policy"
if is_ie:
- csp_header = 'X-Content-Security-Policy'
- report_only_header = 'Content-Security-Policy-Report-Only'
+ csp_header = "X-Content-Security-Policy"
+ report_only_header = "Content-Security-Policy-Report-Only"
# actually add appropriate headers
- if self._csp_mode == 'enforce':
+ if self._csp_mode == "enforce":
response[csp_header] = self._csp_string
- elif self._csp_mode == 'report-only':
+ elif self._csp_mode == "report-only":
response[report_only_header] = self._csp_report_string
- elif self._csp_mode == 'enforce-and-report-only':
+ elif self._csp_mode == "enforce-and-report-only":
response[csp_header] = self._csp_string
response[report_only_header] = self._csp_report_string
return response
-class StrictTransportSecurityMiddleware(MiddlewareMixin):
- """
- DEPRECATED: Will be removed in future releases. Consider
- django.middleware.security.SecurityMiddleware as a replacement for this via
- SECURE_HSTS_SECONDS, SECURE_HSTS_INCLUDE_SUBDOMAINS and
- SECURE_HSTS_PRELOAD settings.
-
- Adds Strict-Transport-Security header to HTTP
- response that enforces SSL connections on compliant browsers. Two
- parameters can be set in settings file, otherwise reasonable
- defaults will be used:
-
- - ``STS_MAX_AGE`` time in seconds to preserve host's STS
- policy (default: 1 year)
- - ``STS_INCLUDE_SUBDOMAINS`` True if subdomains should be covered by
- the policy as well (default: True)
- - ``STS_PRELOAD`` add ``preload`` flag to the STS header
- so that your website can be added to preloaded websites list
-
- **Note:** As of 1.8, Django's `SECURE_HSTS_SECONDS
- `_
- controls the HTTP Strict Transport Security header.
-
- Reference:
-
- - `HTTP Strict Transport Security (HSTS)
- `_
- - `Preloaded HSTS sites `_
- """
- def __init__(self, get_response=None):
- warnings.warn((
- 'DEPRECATED: The middleware "{name}" will no longer be '
- 'supported in future releases of this library. Refer to {url} for '
- 'an alternative approach with regards to the settings: {settings}'
- ).format(
- name=self.__class__.__name__,
- url=DJANGO_SECURITY_MIDDLEWARE_URL,
- settings=", ".join([
- "SECURE_HSTS_SECONDS",
- "SECURE_HSTS_INCLUDE_SUBDOMAINS",
- "SECURE_HSTS_PRELOAD",
- ])))
-
- self.get_response = get_response
-
- try:
- self.max_age = django.conf.settings.STS_MAX_AGE
- except AttributeError:
- self.max_age = 3600 * 24 * 365 # one year
-
- try:
- self.subdomains = django.conf.settings.STS_INCLUDE_SUBDOMAINS
- except AttributeError:
- self.subdomains = True
-
- try:
- self.preload = django.conf.settings.STS_PRELOAD
- except AttributeError:
- self.preload = True
-
- self.value = 'max-age={0}'.format(self.max_age)
-
- if self.subdomains:
- self.value += ' ; includeSubDomains'
-
- if self.preload:
- self.value += ' ; preload'
-
- def process_response(self, request, response):
- """
- Add Strict-Transport-Security header.
- """
- response['Strict-Transport-Security'] = self.value
- return response
-
-
-class P3PPolicyMiddleware(BaseMiddleware):
- """
- DEPRECATED: Will be removed in future releases.
-
- Adds the HTTP header attribute specifying compact P3P policy
- defined in P3P_COMPACT_POLICY setting and location of full
- policy defined in P3P_POLICY_URL. If the latter is not defined,
- a default value is used (/w3c/p3p.xml). The policy file needs to
- be created by website owner.
-
- **Note:** P3P work stopped in 2002 and the only popular
- browser with **limited** P3P support is MSIE.
-
- Reference:
-
- - `The Platform for Privacy Preferences 1.0 (P3P1.0) Specification - The
- Compact Policies `_
- """
-
- REQUIRED_SETTINGS = ("P3P_COMPACT_POLICY",)
- OPTIONAL_SETTINGS = ("P3P_POLICY_URL",)
-
- def __init__(self, get_response=None):
- super().__init__(get_response)
- warnings.warn((
- 'DEPRECATED: The middleware "{name}" will no longer be '
- 'supported in future releases of this library.'
- ).format(name=self.__class__.__name__))
-
- def load_setting(self, setting, value):
- if setting == 'P3P_COMPACT_POLICY':
- self.policy = value
- elif setting == 'P3P_POLICY_URL':
- self.policy_url = value or '/w3c/p3p.xml'
-
- def process_response(self, request, response):
- """
- Add P3P policy to the response header.
- """
- response['P3P'] = 'policyref="{0}" CP="{1}"'.format(
- self.policy_url,
- self.policy,
- )
- return response
-
-
class SessionExpiryPolicyMiddleware(CustomLogoutMixin, BaseMiddleware):
"""
The session expiry middleware will let you expire sessions on
@@ -1067,15 +834,19 @@ class SessionExpiryPolicyMiddleware(CustomLogoutMixin, BaseMiddleware):
e.g. 'django.contrib.auth.logout'.
"""
- OPTIONAL_SETTINGS = ('SESSION_COOKIE_AGE', 'SESSION_INACTIVITY_TIMEOUT',
- 'SESSION_EXPIRY_EXEMPT_URLS', 'CUSTOM_LOGOUT_MODULE')
+ OPTIONAL_SETTINGS = (
+ "SESSION_COOKIE_AGE",
+ "SESSION_INACTIVITY_TIMEOUT",
+ "SESSION_EXPIRY_EXEMPT_URLS",
+ "CUSTOM_LOGOUT_MODULE",
+ )
SECONDS_PER_DAY = 86400
SECONDS_PER_30MINS = 1800
# Session keys
- START_TIME_KEY = 'starttime'
- LAST_ACTIVITY_KEY = 'lastactivity'
+ START_TIME_KEY = "starttime"
+ LAST_ACTIVITY_KEY = "lastactivity"
@classmethod
def _get_datetime_in_session(cls, key, session):
@@ -1087,47 +858,34 @@ def _set_datetime_in_session(cls, key, value, session):
@classmethod
def get_start_time(cls, request):
- return cls._get_datetime_in_session(
- cls.START_TIME_KEY,
- request.session
- )
+ return cls._get_datetime_in_session(cls.START_TIME_KEY, request.session)
@classmethod
def set_start_time(cls, request, date):
- cls._set_datetime_in_session(
- cls.START_TIME_KEY,
- date,
- request.session
- )
+ cls._set_datetime_in_session(cls.START_TIME_KEY, date, request.session)
@classmethod
def get_last_activity(cls, request):
- return cls._get_datetime_in_session(
- cls.LAST_ACTIVITY_KEY,
- request.session
- )
+ return cls._get_datetime_in_session(cls.LAST_ACTIVITY_KEY, request.session)
@classmethod
def set_last_activity(cls, request, date):
- cls._set_datetime_in_session(
- cls.LAST_ACTIVITY_KEY,
- date,
- request.session
- )
+ cls._set_datetime_in_session(cls.LAST_ACTIVITY_KEY, date, request.session)
def load_setting(self, setting, value):
- if setting == 'SESSION_COOKIE_AGE':
+ if setting == "SESSION_COOKIE_AGE":
self.SESSION_COOKIE_AGE = value or self.SECONDS_PER_DAY
- logger.debug("Max Session Cookie Age is %d seconds",
- self.SESSION_COOKIE_AGE
- )
- elif setting == 'SESSION_INACTIVITY_TIMEOUT':
+ logger.debug(
+ "Max Session Cookie Age is %d seconds", self.SESSION_COOKIE_AGE
+ )
+ elif setting == "SESSION_INACTIVITY_TIMEOUT":
# half an hour in seconds
self.SESSION_INACTIVITY_TIMEOUT = value or self.SECONDS_PER_30MINS
- logger.debug("Session Inactivity Timeout is %d seconds",
- self.SESSION_INACTIVITY_TIMEOUT
- )
- elif setting == 'SESSION_EXPIRY_EXEMPT_URLS':
+ logger.debug(
+ "Session Inactivity Timeout is %d seconds",
+ self.SESSION_INACTIVITY_TIMEOUT,
+ )
+ elif setting == "SESSION_EXPIRY_EXEMPT_URLS":
self.exempt_urls = [compile(expr) for expr in (value or ())]
else:
setattr(self, setting, value)
@@ -1139,13 +897,13 @@ def process_request(self, request):
is the case. We set the last activity time to now() if the session
is still active.
"""
- if not hasattr(request, 'user'):
+ if not hasattr(request, "user"):
raise ImproperlyConfigured(
"The Login Required middleware "
"requires authentication middleware to be installed."
)
- path = request.path_info.lstrip('/')
+ path = request.path_info.lstrip("/")
if any(m.match(path) for m in self.exempt_urls):
return
@@ -1177,14 +935,10 @@ def process_existing_session(self, request):
start_time = self.get_start_time(request)
last_activity_time = self.get_last_activity(request)
- logger.debug("Session %s started: %s",
- session.session_key,
- start_time
- )
- logger.debug("Session %s last active: %s",
- session.session_key,
- last_activity_time
- )
+ logger.debug("Session %s started: %s", session.session_key, start_time)
+ logger.debug(
+ "Session %s last active: %s", session.session_key, last_activity_time
+ )
session_age = self.get_diff_in_seconds(now, start_time)
session_too_old = session_age > self.SESSION_COOKIE_AGE
@@ -1213,6 +967,7 @@ def get_diff_in_seconds(self, now, time):
age = diff.days * self.SECONDS_PER_DAY + diff.seconds
return age
+
# Modified a little bit by us.
# Copyright (c) 2008, Ryan Witt
@@ -1262,19 +1017,19 @@ class LoginRequiredMiddleware(BaseMiddleware, CustomLogoutMixin):
e.g. 'django.contrib.auth.logout'.
"""
- REQUIRED_SETTINGS = ('LOGIN_URL',)
- OPTIONAL_SETTINGS = ('LOGIN_EXEMPT_URLS', 'CUSTOM_LOGOUT_MODULE')
+ REQUIRED_SETTINGS = ("LOGIN_URL",)
+ OPTIONAL_SETTINGS = ("LOGIN_EXEMPT_URLS", "CUSTOM_LOGOUT_MODULE")
def load_setting(self, setting, value):
- if setting == 'LOGIN_URL':
+ if setting == "LOGIN_URL":
self.login_url = value
- elif setting == 'LOGIN_EXEMPT_URLS':
+ elif setting == "LOGIN_EXEMPT_URLS":
self.exempt_urls = [compile(expr) for expr in (value or ())]
else:
setattr(self, setting, value)
def assert_authentication_middleware_installed(self, request):
- if not hasattr(request, 'user'):
+ if not hasattr(request, "user"):
raise ImproperlyConfigured(
"The Login Required middleware "
"requires authentication middleware to be installed."
@@ -1292,19 +1047,19 @@ def process_request(self, request):
if request.user.is_authenticated:
return
- path = request.path_info.lstrip('/')
+ path = request.path_info.lstrip("/")
if any(m.match(path) for m in self.exempt_urls):
return
- if hasattr(request, 'login_url'):
+ if hasattr(request, "login_url"):
login_url = request.login_url
next_url = None
else:
login_url = self.login_url
next_url = request.path
- if request.is_ajax():
+ if request.headers.get("x-requested-with") == "XMLHttpRequest":
return HttpResponse(
json.dumps({"login_url": login_url}),
status=401,
@@ -1312,10 +1067,11 @@ def process_request(self, request):
)
if next_url:
- login_url = login_url + '?next=' + next_url
+ login_url = login_url + "?next=" + next_url
return HttpResponseRedirect(login_url)
+
class ReferrerPolicyMiddleware(BaseMiddleware):
"""
Sends Referrer-Policy HTTP header that controls when the browser will set
@@ -1339,11 +1095,19 @@ class ReferrerPolicyMiddleware(BaseMiddleware):
OPTIONAL_SETTINGS = ("REFERRER_POLICY",)
- OPTIONS = [ 'no-referrer', 'no-referrer-when-downgrade', 'origin',
- 'origin-when-cross-origin', 'same-origin', 'strict-origin',
- 'strict-origin-when-cross-origin', 'unsafe-url', 'off' ]
+ OPTIONS = [
+ "no-referrer",
+ "no-referrer-when-downgrade",
+ "origin",
+ "origin-when-cross-origin",
+ "same-origin",
+ "strict-origin",
+ "strict-origin-when-cross-origin",
+ "unsafe-url",
+ "off",
+ ]
- DEFAULT = 'same-origin'
+ DEFAULT = "same-origin"
def load_setting(self, setting, value):
if not value:
@@ -1364,7 +1128,93 @@ def process_response(self, request, response):
"""
Add Referrer-Policy to the response header.
"""
- if self.option != 'off':
+ if self.option != "off":
header = self.option
- response['Referrer-Policy'] = header
+ response["Referrer-Policy"] = header
+ return response
+
+
+class ProfilingMiddleware(BaseMiddleware):
+ """
+ Adds the ability to profile requests via a header.
+
+ Usage:
+ Add the middleware to the MIDDLEWARE list. New boolean setting
+ "ENABLE_PROFILING" will be required to be set in the settings file. When
+ set to False, the middleware will deactivate itself. When set to True, the
+ middleware will be active.
+
+ When the middleware is active, it will log the data for any request that
+ supplies the X-Profile header in the HTTP request. This data will be logged
+ to the 'profiling' logger, so in order to see the results of this profiling
+ the Django logging will need to configure handlers for the 'profiling'
+ logger. Profiling will be configured at the DEBUG level.
+ """
+
+ REQUIRED_SETTINGS = ("ENABLE_PROFILING", "DEBUG")
+ request_separator = f"\n{'=' * 80}\n"
+ query_separator = f"\n{'*' * 80}\n"
+
+ def __init__(self, get_response=None):
+ super().__init__(get_response)
+ if not self.enable_profiling:
+ raise MiddlewareNotUsed()
+
+ def load_setting(self, setting, value):
+ setattr(self, setting.lower(), value)
+
+ def format_queries_and_time_for_logs(self, queries):
+ formatted_queries = []
+ total_time = 0
+ for query in queries:
+ formatted_sql = sqlparse.format(
+ query["sql"], reindent=True, keyword_case="upper"
+ )
+
+ formatted_queries.append("{}:\n{}".format(query["time"], formatted_sql))
+ total_time += float(query["time"])
+
+ log_messages = [
+ f"\n{len(queries)} Queries\nTotal time for queries: {total_time}"
+ ] + formatted_queries
+ return self.query_separator.join(log_messages)
+
+ def __call__(self, request):
+ # Only profile requests that have a 'X-Profile' HTTP header
+ if "HTTP_X_PROFILE" not in request.META:
+ return self.get_response(request)
+
+ out = StringIO()
+ out.write(self.request_separator)
+
+ # Add method & path info to differentiate requests
+ out.write(f"{request.method} {request.path}\n\n")
+
+ # We can only profile queries in debug mode
+ if self.debug:
+ num_previous_queries = len(connection.queries)
+
+ # Begin collecting time profiling data
+ profile = cProfile.Profile()
+ profile.enable()
+
+ # Continue down the middleware chain
+ response = self.get_response(request)
+
+ # Get the profile stats & pull out the top cumulative & total time
+ # data
+ profile_stats = pstats.Stats(profile, stream=out)
+ profile_stats = profile_stats.sort_stats("cumulative")
+ profile_stats.print_stats(128)
+ profile_stats.sort_stats("tottime")
+ profile_stats.print_stats(15)
+
+ # Print out our queries
+ if self.debug:
+ queries = connection.queries[num_previous_queries:]
+ out.write(self.format_queries_and_time_for_logs(queries))
+
+ out.write(self.request_separator)
+ logger.debug(out.getvalue())
+
return response
diff --git a/security/migrations/0001_initial.py b/security/migrations/0001_initial.py
index 8de0a0b..0c165b8 100644
--- a/security/migrations/0001_initial.py
+++ b/security/migrations/0001_initial.py
@@ -1,8 +1,8 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
-from django.db import models, migrations
from django.conf import settings
+from django.db import migrations, models
class Migration(migrations.Migration):
@@ -13,31 +13,106 @@ class Migration(migrations.Migration):
operations = [
migrations.CreateModel(
- name='CspReport',
+ name="CspReport",
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('document_uri', models.URLField(help_text='The address of the protected resource, with any fragment component removed', max_length=1000)),
- ('referrer', models.URLField(help_text='The referrer attribute of the protected resource', max_length=1000)),
- ('blocked_uri', models.URLField(help_text='URI of the resource that was prevented from loading due to the policy violation, with any fragment component removed', max_length=1000)),
- ('violated_directive', models.CharField(help_text='The policy directive that was violated', max_length=1000)),
- ('original_policy', models.TextField(help_text='The original policy as received by the user-agent.', max_length=1000, null=True)),
- ('date_received', models.DateTimeField(help_text='When this report was received', auto_now_add=True)),
- ('sender_ip', models.GenericIPAddressField(help_text='IP of the browser sending this report')),
- ('user_agent', models.CharField(help_text='User-Agent of reporting browser', max_length=1000)),
+ (
+ "id",
+ models.AutoField(
+ verbose_name="ID",
+ serialize=False,
+ auto_created=True,
+ primary_key=True,
+ ),
+ ),
+ (
+ "document_uri",
+ models.URLField(
+ help_text="The address of the protected resource, with any fragment component removed",
+ max_length=1000,
+ ),
+ ),
+ (
+ "referrer",
+ models.URLField(
+ help_text="The referrer attribute of the protected resource",
+ max_length=1000,
+ ),
+ ),
+ (
+ "blocked_uri",
+ models.URLField(
+ help_text="URI of the resource that was prevented from loading due to the policy violation, with any fragment component removed",
+ max_length=1000,
+ ),
+ ),
+ (
+ "violated_directive",
+ models.CharField(
+ help_text="The policy directive that was violated",
+ max_length=1000,
+ ),
+ ),
+ (
+ "original_policy",
+ models.TextField(
+ help_text="The original policy as received by the user-agent.",
+ max_length=1000,
+ null=True,
+ ),
+ ),
+ (
+ "date_received",
+ models.DateTimeField(
+ help_text="When this report was received", auto_now_add=True
+ ),
+ ),
+ (
+ "sender_ip",
+ models.GenericIPAddressField(
+ help_text="IP of the browser sending this report"
+ ),
+ ),
+ (
+ "user_agent",
+ models.CharField(
+ help_text="User-Agent of reporting browser", max_length=1000
+ ),
+ ),
],
- options={
- },
+ options={},
bases=(models.Model,),
),
migrations.CreateModel(
- name='PasswordExpiry',
+ name="PasswordExpiry",
fields=[
- ('id', models.AutoField(verbose_name='ID', serialize=False, auto_created=True, primary_key=True)),
- ('password_expiry_date', models.DateTimeField(help_text="The date and time when the user's password expires. If this is empty, the password never expires.", auto_now_add=True, null=True)),
- ('user', models.ForeignKey(to=settings.AUTH_USER_MODEL, unique=True, on_delete=models.CASCADE)),
+ (
+ "id",
+ models.AutoField(
+ verbose_name="ID",
+ serialize=False,
+ auto_created=True,
+ primary_key=True,
+ ),
+ ),
+ (
+ "password_expiry_date",
+ models.DateTimeField(
+ help_text="The date and time when the user's password expires. If this is empty, the password never expires.",
+ auto_now_add=True,
+ null=True,
+ ),
+ ),
+ (
+ "user",
+ models.ForeignKey(
+ to=settings.AUTH_USER_MODEL,
+ unique=True,
+ on_delete=models.CASCADE,
+ ),
+ ),
],
options={
- 'verbose_name_plural': 'PasswordExpiries',
+ "verbose_name_plural": "PasswordExpiries",
},
bases=(models.Model,),
),
diff --git a/security/migrations/0002_convert_pass_expiry_user_to_one2one.py b/security/migrations/0002_convert_pass_expiry_user_to_one2one.py
index dc325b6..5805a45 100644
--- a/security/migrations/0002_convert_pass_expiry_user_to_one2one.py
+++ b/security/migrations/0002_convert_pass_expiry_user_to_one2one.py
@@ -1,21 +1,23 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
+import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
-import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
- ('security', '0001_initial'),
+ ("security", "0001_initial"),
]
operations = [
migrations.AlterField(
- model_name='passwordexpiry',
- name='user',
- field=models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL),
+ model_name="passwordexpiry",
+ name="user",
+ field=models.OneToOneField(
+ on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL
+ ),
),
]
diff --git a/security/models.py b/security/models.py
index 19803f6..9969752 100644
--- a/security/models.py
+++ b/security/models.py
@@ -1,14 +1,13 @@
# Copyright (c) 2011, SD Elements. See LICENSE.txt for details.
+from django.conf import settings
from django.contrib.auth.models import User
from django.db import models
from django.utils import timezone
-from django.conf import settings
-
# Finding proper User model that we can set Foreign key to.
# In newer versions of Django default user model can be specified in settings
# as `AUTH_USER_MODEL`
-USER_MODEL = getattr(settings, 'AUTH_USER_MODEL', User)
+USER_MODEL = getattr(settings, "AUTH_USER_MODEL", User)
class PasswordExpiry(models.Model):
@@ -23,16 +22,13 @@ class PasswordExpiry(models.Model):
class Meta(object):
verbose_name_plural = "PasswordExpiries"
- user = models.OneToOneField(
- USER_MODEL,
- on_delete=models.deletion.CASCADE
- )
+ user = models.OneToOneField(USER_MODEL, on_delete=models.deletion.CASCADE)
password_expiry_date = models.DateTimeField(
auto_now_add=True,
null=True,
help_text="The date and time when the user's password expires. If "
- "this is empty, the password never expires.",
+ "this is empty, the password never expires.",
)
def is_expired(self):
@@ -46,7 +42,7 @@ def never_expire(self):
self.save()
def __unicode__(self):
- return u'Password Expiry: {0}'.format(self.user)
+ return "Password Expiry: {0}".format(self.user)
# http://www.w3.org/TR/CSP/#sample-violation-report
@@ -73,7 +69,7 @@ class CspReport(models.Model):
document_uri = models.URLField(
max_length=1000,
help_text="The address of the protected resource, "
- "with any fragment component removed",
+ "with any fragment component removed",
)
referrer = models.URLField(
max_length=1000,
@@ -82,7 +78,7 @@ class CspReport(models.Model):
blocked_uri = models.URLField(
max_length=1000,
help_text="URI of the resource that was prevented from loading due to "
- "the policy violation, with any fragment component removed",
+ "the policy violation, with any fragment component removed",
)
violated_directive = models.CharField(
max_length=1000,
@@ -107,7 +103,7 @@ class CspReport(models.Model):
)
def __unicode__(self):
- return u'CSP Report: {0} from {1}'.format(
+ return "CSP Report: {0} from {1}".format(
self.blocked_uri,
self.document_uri,
)
diff --git a/security/password_expiry.py b/security/password_expiry.py
index 7f90855..3e2ce29 100644
--- a/security/password_expiry.py
+++ b/security/password_expiry.py
@@ -1,15 +1,16 @@
# Copyright (c) 2011, SD Elements. See LICENSE.txt for details.
-from .models import PasswordExpiry
from django.conf import settings
+from .models import PasswordExpiry
+
def password_is_expired(user):
password_expiry, _ = PasswordExpiry.objects.get_or_create(user=user)
- password_settings = getattr(settings, 'MANDATORY_PASSWORD_CHANGE', {})
- include_superusers = password_settings.get('INCLUDE_SUPERUSERS', False)
+ password_settings = getattr(settings, "MANDATORY_PASSWORD_CHANGE", {})
+ include_superusers = password_settings.get("INCLUDE_SUPERUSERS", False)
if include_superusers:
return password_expiry.is_expired()
diff --git a/security/south_migrations/0001_initial.py b/security/south_migrations/0001_initial.py
deleted file mode 100644
index 004ee61..0000000
--- a/security/south_migrations/0001_initial.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# encoding: utf-8
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
-
- # Adding model 'PasswordExpiry'
- db.create_table('security_passwordexpiry', (
- ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('user', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['auth.User'], unique=True, on_delete=models.CASCADE)),
- ('password_expiry_date', self.gf('django.db.models.fields.DateTimeField')(default=datetime.datetime(1, 1, 1, 0, 0))),
- ))
- db.send_create_signal('security', ['PasswordExpiry'])
-
-
- def backwards(self, orm):
-
- # Deleting model 'PasswordExpiry'
- db.delete_table('security_passwordexpiry')
-
-
- models = {
- 'auth.group': {
- 'Meta': {'object_name': 'Group'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
- 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
- },
- 'auth.permission': {
- 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
- 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']", 'on_delete': 'django.db.models.CASCADE'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
- },
- 'auth.user': {
- 'Meta': {'object_name': 'User'},
- 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
- 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
- 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
- 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
- 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
- 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
- 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
- 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
- },
- 'contenttypes.contenttype': {
- 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
- 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
- },
- 'security.passwordexpiry': {
- 'Meta': {'object_name': 'PasswordExpiry'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'password_expiry_date': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime(1, 1, 2, 0, 0)'}),
- 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True', 'on_delete': 'django.db.models.CASCADE'})
- }
- }
-
- complete_apps = ['security']
diff --git a/security/south_migrations/0002_allow_null_password_expiry.py b/security/south_migrations/0002_allow_null_password_expiry.py
deleted file mode 100644
index 68d8262..0000000
--- a/security/south_migrations/0002_allow_null_password_expiry.py
+++ /dev/null
@@ -1,65 +0,0 @@
-# -*- coding: utf-8 -*-
-import datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
-
- # Changing field 'PasswordExpiry.password_expiry_date'
- db.alter_column('security_passwordexpiry', 'password_expiry_date', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, null=True))
-
- def backwards(self, orm):
-
- # Changing field 'PasswordExpiry.password_expiry_date'
- db.alter_column('security_passwordexpiry', 'password_expiry_date', self.gf('django.db.models.fields.DateTimeField')())
-
- models = {
- 'auth.group': {
- 'Meta': {'object_name': 'Group'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
- 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
- },
- 'auth.permission': {
- 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'},
- 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
- },
- 'auth.user': {
- 'Meta': {'object_name': 'User'},
- 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
- 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
- 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
- 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
- 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
- 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
- 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}),
- 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'})
- },
- 'contenttypes.contenttype': {
- 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
- 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
- },
- 'security.passwordexpiry': {
- 'Meta': {'object_name': 'PasswordExpiry'},
- 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'password_expiry_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
- 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'})
- }
- }
-
- complete_apps = ['security']
\ No newline at end of file
diff --git a/security/south_migrations/0003_auto__add_cspreport.py b/security/south_migrations/0003_auto__add_cspreport.py
deleted file mode 100644
index 657fbb2..0000000
--- a/security/south_migrations/0003_auto__add_cspreport.py
+++ /dev/null
@@ -1,88 +0,0 @@
-# -*- coding: utf-8 -*-
-from south.utils import datetime_utils as datetime
-from south.db import db
-from south.v2 import SchemaMigration
-from django.db import models
-
-
-class Migration(SchemaMigration):
-
- def forwards(self, orm):
- # Adding model 'CspReport'
- db.create_table(u'security_cspreport', (
- (u'id', self.gf('django.db.models.fields.AutoField')(primary_key=True)),
- ('document_uri', self.gf('django.db.models.fields.URLField')(max_length=1000)),
- ('referrer', self.gf('django.db.models.fields.URLField')(max_length=1000)),
- ('blocked_uri', self.gf('django.db.models.fields.URLField')(max_length=1000)),
- ('violated_directive', self.gf('django.db.models.fields.CharField')(max_length=1000)),
- ('original_policy', self.gf('django.db.models.fields.TextField')(max_length=1000, null=True)),
- ('date_received', self.gf('django.db.models.fields.DateTimeField')(auto_now_add=True, blank=True)),
- ('sender_ip', self.gf('django.db.models.fields.GenericIPAddressField')(max_length=39)),
- ('user_agent', self.gf('django.db.models.fields.CharField')(max_length=1000)),
- ))
- db.send_create_signal(u'security', ['CspReport'])
-
-
- def backwards(self, orm):
- # Deleting model 'CspReport'
- db.delete_table(u'security_cspreport')
-
-
- models = {
- u'auth.group': {
- 'Meta': {'object_name': 'Group'},
- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}),
- 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': u"orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'})
- },
- u'auth.permission': {
- 'Meta': {'ordering': "(u'content_type__app_label', u'content_type__model', u'codename')", 'unique_together': "((u'content_type', u'codename'),)", 'object_name': 'Permission'},
- 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['contenttypes.ContentType']", 'on_delete': 'django.db.models.CASCADE'}),
- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'})
- },
- u'auth.user': {
- 'Meta': {'object_name': 'User'},
- 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
- 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}),
- 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
- 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Group']"}),
- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}),
- 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}),
- 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}),
- 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}),
- 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}),
- 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "u'user_set'", 'blank': 'True', 'to': u"orm['auth.Permission']"}),
- 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '255'})
- },
- u'contenttypes.contenttype': {
- 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"},
- 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}),
- 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'})
- },
- u'security.cspreport': {
- 'Meta': {'object_name': 'CspReport'},
- 'blocked_uri': ('django.db.models.fields.URLField', [], {'max_length': '1000'}),
- 'date_received': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}),
- 'document_uri': ('django.db.models.fields.URLField', [], {'max_length': '1000'}),
- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'original_policy': ('django.db.models.fields.TextField', [], {'max_length': '1000', 'null': 'True'}),
- 'referrer': ('django.db.models.fields.URLField', [], {'max_length': '1000'}),
- 'sender_ip': ('django.db.models.fields.GenericIPAddressField', [], {'max_length': '39'}),
- 'user_agent': ('django.db.models.fields.CharField', [], {'max_length': '1000'}),
- 'violated_directive': ('django.db.models.fields.CharField', [], {'max_length': '1000'})
- },
- u'security.passwordexpiry': {
- 'Meta': {'object_name': 'PasswordExpiry'},
- u'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}),
- 'password_expiry_date': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'null': 'True', 'blank': 'True'}),
- 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': u"orm['auth.User']", 'unique': 'True', 'on_delete': 'django.db.models.CASCADE'})
- }
- }
-
- complete_apps = ['security']
diff --git a/security/urls.py b/security/urls.py
index fedafe9..07c625b 100644
--- a/security/urls.py
+++ b/security/urls.py
@@ -1,8 +1,7 @@
-from django.conf.urls import url
+from django.urls import path
from security import views
-
urlpatterns = [
- url('^/csp-report/$', views.csp_report),
+ path("/csp-report/", views.csp_report),
]
diff --git a/security/views.py b/security/views.py
index 26ccd32..8ff6536 100644
--- a/security/views.py
+++ b/security/views.py
@@ -1,14 +1,14 @@
# Copyright (c) 2011, SD Elements. See LICENSE.txt for details.
import json
+import logging
-from django.http import HttpResponseForbidden, HttpResponse
+from django.http import HttpResponse, HttpResponseForbidden
from django.views.decorators.csrf import csrf_exempt
-import logging
log = logging.getLogger(__name__)
-ACCEPTABLE_CONTENT_TYPES = ['application/json', 'application/csp-report']
+ACCEPTABLE_CONTENT_TYPES = ["application/json", "application/csp-report"]
def require_ajax(view):
@@ -17,8 +17,9 @@ def require_ajax(view):
by view is an AJAX request. We return a 403 error if the request
is not an AJAX request.
"""
+
def check_ajax(request, *args, **kwargs):
- if request.is_ajax():
+ if request.headers.get("x-requested-with") == "XMLHttpRequest":
return view(request, *args, **kwargs)
else:
return HttpResponseForbidden()
@@ -55,46 +56,48 @@ def csp_report(request, csp_save=False, csp_log=True):
"""
# http://www.w3.org/TR/CSP/#sample-violation-report
- if not request.method == 'POST':
- log.debug('Unexpect CSP report method %s', request.method)
+ if not request.method == "POST":
+ log.debug("Unexpect CSP report method %s", request.method)
return HttpResponseForbidden()
- content_type = request.META.get('CONTENT_TYPE', None)
+ content_type = request.META.get("CONTENT_TYPE", None)
if content_type not in ACCEPTABLE_CONTENT_TYPES:
- log.debug('Missing CSP report Content-Type %s', request.META)
+ log.debug("Missing CSP report Content-Type %s", request.META)
return HttpResponseForbidden()
try:
csp_dict = json.loads(request.body)
except ValueError:
- log.debug('Cannot JSON decode CSP report %s', request.body)
+ log.debug("Cannot JSON decode CSP report %s", request.body)
return HttpResponseForbidden()
- if 'csp-report' not in csp_dict:
- log.debug('Invalid CSP report structure %s', csp_dict)
+ if "csp-report" not in csp_dict:
+ log.debug("Invalid CSP report structure %s", csp_dict)
return HttpResponseForbidden()
- report = csp_dict['csp-report']
- reporting_ip = request.META['REMOTE_ADDR']
- reporting_ua = request.META['HTTP_USER_AGENT']
+ report = csp_dict["csp-report"]
+ reporting_ip = request.META["REMOTE_ADDR"]
+ reporting_ua = request.META["HTTP_USER_AGENT"]
# log message about received CSP violation to Django log
if csp_log:
log.warn(
- 'Content Security Policy violation: '
- '%s, reporting IP %s, user agent %s',
- report, reporting_ip, reporting_ua
+ "Content Security Policy violation: " "%s, reporting IP %s, user agent %s",
+ report,
+ reporting_ip,
+ reporting_ua,
)
# save received CSP violation to database
if csp_save:
from security.models import CspReport
+
csp_report = CspReport(
- document_uri=report.get('document-uri'),
- referrer=report.get('referrer'),
- blocked_uri=report.get('blocked-uri'),
- violated_directive=report.get('violated-directive'),
- original_policy=report.get('original-policy'),
+ document_uri=report.get("document-uri"),
+ referrer=report.get("referrer"),
+ blocked_uri=report.get("blocked-uri"),
+ violated_directive=report.get("violated-directive"),
+ original_policy=report.get("original-policy"),
sender_ip=reporting_ip,
user_agent=reporting_ua,
)
diff --git a/setup.py b/setup.py
deleted file mode 100644
index 2aba0ec..0000000
--- a/setup.py
+++ /dev/null
@@ -1,63 +0,0 @@
-# Copyright (c) 2011, SD Elements. See LICENSE.txt for details.
-
-import os
-import sys
-import subprocess
-from distutils.core import Command
-from setuptools import setup
-
-with open(os.path.join(os.path.dirname(__file__), "README.md")) as f:
- readme = f.read()
-
-
-class Test(Command):
- user_options = []
-
- def initialize_options(self):
- pass
-
- def finalize_options(self):
- pass
-
- def run(self):
- errno = subprocess.call([sys.executable, "testing/manage.py", "test"])
- raise SystemExit(errno)
-
-
-setup(
- name="django-security",
- description="A collection of tools to help secure a Django project.",
- long_description=readme,
- long_description_content_type="text/markdown",
- maintainer="SD Elements",
- maintainer_email="django-security@sdelements.com",
- version="0.14.0",
- packages=[
- "security",
- "security.south_migrations",
- "security.migrations",
- "security.auth_throttling",
- ],
- url="https://github.com/sdelements/django-security",
- classifiers=[
- "Framework :: Django",
- "Framework :: Django :: 1.11",
- "Framework :: Django :: 2.2",
- "Framework :: Django :: 3.0",
- "Environment :: Web Environment",
- "Programming Language :: Python",
- "Programming Language :: Python :: 3",
- "Programming Language :: Python :: 3.6",
- "Intended Audience :: Developers",
- "Operating System :: OS Independent",
- "License :: OSI Approved :: BSD License",
- "Topic :: Software Development :: Libraries :: Python Modules",
- "Topic :: Security",
- ],
- install_requires=[
- "django>=1.11",
- "ua_parser>=0.7.1",
- "python-dateutil>=2.8.1",
- ],
- cmdclass={"test": Test},
-)
diff --git a/testing/__init__.py b/testing/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/testing/manage.py b/testing/manage.py
deleted file mode 100755
index ce678a1..0000000
--- a/testing/manage.py
+++ /dev/null
@@ -1,15 +0,0 @@
-#!/usr/bin/env python
-import os
-import sys
-
-path, scriptname = os.path.split(__file__)
-
-sys.path.append(os.path.abspath(path))
-sys.path.append(os.path.abspath(os.path.join(path, '..')))
-
-if __name__ == "__main__":
- os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings")
-
- from django.core.management import execute_from_command_line
-
- execute_from_command_line(sys.argv)
diff --git a/testing/settings.py b/testing/settings.py
deleted file mode 100644
index a81213b..0000000
--- a/testing/settings.py
+++ /dev/null
@@ -1,150 +0,0 @@
-import os as _os
-
-
-_PROJECT_PATH = _os.path.abspath(_os.path.dirname(__file__))
-
-DEBUG = True
-ADMINS = ()
-MANAGERS = ADMINS
-DATABASES = {
- 'default': {
- 'ENGINE': 'django.db.backends.sqlite3',
- 'NAME': 'testing.db',
- 'USER': '',
- 'PASSWORD': '',
- 'HOST': '',
- 'PORT': '',
- }
-}
-TIME_ZONE = 'America/Chicago'
-USE_TZ = True
-LANGUAGE_CODE = 'en-us'
-SITE_ID = 1
-USE_I18N = True
-USE_L10N = True
-MEDIA_ROOT = ''
-MEDIA_URL = ''
-STATIC_ROOT = ''
-STATIC_URL = '/static/'
-STATICFILES_DIRS = ()
-STATICFILES_FINDERS = (
- 'django.contrib.staticfiles.finders.FileSystemFinder',
- 'django.contrib.staticfiles.finders.AppDirectoriesFinder',
-)
-SECRET_KEY = 'p_2zsf+@4uw$kcdl$!tkf0lrh%w^!#@2@iwo4plef2n$(@uj4_'
-
-MIDDLEWARE = (
- 'django.middleware.common.CommonMiddleware',
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'django.middleware.csrf.CsrfViewMiddleware',
- 'django.contrib.messages.middleware.MessageMiddleware',
- 'security.middleware.SessionExpiryPolicyMiddleware',
- 'security.middleware.LoginRequiredMiddleware',
- 'security.middleware.XFrameOptionsMiddleware',
- 'security.middleware.ContentNoSniff',
- 'security.middleware.ContentSecurityPolicyMiddleware',
- 'security.middleware.StrictTransportSecurityMiddleware',
- 'security.middleware.P3PPolicyMiddleware',
- 'security.middleware.XssProtectMiddleware',
- 'security.middleware.MandatoryPasswordChangeMiddleware',
- 'security.middleware.NoConfidentialCachingMiddleware',
- 'security.auth_throttling.Middleware',
- 'security.middleware.ReferrerPolicyMiddleware',
-)
-
-ROOT_URLCONF = 'testing.urls'
-
-TEMPLATES = [
- {
- 'BACKEND': 'django.template.backends.django.DjangoTemplates',
- 'DIRS': [_os.path.join(_PROJECT_PATH, "templates")],
- 'OPTIONS': {
- 'context_processors': [
- 'django.contrib.auth.context_processors.auth',
- 'django.contrib.messages.context_processors.messages',
- ]
- }
- }
-]
-
-
-INSTALLED_APPS = (
- 'django.contrib.auth',
- 'django.contrib.contenttypes',
- 'django.contrib.sessions',
- 'django.contrib.sites',
- 'django.contrib.staticfiles',
- 'django.contrib.messages',
- 'django.contrib.admin',
- 'security',
- 'tests'
-)
-
-TEST_RUNNER = 'django.test.runner.DiscoverRunner'
-
-LOGIN_REDIRECT_URL = "/home/"
-
-# The tests for django.contrib.auth use certain URLs, and they'll fail if we
-# interfere with these.
-_DJANGO_TESTING_URLS = [
- 'login/', 'login_required/', 'login_required_login_url/',
- 'admin_password_reset/', 'logout/', 'password_reset/',
- 'password_reset_from_email/', 'reset/', 'password_change/', 'remote_user/',
- 'auth_processor_messages/', 'auth_processor_perms/',
- 'auth_processor_user/', 'auth_processor_perm_in_perms/',
- 'admin/auth/user/',
-]
-
-LOGIN_EXEMPT_URLS = [
- "accounts/login",
- "custom-login",
- "admin/reset-account-throttling",
-] + _DJANGO_TESTING_URLS
-
-SESSION_EXPIRY_EXEMPT_URLS = LOGIN_EXEMPT_URLS
-
-CUSTOM_LOGOUT_MODULE = 'tests.tests.mocked_custom_logout'
-
-MANDATORY_PASSWORD_CHANGE = {
- "URL_NAME": "change_password",
- "EXEMPT_URL_NAMES": (),
- "EXEMPT_URLS": _DJANGO_TESTING_URLS,
-}
-
-AUTHENTICATION_THROTTLING = {
- "DELAY_FUNCTION": lambda x, y: (0, 0),
- "LOGIN_URLS_WITH_TEMPLATES": [
- ("accounts/login/", "login.html")
- ]
-}
-
-XSS_PROTECT = 'on'
-X_FRAME_OPTIONS = 'allow-from: http://example.com'
-X_FRAME_OPTIONS_EXCLUDE_URLS = (
- r'^/test\d/$',
-)
-CSP_STRING = "allow 'self'; script-src *.google.com"
-CSP_MODE = 'enforce'
-P3P_POLICY_URL = '/w3c/p3p.xml'
-P3P_COMPACT_POLICY = 'PRIVATE'
-
-LOGGING = {
- 'version': 1,
- 'disable_existing_loggers': False,
- 'handlers': {
- 'console': {
- 'level': 'DEBUG',
- 'class': 'logging.StreamHandler',
- },
- },
- 'loggers': {
- '': {
- 'handlers': ['console'],
- 'level': 'WARNING',
- 'propagate': True,
- },
- },
-}
-
-CLEAR_SITE_DATA_URL_WHITELIST = ('/home/')
diff --git a/testing/testing.db b/testing/testing.db
deleted file mode 100644
index 0fbdb17..0000000
Binary files a/testing/testing.db and /dev/null differ
diff --git a/testing/tests/__init__.py b/testing/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/testing/urls.py b/testing/urls.py
deleted file mode 100644
index 4428048..0000000
--- a/testing/urls.py
+++ /dev/null
@@ -1,24 +0,0 @@
-# Copyright (c) 2011, SD Elements. See ../LICENSE.txt for details.
-
-from django.conf.urls import url
-from django.http import HttpResponse
-from django.contrib.auth.views import LoginView, PasswordChangeView
-
-from security.auth_throttling.views import reset_username_throttle
-from security.views import csp_report
-
-urlpatterns = [
- url("^accounts/login/$", LoginView.as_view(), {}, "login"),
- url("^change_password/$", PasswordChangeView.as_view(),
- {"post_change_redirect": "/home/"}, "change_password"),
- url(r"^admin/reset-account-throttling/(?P-?[0-9]+)/",
- reset_username_throttle,
- {"redirect_url": "/admin"}, "reset_username_throttle"),
- url("^home/$", lambda request: HttpResponse()),
- url("^custom-login/$", lambda request: HttpResponse()),
- url("^test1/$", lambda request: HttpResponse(), {}, "test1"),
- url("^test2/$", lambda request: HttpResponse(), {}, "test2"),
- url("^test3/$", lambda request: HttpResponse(), {}, "test3"),
- url("^test4/$", lambda request: HttpResponse(), {}, "test4"),
- url("^csp-report/$", csp_report),
-]
diff --git a/security/south_migrations/__init__.py b/tests/__init__.py
similarity index 100%
rename from security/south_migrations/__init__.py
rename to tests/__init__.py
diff --git a/testing/tests/models.py b/tests/models.py
similarity index 100%
rename from testing/tests/models.py
rename to tests/models.py
diff --git a/tests/settings.py b/tests/settings.py
new file mode 100644
index 0000000..55aea59
--- /dev/null
+++ b/tests/settings.py
@@ -0,0 +1,151 @@
+import os as _os
+
+_PROJECT_PATH = _os.path.abspath(_os.path.dirname(__file__))
+
+DEBUG = True
+ADMINS = ()
+MANAGERS = ADMINS
+DATABASES = {
+ "default": {
+ "ENGINE": "django.db.backends.sqlite3",
+ "NAME": "testing.db",
+ "USER": "",
+ "PASSWORD": "",
+ "HOST": "",
+ "PORT": "",
+ }
+}
+TIME_ZONE = "America/Chicago"
+USE_TZ = True
+LANGUAGE_CODE = "en-us"
+SITE_ID = 1
+USE_I18N = True
+USE_L10N = True
+MEDIA_ROOT = ""
+MEDIA_URL = ""
+STATIC_ROOT = ""
+STATIC_URL = "/static/"
+STATICFILES_DIRS = ()
+STATICFILES_FINDERS = (
+ "django.contrib.staticfiles.finders.FileSystemFinder",
+ "django.contrib.staticfiles.finders.AppDirectoriesFinder",
+)
+SECRET_KEY = "foobar"
+DEFAULT_AUTO_FIELD = "django.db.models.AutoField"
+
+MIDDLEWARE = (
+ "django.middleware.common.CommonMiddleware",
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "django.middleware.csrf.CsrfViewMiddleware",
+ "django.contrib.messages.middleware.MessageMiddleware",
+ "security.middleware.SessionExpiryPolicyMiddleware",
+ "security.middleware.LoginRequiredMiddleware",
+ "security.middleware.XFrameOptionsMiddleware",
+ "security.middleware.ContentSecurityPolicyMiddleware",
+ "security.middleware.MandatoryPasswordChangeMiddleware",
+ "security.middleware.NoConfidentialCachingMiddleware",
+ "security.auth_throttling.Middleware",
+ "security.middleware.ReferrerPolicyMiddleware",
+ "security.middleware.ProfilingMiddleware",
+)
+
+ROOT_URLCONF = "tests.urls"
+
+TEMPLATES = [
+ {
+ "BACKEND": "django.template.backends.django.DjangoTemplates",
+ "DIRS": [_os.path.join(_PROJECT_PATH, "templates")],
+ "OPTIONS": {
+ "context_processors": [
+ "django.template.context_processors.request",
+ "django.contrib.auth.context_processors.auth",
+ "django.contrib.messages.context_processors.messages",
+ ]
+ },
+ }
+]
+
+
+INSTALLED_APPS = (
+ "django.contrib.auth",
+ "django.contrib.contenttypes",
+ "django.contrib.sessions",
+ "django.contrib.sites",
+ "django.contrib.staticfiles",
+ "django.contrib.messages",
+ "django.contrib.admin",
+ "security",
+ "tests",
+)
+
+TEST_RUNNER = "django.test.runner.DiscoverRunner"
+
+LOGIN_REDIRECT_URL = "/home/"
+
+# The tests for django.contrib.auth use certain URLs, and they'll fail if we
+# interfere with these.
+_DJANGO_TESTING_URLS = [
+ "login/",
+ "login_required/",
+ "login_required_login_url/",
+ "admin_password_reset/",
+ "logout/",
+ "password_reset/",
+ "password_reset_from_email/",
+ "reset/",
+ "password_change/",
+ "remote_user/",
+ "auth_processor_messages/",
+ "auth_processor_perms/",
+ "auth_processor_user/",
+ "auth_processor_perm_in_perms/",
+ "admin/auth/user/",
+]
+
+LOGIN_EXEMPT_URLS = [
+ "accounts/login",
+ "custom-login",
+ "admin/reset-account-throttling",
+] + _DJANGO_TESTING_URLS
+
+SESSION_EXPIRY_EXEMPT_URLS = LOGIN_EXEMPT_URLS
+
+CUSTOM_LOGOUT_MODULE = "tests.tests.mocked_custom_logout"
+
+MANDATORY_PASSWORD_CHANGE = {
+ "URL_NAME": "change_password",
+ "EXEMPT_URL_NAMES": (),
+ "EXEMPT_URLS": _DJANGO_TESTING_URLS,
+}
+
+AUTHENTICATION_THROTTLING = {
+ "DELAY_FUNCTION": lambda x, y: (0, 0),
+ "LOGIN_URLS_WITH_TEMPLATES": [("accounts/login/", "login.html")],
+}
+
+XSS_PROTECT = "on"
+X_FRAME_OPTIONS = "allow-from: http://example.com"
+X_FRAME_OPTIONS_EXCLUDE_URLS = (r"^/test\d/$",)
+CSP_STRING = "allow 'self'; script-src *.google.com"
+CSP_MODE = "enforce"
+
+LOGGING = {
+ "version": 1,
+ "disable_existing_loggers": False,
+ "handlers": {
+ "console": {
+ "level": "DEBUG",
+ "class": "logging.StreamHandler",
+ },
+ },
+ "loggers": {
+ "": {
+ "handlers": ["console"],
+ "level": "WARNING",
+ "propagate": True,
+ },
+ },
+}
+
+CLEAR_SITE_DATA_URL_WHITELIST = "/home/"
diff --git a/testing/templates/404.html b/tests/templates/404.html
similarity index 100%
rename from testing/templates/404.html
rename to tests/templates/404.html
diff --git a/testing/templates/registration/login.html b/tests/templates/registration/login.html
similarity index 100%
rename from testing/templates/registration/login.html
rename to tests/templates/registration/login.html
diff --git a/testing/templates/registration/password_change_form.html b/tests/templates/registration/password_change_form.html
similarity index 100%
rename from testing/templates/registration/password_change_form.html
rename to tests/templates/registration/password_change_form.html
diff --git a/testing/tests/tests.py b/tests/tests.py
similarity index 60%
rename from testing/tests/tests.py
rename to tests/tests.py
index 3ce346d..2f249cd 100644
--- a/testing/tests/tests.py
+++ b/tests/tests.py
@@ -5,30 +5,32 @@
import time # We monkeypatch this.
from django.conf import settings
-from django.contrib.auth.models import User
from django.contrib.auth import logout
+from django.contrib.auth.models import User
from django.core.cache import cache
from django.core.exceptions import ImproperlyConfigured, MiddlewareNotUsed
-from django.urls import reverse
from django.forms import ValidationError
-from django.http import HttpResponseForbidden, HttpRequest, HttpResponse
+from django.http import HttpRequest, HttpResponse, HttpResponseForbidden
from django.test import TestCase
from django.test.utils import override_settings
+from django.urls import reverse
from django.utils import timezone
from security.auth import min_length
-from security.auth_throttling import (
- attempt_count, default_delay_function, delay_message, increment_counters,
- reset_counters, Middleware as AuthThrottlingMiddleware
-)
-from security.middleware import (
- BaseMiddleware, ContentSecurityPolicyMiddleware, DoNotTrackMiddleware,
- SessionExpiryPolicyMiddleware, MandatoryPasswordChangeMiddleware,
- XssProtectMiddleware, XFrameOptionsMiddleware, ReferrerPolicyMiddleware
-)
+from security.auth_throttling import Middleware as AuthThrottlingMiddleware
+from security.auth_throttling import (attempt_count, default_delay_function,
+ delay_message, increment_counters,
+ reset_counters)
+from security.middleware import (BaseMiddleware,
+ ContentSecurityPolicyMiddleware,
+ DoNotTrackMiddleware,
+ MandatoryPasswordChangeMiddleware,
+ ReferrerPolicyMiddleware,
+ SessionExpiryPolicyMiddleware,
+ XFrameOptionsMiddleware)
from security.models import PasswordExpiry
from security.password_expiry import never_expire_password
-from security.views import require_ajax, csp_report
+from security.views import csp_report, require_ajax
try:
# Python 3
@@ -46,10 +48,11 @@ def login_user(func):
then log that user in. We expect self to be a DjangoTestCase,
or some object with a similar interface.
"""
+
def wrapper(self, *args, **kwargs):
- username_local = 'a2fcf54f63993b7'
- password_local = 'd8327deb882cf90'
- email_local = 'testuser@example.com'
+ username_local = "a2fcf54f63993b7"
+ password_local = "d8327deb882cf90"
+ email_local = "testuser@example.com"
user = User.objects.create_user(
username=username_local,
email=email_local,
@@ -62,21 +65,23 @@ def wrapper(self, *args, **kwargs):
func(self, *args, **kwargs)
self.client.logout()
user.delete()
+
return wrapper
class CustomLoginURLMiddleware(BaseMiddleware):
"""Used to test the custom url support in the login required middleware."""
+
def process_request(self, request):
- request.login_url = '/custom-login/'
+ request.login_url = "/custom-login/"
class BaseMiddlewareTestMiddleware(BaseMiddleware):
- REQUIRED_SETTINGS = ('R1', 'R2')
- OPTIONAL_SETTINGS = ('O1', 'O2')
+ REQUIRED_SETTINGS = ("R1", "R2")
+ OPTIONAL_SETTINGS = ("O1", "O2")
def load_setting(self, setting, value):
- if not hasattr(self, 'loaded_settings'):
+ if not hasattr(self, "loaded_settings"):
self.loaded_settings = {}
self.loaded_settings[setting] = value
@@ -92,96 +97,90 @@ class BaseMiddlewareTests(TestCase):
def __init__(self, *args, **kwargs):
super(BaseMiddlewareTests, self).__init__(*args, **kwargs)
module_name = BaseMiddlewareTests.__module__
- self.MIDDLEWARE_NAME = module_name + '.BaseMiddlewareTestMiddleware'
+ self.MIDDLEWARE_NAME = module_name + ".BaseMiddlewareTestMiddleware"
def test_settings_initially_loaded(self):
- expected_settings = {'R1': 1, 'R2': 2, 'O1': 3, 'O2': 4}
- with self.settings(
- MIDDLEWARE=(self.MIDDLEWARE_NAME,), **expected_settings
- ):
- response = self.client.get('/home/')
+ expected_settings = {"R1": 1, "R2": 2, "O1": 3, "O2": 4}
+ with self.settings(MIDDLEWARE=(self.MIDDLEWARE_NAME,), **expected_settings):
+ response = self.client.get("/home/")
self.assertEqual(expected_settings, response.loaded_settings)
def test_required_settings(self):
with self.settings(MIDDLEWARE=(self.MIDDLEWARE_NAME,)):
- self.assertRaises(ImproperlyConfigured, self.client.get, '/home/')
+ self.assertRaises(ImproperlyConfigured, self.client.get, "/home/")
def test_optional_settings(self):
- with self.settings(
- MIDDLEWARE=(self.MIDDLEWARE_NAME,), R1=True, R2=True
- ):
- response = self.client.get('/home/')
- self.assertEqual(None, response.loaded_settings['O1'])
- self.assertEqual(None, response.loaded_settings['O2'])
+ with self.settings(MIDDLEWARE=(self.MIDDLEWARE_NAME,), R1=True, R2=True):
+ response = self.client.get("/home/")
+ self.assertEqual(None, response.loaded_settings["O1"])
+ self.assertEqual(None, response.loaded_settings["O2"])
def test_setting_change(self):
- with self.settings(
- MIDDLEWARE=(self.MIDDLEWARE_NAME,), R1=123, R2=True
- ):
- response = self.client.get('/home/')
- self.assertEqual(123, response.loaded_settings['R1'])
+ with self.settings(MIDDLEWARE=(self.MIDDLEWARE_NAME,), R1=123, R2=True):
+ response = self.client.get("/home/")
+ self.assertEqual(123, response.loaded_settings["R1"])
with override_settings(R1=456):
- response = self.client.get('/home/')
- self.assertEqual(456, response.loaded_settings['R1'])
+ response = self.client.get("/home/")
+ self.assertEqual(456, response.loaded_settings["R1"])
- response = self.client.get('/home/')
- self.assertEqual(123, response.loaded_settings['R1'])
+ response = self.client.get("/home/")
+ self.assertEqual(123, response.loaded_settings["R1"])
def test_load_setting_abstract_method(self):
base = BaseMiddleware()
self.assertRaises(NotImplementedError, base.load_setting, None, None)
-@override_settings(MIDDLEWARE=(
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'security.middleware.LoginRequiredMiddleware',
-))
+@override_settings(
+ MIDDLEWARE=(
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "security.middleware.LoginRequiredMiddleware",
+ )
+)
class LoginRequiredMiddlewareTests(TestCase):
def setUp(self):
self.login_url = reverse("login")
def test_aborts_if_auth_middleware_missing(self):
middleware_classes = settings.MIDDLEWARE
- auth_mw = 'django.contrib.auth.middleware.AuthenticationMiddleware'
- middleware_classes = [
- m for m in middleware_classes if m != auth_mw
- ]
+ auth_mw = "django.contrib.auth.middleware.AuthenticationMiddleware"
+ middleware_classes = [m for m in middleware_classes if m != auth_mw]
with self.settings(MIDDLEWARE=middleware_classes):
- self.assertRaises(ImproperlyConfigured, self.client.get, '/home/')
+ self.assertRaises(ImproperlyConfigured, self.client.get, "/home/")
def test_redirects_unauthenticated_request(self):
- response = self.client.get('/home/')
+ response = self.client.get("/home/")
self.assertRedirects(response, self.login_url + "?next=/home/")
def test_redirects_unauthenticated_ajax_request(self):
response = self.client.get(
- '/home/',
- HTTP_X_REQUESTED_WITH='XMLHttpRequest',
+ "/home/",
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 401)
self.assertEqual(
- json.loads(response.content.decode('utf-8')),
+ json.loads(response.content.decode("utf-8")),
{"login_url": self.login_url},
)
def test_redirects_to_custom_login_url(self):
middlware_classes = list(settings.MIDDLEWARE)
- custom_login_middleware = 'tests.tests.CustomLoginURLMiddleware'
+ custom_login_middleware = "tests.tests.CustomLoginURLMiddleware"
with self.settings(
MIDDLEWARE=[custom_login_middleware] + middlware_classes,
):
- response = self.client.get('/home/')
- self.assertRedirects(response, '/custom-login/')
+ response = self.client.get("/home/")
+ self.assertRedirects(response, "/custom-login/")
response = self.client.get(
- '/home/',
- HTTP_X_REQUESTED_WITH='XMLHttpRequest',
+ "/home/",
+ HTTP_X_REQUESTED_WITH="XMLHttpRequest",
)
self.assertEqual(response.status_code, 401)
self.assertEqual(
- json.loads(response.content.decode('utf-8')),
- {"login_url": '/custom-login/'},
+ json.loads(response.content.decode("utf-8")),
+ {"login_url": "/custom-login/"},
)
def test_logs_out_inactive_users(self):
@@ -192,28 +191,30 @@ def test_logs_out_inactive_users(self):
)
never_expire_password(user)
self.client.login(username="foo", password="foo")
- resp = self.client.get('/home/')
+ resp = self.client.get("/home/")
self.assertEqual(resp.status_code, 200) # check we are logged in
user.is_active = False
user.save()
- resp = self.client.get('/home/')
+ resp = self.client.get("/home/")
self.assertRedirects(resp, self.login_url + "?next=/home/")
-@override_settings(MIDDLEWARE=(
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'security.middleware.MandatoryPasswordChangeMiddleware',
-))
+@override_settings(
+ MIDDLEWARE=(
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "security.middleware.MandatoryPasswordChangeMiddleware",
+ )
+)
class RequirePasswordChangeTests(TestCase):
def test_require_password_change(self):
"""
A brand-new user should have an already-expired password, and therefore
be redirected to the password change form on any request.
"""
- user = User.objects.create_user(username="foo",
- password="foo",
- email="foo@foo.com")
+ user = User.objects.create_user(
+ username="foo", password="foo", email="foo@foo.com"
+ )
self.client.login(username="foo", password="foo")
try:
with self.settings(
@@ -233,19 +234,20 @@ def test_superuser_password_change(self):
"""
A superuser can be forced to change their password via settings.
"""
- user = User.objects.create_superuser(username="foo",
- password="foo",
- email="foo@foo.com")
+ user = User.objects.create_superuser(
+ username="foo", password="foo", email="foo@foo.com"
+ )
self.client.login(username="foo", password="foo")
- with self.settings(MANDATORY_PASSWORD_CHANGE={
- "URL_NAME": "change_password"}):
+ with self.settings(MANDATORY_PASSWORD_CHANGE={"URL_NAME": "change_password"}):
self.assertEqual(self.client.get("/home/").status_code, 200)
try:
- with self.settings(MANDATORY_PASSWORD_CHANGE={
- "URL_NAME": "change_password",
- "INCLUDE_SUPERUSERS": True
- }):
+ with self.settings(
+ MANDATORY_PASSWORD_CHANGE={
+ "URL_NAME": "change_password",
+ "INCLUDE_SUPERUSERS": True,
+ }
+ ):
self.assertRedirects(
self.client.get("/home/"),
reverse("change_password"),
@@ -256,18 +258,18 @@ def test_superuser_password_change(self):
def test_dont_redirect_exempt_urls(self):
user = User.objects.create_user(
- username="foo",
- password="foo",
- email="foo@foo.com"
+ username="foo", password="foo", email="foo@foo.com"
)
self.client.login(username="foo", password="foo")
try:
- with self.settings(MANDATORY_PASSWORD_CHANGE={
- "URL_NAME": "change_password",
- "EXEMPT_URLS": (r'^test1/$', r'^test2/$'),
- "EXEMPT_URL_NAMES": ("test3", "test4"),
- }):
+ with self.settings(
+ MANDATORY_PASSWORD_CHANGE={
+ "URL_NAME": "change_password",
+ "EXEMPT_URLS": (r"^test1/$", r"^test2/$"),
+ "EXEMPT_URL_NAMES": ("test3", "test4"),
+ }
+ ):
# Redirect pages in general
self.assertRedirects(
self.client.get("/home/"),
@@ -290,16 +292,18 @@ def test_dont_redirect_exempt_urls(self):
user.delete()
def test_dont_choke_on_exempt_urls_that_dont_resolve(self):
- user = User.objects.create_user(username="foo",
- password="foo",
- email="foo@foo.com")
+ user = User.objects.create_user(
+ username="foo", password="foo", email="foo@foo.com"
+ )
self.client.login(username="foo", password="foo")
try:
- with self.settings(MANDATORY_PASSWORD_CHANGE={
- "URL_NAME": "change_password",
- "EXEMPT_URL_NAMES": ("fake1", "fake2"),
- }):
+ with self.settings(
+ MANDATORY_PASSWORD_CHANGE={
+ "URL_NAME": "change_password",
+ "EXEMPT_URL_NAMES": ("fake1", "fake2"),
+ }
+ ):
# Redirect pages in general
self.assertRedirects(
self.client.get("/home/"),
@@ -314,8 +318,8 @@ def test_raises_improperly_configured(self):
self.assertRaises(
ImproperlyConfigured,
change.load_setting,
- 'MANDATORY_PASSWORD_CHANGE',
- {'EXEMPT_URLS': []},
+ "MANDATORY_PASSWORD_CHANGE",
+ {"EXEMPT_URLS": []},
)
@@ -327,38 +331,38 @@ class DecoratorTest(TestCase):
def require_ajax_test(self):
@require_ajax
def ajax_only_view(request):
- self.assertTrue(request.is_ajax())
+ self.assertTrue(request.headers.get("x-requested-with") == "XMLHttpRequest")
request = HttpRequest()
response = ajax_only_view(request)
self.assertTrue(isinstance(response, HttpResponseForbidden))
- request.META['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
+ request.META["HTTP_X_REQUESTED_WITH"] = "XMLHttpRequest"
response = ajax_only_view(request)
self.assertFalse(isinstance(response, HttpResponseForbidden))
-@override_settings(MIDDLEWARE=(
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'security.middleware.SessionExpiryPolicyMiddleware',
- 'security.middleware.LoginRequiredMiddleware',
-))
+@override_settings(
+ MIDDLEWARE=(
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "security.middleware.SessionExpiryPolicyMiddleware",
+ "security.middleware.LoginRequiredMiddleware",
+ )
+)
class SessionExpiryTests(TestCase):
def test_session_variables_are_set(self):
"""
Verify the session cookie stores the start time and last active time.
"""
- self.client.get('/home/')
+ self.client.get("/home/")
now = timezone.now()
start_time = SessionExpiryPolicyMiddleware._get_datetime_in_session(
- SessionExpiryPolicyMiddleware.START_TIME_KEY,
- self.client.session
+ SessionExpiryPolicyMiddleware.START_TIME_KEY, self.client.session
)
last_activity = SessionExpiryPolicyMiddleware._get_datetime_in_session(
- SessionExpiryPolicyMiddleware.LAST_ACTIVITY_KEY,
- self.client.session
+ SessionExpiryPolicyMiddleware.LAST_ACTIVITY_KEY, self.client.session
)
self.assertTrue(now - start_time < datetime.timedelta(seconds=10))
@@ -369,17 +373,12 @@ def session_expiry_test(self, key, expired):
Verify that expired sessions are cleared from the system. (And that we
redirect to the login page.)
"""
- self.assertTrue(self.client.get('/home/').status_code, 200)
+ self.assertTrue(self.client.get("/home/").status_code, 200)
session = self.client.session
- SessionExpiryPolicyMiddleware._set_datetime_in_session(
- key,
- expired,
- session
- )
+ SessionExpiryPolicyMiddleware._set_datetime_in_session(key, expired, session)
session.save()
- response = self.client.get('/home/')
- self.assertRedirects(response,
- reverse("login") + '?next=/home/')
+ response = self.client.get("/home/")
+ self.assertRedirects(response, reverse("login") + "?next=/home/")
@login_user
def test_session_too_old(self):
@@ -389,8 +388,7 @@ def test_session_too_old(self):
"""
delta = SessionExpiryPolicyMiddleware().SESSION_COOKIE_AGE + 1
expired = timezone.now() - datetime.timedelta(seconds=delta)
- self.session_expiry_test(SessionExpiryPolicyMiddleware.START_TIME_KEY,
- expired)
+ self.session_expiry_test(SessionExpiryPolicyMiddleware.START_TIME_KEY, expired)
@login_user
def test_session_inactive_too_long(self):
@@ -410,22 +408,19 @@ def test_exempted_session_expiry_urls(self):
delta = SessionExpiryPolicyMiddleware().SESSION_INACTIVITY_TIMEOUT + 1
expired = timezone.now() - datetime.timedelta(seconds=delta)
- self.assertTrue(self.client.get('/home/').status_code, 200)
+ self.assertTrue(self.client.get("/home/").status_code, 200)
session = self.client.session
SessionExpiryPolicyMiddleware._set_datetime_in_session(
- SessionExpiryPolicyMiddleware.LAST_ACTIVITY_KEY,
- expired,
- session
+ SessionExpiryPolicyMiddleware.LAST_ACTIVITY_KEY, expired, session
)
session.save()
- exempted_response = self.client.get('/accounts/login/')
- not_exempted_response = self.client.get('/home/')
+ exempted_response = self.client.get("/accounts/login/")
+ not_exempted_response = self.client.get("/home/")
self.assertTrue(exempted_response.status_code, 200)
- self.assertRedirects(not_exempted_response,
- reverse("login") + '?next=/home/')
+ self.assertRedirects(not_exempted_response, reverse("login") + "?next=/home/")
@login_user
def test_custom_logout(self):
@@ -438,26 +433,26 @@ def test_custom_logout(self):
assert mocked_custom_logout.called
-@override_settings(MIDDLEWARE=(
- 'security.middleware.NoConfidentialCachingMiddleware',
-))
+@override_settings(MIDDLEWARE=("security.middleware.NoConfidentialCachingMiddleware",))
class ConfidentialCachingTests(TestCase):
def setUp(self):
self.header_values = {
- "Cache-Control": 'no-cache, no-store, max-age=0, must-revalidate',
+ "Cache-Control": "no-cache, no-store, max-age=0, must-revalidate",
"Pragma": "no-cache",
- "Expires": '-1'
+ "Expires": "-1",
}
- @override_settings(NO_CONFIDENTIAL_CACHING={
- "WHITELIST_ON": True,
- "BLACKLIST_ON": False,
- "WHITELIST_REGEXES": ["accounts/login/$"],
- "BLACKLIST_REGEXES": ["accounts/logout/$"]
- })
+ @override_settings(
+ NO_CONFIDENTIAL_CACHING={
+ "WHITELIST_ON": True,
+ "BLACKLIST_ON": False,
+ "WHITELIST_REGEXES": ["accounts/login/$"],
+ "BLACKLIST_REGEXES": ["accounts/logout/$"],
+ }
+ )
def test_whitelisting(self):
# Get Non Confidential Page
- response = self.client.get('/accounts/login/')
+ response = self.client.get("/accounts/login/")
for header, value in self.header_values.items():
self.assertNotEqual(response.get(header, None), value)
# Get Confidential Page
@@ -465,15 +460,17 @@ def test_whitelisting(self):
for header, value in self.header_values.items():
self.assertEqual(response.get(header, None), value)
- @override_settings(NO_CONFIDENTIAL_CACHING={
- "WHITELIST_ON": False,
- "BLACKLIST_ON": True,
- "WHITELIST_REGEXES": ["accounts/login/$"],
- "BLACKLIST_REGEXES": ["accounts/logout/$"]
- })
+ @override_settings(
+ NO_CONFIDENTIAL_CACHING={
+ "WHITELIST_ON": False,
+ "BLACKLIST_ON": True,
+ "WHITELIST_REGEXES": ["accounts/login/$"],
+ "BLACKLIST_REGEXES": ["accounts/logout/$"],
+ }
+ )
def test_blacklisting(self):
# Get Non Confidential Page
- response = self.client.get('/accounts/login/')
+ response = self.client.get("/accounts/login/")
for header, value in self.header_values.items():
self.assertNotEqual(response.get(header, None), value)
# Get Confidential Page
@@ -482,124 +479,69 @@ def test_blacklisting(self):
self.assertEqual(response.get(header, None), value)
-@override_settings(MIDDLEWARE=('security.middleware.XFrameOptionsMiddleware',))
+@override_settings(MIDDLEWARE=("security.middleware.XFrameOptionsMiddleware",))
class XFrameOptionsDenyTests(TestCase):
def test_option_set(self):
"""
Verify the HTTP Response Header is set.
"""
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['X-Frame-Options'], settings.X_FRAME_OPTIONS)
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["X-Frame-Options"], settings.X_FRAME_OPTIONS)
def test_exclude_urls(self):
"""
Verify that pages can be excluded from the X-Frame-Options header.
"""
- response = self.client.get('/home/')
- self.assertEqual(response['X-Frame-Options'], settings.X_FRAME_OPTIONS)
- response = self.client.get('/test1/')
- self.assertNotIn('X-Frame-Options', response)
+ response = self.client.get("/home/")
+ self.assertEqual(response["X-Frame-Options"], settings.X_FRAME_OPTIONS)
+ response = self.client.get("/test1/")
+ self.assertNotIn("X-Frame-Options", response)
def test_improperly_configured(self):
xframe = XFrameOptionsMiddleware()
self.assertRaises(
ImproperlyConfigured,
xframe.load_setting,
- 'X_FRAME_OPTIONS',
- 'invalid',
+ "X_FRAME_OPTIONS",
+ "invalid",
)
self.assertRaises(
ImproperlyConfigured,
xframe.load_setting,
- 'X_FRAME_OPTIONS_EXCLUDE_URLS',
+ "X_FRAME_OPTIONS_EXCLUDE_URLS",
1,
)
@override_settings(X_FRAME_OPTIONS_EXCLUDE_URLS=None)
def test_default_exclude_urls(self):
# This URL is excluded in other tests, see settings.py
- response = self.client.get('/test1/')
+ response = self.client.get("/test1/")
self.assertEqual(
- response['X-Frame-Options'],
+ response["X-Frame-Options"],
settings.X_FRAME_OPTIONS,
)
@override_settings(X_FRAME_OPTIONS=None)
def test_default_xframe_option(self):
- response = self.client.get('/home/')
+ response = self.client.get("/home/")
self.assertEqual(
- response['X-Frame-Options'],
- 'deny',
+ response["X-Frame-Options"],
+ "deny",
)
-@override_settings(MIDDLEWARE=('security.middleware.XssProtectMiddleware',))
-class XXssProtectTests(TestCase):
-
- def test_option_set(self):
- """
- Verify the HTTP Response Header is set.
- """
- response = self.client.get('/accounts/login/')
- self.assertNotEqual(response['X-XSS-Protection'], None)
-
- def test_default_setting(self):
- with self.settings(XSS_PROTECT=None):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['X-XSS-Protection'], '1') # sanitize
-
- def test_option_off(self):
- with self.settings(XSS_PROTECT='off'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['X-XSS-Protection'], '0') # off
-
- def test_improper_configuration_raises(self):
- xss = XssProtectMiddleware()
- self.assertRaises(
- ImproperlyConfigured,
- xss.load_setting,
- 'XSS_PROTECT',
- 'invalid',
- )
-
-
-@override_settings(MIDDLEWARE=('security.middleware.ContentNoSniff',))
-class ContentNoSniffTests(TestCase):
-
- def test_option_set(self):
- """
- Verify the HTTP Response Header is set.
- """
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['X-Content-Options'], 'nosniff')
-
-
-@override_settings(MIDDLEWARE=(
- 'security.middleware.StrictTransportSecurityMiddleware',
-))
-class StrictTransportSecurityTests(TestCase):
-
- def test_option_set(self):
- """
- Verify the HTTP Response Header is set.
- """
- response = self.client.get('/accounts/login/')
- self.assertNotEqual(response['Strict-Transport-Security'], None)
-
-
@override_settings(
AUTHENTICATION_THROTTLING={
"DELAY_FUNCTION": lambda x, _: (2 ** (x - 1) if x else 0, 0),
- "LOGIN_URLS_WITH_TEMPLATES": [
- ("accounts/login/", "registration/login.html")
- ]
+ "LOGIN_URLS_WITH_TEMPLATES": [("accounts/login/", "registration/login.html")],
},
MIDDLEWARE=(
- 'django.contrib.sessions.middleware.SessionMiddleware',
- 'django.contrib.auth.middleware.AuthenticationMiddleware',
- 'security.auth_throttling.Middleware',)
+ "django.contrib.sessions.middleware.SessionMiddleware",
+ "django.contrib.auth.middleware.AuthenticationMiddleware",
+ "security.auth_throttling.Middleware",
+ ),
)
class AuthenticationThrottlingTests(TestCase):
def setUp(self):
@@ -607,17 +549,17 @@ def setUp(self):
self.old_time = time.time
self.time = 0
time.time = lambda: self.time
- self.user = User.objects.create_user(username="foo", password="foo",
- email="a@foo.org")
+ self.user = User.objects.create_user(
+ username="foo", password="foo", email="a@foo.org"
+ )
def tearDown(self):
time.time = self.old_time
def attempt(self, password):
- return self.client.post("/accounts/login/",
- {"username": "foo",
- "password": password},
- follow=True)
+ return self.client.post(
+ "/accounts/login/", {"username": "foo", "password": password}, follow=True
+ )
def reset(self):
self.client.logout()
@@ -627,8 +569,7 @@ def typo(self):
self.assertTemplateUsed(self.attempt("bar"), "registration/login.html")
def _succeed(self):
- self.assertTemplateNotUsed(self.attempt("foo"),
- "registration/login.html")
+ self.assertTemplateNotUsed(self.attempt("foo"), "registration/login.html")
self.reset()
def _fail(self):
@@ -712,12 +653,12 @@ def test_per_account_throttling(self):
self.set_time(3)
self._succeed()
- @override_settings(AUTHENTICATION_THROTTLING={
- "DELAY_FUNCTION": lambda x, y: (x, y),
- "LOGIN_URLS_WITH_TEMPLATES": [
- ("accounts/login/", None)
- ]
- })
+ @override_settings(
+ AUTHENTICATION_THROTTLING={
+ "DELAY_FUNCTION": lambda x, y: (x, y),
+ "LOGIN_URLS_WITH_TEMPLATES": [("accounts/login/", None)],
+ }
+ )
def test_too_many_requests_error_when_no_template_provided(self):
"""
Verify we simply return a 429 error when there is no login template
@@ -745,8 +686,9 @@ def test_reset_button(self):
"""
self.set_time(0)
self.typo()
- admin = User.objects.create_user(username="bar", password="bar",
- email="a@bar.org")
+ admin = User.objects.create_user(
+ username="bar", password="bar", email="a@bar.org"
+ )
admin.is_superuser = True
admin.save()
self.client.login(username="bar", password="bar")
@@ -756,9 +698,11 @@ def test_reset_button(self):
self.client.logout()
self._succeed()
- @override_settings(AUTHENTICATION_THROTTLING={
- "DELAY_FUNCTION": lambda x, y: (x, y),
- })
+ @override_settings(
+ AUTHENTICATION_THROTTLING={
+ "DELAY_FUNCTION": lambda x, y: (x, y),
+ }
+ )
def test_improperly_configured_middleware(self):
self.assertRaises(ImproperlyConfigured, AuthThrottlingMiddleware)
@@ -785,32 +729,18 @@ def test_throttle_reset_404_on_not_found(self):
self.assertEqual(resp.status_code, 404)
-@override_settings(MIDDLEWARE=('security.middleware.P3PPolicyMiddleware',))
-class P3PPolicyTests(TestCase):
-
- def setUp(self):
- self.policy = "NN AD BLAH"
- settings.P3P_COMPACT_POLICY = self.policy
-
- def test_p3p_header(self):
- expected_header = 'policyref="/w3c/p3p.xml" CP="%s"' % self.policy
- response = self.client.get('/accounts/login/')
- self.assertEqual(response["P3P"], expected_header)
-
-
class AuthTests(TestCase):
def test_min_length(self):
self.assertRaises(ValidationError, min_length(6), "abcde")
min_length(6)("abcdef")
-@override_settings(MIDDLEWARE=(
- 'security.middleware.ContentSecurityPolicyMiddleware',
-))
+@override_settings(MIDDLEWARE=("security.middleware.ContentSecurityPolicyMiddleware",))
class ContentSecurityPolicyTests(TestCase):
class FakeHttpRequest(object):
- method = 'POST'
- body = """{
+ method = "POST"
+ body = (
+ """{
"csp-report": {
"document-uri": "http://example.org/page.html",
"referrer": "http://evil.example.com/haxor.html",
@@ -819,20 +749,22 @@ class FakeHttpRequest(object):
"original-policy": "%s"
}
}
- """ % settings.CSP_STRING
+ """
+ % settings.CSP_STRING
+ )
META = {
- 'CONTENT_TYPE': 'application/json',
- 'REMOTE_ADDR': '127.0.0.1',
- 'HTTP_USER_AGENT': 'FakeHTTPRequest'
+ "CONTENT_TYPE": "application/json",
+ "REMOTE_ADDR": "127.0.0.1",
+ "HTTP_USER_AGENT": "FakeHTTPRequest",
}
def test_option_set(self):
"""
Verify the HTTP Response Header is set.
"""
- response = self.client.get('/accounts/login/')
+ response = self.client.get("/accounts/login/")
self.assertEqual(
- response['Content-Security-Policy'],
+ response["Content-Security-Policy"],
settings.CSP_STRING,
)
@@ -857,19 +789,29 @@ def test_csp_view(self):
def test_csp_gen_1(self):
csp_dict = {
- 'default-src': ['self', 'cdn.example.com'],
- 'script-src': ['self', 'js.example.com'],
- 'style-src': ['self', 'css.example.com'],
- 'img-src': ['self', 'img.example.com'],
- 'connect-src': ['self', ],
- 'font-src': ['fonts.example.com', ],
- 'object-src': ['self'],
- 'media-src': ['media.example.com', ],
- 'frame-src': ['*', ],
- 'sandbox': ['', ],
- 'reflected-xss': 'filter',
- 'referrer': 'origin',
- 'report-uri': 'http://example.com/csp-report',
+ "default-src": ["self", "cdn.example.com"],
+ "script-src": ["self", "js.example.com"],
+ "style-src": ["self", "css.example.com"],
+ "img-src": ["self", "img.example.com"],
+ "connect-src": [
+ "self",
+ ],
+ "font-src": [
+ "fonts.example.com",
+ ],
+ "object-src": ["self"],
+ "media-src": [
+ "media.example.com",
+ ],
+ "frame-src": [
+ "*",
+ ],
+ "sandbox": [
+ "",
+ ],
+ "reflected-xss": "filter",
+ "referrer": "origin",
+ "report-uri": "http://example.com/csp-report",
}
expected = (
@@ -894,36 +836,33 @@ def test_csp_gen_1(self):
# We can't assume the iteration order on the csp_dict, so we split the
# output, sort, and ensure we got all the results back, regardless of
# the order.
- expected_list = sorted(x.strip() for x in expected.split(';'))
- generated_list = sorted(x.strip() for x in generated.split(';'))
+ expected_list = sorted(x.strip() for x in expected.split(";"))
+ generated_list = sorted(x.strip() for x in generated.split(";"))
self.assertEqual(generated_list, expected_list)
def test_csp_gen_2(self):
- csp_dict = {'default-src': ('none',), 'script-src': ['none']}
+ csp_dict = {"default-src": ("none",), "script-src": ["none"]}
expected = "default-src 'none'; script-src 'none'"
csp = ContentSecurityPolicyMiddleware()
generated = csp._csp_builder(csp_dict)
- expected_list = sorted(x.strip() for x in expected.split(';'))
- generated_list = sorted(x.strip() for x in generated.split(';'))
+ expected_list = sorted(x.strip() for x in expected.split(";"))
+ generated_list = sorted(x.strip() for x in generated.split(";"))
self.assertEqual(generated_list, expected_list)
def test_csp_gen_3(self):
csp_dict = {
- 'script-src': [
- 'self',
- 'www.google-analytics.com',
- 'ajax.googleapis.com',
+ "script-src": [
+ "self",
+ "www.google-analytics.com",
+ "ajax.googleapis.com",
],
}
- expected = (
- "script-src "
- "'self' www.google-analytics.com ajax.googleapis.com"
- )
+ expected = "script-src " "'self' www.google-analytics.com ajax.googleapis.com"
csp = ContentSecurityPolicyMiddleware()
generated = csp._csp_builder(csp_dict)
@@ -932,40 +871,40 @@ def test_csp_gen_3(self):
def test_csp_gen_err(self):
# argument not passed as array, expect failure
- csp_dict = {'default-src': 'self'}
+ csp_dict = {"default-src": "self"}
csp = ContentSecurityPolicyMiddleware()
self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict)
def test_csp_gen_err2(self):
- csp_dict = {'invalid': 'self'} # invalid directive
+ csp_dict = {"invalid": "self"} # invalid directive
csp = ContentSecurityPolicyMiddleware()
self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict)
def test_csp_gen_err3(self):
- csp_dict = {'sandbox': 'none'} # not a list or tuple, expect failure
+ csp_dict = {"sandbox": "none"} # not a list or tuple, expect failure
csp = ContentSecurityPolicyMiddleware()
self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict)
def test_csp_gen_err4(self):
# Not an allowed directive, expect failure
- csp_dict = {'sandbox': ('invalid', )}
+ csp_dict = {"sandbox": ("invalid",)}
csp = ContentSecurityPolicyMiddleware()
self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict)
def test_csp_gen_err5(self):
# Not an allowed directive, expect failure
- csp_dict = {'referrer': 'invalid'}
+ csp_dict = {"referrer": "invalid"}
csp = ContentSecurityPolicyMiddleware()
self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict)
def test_csp_gen_err6(self):
# Not an allowed directive, expect failure
- csp_dict = {'reflected-xss': 'invalid'}
+ csp_dict = {"reflected-xss": "invalid"}
csp = ContentSecurityPolicyMiddleware()
self.assertRaises(MiddlewareNotUsed, csp._csp_builder, csp_dict)
@@ -973,29 +912,29 @@ def test_csp_gen_err6(self):
def test_enforced_by_default(self):
with self.settings(CSP_MODE=None):
response = self.client.get(settings.LOGIN_URL)
- self.assertIn('Content-Security-Policy', response)
- self.assertNotIn('Content-Security-Policy-Report-Only', response)
+ self.assertIn("Content-Security-Policy", response)
+ self.assertNotIn("Content-Security-Policy-Report-Only", response)
def test_enforced_when_on(self):
- with self.settings(CSP_MODE='enforce'):
+ with self.settings(CSP_MODE="enforce"):
response = self.client.get(settings.LOGIN_URL)
- self.assertIn('Content-Security-Policy', response)
- self.assertNotIn('Content-Security-Policy-Report-Only', response)
+ self.assertIn("Content-Security-Policy", response)
+ self.assertNotIn("Content-Security-Policy-Report-Only", response)
def test_report_only_set(self):
- with self.settings(CSP_MODE='report-only'):
+ with self.settings(CSP_MODE="report-only"):
response = self.client.get(settings.LOGIN_URL)
- self.assertNotIn('Content-Security-Policy', response)
- self.assertIn('Content-Security-Policy-Report-Only', response)
+ self.assertNotIn("Content-Security-Policy", response)
+ self.assertIn("Content-Security-Policy-Report-Only", response)
def test_both_enforce_and_report_only(self):
- with self.settings(CSP_MODE='enforce-and-report-only'):
+ with self.settings(CSP_MODE="enforce-and-report-only"):
response = self.client.get(settings.LOGIN_URL)
- self.assertIn('Content-Security-Policy', response)
- self.assertIn('Content-Security-Policy-Report-Only', response)
+ self.assertIn("Content-Security-Policy", response)
+ self.assertIn("Content-Security-Policy-Report-Only", response)
def test_invalid_csp_mode(self):
- with self.settings(CSP_MODE='invalid'):
+ with self.settings(CSP_MODE="invalid"):
self.assertRaises(
MiddlewareNotUsed,
ContentSecurityPolicyMiddleware,
@@ -1009,7 +948,7 @@ def test_no_csp_options_set(self):
)
def test_both_csp_options_set(self):
- with self.settings(CSP_DICT={'x': 'y'}, CSP_STRING='x y;'):
+ with self.settings(CSP_DICT={"x": "y"}, CSP_STRING="x y;"):
self.assertRaises(
MiddlewareNotUsed,
ContentSecurityPolicyMiddleware,
@@ -1017,31 +956,31 @@ def test_both_csp_options_set(self):
def test_sets_from_csp_dict(self):
with self.settings(
- CSP_DICT={'default-src': ('self',)},
+ CSP_DICT={"default-src": ("self",)},
CSP_STRING=None,
):
- response = self.client.get('/accounts/login/')
+ response = self.client.get("/accounts/login/")
self.assertEqual(
- response['Content-Security-Policy'],
+ response["Content-Security-Policy"],
"default-src 'self'",
)
-@override_settings(MIDDLEWARE=('security.middleware.DoNotTrackMiddleware',))
+@override_settings(MIDDLEWARE=("security.middleware.DoNotTrackMiddleware",))
class DoNotTrackTests(TestCase):
def setUp(self):
- self.dnt = DoNotTrackMiddleware()
self.request = HttpRequest()
self.response = HttpResponse()
+ self.dnt = DoNotTrackMiddleware(self.response)
def test_set_DNT_on(self):
- self.request.META['HTTP_DNT'] = '1'
+ self.request.META["HTTP_DNT"] = "1"
self.dnt.process_request(self.request)
self.assertTrue(self.request.dnt)
def test_set_DNT_off(self):
- self.request.META['HTTP_DNT'] = 'off'
+ self.request.META["HTTP_DNT"] = "off"
self.dnt.process_request(self.request)
self.assertFalse(self.request.dnt)
@@ -1050,19 +989,20 @@ def test_default_DNT(self):
self.assertFalse(self.request.dnt)
def test_DNT_echo_on(self):
- self.request.META['HTTP_DNT'] = '1'
+ self.request.META["HTTP_DNT"] = "1"
self.dnt.process_response(self.request, self.response)
- self.assertIn('DNT', self.response)
- self.assertEqual(self.response['DNT'], '1')
+ self.assertIn("DNT", self.response)
+ self.assertEqual(self.response["DNT"], "1")
def test_DNT_echo_off(self):
- self.request.META['HTTP_DNT'] = 'off'
+ self.request.META["HTTP_DNT"] = "off"
self.dnt.process_response(self.request, self.response)
- self.assertEqual(self.response['DNT'], 'off')
+ self.assertEqual(self.response["DNT"], "off")
def test_DNT_echo_default(self):
self.dnt.process_response(self.request, self.response)
- self.assertNotIn('DNT', self.response)
+ self.assertNotIn("DNT", self.response)
+
class ReferrerPolicyTests(TestCase):
@@ -1070,64 +1010,66 @@ def test_option_set(self):
"""
Verify the HTTP Referrer-Policy Header is set.
"""
- response = self.client.get('/accounts/login/')
- self.assertNotEqual(response['Referrer-Policy'], None)
+ response = self.client.get("/accounts/login/")
+ self.assertNotEqual(response["Referrer-Policy"], None)
def test_default_setting(self):
with self.settings(REFERRER_POLICY=None):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'same-origin')
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["Referrer-Policy"], "same-origin")
def test_no_referrer_setting(self):
- with self.settings(REFERRER_POLICY='no-referrer'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'no-referrer')
+ with self.settings(REFERRER_POLICY="no-referrer"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["Referrer-Policy"], "no-referrer")
def test_no_referrer_when_downgrade_setting(self):
- with self.settings(REFERRER_POLICY='no-referrer-when-downgrade'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'no-referrer-when-downgrade')
+ with self.settings(REFERRER_POLICY="no-referrer-when-downgrade"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["Referrer-Policy"], "no-referrer-when-downgrade")
def test_origin_setting(self):
- with self.settings(REFERRER_POLICY='origin'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'origin')
+ with self.settings(REFERRER_POLICY="origin"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["Referrer-Policy"], "origin")
def test_origin_when_cross_origin_setting(self):
- with self.settings(REFERRER_POLICY='origin-when-cross-origin'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'origin-when-cross-origin')
+ with self.settings(REFERRER_POLICY="origin-when-cross-origin"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["Referrer-Policy"], "origin-when-cross-origin")
def test_same_origin_setting(self):
- with self.settings(REFERRER_POLICY='same-origin'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'same-origin')
+ with self.settings(REFERRER_POLICY="same-origin"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["Referrer-Policy"], "same-origin")
def test_strict_origin_setting(self):
- with self.settings(REFERRER_POLICY='strict-origin'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'strict-origin')
+ with self.settings(REFERRER_POLICY="strict-origin"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["Referrer-Policy"], "strict-origin")
def test_strict_origin_when_cross_origin_setting(self):
- with self.settings(REFERRER_POLICY='strict-origin-when-cross-origin'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'strict-origin-when-cross-origin')
+ with self.settings(REFERRER_POLICY="strict-origin-when-cross-origin"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(
+ response["Referrer-Policy"], "strict-origin-when-cross-origin"
+ )
def test_unsafe_url_setting(self):
- with self.settings(REFERRER_POLICY='unsafe-url'):
- response = self.client.get('/accounts/login/')
- self.assertEqual(response['Referrer-Policy'], 'unsafe-url')
+ with self.settings(REFERRER_POLICY="unsafe-url"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual(response["Referrer-Policy"], "unsafe-url")
def test_off_setting(self):
- with self.settings(REFERRER_POLICY='off'):
- response = self.client.get('/accounts/login/')
- self.assertEqual('Referrer-Policy' in response, False)
+ with self.settings(REFERRER_POLICY="off"):
+ response = self.client.get("/accounts/login/")
+ self.assertEqual("Referrer-Policy" in response, False)
def test_improper_configuration_raises(self):
referer_policy_middleware = ReferrerPolicyMiddleware()
self.assertRaises(
ImproperlyConfigured,
referer_policy_middleware.load_setting,
- 'REFERRER_POLICY',
- 'invalid',
+ "REFERRER_POLICY",
+ "invalid",
)
diff --git a/tests/urls.py b/tests/urls.py
new file mode 100644
index 0000000..5c302a2
--- /dev/null
+++ b/tests/urls.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2011, SD Elements. See ../LICENSE.txt for details.
+
+from django.contrib.auth.views import LoginView, PasswordChangeView
+from django.http import HttpResponse
+from django.urls import path, re_path
+
+from security.auth_throttling.views import reset_username_throttle
+from security.views import csp_report
+
+urlpatterns = [
+ path("accounts/login/", LoginView.as_view(), {}, "login"),
+ path(
+ "change_password/",
+ PasswordChangeView.as_view(),
+ {"post_change_redirect": "/home/"},
+ "change_password",
+ ),
+ re_path(
+ r"^admin/reset-account-throttling/(?P-?[0-9]+)/",
+ reset_username_throttle,
+ {"redirect_url": "/admin"},
+ "reset_username_throttle",
+ ),
+ path("home/", lambda request: HttpResponse()),
+ path("custom-login/", lambda request: HttpResponse()),
+ path("test1/", lambda request: HttpResponse(), {}, "test1"),
+ path("test2/", lambda request: HttpResponse(), {}, "test2"),
+ path("test3/", lambda request: HttpResponse(), {}, "test3"),
+ path("test4/", lambda request: HttpResponse(), {}, "test4"),
+ path("csp-report/", csp_report),
+]
diff --git a/tox.ini b/tox.ini
deleted file mode 100644
index b2e8add..0000000
--- a/tox.ini
+++ /dev/null
@@ -1,43 +0,0 @@
-[tox]
-envlist = {py36}-django{111,22,30}, docs, pep8
-
-[testenv]
-whitelist_externals = make
-commands = coverage run --source {envsitepackagesdir}/security --omit="*migrations/*" testing/manage.py test tests
-commands_post = coverage report
-basepython =
- py36: python3.6
-deps =
- django-discover-runner
- django111: django==1.11
- django22: django==2.2
- django30: django==3.0
- coverage
- ua_parser==0.7.1
- mock==2.0.0
-
-[testenv:docs]
-basepython = python3.6
-deps =
- Sphinx
- django==3.0
- ua_parser==0.7.1
- coverage
-commands =
- make clean
- make html
-commands_post = - coverage report
-
-[testenv:pep8]
-basepython = python3.6
-deps=
- pep8-naming
- hacking
- flake8
- coverage
-commands=flake8 security testing
-
-[flake8]
-ignore=E131,H306,H301,H404,H405,H101,N802,N812,W503
-max-complexity=10
-exclude=*migrations*