Compare commits

..

105 commits

Author SHA1 Message Date
Enrico Tröger
fdd7b22cd3 Clarify and/or conditions in AXES_LOCKOUT_PARAMETERS examples
Some checks are pending
Test / build (Python 3.10, Django 4.2) (push) Waiting to run
Test / build (Python 3.11, Django 4.2) (push) Waiting to run
Test / build (Python 3.12, Django 4.2) (push) Waiting to run
Test / build (Python 3.14, Django 4.2) (push) Waiting to run
Test / build (Python 3.10, Django 5.2) (push) Waiting to run
Test / build (Python 3.11, Django 5.2) (push) Waiting to run
Test / build (Python 3.12, Django 5.2) (push) Waiting to run
Test / build (Python 3.13, Django 5.2) (push) Waiting to run
Test / build (Python 3.14, Django 5.2) (push) Waiting to run
Test / build (Python 3.12, Django 6.0) (push) Waiting to run
Test / build (Python 3.13, Django 6.0) (push) Waiting to run
Test / build (Python 3.14, Django 6.0) (push) Waiting to run
Test / build (Python 3.14, Django main) (push) Waiting to run
Test / build (Python 3.14, Django qa) (push) Waiting to run
2026-03-16 18:55:20 +02:00
dependabot[bot]
a5d14cd630 chore(deps): bump black from 26.1.0 to 26.3.1
Bumps [black](https://github.com/psf/black) from 26.1.0 to 26.3.1.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/26.1.0...26.3.1)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 26.3.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 18:54:27 +02:00
dependabot[bot]
2a31c0133f chore(deps): bump tox from 4.34.1 to 4.49.1
Bumps [tox](https://github.com/tox-dev/tox) from 4.34.1 to 4.49.1.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.34.1...4.49.1)

---
updated-dependencies:
- dependency-name: tox
  dependency-version: 4.49.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 18:54:17 +02:00
dependabot[bot]
4624eed684 chore(deps): bump pytest-django from 4.11.1 to 4.12.0
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.11.1 to 4.12.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.11.1...v4.12.0)

---
updated-dependencies:
- dependency-name: pytest-django
  dependency-version: 4.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-03-16 18:54:13 +02:00
Aleksi Häkli
e27ce891ea
Version 8.3.1
Some checks failed
Test / build (Python 3.10, Django 4.2) (push) Has been cancelled
Test / build (Python 3.11, Django 4.2) (push) Has been cancelled
Test / build (Python 3.12, Django 4.2) (push) Has been cancelled
Test / build (Python 3.14, Django 4.2) (push) Has been cancelled
Test / build (Python 3.10, Django 5.2) (push) Has been cancelled
Test / build (Python 3.11, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 5.2) (push) Has been cancelled
Test / build (Python 3.13, Django 5.2) (push) Has been cancelled
Test / build (Python 3.14, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 6.0) (push) Has been cancelled
Test / build (Python 3.13, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django main) (push) Has been cancelled
Test / build (Python 3.14, Django qa) (push) Has been cancelled
2026-02-11 22:15:47 +02:00
dependabot[bot]
c3dcd1ba51 chore(deps): bump coverage from 7.13.3 to 7.13.4
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.3 to 7.13.4.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](https://github.com/coveragepy/coveragepy/compare/7.13.3...7.13.4)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.13.4
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-11 22:14:42 +02:00
Aleksi Häkli
41ebdc3063 Try to run all tox QA commands even if some fail 2026-02-11 22:14:31 +02:00
Aleksi Häkli
31c69dbea5 Simplify black formatting rules 2026-02-11 22:14:31 +02:00
Aleksi Häkli
bdd0c9546a Fix prospector errors 2026-02-11 22:14:31 +02:00
Aleksi Häkli
4b77eb69ee Run black autoformatting 2026-02-11 22:14:31 +02:00
Aleksi Häkli
5acae054b4 Update Black formatting rules 2026-02-11 22:14:31 +02:00
Aleksi Häkli
d59a289407 Suppress mypy type errors
Update Mypy Python version to 3.14
2026-02-11 22:14:31 +02:00
Aleksi Häkli
23ee2fca44 Update version support matrix to run tox QA tests properly on GitHub 2026-02-11 22:14:31 +02:00
Aleksi Häkli
4ea615811b Implement custom lazy object to avoid JSON errors with Celery
Fixes jazzband/django-axes#1391
2026-02-11 22:14:31 +02:00
Aleksi Häkli
b4fb3088b4
Version 8.3.0
Some checks failed
Test / build (Python 3.10, Django 4.2) (push) Has been cancelled
Test / build (Python 3.11, Django 4.2) (push) Has been cancelled
Test / build (Python 3.12, Django 4.2) (push) Has been cancelled
Test / build (Python 3.14, Django 4.2) (push) Has been cancelled
Test / build (Python 3.10, Django 5.2) (push) Has been cancelled
Test / build (Python 3.11, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 5.2) (push) Has been cancelled
Test / build (Python 3.13, Django 5.2) (push) Has been cancelled
Test / build (Python 3.14, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 6.0) (push) Has been cancelled
Test / build (Python 3.13, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django main) (push) Has been cancelled
Test / build (Python 3.14, Django qa) (push) Has been cancelled
2026-02-09 10:37:38 +02:00
Hugo van Kemenade
6c8feada83 Replace removed pkg_resources with stdlib 2026-02-09 10:35:54 +02:00
Aleksi Häkli
b441ccd5fc
Version 8.2.0
Some checks failed
Test / build (Python 3.10, Django 4.2) (push) Has been cancelled
Test / build (Python 3.11, Django 4.2) (push) Has been cancelled
Test / build (Python 3.12, Django 4.2) (push) Has been cancelled
Test / build (Python 3.14, Django 4.2) (push) Has been cancelled
Test / build (Python 3.10, Django 5.2) (push) Has been cancelled
Test / build (Python 3.11, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 5.2) (push) Has been cancelled
Test / build (Python 3.13, Django 5.2) (push) Has been cancelled
Test / build (Python 3.14, Django 5.2) (push) Has been cancelled
Test / build (Python 3.12, Django 6.0) (push) Has been cancelled
Test / build (Python 3.13, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django 6.0) (push) Has been cancelled
Test / build (Python 3.14, Django main) (push) Has been cancelled
Test / build (Python 3.14, Django qa) (push) Has been cancelled
2026-02-06 20:50:14 +02:00
dependabot[bot]
1d9964be16 chore(deps): bump tox from 4.32.0 to 4.34.1
Bumps [tox](https://github.com/tox-dev/tox) from 4.32.0 to 4.34.1.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.32.0...4.34.1)

---
updated-dependencies:
- dependency-name: tox
  dependency-version: 4.34.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-06 20:37:05 +02:00
dependabot[bot]
60e3cceb1d chore(deps): bump black from 25.11.0 to 26.1.0
Bumps [black](https://github.com/psf/black) from 25.11.0 to 26.1.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.11.0...26.1.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 26.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-06 20:36:57 +02:00
shayan taki
8f5e9965d8 Add unit tests for security check W006 2026-02-06 20:31:41 +02:00
dependabot[bot]
cf0be90f11 chore(deps): bump sphinx-rtd-theme from 3.0.2 to 3.1.0
Bumps [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme) from 3.0.2 to 3.1.0.
- [Changelog](https://github.com/readthedocs/sphinx_rtd_theme/blob/master/docs/changelog.rst)
- [Commits](https://github.com/readthedocs/sphinx_rtd_theme/compare/3.0.2...3.1.0)

---
updated-dependencies:
- dependency-name: sphinx-rtd-theme
  dependency-version: 3.1.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-06 20:23:07 +02:00
dependabot[bot]
d033b70235 chore(deps): bump prospector from 1.17.3 to 1.18.0
Bumps [prospector](https://github.com/prospector-dev/prospector) from 1.17.3 to 1.18.0.
- [Release notes](https://github.com/prospector-dev/prospector/releases)
- [Changelog](https://github.com/prospector-dev/prospector/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/prospector-dev/prospector/compare/v1.17.3...v1.18.0)

---
updated-dependencies:
- dependency-name: prospector
  dependency-version: 1.18.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-06 20:22:52 +02:00
dependabot[bot]
b14e861631 chore(deps): bump coverage from 7.13.0 to 7.13.3
Bumps [coverage](https://github.com/coveragepy/coveragepy) from 7.13.0 to 7.13.3.
- [Release notes](https://github.com/coveragepy/coveragepy/releases)
- [Changelog](https://github.com/coveragepy/coveragepy/blob/main/CHANGES.rst)
- [Commits](https://github.com/coveragepy/coveragepy/compare/7.13.0...7.13.3)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.13.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-02-06 20:22:46 +02:00
rodrigo.nogueira
6703b66f17 Fix circular import with custom user models
Fixes #1280

- Use SimpleLazyObject to defer get_user_model() evaluation
- Prevents circular import when custom user models import from axes
- Add test coverage for lazy evaluation in test_conf.py
2026-02-06 20:19:42 +02:00
rodrigo.nogueira
95a8043341 Fix AttributeError when optional settings are undefined
Fixes #1328
- Add None as default value in axes_conf_check
- Add test coverage for missing settings scenario
2026-02-06 20:19:42 +02:00
Aleksi Häkli
f2af7c993b chore: remove deprecated requirements-*.txt files 2025-12-19 22:07:09 +02:00
Aleksi Häkli
8869a9e594 chore: update dependencies 2025-12-19 22:02:28 +02:00
Aleksi Häkli
0735c71432 chore: use single requirements file
Multiple files seem to break some IDEs and package management solutions
2025-12-19 21:54:43 +02:00
Aleksi Häkli
332a5f57d0 Version 8.1.0 2025-12-19 21:41:15 +02:00
Aleksi Häkli
a30b68aec9 chore: update mypy to 1.19.1 2025-12-19 21:28:28 +02:00
dependabot[bot]
29005e2f6f chore(deps): bump actions/cache from 4 to 5
Bumps [actions/cache](https://github.com/actions/cache) from 4 to 5.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/cache
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 21:20:39 +02:00
dependabot[bot]
dd172ec1a5 chore(deps): bump actions/checkout from 5 to 6
Bumps [actions/checkout](https://github.com/actions/checkout) from 5 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 21:20:19 +02:00
dependabot[bot]
0d5795cdf2 chore(deps): bump black from 25.9.0 to 25.11.0
Bumps [black](https://github.com/psf/black) from 25.9.0 to 25.11.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.9.0...25.11.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 21:20:12 +02:00
dependabot[bot]
2fce8fafdf chore(deps): bump pytest-subtests from 0.14.2 to 0.15.0
Bumps [pytest-subtests](https://github.com/pytest-dev/pytest-subtests) from 0.14.2 to 0.15.0.
- [Release notes](https://github.com/pytest-dev/pytest-subtests/releases)
- [Changelog](https://github.com/pytest-dev/pytest-subtests/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-subtests/compare/v0.14.2...v0.15.0)

---
updated-dependencies:
- dependency-name: pytest-subtests
  dependency-version: 0.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 21:20:06 +02:00
dependabot[bot]
53dfc9a821 chore(deps): bump github/codeql-action from 3 to 4
Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](https://github.com/github/codeql-action/compare/v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 21:19:59 +02:00
dependabot[bot]
9ba27a0755 chore(deps): bump coverage from 7.9.2 to 7.10.7
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.9.2 to 7.10.7.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.9.2...7.10.7)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.10.7
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-19 21:19:52 +02:00
Aleksi Häkli
6866a53728 chore: add Python 3.14 and Django 6.0 support 2025-12-19 21:19:15 +02:00
Mounir Messelmeni
955f39da73 Update docs 2025-12-19 21:19:03 +02:00
Mounir Messelmeni
04fd39fa57 Enhance get_lockout_response to support original_response parameter 2025-12-19 21:19:03 +02:00
Ram
69c97d5c7b docs: Add missing AXES_IPWARE_PROXY_ORDER setting documentation 2025-12-19 21:14:16 +02:00
shayan taki
3f6e773f7d Add security check (W006) for missing ip_address in lockout params 2025-12-19 21:13:43 +02:00
Ameer Taweel
88827c381e docs: Clarify ordering of AxesMiddleware
Closes #1015
2025-10-08 14:55:21 +03:00
dependabot[bot]
e8c3bf7be7 chore(deps): bump black from 25.1.0 to 25.9.0
Bumps [black](https://github.com/psf/black) from 25.1.0 to 25.9.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/25.1.0...25.9.0)

---
updated-dependencies:
- dependency-name: black
  dependency-version: 25.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 14:54:43 +03:00
dependabot[bot]
9962313199 chore(deps): bump pytest from 8.4.0 to 8.4.2
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.4.0 to 8.4.2.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.4.0...8.4.2)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 14:54:30 +03:00
dependabot[bot]
cf3d3eda2c chore(deps): bump actions/setup-python from 5 to 6
Bumps [actions/setup-python](https://github.com/actions/setup-python) from 5 to 6.
- [Release notes](https://github.com/actions/setup-python/releases)
- [Commits](https://github.com/actions/setup-python/compare/v5...v6)

---
updated-dependencies:
- dependency-name: actions/setup-python
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 14:54:23 +03:00
dependabot[bot]
2a02585d23 chore(deps): bump prospector from 1.17.2 to 1.17.3
Bumps [prospector](https://github.com/prospector-dev/prospector) from 1.17.2 to 1.17.3.
- [Release notes](https://github.com/prospector-dev/prospector/releases)
- [Changelog](https://github.com/prospector-dev/prospector/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/prospector-dev/prospector/compare/v1.17.2...v1.17.3)

---
updated-dependencies:
- dependency-name: prospector
  dependency-version: 1.17.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 14:53:33 +03:00
dependabot[bot]
13f293b650 chore(deps): bump actions/checkout from 4 to 5
Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 5.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4...v5)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 14:53:13 +03:00
dependabot[bot]
592452e446 chore(deps): bump tox from 4.27.0 to 4.30.3
Bumps [tox](https://github.com/tox-dev/tox) from 4.27.0 to 4.30.3.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.27.0...4.30.3)

---
updated-dependencies:
- dependency-name: tox
  dependency-version: 4.30.3
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-08 14:52:59 +03:00
kuldeepkhatke
2a8c42c3eb Added cleanup_expired_attempts action 2025-07-08 21:09:10 +03:00
kuldeepkhatke
29fd4bd4fe Added IsLockedOutFilter to AccessAttemptAdmin 2025-07-06 23:06:41 +03:00
kuldeepkhatke
af65488dc6 Added status column to list display 2025-07-06 23:06:41 +03:00
kuldeepkhatke
e4e0299252 AccessAttemptAdmin.list_display datatype change tuple->list for customization 2025-07-06 23:06:41 +03:00
dependabot[bot]
75c29bd6f8 chore(deps): bump coverage from 7.8.2 to 7.9.2
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.8.2 to 7.9.2.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.8.2...7.9.2)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.9.2
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 16:24:01 +03:00
dependabot[bot]
95f321e7c7 chore(deps): bump tox from 4.26.0 to 4.27.0
Bumps [tox](https://github.com/tox-dev/tox) from 4.26.0 to 4.27.0.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.26.0...4.27.0)

---
updated-dependencies:
- dependency-name: tox
  dependency-version: 4.27.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 16:23:53 +03:00
dependabot[bot]
bebbbe924e chore(deps): bump pytest-subtests from 0.14.1 to 0.14.2
Bumps [pytest-subtests](https://github.com/pytest-dev/pytest-subtests) from 0.14.1 to 0.14.2.
- [Release notes](https://github.com/pytest-dev/pytest-subtests/releases)
- [Changelog](https://github.com/pytest-dev/pytest-subtests/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-subtests/compare/v0.14.1...v0.14.2)

---
updated-dependencies:
- dependency-name: pytest-subtests
  dependency-version: 0.14.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 16:23:44 +03:00
dependabot[bot]
34a350568e chore(deps): bump pytest-cov from 6.1.1 to 6.2.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.1.1 to 6.2.1.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.1.1...v6.2.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.2.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 16:23:36 +03:00
dependabot[bot]
8340a7a82f chore(deps): bump prospector from 1.17.1 to 1.17.2
Bumps [prospector](https://github.com/prospector-dev/prospector) from 1.17.1 to 1.17.2.
- [Release notes](https://github.com/prospector-dev/prospector/releases)
- [Changelog](https://github.com/prospector-dev/prospector/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/prospector-dev/prospector/compare/v1.17.1...v1.17.2)

---
updated-dependencies:
- dependency-name: prospector
  dependency-version: 1.17.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-05 16:23:20 +03:00
kuldeepkhatke
392dfa0e44 Reverted , remove change from AxesDatabaseHandler.user_login_failed 2025-07-05 16:19:19 +03:00
kuldeepkhatke
baace5c27b Added separate UT for complete, partial & no delete senarios 2025-07-05 16:19:19 +03:00
kuldeepkhatke
ba7b72f9d9 Updated expires_at for null, blank False, lte query update, admin expiration logic simplify 2025-07-05 16:19:19 +03:00
kuldeepkhatke
01ccf5b213 Updated get_individual_attempt_expiry() func placement & renamed to get_attempt_expiration() 2025-07-05 16:19:19 +03:00
kuldeepkhatke
d8e6c939fe Modified expiration create queryset logic 2025-07-05 16:19:19 +03:00
kuldeepkhatke
94a66c7346 Added actual queryset call UT 2025-07-05 16:19:19 +03:00
kuldeepkhatke
f5951e966c Modified access_attempt as pk 2025-07-05 16:19:19 +03:00
kuldeepkhatke
f583e93718 Modified verbiage changes & removed comment 2025-07-05 16:19:19 +03:00
kuldeepkhatke
74c24c0e78 Added unittest for AXES_USE_ATTEMPT_EXPIRATION flag 2025-07-05 16:19:19 +03:00
kuldeepkhatke
df8fb35e18 Shifted epired_at filed to new model 2025-07-05 16:19:19 +03:00
kuldeepkhatke
a1e9eff875 Renamed AXES_INDIVIDUAL_ATTEMPT_EXPIRY flag to AXES_USE_ATTEMPT_EXPIRATION 2025-07-05 16:19:19 +03:00
kuldeepkhatke
0fd9ccd1d4 Added individual attempt expiry feature 2025-07-05 16:19:19 +03:00
dependabot[bot]
864dfc2d9a
Merge pull request #1315 from jazzband/dependabot/pip/pytest-8.4.0 2025-06-06 17:19:25 +00:00
dependabot[bot]
d1fad02076
Merge pull request #1314 from jazzband/dependabot/pip/mypy-1.16.0 2025-06-06 17:18:44 +00:00
dependabot[bot]
d79c7de4e5
chore(deps): bump pytest from 8.3.5 to 8.4.0
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.5 to 8.4.0.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.5...8.4.0)

---
updated-dependencies:
- dependency-name: pytest
  dependency-version: 8.4.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 17:14:34 +00:00
dependabot[bot]
c9f092a3be
chore(deps): bump mypy from 1.15.0 to 1.16.0
Bumps [mypy](https://github.com/python/mypy) from 1.15.0 to 1.16.0.
- [Changelog](https://github.com/python/mypy/blob/master/CHANGELOG.md)
- [Commits](https://github.com/python/mypy/compare/v1.15.0...v1.16.0)

---
updated-dependencies:
- dependency-name: mypy
  dependency-version: 1.16.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 17:14:32 +00:00
dependabot[bot]
9becd0061e chore(deps): bump coverage from 7.8.0 to 7.8.2
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.8.0 to 7.8.2.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.8.0...7.8.2)

---
updated-dependencies:
- dependency-name: coverage
  dependency-version: 7.8.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 20:13:06 +03:00
dependabot[bot]
7e495fb5fd chore(deps): bump prospector from 1.16.1 to 1.17.1
Bumps [prospector](https://github.com/prospector-dev/prospector) from 1.16.1 to 1.17.1.
- [Release notes](https://github.com/prospector-dev/prospector/releases)
- [Changelog](https://github.com/prospector-dev/prospector/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/prospector-dev/prospector/compare/v1.16.1...v1.17.1)

---
updated-dependencies:
- dependency-name: prospector
  dependency-version: 1.17.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 20:12:50 +03:00
dependabot[bot]
6cb8dc7a46 chore(deps): bump tox from 4.25.0 to 4.26.0
Bumps [tox](https://github.com/tox-dev/tox) from 4.25.0 to 4.26.0.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.25.0...4.26.0)

---
updated-dependencies:
- dependency-name: tox
  dependency-version: 4.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-06-06 20:12:41 +03:00
AmirAli Bahramjerdi
a340dec892
Merge pull request #1308 from AmirAli-BahramJerdi/add-farsi-locale
Add Persian (fa) translation for django-axes
2025-05-16 13:28:54 -07:00
AmirAli-BahramJerdi
eea9939a45 Add Persian translation for django-axes 2025-05-16 23:49:58 +03:30
Aleksi Häkli
31038278bd
Add version 8 migration notes. 2025-05-10 14:01:18 +03:00
Aleksi Häkli
a58344c3ef
Remove Django 5.0 from supported versions 2025-05-10 13:56:07 +03:00
Aleksi Häkli
9bc11398f4
Remove Django 5.0 from supported versions 2025-05-10 13:55:20 +03:00
Aleksi Häkli
dfa39d07c0
Remove Django 5.0 from supported versions 2025-05-10 13:54:14 +03:00
Aleksi Häkli
6d2c7b1431
Version 8.0.0 2025-05-10 13:53:23 +03:00
nefrob
bd3b56237d refactor: move db accessing attempt fns to handler methods 2025-05-10 13:46:09 +03:00
nefrob
8356498a44 chore: clean attempt logic as db handler method 2025-04-29 10:10:25 +03:00
Aleksi Häkli
933756090a
Version 7.1.0 2025-04-23 18:52:54 +03:00
dependabot[bot]
3ff5ada46d chore(deps): bump pytest from 8.3.4 to 8.3.5
Bumps [pytest](https://github.com/pytest-dev/pytest) from 8.3.4 to 8.3.5.
- [Release notes](https://github.com/pytest-dev/pytest/releases)
- [Changelog](https://github.com/pytest-dev/pytest/blob/main/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest/compare/8.3.4...8.3.5)

---
updated-dependencies:
- dependency-name: pytest
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 17:03:06 +03:00
dependabot[bot]
4115d59d14 chore(deps): bump pytest-django from 4.10.0 to 4.11.1
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.10.0 to 4.11.1.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.10.0...v4.11.1)

---
updated-dependencies:
- dependency-name: pytest-django
  dependency-version: 4.11.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 17:02:55 +03:00
Aleksi Häkli
5e7fbca52c fix: resolve credentials for clean_expired_user_attempts 2025-04-23 17:02:37 +03:00
dependabot[bot]
599fbc0da0 chore(deps): bump tox from 4.24.1 to 4.25.0
Bumps [tox](https://github.com/tox-dev/tox) from 4.24.1 to 4.25.0.
- [Release notes](https://github.com/tox-dev/tox/releases)
- [Changelog](https://github.com/tox-dev/tox/blob/main/docs/changelog.rst)
- [Commits](https://github.com/tox-dev/tox/compare/4.24.1...4.25.0)

---
updated-dependencies:
- dependency-name: tox
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 11:59:22 +03:00
dependabot[bot]
b4792ff868 chore(deps): bump prospector from 1.14.1 to 1.16.1
Bumps [prospector](https://github.com/PyCQA/prospector) from 1.14.1 to 1.16.1.
- [Release notes](https://github.com/PyCQA/prospector/releases)
- [Changelog](https://github.com/prospector-dev/prospector/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/PyCQA/prospector/compare/v1.14.1...v1.16.1)

---
updated-dependencies:
- dependency-name: prospector
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 11:59:15 +03:00
dependabot[bot]
82a6ac63bb chore(deps): bump coverage from 7.6.12 to 7.8.0
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.6.12 to 7.8.0.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.6.12...7.8.0)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 11:59:08 +03:00
dependabot[bot]
93d8285006 chore(deps): bump pytest-cov from 6.0.0 to 6.1.1
Bumps [pytest-cov](https://github.com/pytest-dev/pytest-cov) from 6.0.0 to 6.1.1.
- [Changelog](https://github.com/pytest-dev/pytest-cov/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/pytest-dev/pytest-cov/compare/v6.0.0...v6.1.1)

---
updated-dependencies:
- dependency-name: pytest-cov
  dependency-version: 6.1.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-04-23 11:58:59 +03:00
parul-aro
0115648a1d feat(cleanup): allow credentials in cleanup method 2025-04-23 11:54:17 +03:00
Mathieu Kniewallner
479a355d22 chore: explicitly require Python >= 3.9 2025-04-23 11:42:02 +03:00
Mathieu Kniewallner
fdf22fffba chore: explicitly require Django >= 4.2 2025-04-23 11:42:02 +03:00
Mathieu Kniewallner
682e4261c9 ci: exclude Python 3.13 for Django 4.2 2025-04-23 11:42:02 +03:00
Mathieu Kniewallner
133f19b2f5 chore: correctly test against Django 4.2 2025-04-23 11:42:02 +03:00
Mathieu Kniewallner
3e3da350ea chore: support Django 5.2 2025-04-23 11:42:02 +03:00
dependabot[bot]
ff9c3296ef chore(deps): bump prospector from 1.14.0 to 1.14.1
Bumps [prospector](https://github.com/PyCQA/prospector) from 1.14.0 to 1.14.1.
- [Release notes](https://github.com/PyCQA/prospector/releases)
- [Changelog](https://github.com/prospector-dev/prospector/blob/master/CHANGELOG.rst)
- [Commits](https://github.com/PyCQA/prospector/compare/1.14.0...v1.14.1)

---
updated-dependencies:
- dependency-name: prospector
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 13:17:31 +02:00
dependabot[bot]
6aedd78c0b chore(deps): bump pytest-django from 4.9.0 to 4.10.0
Bumps [pytest-django](https://github.com/pytest-dev/pytest-django) from 4.9.0 to 4.10.0.
- [Release notes](https://github.com/pytest-dev/pytest-django/releases)
- [Changelog](https://github.com/pytest-dev/pytest-django/blob/main/docs/changelog.rst)
- [Commits](https://github.com/pytest-dev/pytest-django/compare/v4.9.0...v4.10.0)

---
updated-dependencies:
- dependency-name: pytest-django
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 13:17:23 +02:00
dependabot[bot]
be18f038f9 chore(deps): bump black from 24.10.0 to 25.1.0
Bumps [black](https://github.com/psf/black) from 24.10.0 to 25.1.0.
- [Release notes](https://github.com/psf/black/releases)
- [Changelog](https://github.com/psf/black/blob/main/CHANGES.md)
- [Commits](https://github.com/psf/black/compare/24.10.0...25.1.0)

---
updated-dependencies:
- dependency-name: black
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 13:17:15 +02:00
dependabot[bot]
04638811d7 chore(deps): bump coverage from 7.6.10 to 7.6.12
Bumps [coverage](https://github.com/nedbat/coveragepy) from 7.6.10 to 7.6.12.
- [Release notes](https://github.com/nedbat/coveragepy/releases)
- [Changelog](https://github.com/nedbat/coveragepy/blob/master/CHANGES.rst)
- [Commits](https://github.com/nedbat/coveragepy/compare/7.6.10...7.6.12)

---
updated-dependencies:
- dependency-name: coverage
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-21 13:17:08 +02:00
Django Goat
a0df8ae7c4 docs - fixed wy-table-responsive html not not displaying 2025-02-21 13:16:56 +02:00
Django Goat
784f1930af docs - added css to increase content width 2025-02-21 13:16:56 +02:00
31 changed files with 871 additions and 184 deletions

View file

@ -14,11 +14,11 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v4
uses: actions/checkout@v6
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
uses: github/codeql-action/init@v4
# Override language selection by uncommenting this and choosing your languages
# with:
# languages: go, javascript, csharp, python, cpp, java
@ -26,7 +26,7 @@ jobs:
# Autobuild attempts to build any compiled languages (C/C++, C#, or Java).
# If this step fails, then you should remove it and run the build manually (see below).
- name: Autobuild
uses: github/codeql-action/autobuild@v3
uses: github/codeql-action/autobuild@v4
# Command-line programs to run using the OS shell.
# 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun
@ -40,4 +40,4 @@ jobs:
# make release
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v3
uses: github/codeql-action/analyze@v4

View file

@ -14,14 +14,14 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
with:
fetch-depth: 0
- name: Set up Python
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: 3.9
python-version: 3.12
- name: Install dependencies
run: |

View file

@ -12,29 +12,31 @@ jobs:
strategy:
fail-fast: false
matrix:
python-version: ['3.9', '3.10', '3.11', '3.12', '3.13']
django-version: ['4.2', '5.0', '5.1']
python-version: ['3.10', '3.11', '3.12', '3.13', '3.14']
django-version: ['4.2', '5.2', '6.0']
include:
# Tox configuration for QA environment
- python-version: '3.13'
- python-version: '3.14'
django-version: 'qa'
# Django main
- python-version: '3.13'
- python-version: '3.14'
django-version: 'main'
experimental: true
exclude:
- python-version: '3.9'
django-version: '5.0'
- python-version: '3.13'
django-version: '5.0'
django-version: '4.2'
- python-version: '3.9'
django-version: '5.1'
django-version: '5.2'
- python-version: '3.10'
django-version: '6.0'
- python-version: '3.11'
django-version: '6.0'
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v6
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
@ -44,7 +46,7 @@ jobs:
echo "::set-output name=dir::$(pip cache dir)"
- name: Cache
uses: actions/cache@v4
uses: actions/cache@v5
with:
path: ${{ steps.pip-cache.outputs.dir }}
key:

View file

@ -2,6 +2,63 @@
Changes
=======
8.3.1 (2026-02-11)
------------------
- Fix configuration JSON serialization errors for Celery.
[aleksihakli]
8.3.0 (2026-02-09)
------------------
- Remove deprecated pkg_resources in favour of new importlib.
[hugovk]
8.2.0 (2026-02-06)
------------------
- Fix AttributeError when optional settings are undefined.
[rodrigo.nogueira]
- Fix circular import with custom user models.
[rodrigo.nogueira]
- Add unit tests for security check W006.
[shayanTaki]
8.1.0 (2025-12-19)
------------------
- Add Persion (fa) translations for django-axes.
[AmirAli-BahramJerdi]
- Add individual attempt expiry support.
[kuldeepkhatke]
- Add checks for missing ip_address in lockout params.
[shayanTaki]
- Add missing ``settings.AXES_IPWARE_PROXY_ORDER`` documentation.
[ram98kgp]
- Enhance ``get_lockout_response`` to receive original response as parameter.
[mounirmesselmeni]
- Update documentation.
- Add Python 3.14 support.
- Add Django 6.0 support.
- Remove Python 3.9 support (EOL).
- Remove Django 5.1 support (EOL).
8.0.0 (2025-05-10)
------------------
- Move all database related logic to the default ``axes.handlers.database.AxesDatabaseHandler``.
[nefrob]
7.1.0 (2025-04-23)
------------------
- Provide credentials to expired credentials cleanup method.
[parul-aro]
- Update support matrix for Django 5.2.
[mkniewallner]
- Fix documentation.
[chango-goat]
7.0.2 (2025-02-19)
------------------

View file

@ -4,26 +4,59 @@ from django.utils.translation import gettext_lazy as _
from axes.conf import settings
from axes.models import AccessAttempt, AccessLog, AccessFailureLog
from axes.handlers.database import AxesDatabaseHandler
class IsLockedOutFilter(admin.SimpleListFilter):
title = _("Locked Out")
parameter_name = "locked_out"
def lookups(self, request, model_admin):
return (
("yes", _("Yes")),
("no", _("No")),
)
def queryset(self, request, queryset):
if self.value() == "yes":
return queryset.filter(
failures_since_start__gte=settings.AXES_FAILURE_LIMIT
)
if self.value() == "no":
return queryset.filter(failures_since_start__lt=settings.AXES_FAILURE_LIMIT)
return queryset
class AccessAttemptAdmin(admin.ModelAdmin):
list_display = (
list_display = [
"attempt_time",
"ip_address",
"user_agent",
"username",
"path_info",
"failures_since_start",
)
]
if settings.AXES_USE_ATTEMPT_EXPIRATION:
list_display.append("expiration")
list_filter = ["attempt_time", "path_info"]
if isinstance(settings.AXES_FAILURE_LIMIT, int) and settings.AXES_FAILURE_LIMIT > 0:
# This will only add the status field if AXES_FAILURE_LIMIT is set to a positive integer
# Because callable failure limit requires scope of request object
list_display.append("status")
list_filter.append(IsLockedOutFilter) # type: ignore[arg-type]
search_fields = ["ip_address", "username", "user_agent", "path_info"]
date_hierarchy = "attempt_time"
fieldsets = (
(None, {"fields": ("username", "path_info", "failures_since_start")}),
(
None,
{"fields": ("username", "path_info", "failures_since_start", "expiration")},
),
(_("Form Data"), {"fields": ("get_data", "post_data")}),
(_("Meta Data"), {"fields": ("user_agent", "ip_address", "http_accept")}),
)
@ -38,11 +71,34 @@ class AccessAttemptAdmin(admin.ModelAdmin):
"get_data",
"post_data",
"failures_since_start",
"expiration",
]
actions = ["cleanup_expired_attempts"]
@admin.action(description=_("Clean up expired attempts"))
def cleanup_expired_attempts(self, request, queryset): # noqa
count = self.handler.clean_expired_user_attempts(request=request)
self.message_user(request, _(f"Cleaned up {count} expired access attempts."))
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.handler = AxesDatabaseHandler()
def has_add_permission(self, request: HttpRequest) -> bool:
return False
def expiration(self, obj: AccessAttempt):
return obj.expiration.expires_at if hasattr(obj, "expiration") else _("Not set")
def status(self, obj: AccessAttempt):
return (
f"{settings.AXES_FAILURE_LIMIT - obj.failures_since_start} "
+ _("Attempt Remaining")
if obj.failures_since_start < settings.AXES_FAILURE_LIMIT
else _("Locked Out")
)
class AccessLogAdmin(admin.ModelAdmin):
list_display = (

View file

@ -1,13 +1,10 @@
from logging import getLogger
from typing import List, Optional
from typing import Optional
from django.db.models import QuerySet
from django.http import HttpRequest
from django.utils.timezone import datetime, now
from axes.conf import settings
from axes.helpers import get_client_username, get_client_parameters, get_cool_off
from axes.models import AccessAttempt
from axes.helpers import get_cool_off
log = getLogger(__name__)
@ -23,85 +20,7 @@ def get_cool_off_threshold(request: Optional[HttpRequest] = None) -> datetime:
"Cool off threshold can not be calculated with settings.AXES_COOLOFF_TIME set to None"
)
attempt_time = request.axes_attempt_time
attempt_time = request.axes_attempt_time # type: ignore[union-attr]
if attempt_time is None:
return now() - cool_off
return attempt_time - cool_off
def filter_user_attempts(
request: HttpRequest, credentials: Optional[dict] = None
) -> List[QuerySet]:
"""
Return a list querysets of AccessAttempts that match the given request and credentials.
"""
username = get_client_username(request, credentials)
filter_kwargs_list = get_client_parameters(
username, request.axes_ip_address, request.axes_user_agent, request, credentials
)
attempts_list = [
AccessAttempt.objects.filter(**filter_kwargs)
for filter_kwargs in filter_kwargs_list
]
return attempts_list
def get_user_attempts(
request: HttpRequest, credentials: Optional[dict] = None
) -> List[QuerySet]:
"""
Get list of querysets with valid user attempts that match the given request and credentials.
"""
attempts_list = filter_user_attempts(request, credentials)
if settings.AXES_COOLOFF_TIME is None:
log.debug(
"AXES: Getting all access attempts from database because no AXES_COOLOFF_TIME is configured"
)
return attempts_list
threshold = get_cool_off_threshold(request)
log.debug("AXES: Getting access attempts that are newer than %s", threshold)
return [attempts.filter(attempt_time__gte=threshold) for attempts in attempts_list]
def clean_expired_user_attempts(request: Optional[HttpRequest] = None) -> int:
"""
Clean expired user attempts from the database.
"""
if settings.AXES_COOLOFF_TIME is None:
log.debug(
"AXES: Skipping clean for expired access attempts because no AXES_COOLOFF_TIME is configured"
)
return 0
threshold = get_cool_off_threshold(request)
count, _ = AccessAttempt.objects.filter(attempt_time__lt=threshold).delete()
log.info(
"AXES: Cleaned up %s expired access attempts from database that were older than %s",
count,
threshold,
)
return count
def reset_user_attempts(
request: HttpRequest, credentials: Optional[dict] = None
) -> int:
"""
Reset all user attempts that match the given request and credentials.
"""
attempts_list = filter_user_attempts(request, credentials)
count = 0
for attempts in attempts_list:
_count, _ = attempts.delete()
count += _count
log.info("AXES: Reset %s access attempts from database.", count)
return count

View file

@ -22,6 +22,10 @@ class Messages:
BACKEND_INVALID = "You do not have 'axes.backends.AxesStandaloneBackend' or a subclass in your settings.AUTHENTICATION_BACKENDS."
SETTING_DEPRECATED = "You have a deprecated setting {deprecated_setting} configured in your project settings"
CALLABLE_INVALID = "{callable_setting} is not a valid callable."
LOCKOUT_PARAMETERS_INVALID = (
"AXES_LOCKOUT_PARAMETERS does not contain 'ip_address'."
" This configuration allows attackers to bypass rate limits by rotating User-Agents or Cookies."
)
class Hints:
@ -30,6 +34,7 @@ class Hints:
BACKEND_INVALID = "AxesModelBackend was renamed to AxesStandaloneBackend in django-axes version 5.0."
SETTING_DEPRECATED = None
CALLABLE_INVALID = None
LOCKOUT_PARAMETERS_INVALID = "Add 'ip_address' to AXES_LOCKOUT_PARAMETERS."
class Codes:
@ -38,6 +43,7 @@ class Codes:
BACKEND_INVALID = "axes.W003"
SETTING_DEPRECATED = "axes.W004"
CALLABLE_INVALID = "axes.W005"
LOCKOUT_PARAMETERS_INVALID = "axes.W006"
@register(Tags.security, Tags.caches, Tags.compatibility)
@ -158,6 +164,34 @@ def axes_deprecation_check(app_configs, **kwargs): # pylint: disable=unused-arg
return warnings
@register(Tags.security)
def axes_lockout_params_check(app_configs, **kwargs): # pylint: disable=unused-argument
warnings = []
lockout_params = getattr(settings, "AXES_LOCKOUT_PARAMETERS", None)
if isinstance(lockout_params, (list, tuple)):
has_ip = False
for param in lockout_params:
if param == "ip_address":
has_ip = True
break
if isinstance(param, (list, tuple)) and "ip_address" in param:
has_ip = True
break
if not has_ip:
warnings.append(
Warning(
msg=Messages.LOCKOUT_PARAMETERS_INVALID,
hint=Hints.LOCKOUT_PARAMETERS_INVALID,
id=Codes.LOCKOUT_PARAMETERS_INVALID,
)
)
return warnings
@register
def axes_conf_check(app_configs, **kwargs): # pylint: disable=unused-argument
warnings = []
@ -173,7 +207,7 @@ def axes_conf_check(app_configs, **kwargs): # pylint: disable=unused-argument
]
for callable_setting in callable_settings:
value = getattr(settings, callable_setting)
value = getattr(settings, callable_setting, None)
if not is_valid_callable(value):
warnings.append(
Warning(

View file

@ -1,7 +1,21 @@
from django.conf import settings
from django.contrib.auth import get_user_model
from django.utils.functional import SimpleLazyObject
from django.utils.translation import gettext_lazy as _
class JSONSerializableLazyObject(SimpleLazyObject):
"""
Celery/Kombu config inspection may JSON-encode Django settings.
Provide a JSON-friendly representation for lazy values.
Fixes jazzband/django-axes#1391
"""
def __json__(self):
return str(self)
# disable plugin when set to False
settings.AXES_ENABLED = getattr(settings, "AXES_ENABLED", True)
@ -42,9 +56,16 @@ settings.AXES_ONLY_ADMIN_SITE = getattr(settings, "AXES_ONLY_ADMIN_SITE", False)
# show Axes logs in admin
settings.AXES_ENABLE_ADMIN = getattr(settings, "AXES_ENABLE_ADMIN", True)
# use a specific username field to retrieve from login POST data
def _get_username_field_default():
return get_user_model().USERNAME_FIELD
settings.AXES_USERNAME_FORM_FIELD = getattr(
settings, "AXES_USERNAME_FORM_FIELD", get_user_model().USERNAME_FIELD
settings,
"AXES_USERNAME_FORM_FIELD",
JSONSerializableLazyObject(_get_username_field_default),
)
# use a specific password field to retrieve from login POST data
@ -87,6 +108,10 @@ settings.AXES_LOCKOUT_URL = getattr(settings, "AXES_LOCKOUT_URL", None)
settings.AXES_COOLOFF_TIME = getattr(settings, "AXES_COOLOFF_TIME", None)
settings.AXES_USE_ATTEMPT_EXPIRATION = getattr(
settings, "AXES_USE_ATTEMPT_EXPIRATION", False
)
settings.AXES_VERBOSE = getattr(settings, "AXES_VERBOSE", settings.AXES_ENABLED)
# whitelist and blacklist

View file

@ -1,19 +1,17 @@
from logging import getLogger
from typing import Optional
from typing import List, Optional
from django.db import router, transaction
from django.db.models import F, Q, Sum, Value
from django.db.models import F, Q, QuerySet, Sum, Value
from django.db.models.functions import Concat
from django.http import HttpRequest
from django.utils import timezone
from axes.attempts import (
clean_expired_user_attempts,
get_user_attempts,
reset_user_attempts,
)
from axes.attempts import get_cool_off_threshold
from axes.conf import settings
from axes.handlers.base import AxesBaseHandler, AbstractAxesHandler
from axes.handlers.base import AbstractAxesHandler, AxesBaseHandler
from axes.helpers import (
get_client_parameters,
get_client_session_hash,
get_client_str,
get_client_username,
@ -21,8 +19,14 @@ from axes.helpers import (
get_failure_limit,
get_lockout_parameters,
get_query_str,
get_attempt_expiration,
)
from axes.models import (
AccessAttempt,
AccessAttemptExpiration,
AccessFailureLog,
AccessLog,
)
from axes.models import AccessLog, AccessAttempt, AccessFailureLog
from axes.signals import user_locked_out
log = getLogger(__name__)
@ -104,7 +108,7 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
return count
def get_failures(self, request, credentials: Optional[dict] = None) -> int:
attempts_list = get_user_attempts(request, credentials)
attempts_list = self.get_user_attempts(request, credentials)
attempt_count = max(
(
attempts.aggregate(Sum("failures_since_start"))[
@ -117,10 +121,10 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
return attempt_count
def user_login_failed(self, sender, credentials: dict, request=None, **kwargs):
"""When user login fails, save AccessFailureLog record in database,
"""
When user login fails, save AccessFailureLog record in database,
save AccessAttempt record in database, mark request with
lockout attribute and emit lockout signal.
"""
log.info("AXES: User login failed, running database handler for failure.")
@ -132,7 +136,7 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
return
# 1. database query: Clean up expired user attempts from the database before logging new attempts
clean_expired_user_attempts(request)
self.clean_expired_user_attempts(request, credentials)
username = get_client_username(request, credentials)
client_str = get_client_str(
@ -221,6 +225,23 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
client_str,
)
if settings.AXES_USE_ATTEMPT_EXPIRATION:
if not hasattr(attempt, "expiration") or attempt.expiration is None:
log.debug(
"AXES: Creating new AccessAttemptExpiration for %s",
client_str,
)
attempt.expiration = AccessAttemptExpiration.objects.create(
access_attempt=attempt,
expires_at=get_attempt_expiration(request),
)
else:
attempt.expiration.expires_at = max(
get_attempt_expiration(request),
attempt.expiration.expires_at,
)
attempt.expiration.save()
# 3. or 4. database query: Calculate the current maximum failure number from the existing attempts
failures_since_start = self.get_failures(request, credentials)
request.axes_failures_since_start = failures_since_start
@ -261,9 +282,6 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
When user logs in, update the AccessLog related to the user.
"""
# 1. database query: Clean up expired user attempts from the database
clean_expired_user_attempts(request)
username = user.get_username()
credentials = get_credentials(username)
client_str = get_client_str(
@ -276,6 +294,9 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
log.info("AXES: Successful login by %s.", client_str)
# 1. database query: Clean up expired user attempts from the database
self.clean_expired_user_attempts(request, credentials)
if not settings.AXES_DISABLE_ACCESS_LOG:
# 2. database query: Insert new access logs with login time
AccessLog.objects.create(
@ -292,7 +313,7 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
if settings.AXES_RESET_ON_SUCCESS:
# 3. database query: Reset failed attempts for the logging in user
count = reset_user_attempts(request, credentials)
count = self.reset_user_attempts(request, credentials)
log.info(
"AXES: Deleted %d failed login attempts by %s from database.",
count,
@ -304,10 +325,8 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
When user logs out, update the AccessLog related to the user.
"""
# 1. database query: Clean up expired user attempts from the database
clean_expired_user_attempts(request)
username = user.get_username() if user else None
credentials = get_credentials(username) if username else None
client_str = get_client_str(
username,
request.axes_ip_address,
@ -316,6 +335,9 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
request,
)
# 1. database query: Clean up expired user attempts from the database
self.clean_expired_user_attempts(request, credentials)
log.info("AXES: Successful logout by %s.", client_str)
if username and not settings.AXES_DISABLE_ACCESS_LOG:
@ -327,6 +349,103 @@ class AxesDatabaseHandler(AbstractAxesHandler, AxesBaseHandler):
session_hash=get_client_session_hash(request),
).update(logout_time=request.axes_attempt_time)
def filter_user_attempts(
self, request: HttpRequest, credentials: Optional[dict] = None
) -> List[QuerySet]:
"""
Return a list querysets of AccessAttempts that match the given request and credentials.
"""
username = get_client_username(request, credentials)
filter_kwargs_list = get_client_parameters(
username,
request.axes_ip_address,
request.axes_user_agent,
request,
credentials,
)
attempts_list = [
AccessAttempt.objects.filter(**filter_kwargs)
for filter_kwargs in filter_kwargs_list
]
return attempts_list
def get_user_attempts(
self, request: HttpRequest, credentials: Optional[dict] = None # noqa
) -> List[QuerySet]:
"""
Get list of querysets with valid user attempts that match the given request and credentials.
"""
attempts_list = self.filter_user_attempts(request, credentials)
if settings.AXES_COOLOFF_TIME is None:
log.debug(
"AXES: Getting all access attempts from database because no AXES_COOLOFF_TIME is configured"
)
return attempts_list
threshold = get_cool_off_threshold(request)
log.debug("AXES: Getting access attempts that are newer than %s", threshold)
return [
attempts.filter(attempt_time__gte=threshold) for attempts in attempts_list
]
def clean_expired_user_attempts(
self,
request: Optional[HttpRequest] = None,
credentials: Optional[dict] = None, # noqa
) -> int:
"""
Clean expired user attempts from the database.
"""
if settings.AXES_COOLOFF_TIME is None:
log.debug(
"AXES: Skipping clean for expired access attempts because no AXES_COOLOFF_TIME is configured"
)
return 0
if settings.AXES_USE_ATTEMPT_EXPIRATION:
threshold = timezone.now()
count, _ = AccessAttempt.objects.filter(
expiration__expires_at__lte=threshold
).delete()
log.info(
"AXES: Cleaned up %s expired access attempts from database that expiry were older than %s",
count,
threshold,
)
else:
threshold = get_cool_off_threshold(request)
count, _ = AccessAttempt.objects.filter(
attempt_time__lte=threshold
).delete()
log.info(
"AXES: Cleaned up %s expired access attempts from database that were older than %s",
count,
threshold,
)
return count
def reset_user_attempts(
self, request: HttpRequest, credentials: Optional[dict] = None
) -> int:
"""
Reset all user attempts that match the given request and credentials.
"""
attempts_list = self.filter_user_attempts(request, credentials)
count = 0
for attempts in attempts_list:
_count, _ = attempts.delete()
count += _count
log.info("AXES: Reset %s access attempts from database.", count)
return count
def post_save_access_attempt(self, instance, **kwargs):
"""
Handles the ``axes.models.AccessAttempt`` object post save signal.

View file

@ -1,4 +1,4 @@
from datetime import timedelta
from datetime import timedelta, datetime
from hashlib import sha256
from logging import getLogger
from string import Template
@ -101,6 +101,23 @@ def get_cool_off_iso8601(delta: timedelta) -> str:
return f"P{days_str}"
def get_attempt_expiration(request: Optional[HttpRequest] = None) -> datetime:
"""
Get threshold for fetching access attempts from the database.
"""
cool_off = get_cool_off(request)
if cool_off is None:
raise TypeError(
"Cool off threshold can not be calculated with settings.AXES_COOLOFF_TIME set to None"
)
attempt_time = request.axes_attempt_time # type: ignore[union-attr]
if attempt_time is None:
return datetime.now() + cool_off
return attempt_time + cool_off
def get_credentials(username: Optional[str] = None, **kwargs) -> dict:
"""
Calculate credentials for Axes to use internally from given username and kwargs.
@ -147,7 +164,7 @@ def get_client_username(
log.debug(
"Using parameter credentials to get username with key settings.AXES_USERNAME_FORM_FIELD"
)
return credentials.get(settings.AXES_USERNAME_FORM_FIELD, None)
return credentials.get(settings.AXES_USERNAME_FORM_FIELD, None) # type: ignore[return-value]
log.debug(
"Using parameter request.POST to get username with key settings.AXES_USERNAME_FORM_FIELD"
@ -445,15 +462,27 @@ def get_lockout_message() -> str:
def get_lockout_response(
request: HttpRequest, credentials: Optional[dict] = None
request: HttpRequest,
original_response: Optional[HttpResponse] = None,
credentials: Optional[dict] = None,
) -> HttpResponse:
if settings.AXES_LOCKOUT_CALLABLE:
if callable(settings.AXES_LOCKOUT_CALLABLE):
return settings.AXES_LOCKOUT_CALLABLE( # pylint: disable=not-callable
request, credentials
)
# Try calling with 3 args, fallback to 2 for backward compatibility
try:
return settings.AXES_LOCKOUT_CALLABLE(
request, original_response, credentials
)
except TypeError:
# Fallback: old signature without original_response
return settings.AXES_LOCKOUT_CALLABLE(request, credentials)
if isinstance(settings.AXES_LOCKOUT_CALLABLE, str):
return import_string(settings.AXES_LOCKOUT_CALLABLE)(request, credentials)
callable_obj = import_string(settings.AXES_LOCKOUT_CALLABLE)
# Try calling with 3 args, fallback to 2 for backward compatibility
try:
return callable_obj(request, original_response, credentials)
except TypeError:
return callable_obj(request, credentials)
raise TypeError(
"settings.AXES_LOCKOUT_CALLABLE needs to be a string, callable, or None."
)

Binary file not shown.

View file

@ -0,0 +1,109 @@
# ترجمه فارسی برای django-axes
# Copyright (C) 2025 jazzband
# This file is distributed under the same license as the django-axes package.
# AmirAli Bahramjerdi <amiralibahramjerdi@gmail.com>, 2025.
#
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: django-axes\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-05-16 23:28+0330\n"
"PO-Revision-Date: 2025-05-16 23:30+0330\n"
"Last-Translator: AmirAli Bahramjerdi <amiralibahramjerdi@gmail.com>"
"Language-Team: فارسی <fa@li.org>\n"
"Language: fa\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
#: admin.py:27
msgid "Form Data"
msgstr "داده‌های فرم"
#: admin.py:28 admin.py:65 admin.py:100
msgid "Meta Data"
msgstr "فراداده"
#: conf.py:109
msgid "Account locked: too many login attempts. Please try again later."
msgstr "حساب کاربری قفل شد: تلاش‌های زیادی برای ورود انجام شده است. لطفاً بعداً دوباره امتحان کنید."
#: conf.py:117
msgid ""
"Account locked: too many login attempts. Contact an admin to unlock your "
"account."
msgstr "حساب کاربری قفل شد: تلاش‌های زیادی برای ورود انجام شده است. برای باز کردن حساب با مدیر تماس بگیرید."
#: models.py:6
msgid "User Agent"
msgstr "عامل کاربر (User Agent)"
#: models.py:8
msgid "IP Address"
msgstr "آدرس IP"
#: models.py:10
msgid "Username"
msgstr "نام کاربری"
#: models.py:12
msgid "HTTP Accept"
msgstr "پذیرش HTTP"
#: models.py:14
msgid "Path"
msgstr "مسیر"
#: models.py:16
msgid "Attempt Time"
msgstr "زمان تلاش"
#: models.py:26
msgid "Access lock out"
msgstr "قفل دسترسی"
#: models.py:34
msgid "access failure"
msgstr "شکست در دسترسی"
#: models.py:35
msgid "access failures"
msgstr "شکست‌های دسترسی"
#: models.py:39
msgid "GET Data"
msgstr "داده‌های GET"
#: models.py:41
msgid "POST Data"
msgstr "داده‌های POST"
#: models.py:43
msgid "Failed Logins"
msgstr "ورودهای ناموفق"
#: models.py:49
msgid "access attempt"
msgstr "تلاش برای دسترسی"
#: models.py:50
msgid "access attempts"
msgstr "تلاش‌های دسترسی"
#: models.py:55
msgid "Logout Time"
msgstr "زمان خروج"
#: models.py:56
msgid "Session key hash (sha256)"
msgstr "هش کلید نشست (sha256)"
#: models.py:62
msgid "access log"
msgstr "گزارش دسترسی"
#: models.py:63
msgid "access logs"
msgstr "گزارش‌های دسترسی"

Binary file not shown.

View file

@ -48,7 +48,7 @@ class AxesMiddleware:
if settings.AXES_ENABLED:
if getattr(request, "axes_locked_out", None):
credentials = getattr(request, "axes_credentials", None)
response = get_lockout_response(request, credentials) # type: ignore
response = get_lockout_response(request, response, credentials) # type: ignore
return response

View file

@ -0,0 +1,41 @@
# Generated by Django 5.2.1 on 2025-06-10 20:21
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
("axes", "0009_add_session_hash"),
]
operations = [
migrations.CreateModel(
name="AccessAttemptExpiration",
fields=[
(
"access_attempt",
models.OneToOneField(
on_delete=django.db.models.deletion.CASCADE,
primary_key=True,
related_name="expiration",
serialize=False,
to="axes.accessattempt",
verbose_name="Access Attempt",
),
),
(
"expires_at",
models.DateTimeField(
help_text="The time when access attempt expires and is no longer valid.",
verbose_name="Expires At",
),
),
],
options={
"verbose_name": "access attempt expiration",
"verbose_name_plural": "access attempt expirations",
},
),
]

View file

@ -51,9 +51,29 @@ class AccessAttempt(AccessBase):
unique_together = [["username", "ip_address", "user_agent"]]
class AccessAttemptExpiration(models.Model):
access_attempt = models.OneToOneField(
AccessAttempt,
primary_key=True,
on_delete=models.CASCADE,
related_name="expiration",
verbose_name=_("Access Attempt"),
)
expires_at = models.DateTimeField(
_("Expires At"),
help_text=_("The time when access attempt expires and is no longer valid."),
)
class Meta:
verbose_name = _("access attempt expiration")
verbose_name_plural = _("access attempt expirations")
class AccessLog(AccessBase):
logout_time = models.DateTimeField(_("Logout Time"), null=True, blank=True)
session_hash = models.CharField(_("Session key hash (sha256)"), default="", blank=True, max_length=64)
session_hash = models.CharField(
_("Session key hash (sha256)"), default="", blank=True, max_length=64
)
def __str__(self):
return f"Access Log for {self.username} @ {self.attempt_time}"

View file

@ -56,6 +56,9 @@ if you have any custom logic to override Django's standard permissions checks.
# on failed user authentication attempts from login views.
# If you do not want Axes to override the authentication response
# you can skip installing the middleware and use your own views.
# AxesMiddleware runs during the reponse phase. It does not conflict
# with middleware that runs in the request phase like
# django.middleware.cache.FetchFromCacheMiddleware.
'axes.middleware.AxesMiddleware',
]
@ -75,6 +78,12 @@ Many people have different configurations for their development and production e
and running the application with misconfigured settings can prevent security features from working.
Version 8 breaking changes and upgrading from django-axes version 7
-------------------------------------------------------------------
Some database related utility functions have moved from ``axes.helpers`` to ``axes.handlers.database`` module and under the ``axes.handlers.database.AxesDatabaseHandler`` class.
Version 7 breaking changes and upgrading from django-axes version 6
-------------------------------------------------------------------

View file

@ -53,7 +53,7 @@ The following ``settings.py`` options are available for customizing Axes behavio
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| AXES_WHITELIST_CALLABLE | None | A callable or a string path to callable that takes two arguments for whitelisting determination and returns True, if user should be whitelisted: ``def is_whitelisted(request: HttpRequest, credentials: dict) -> bool: ...``. This can be any callable similarly to ``AXES_USERNAME_CALLABLE``. |
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| AXES_LOCKOUT_CALLABLE | None | A callable or a string path to callable that takes two arguments returns a response. For example: ``def generate_lockout_response(request: HttpRequest, credentials: dict) -> HttpResponse: ...``. This can be any callable similarly to ``AXES_USERNAME_CALLABLE``. If not callable is defined, then the default implementation in ``axes.helpers.get_lockout_response`` is used for determining the correct lockout response that is sent to the requesting client. |
| AXES_LOCKOUT_CALLABLE | None | A callable or a string path to callable that takes three arguments returns a response. For example: ``def generate_lockout_response(request: HttpRequest, original_response: HttpResponse, credentials: dict) -> HttpResponse: ...``. This can be any callable similarly to ``AXES_USERNAME_CALLABLE``. If not callable is defined, then the default implementation in ``axes.helpers.get_lockout_response`` is used for determining the correct lockout response that is sent to the requesting client. |
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| AXES_CLIENT_IP_CALLABLE | None | A callable or a string path to callable that takes HttpRequest as an argument and returns the resolved IP as a response. For example: ``def get_ip(request: HttpRequest) -> str: ...``. This can be any callable similarly to ``AXES_USERNAME_CALLABLE``. If not callable is defined, then the default implementation in ``axes.helpers.get_client_ip_address`` is used. |
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
@ -109,6 +109,8 @@ following settings to suit your set up to correctly resolve client IP addresses:
* ``AXES_IPWARE_META_PRECEDENCE_ORDER``: The names of ``request.META`` attributes as a tuple of strings
to check to get the client IP address. Check the Django documentation for header naming conventions.
Default: ``IPWARE_META_PRECEDENCE_ORDER`` setting if set, else ``('REMOTE_ADDR', )``
* ``AXES_IPWARE_PROXY_ORDER``: The order in which to evaluate IP addresses from proxy headers when multiple IPs are present
in the header chain. Must be either ``"left-most"`` or ``"right-most"``. **Default:** ``"left-most"``
.. note::
For reverse proxies or e.g. Heroku, you might also want to fetch IP addresses from a HTTP header such as ``X-Forwarded-For``. To configure this, you can fetch IPs through the ``HTTP_X_FORWARDED_FOR`` key from the ``request.META`` property which contains all the HTTP headers in Django:

View file

@ -166,7 +166,7 @@ An example of usage could be e.g. a custom view for processing lockouts.
from django.http import JsonResponse
def lockout(request, credentials, *args, **kwargs):
def lockout(request, response, credentials, *args, **kwargs):
return JsonResponse({"status": "Locked out due to too many login failures"}, status=403)
``settings.py``::
@ -188,7 +188,7 @@ Example ``AXES_LOCKOUT_PARAMETERS`` configuration:
AXES_LOCKOUT_PARAMETERS = ["ip_address", ["username", "user_agent"]]
This way, axes will lock out users using ip_address and/or combination of username and user agent
This way, axes will lock out users using ip_address or combination of username and user_agent
Example of callable ``AXES_LOCKOUT_PARAMETERS``:
@ -213,7 +213,7 @@ Example of callable ``AXES_LOCKOUT_PARAMETERS``:
AXES_LOCKOUT_PARAMETERS = "example.utils.get_lockout_parameters"
This way, if client ip_address is localhost, axes will lockout client only by username. In other case, axes will lockout client by username and/or ip_address.
This way, if client ip_address is localhost, axes will lockout client only by username. In other case, axes will lockout client by username or ip_address.
Customizing client ip address lookups
-------------------------------------

9
docs/_static/css/custom_theme.css vendored Normal file
View file

@ -0,0 +1,9 @@
@import url("theme.css");
.wy-nav-content {
max-width: none;
}
.wy-table-responsive table td, .wy-table-responsive table th {
white-space: inherit;
}

View file

@ -6,8 +6,8 @@ More information on the configuration options is available at:
https://www.sphinx-doc.org/en/master/usage/configuration.html
"""
import sphinx_rtd_theme
from pkg_resources import get_distribution
# import sphinx_rtd_theme
from importlib.metadata import version as get_version
import django
from django.conf import settings
@ -25,7 +25,7 @@ description = ("Keep track of failed login attempts in Django-powered sites.",)
# 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_rtd_theme","sphinx.ext.autodoc"]
# Add any paths that contain templates here, relative to this directory.
templates_path = ["_templates"]
@ -43,7 +43,7 @@ copyright = "2016, Jazzband"
author = "Jazzband"
# The full version, including alpha/beta/rc tags.
release = get_distribution("django-axes").version
release = get_version("django-axes")
# The short X.Y version.
version = ".".join(release.split(".")[:2])
@ -71,8 +71,10 @@ todo_include_todos = False
# a list of builtin themes.
html_theme = "sphinx_rtd_theme"
html_style = "css/custom_theme.css"
# Add any paths that contain custom themes here, relative to this directory.
html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# html_theme_path = [sphinx_rtd_theme.get_html_theme_path()]
# 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,

View file

@ -1,5 +1,5 @@
[mypy]
python_version = 3.9
python_version = 3.14
ignore_missing_imports = True
[mypy-axes.migrations.*]

View file

@ -10,35 +10,35 @@ DJANGO_SETTINGS_MODULE = "tests.settings"
legacy_tox_ini = """
[tox]
envlist =
py{39,310,311,312}-dj42
py{310,311,312}-dj50
py{310,311,312,313}-dj51
py311-djmain
py311-djqa
py{310,311,312}-dj42
py{310,311,312,313}-dj52
py{312,313,314}-dj60
py314-djmain
py314-djqa
[gh-actions]
python =
3.9: py39
3.10: py310
3.11: py311
3.12: py312
3.13: py313
3.14: py314
[gh-actions:env]
DJANGO =
4.2: dj42
5.0: dj50
5.1: dj51
5.2: dj52
6.0: dj60
main: djmain
qa: djqa
# Normal test environment runs pytest which orchestrates other tools
[testenv]
deps =
-r requirements-test.txt
dj42: django>=4.1,<4.2
dj50: django>=5.0,<5.1
dj51: django>=5.1,<5.2
-r requirements.txt
dj42: django>=4.2,<4.3
dj52: django>=5.2,<5.3
dj60: django>=6.0,<6.1
djmain: https://github.com/django/django/archive/main.tar.gz
usedevelop = true
commands = pytest
@ -51,10 +51,11 @@ ignore_errors =
djmain: True
# QA runs type checks, linting, and code formatting checks
[testenv:py313-djqa]
deps = -r requirements-qa.txt
[testenv:py314-djqa]
stoponfail = false
deps = -r requirements.txt
commands =
mypy axes
prospector
black -t py39 --check --diff axes
prospector axes
black --check --diff axes
"""

View file

@ -1,3 +0,0 @@
black==24.10.0
mypy==1.15.0
prospector==1.14.0

View file

@ -1,7 +0,0 @@
-e .
django-ipware>=3
coverage==7.6.10
pytest==8.3.4
pytest-cov==6.0.0
pytest-django==4.9.0
pytest-subtests==0.14.1

View file

@ -1,5 +1,12 @@
-e .
-r requirements-qa.txt
-r requirements-test.txt
sphinx_rtd_theme==3.0.2
tox==4.24.1
black==26.3.1
coverage==7.13.4
django-ipware>=3
mypy==1.19.1
prospector==1.18.0
pytest-cov==7.0.0
pytest-django==4.12.0
pytest-subtests==0.15.0
pytest==9.0.2
sphinx_rtd_theme==3.1.0
tox==4.49.1

View file

@ -35,9 +35,9 @@ setup(
package_dir={"axes": "axes"},
use_scm_version=True,
setup_requires=["setuptools_scm"],
python_requires=">=3.7",
python_requires=">=3.10",
install_requires=[
"django>=3.2",
"django>=4.2",
"asgiref>=3.6.0",
],
extras_require={
@ -51,19 +51,19 @@ setup(
"Environment :: Plugins",
"Framework :: Django",
"Framework :: Django :: 4.2",
"Framework :: Django :: 5.0",
"Framework :: Django :: 5.1",
"Framework :: Django :: 5.2",
"Framework :: Django :: 6.0",
"Intended Audience :: Developers",
"Intended Audience :: System Administrators",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Programming Language :: Python :: Implementation :: CPython",
"Topic :: Internet :: Log Analysis",
"Topic :: Security",

View file

@ -129,3 +129,24 @@ class ConfCheckTestCase(AxesTestCase):
def test_valid_callable(self):
warnings = run_checks()
self.assertEqual(warnings, [])
def test_missing_settings_no_error(self):
warnings = run_checks()
self.assertEqual(warnings, [])
class LockoutParametersCheckTestCase(AxesTestCase):
@override_settings(AXES_LOCKOUT_PARAMETERS=["ip_address", "username"])
def test_valid_configuration(self):
warnings = run_checks()
self.assertEqual(warnings, [])
@override_settings(AXES_LOCKOUT_PARAMETERS=["username", "user_agent"])
def test_invalid_configuration(self):
warnings = run_checks()
warning = Warning(
msg=Messages.LOCKOUT_PARAMETERS_INVALID,
hint=Hints.LOCKOUT_PARAMETERS_INVALID,
id=Codes.LOCKOUT_PARAMETERS_INVALID,
)
self.assertEqual(warnings, [warning])

45
tests/test_conf.py Normal file
View file

@ -0,0 +1,45 @@
from django.test import TestCase
from django.utils.functional import SimpleLazyObject
class ConfTestCase(TestCase):
def test_axes_username_form_field_uses_lazy_evaluation(self):
"""
Test that AXES_USERNAME_FORM_FIELD uses SimpleLazyObject for lazy evaluation.
This prevents circular import issues with custom user models (issue #1280).
"""
from axes.conf import settings
# Verify that AXES_USERNAME_FORM_FIELD is a SimpleLazyObject if not overridden
# This is only the case when the setting is not explicitly defined
username_field = settings.AXES_USERNAME_FORM_FIELD
# The actual type depends on whether AXES_USERNAME_FORM_FIELD was overridden
# If it's using the default, it should be a SimpleLazyObject
# If overridden in settings, it could be a plain string
# Either way, it should be usable as a string
# Force evaluation and verify it works
username_field_str = str(username_field)
# Should get the default USERNAME_FIELD from the user model
# For the test suite, this is "username"
self.assertIsInstance(username_field_str, str)
self.assertTrue(len(username_field_str) > 0)
def test_axes_username_form_field_evaluates_correctly(self):
"""
Test that when AXES_USERNAME_FORM_FIELD is accessed, it correctly
resolves to the user model's USERNAME_FIELD.
"""
from django.contrib.auth import get_user_model
from axes.conf import settings
# Get the expected value
expected_username_field = get_user_model().USERNAME_FIELD
# Get the actual value from axes settings
actual_username_field = str(settings.AXES_USERNAME_FORM_FIELD)
# They should match
self.assertEqual(actual_username_field, expected_username_field)

View file

@ -1,18 +1,20 @@
from platform import python_implementation
from unittest.mock import MagicMock, patch
from datetime import datetime, timezone as dt_timezone
from django.test import override_settings
from django.utils import timezone
from axes.handlers.database import AxesDatabaseHandler
from axes.models import AccessAttempt, AccessLog, AccessFailureLog, AccessAttemptExpiration
from pytest import mark
from django.core.cache import cache
from django.test import override_settings
from django.urls import reverse
from django.utils import timezone
from django.utils.timezone import timedelta
from axes.conf import settings
from axes.handlers.proxy import AxesProxyHandler
from axes.helpers import get_client_str
from axes.models import AccessAttempt, AccessLog, AccessFailureLog
from tests.base import AxesTestCase
@ -567,3 +569,170 @@ class AxesTestHandlerTestCase(AxesHandlerBaseTestCase):
def test_handler_get_failures(self):
self.assertEqual(0, AxesProxyHandler.get_failures(self.request, {}))
@override_settings(AXES_HANDLER="axes.handlers.database.AxesDatabaseHandler", AXES_COOLOFF_TIME=timezone.timedelta(seconds=10))
class AxesDatabaseHandlerExpirationFlagTestCase(AxesTestCase):
def setUp(self):
super().setUp()
self.handler = AxesDatabaseHandler()
self.mock_request = MagicMock()
self.mock_credentials = None
@override_settings(AXES_USE_ATTEMPT_EXPIRATION=True)
@patch("axes.handlers.database.log")
@patch("axes.models.AccessAttempt.objects.filter")
@patch("django.utils.timezone.now")
def test_clean_expired_user_attempts_expiration_true(self, mock_now, mock_filter, mock_log):
mock_now.return_value = datetime(2025, 1, 1, tzinfo=dt_timezone.utc)
mock_qs = MagicMock()
mock_filter.return_value = mock_qs
mock_qs.delete.return_value = (3, None)
count = self.handler.clean_expired_user_attempts(request=None, credentials=None)
mock_filter.assert_called_once_with(expiration__expires_at__lte=mock_now.return_value)
mock_qs.delete.assert_called_once()
mock_log.info.assert_called_with(
"AXES: Cleaned up %s expired access attempts from database that expiry were older than %s",
3,
mock_now.return_value,
)
self.assertEqual(count, 3)
@override_settings(AXES_USE_ATTEMPT_EXPIRATION=True)
@patch("axes.handlers.database.log")
def test_clean_expired_user_attempts_expiration_true_with_complete_deletion(self, mock_log):
AccessAttempt.objects.all().delete()
dummy_attempt = AccessAttempt.objects.create(
username="test_user",
ip_address="192.168.1.1",
failures_since_start=1,
user_agent="test_agent",
)
dummy_attempt.expiration = AccessAttemptExpiration.objects.create(
access_attempt=dummy_attempt,
expires_at=timezone.now() - timezone.timedelta(days=1) # Set to expire in the past
)
count = self.handler.clean_expired_user_attempts(request=None, credentials=None)
mock_log.info.assert_called_once()
# comparing count=2, as one is the dummy attempt and one is the expiration
self.assertEqual(count, 2)
self.assertEqual(
AccessAttempt.objects.count(), 0
)
self.assertEqual(
AccessAttemptExpiration.objects.count(), 0
)
@override_settings(AXES_USE_ATTEMPT_EXPIRATION=True)
@patch("axes.handlers.database.log")
def test_clean_expired_user_attempts_expiration_true_with_partial_deletion(self, mock_log):
attempt_not_expired = AccessAttempt.objects.create(
username="test_user",
ip_address="192.168.1.1",
failures_since_start=1,
user_agent="test_agent",
)
attempt_not_expired.expiration = AccessAttemptExpiration.objects.create(
access_attempt=attempt_not_expired,
expires_at=timezone.now() + timezone.timedelta(days=1) # Set to expire in the future
)
attempt_expired = AccessAttempt.objects.create(
username="test_user_2",
ip_address="192.168.1.2",
failures_since_start=1,
user_agent="test_agent",
)
attempt_expired.expiration = AccessAttemptExpiration.objects.create(
access_attempt=attempt_expired,
expires_at=timezone.now() - timezone.timedelta(days=1) # Set to expire in the past
)
access_attempt_count = AccessAttempt.objects.count()
access_attempt_expiration_count = AccessAttemptExpiration.objects.count()
count = self.handler.clean_expired_user_attempts(request=None, credentials=None)
mock_log.info.assert_called_once()
# comparing count=2, as one is the dummy attempt and one is the expiration
self.assertEqual(count, 2)
self.assertEqual(
AccessAttempt.objects.count(), access_attempt_count - 1
)
self.assertEqual(
AccessAttemptExpiration.objects.count(), access_attempt_expiration_count - 1
)
@override_settings(AXES_USE_ATTEMPT_EXPIRATION=True)
@patch("axes.handlers.database.log")
def test_clean_expired_user_attempts_expiration_true_with_no_deletion(self, mock_log):
attempt_not_expired_1 = AccessAttempt.objects.create(
username="test_user",
ip_address="192.168.1.1",
failures_since_start=1,
user_agent="test_agent",
)
attempt_not_expired_1.expiration = AccessAttemptExpiration.objects.create(
access_attempt=attempt_not_expired_1,
expires_at=timezone.now() + timezone.timedelta(days=1) # Set to expire in the future
)
attempt_not_expired_2 = AccessAttempt.objects.create(
username="test_user_2",
ip_address="192.168.1.2",
failures_since_start=1,
user_agent="test_agent",
)
attempt_not_expired_2.expiration = AccessAttemptExpiration.objects.create(
access_attempt=attempt_not_expired_2,
expires_at=timezone.now() + timezone.timedelta(days=2) # Set to expire in the future
)
access_attempt_count = AccessAttempt.objects.count()
access_attempt_expiration_count = AccessAttemptExpiration.objects.count()
count = self.handler.clean_expired_user_attempts(request=None, credentials=None)
mock_log.info.assert_called_once()
# comparing count=2, as one is the dummy attempt and one is the expiration
self.assertEqual(count, 0)
self.assertEqual(
AccessAttempt.objects.count(), access_attempt_count
)
self.assertEqual(
AccessAttemptExpiration.objects.count(), access_attempt_expiration_count
)
@override_settings(AXES_USE_ATTEMPT_EXPIRATION=False)
@patch("axes.handlers.database.log")
@patch("axes.handlers.database.get_cool_off_threshold")
@patch("axes.models.AccessAttempt.objects.filter")
def test_clean_expired_user_attempts_expiration_false(self, mock_filter, mock_get_threshold, mock_log):
mock_get_threshold.return_value = "fake-threshold"
mock_qs = MagicMock()
mock_filter.return_value = mock_qs
mock_qs.delete.return_value = (2, None)
count = self.handler.clean_expired_user_attempts(request=self.mock_request, credentials=None)
mock_filter.assert_called_once_with(attempt_time__lte="fake-threshold")
mock_qs.delete.assert_called_once()
mock_log.info.assert_called_with(
"AXES: Cleaned up %s expired access attempts from database that were older than %s",
2,
"fake-threshold",
)
self.assertEqual(count, 2)
@override_settings(AXES_COOLOFF_TIME=None)
@patch("axes.handlers.database.log")
def test_clean_expired_user_attempts_no_cooloff(self, mock_log):
count = self.handler.clean_expired_user_attempts(request=None, credentials=None)
mock_log.debug.assert_called_with(
"AXES: Skipping clean for expired access attempts because no AXES_COOLOFF_TIME is configured"
)
self.assertEqual(count, 0)

View file

@ -1013,9 +1013,16 @@ def mock_get_lockout_response(request, credentials):
return HttpResponse(status=400)
def mock_get_lockout_response_with_original_response_param(
request, response, credentials
):
return HttpResponse(status=400)
class AxesLockoutTestCase(AxesTestCase):
def setUp(self):
self.request = HttpRequest()
self.response = HttpResponse()
self.credentials = dict()
def test_get_lockout_response(self):
@ -1039,6 +1046,20 @@ class AxesLockoutTestCase(AxesTestCase):
response = get_lockout_response(self.request, self.credentials)
self.assertEqual(400, response.status_code)
@override_settings(
AXES_LOCKOUT_CALLABLE=mock_get_lockout_response_with_original_response_param
)
def test_get_lockout_response_override_callable_with_original_response_param(self):
response = get_lockout_response(self.request, self.response, self.credentials)
self.assertEqual(400, response.status_code)
@override_settings(
AXES_LOCKOUT_CALLABLE="tests.test_helpers.mock_get_lockout_response_with_original_response_param"
)
def test_get_lockout_response_override_path_with_original_response_param(self):
response = get_lockout_response(self.request, self.response, self.credentials)
self.assertEqual(400, response.status_code)
@override_settings(AXES_LOCKOUT_CALLABLE=42)
def test_get_lockout_response_override_invalid(self):
with self.assertRaises(TypeError):