New upstream release.
Debian Janitor
2 years ago
7 | 7 | runs-on: ubuntu-latest |
8 | 8 | strategy: |
9 | 9 | fail-fast: false |
10 | max-parallel: 5 | |
10 | 11 | matrix: |
11 | python-version: ['3.6', '3.7', '3.8', '3.9', 'pypy3'] | |
12 | django-version: ['2.2', '3.1', '3.2'] | |
13 | # Tox configuration for QA environment | |
12 | python-version: ['3.7', '3.8', '3.9', '3.10', 'pypy-3.8'] | |
13 | django-version: ['3.2', '4.0', 'main'] | |
14 | 14 | include: |
15 | # Tox configuration for QA environment | |
15 | 16 | - python-version: '3.8' |
16 | 17 | django-version: 'qa' |
17 | # Django > 3.2 only supports >= Python 3.8 | |
18 | # Django main | |
18 | 19 | - python-version: '3.8' |
19 | 20 | django-version: 'main' |
20 | 21 | experimental: true |
21 | 22 | - python-version: '3.9' |
22 | 23 | django-version: 'main' |
23 | 24 | experimental: true |
24 | - python-version: 'pypy3' | |
25 | - python-version: '3.10' | |
25 | 26 | django-version: 'main' |
26 | 27 | experimental: true |
28 | - python-version: 'pypy-3.8' | |
29 | django-version: 'main' | |
30 | experimental: true | |
31 | exclude: | |
32 | # Exclude Django 4.0 for Python 3.7 | |
33 | - python-version: '3.7' | |
34 | django-version: '4.0' | |
35 | # Exclude Django 2.2 for Python 3.10 | |
36 | - python-version: '3.10' | |
37 | django-version: '2.2' | |
27 | 38 | |
28 | 39 | steps: |
29 | 40 | - uses: actions/checkout@v2 |
0 | repos: [] |
0 | 0 | |
1 | 1 | Changes |
2 | 2 | ======= |
3 | ||
4 | ||
5 | 5.31.0 (2022-01-08) | |
6 | ------------------- | |
7 | ||
8 | - Adjust version specifiers for newer Python and other package versions. | |
9 | Set package minimum Python version to 3.7. | |
10 | Relax ``django-ipware`` version requirements to allow newer versions. | |
11 | [aleksihakli] | |
12 | ||
13 | ||
14 | 5.30.0 (2022-01-08) | |
15 | ------------------- | |
16 | ||
17 | - Fix package build error in 5.29.0 to allow publishing. | |
18 | [aleksihakli] | |
19 | ||
20 | ||
21 | 5.29.0 (2022-01-08) | |
22 | ------------------- | |
23 | ||
24 | - Drop Python 3.6 support. | |
25 | [aleksihakli] | |
26 | ||
27 | ||
28 | 5.28.0 (2021-12-14) | |
29 | ------------------- | |
30 | ||
31 | - Drop Django < 3.2 support. | |
32 | [hramezani] | |
33 | - Add Django 4.0 to test matrix. | |
34 | [hramezani] | |
35 | ||
36 | ||
37 | 5.27.0 (2021-11-04) | |
38 | ------------------- | |
39 | ||
40 | - Fix ``pkg_resources`` missing for package version resolution on runtime | |
41 | due to ``setuptools`` not being a runtime dependency. | |
42 | [asherf] | |
43 | - Add Python 3.10 and Django 3.2 support. | |
44 | [hramezani] | |
45 | ||
46 | ||
47 | 5.26.0 (2021-10-11) | |
48 | ------------------- | |
49 | ||
50 | - Fix ``AXES_USERNAME_CALLABLE`` not receiving ``credentials`` attribute | |
51 | in Axes middleware lockout response when user is locked out. | |
52 | [rootart] | |
53 | ||
54 | ||
55 | 5.25.0 (2021-09-19) | |
56 | ------------------- | |
57 | ||
58 | - Fix duplicated AccessAttempts | |
59 | with updated database model ``unique_together`` constraints | |
60 | and data and schema migration. | |
61 | [PetrDlouhy] | |
3 | 62 | |
4 | 63 | |
5 | 64 | 5.24.0 (2021-09-09) |
0 | # Code of Conduct | |
1 | ||
2 | As contributors and maintainers of the Jazzband projects, and in the interest of | |
3 | fostering an open and welcoming community, we pledge to respect all people who | |
4 | contribute through reporting issues, posting feature requests, updating documentation, | |
5 | submitting pull requests or patches, and other activities. | |
6 | ||
7 | We are committed to making participation in the Jazzband a harassment-free experience | |
8 | for everyone, regardless of the level of experience, gender, gender identity and | |
9 | expression, sexual orientation, disability, personal appearance, body size, race, | |
10 | ethnicity, age, religion, or nationality. | |
11 | ||
12 | Examples of unacceptable behavior by participants include: | |
13 | ||
14 | - The use of sexualized language or imagery | |
15 | - Personal attacks | |
16 | - Trolling or insulting/derogatory comments | |
17 | - Public or private harassment | |
18 | - Publishing other's private information, such as physical or electronic addresses, | |
19 | without explicit permission | |
20 | - Other unethical or unprofessional conduct | |
21 | ||
22 | The Jazzband roadies have the right and responsibility to remove, edit, or reject | |
23 | comments, commits, code, wiki edits, issues, and other contributions that are not | |
24 | aligned to this Code of Conduct, or to ban temporarily or permanently any contributor | |
25 | for other behaviors that they deem inappropriate, threatening, offensive, or harmful. | |
26 | ||
27 | By adopting this Code of Conduct, the roadies commit themselves to fairly and | |
28 | consistently applying these principles to every aspect of managing the jazzband | |
29 | projects. Roadies who do not follow or enforce the Code of Conduct may be permanently | |
30 | removed from the Jazzband roadies. | |
31 | ||
32 | This code of conduct applies both within project spaces and in public spaces when an | |
33 | individual is representing the project or its community. | |
34 | ||
35 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by | |
36 | contacting the roadies at `roadies@jazzband.co`. All complaints will be reviewed and | |
37 | investigated and will result in a response that is deemed necessary and appropriate to | |
38 | the circumstances. Roadies are obligated to maintain confidentiality with regard to the | |
39 | reporter of an incident. | |
40 | ||
41 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version | |
42 | 1.3.0, available at [https://contributor-covenant.org/version/1/3/0/][version] | |
43 | ||
44 | [homepage]: https://contributor-covenant.org | |
45 | [version]: https://contributor-covenant.org/version/1/3/0/ |
0 | 0 | from pkg_resources import get_distribution |
1 | 1 | |
2 | import django | |
3 | ||
4 | if django.VERSION < (3, 2): | |
5 | default_app_config = "axes.apps.AppConfig" | |
6 | 2 | |
7 | 3 | __version__ = get_distribution("django-axes").version |
141 | 141 | admin_url = reverse("admin:index") |
142 | 142 | except NoReverseMatch: |
143 | 143 | return True |
144 | return not re.match("^%s" % admin_url, request.path) | |
144 | return not re.match(f"^{admin_url}", request.path) | |
145 | 145 | |
146 | 146 | return False |
147 | 147 |
63 | 63 | ) |
64 | 64 | return failure_count |
65 | 65 | |
66 | def user_login_failed( | |
67 | self, sender, credentials: dict, request=None, **kwargs | |
68 | ): # pylint: disable=too-many-locals | |
66 | def user_login_failed(self, sender, credentials: dict, request=None, **kwargs): | |
69 | 67 | """ |
70 | 68 | When user login fails, save attempt record in cache and lock user out if necessary. |
71 | 69 | |
127 | 125 | ) |
128 | 126 | |
129 | 127 | request.axes_locked_out = True |
128 | request.axes_credentials = credentials | |
130 | 129 | user_locked_out.send( |
131 | 130 | "axes", |
132 | 131 | request=request, |
134 | 133 | ip_address=request.axes_ip_address, |
135 | 134 | ) |
136 | 135 | |
137 | def user_logged_in( | |
138 | self, sender, request, user, **kwargs | |
139 | ): # pylint: disable=unused-argument | |
136 | def user_logged_in(self, sender, request, user, **kwargs): | |
140 | 137 | """ |
141 | 138 | When user logs in, update the AccessLog related to the user. |
142 | 139 | """ |
82 | 82 | ) |
83 | 83 | return attempt_count |
84 | 84 | |
85 | def user_login_failed( | |
86 | self, sender, credentials: dict, request=None, **kwargs | |
87 | ): # pylint: disable=too-many-locals | |
85 | def user_login_failed(self, sender, credentials: dict, request=None, **kwargs): | |
88 | 86 | """ |
89 | 87 | When user login fails, save AccessAttempt record in database, mark request with lockout attribute and emit lockout signal. |
90 | 88 | """ |
185 | 183 | ) |
186 | 184 | |
187 | 185 | request.axes_locked_out = True |
186 | request.axes_credentials = credentials | |
188 | 187 | user_locked_out.send( |
189 | 188 | "axes", |
190 | 189 | request=request, |
192 | 191 | ip_address=request.axes_ip_address, |
193 | 192 | ) |
194 | 193 | |
195 | def user_logged_in( | |
196 | self, sender, request, user, **kwargs | |
197 | ): # pylint: disable=unused-argument | |
194 | def user_logged_in(self, sender, request, user, **kwargs): | |
198 | 195 | """ |
199 | 196 | When user logs in, update the AccessLog related to the user. |
200 | 197 | """ |
234 | 231 | client_str, |
235 | 232 | ) |
236 | 233 | |
237 | def user_logged_out( | |
238 | self, sender, request, user, **kwargs | |
239 | ): # pylint: disable=unused-argument | |
234 | def user_logged_out(self, sender, request, user, **kwargs): | |
240 | 235 | """ |
241 | 236 | When user logs out, update the AccessLog related to the user. |
242 | 237 | """ |
77 | 77 | request.axes_http_accept = get_client_http_accept(request) |
78 | 78 | request.axes_failures_since_start = None |
79 | 79 | request.axes_updated = True |
80 | request.axes_credentials = None | |
80 | 81 | |
81 | 82 | @classmethod |
82 | 83 | def is_locked(cls, request, credentials: dict = None) -> bool: |
61 | 61 | if isinstance(cool_off, str): |
62 | 62 | return import_string(cool_off)() |
63 | 63 | if callable(cool_off): |
64 | return cool_off() | |
64 | return cool_off() # pylint: disable=not-callable | |
65 | 65 | |
66 | 66 | return cool_off |
67 | 67 | |
120 | 120 | log.debug("Using settings.AXES_USERNAME_CALLABLE to get username") |
121 | 121 | |
122 | 122 | if callable(settings.AXES_USERNAME_CALLABLE): |
123 | return settings.AXES_USERNAME_CALLABLE(request, credentials) | |
123 | return settings.AXES_USERNAME_CALLABLE( # pylint: disable=not-callable | |
124 | request, credentials | |
125 | ) | |
124 | 126 | if isinstance(settings.AXES_USERNAME_CALLABLE, str): |
125 | 127 | return import_string(settings.AXES_USERNAME_CALLABLE)(request, credentials) |
126 | 128 | raise TypeError( |
257 | 259 | log.debug("Using settings.AXES_CLIENT_STR_CALLABLE to get client string.") |
258 | 260 | |
259 | 261 | if callable(settings.AXES_CLIENT_STR_CALLABLE): |
260 | return settings.AXES_CLIENT_STR_CALLABLE( | |
262 | return settings.AXES_CLIENT_STR_CALLABLE( # pylint: disable=not-callable | |
261 | 263 | username, ip_address, user_agent, path_info, request |
262 | 264 | ) |
263 | 265 | if isinstance(settings.AXES_CLIENT_STR_CALLABLE, str): |
268 | 270 | "settings.AXES_CLIENT_STR_CALLABLE needs to be a string, callable or None." |
269 | 271 | ) |
270 | 272 | |
271 | client_dict = dict() | |
273 | client_dict = {} | |
272 | 274 | |
273 | 275 | if settings.AXES_VERBOSE: |
274 | 276 | # Verbose mode logs every attribute that is available |
341 | 343 | |
342 | 344 | def get_failure_limit(request, credentials) -> int: |
343 | 345 | if callable(settings.AXES_FAILURE_LIMIT): |
344 | return settings.AXES_FAILURE_LIMIT(request, credentials) | |
346 | return settings.AXES_FAILURE_LIMIT( # pylint: disable=not-callable | |
347 | request, credentials | |
348 | ) | |
345 | 349 | if isinstance(settings.AXES_FAILURE_LIMIT, str): |
346 | 350 | return import_string(settings.AXES_FAILURE_LIMIT)(request, credentials) |
347 | 351 | if isinstance(settings.AXES_FAILURE_LIMIT, int): |
358 | 362 | def get_lockout_response(request, credentials: dict = None) -> HttpResponse: |
359 | 363 | if settings.AXES_LOCKOUT_CALLABLE: |
360 | 364 | if callable(settings.AXES_LOCKOUT_CALLABLE): |
361 | return settings.AXES_LOCKOUT_CALLABLE(request, credentials) | |
365 | return settings.AXES_LOCKOUT_CALLABLE( # pylint: disable=not-callable | |
366 | request, credentials | |
367 | ) | |
362 | 368 | if isinstance(settings.AXES_LOCKOUT_CALLABLE, str): |
363 | 369 | return import_string(settings.AXES_LOCKOUT_CALLABLE)(request, credentials) |
364 | 370 | raise TypeError( |
399 | 405 | if settings.AXES_LOCKOUT_URL: |
400 | 406 | lockout_url = settings.AXES_LOCKOUT_URL |
401 | 407 | query_string = urlencode({"username": context["username"]}) |
402 | url = "{}?{}".format(lockout_url, query_string) | |
408 | url = f"{lockout_url}?{query_string}" | |
403 | 409 | return redirect(url) |
404 | 410 | |
405 | 411 | return HttpResponse(get_lockout_message(), status=status) |
409 | 415 | if not settings.AXES_IP_WHITELIST: |
410 | 416 | return False |
411 | 417 | |
412 | return ip_address in settings.AXES_IP_WHITELIST | |
418 | return ( # pylint: disable=unsupported-membership-test | |
419 | ip_address in settings.AXES_IP_WHITELIST | |
420 | ) | |
413 | 421 | |
414 | 422 | |
415 | 423 | def is_ip_address_in_blacklist(ip_address: str) -> bool: |
416 | 424 | if not settings.AXES_IP_BLACKLIST: |
417 | 425 | return False |
418 | 426 | |
419 | return ip_address in settings.AXES_IP_BLACKLIST | |
427 | return ( # pylint: disable=unsupported-membership-test | |
428 | ip_address in settings.AXES_IP_BLACKLIST | |
429 | ) | |
420 | 430 | |
421 | 431 | |
422 | 432 | def is_client_ip_address_whitelisted(request): |
493 | 503 | if whitelist_callable is None: |
494 | 504 | return False |
495 | 505 | if callable(whitelist_callable): |
496 | return whitelist_callable(request, credentials) | |
506 | return whitelist_callable(request, credentials) # pylint: disable=not-callable | |
497 | 507 | if isinstance(whitelist_callable, str): |
498 | 508 | return import_string(whitelist_callable)(request, credentials) |
499 | 509 |
5 | 5 | class Command(BaseCommand): |
6 | 6 | help = "List access attempts" |
7 | 7 | |
8 | def handle(self, *args, **options): # pylint: disable=unused-argument | |
8 | def handle(self, *args, **options): | |
9 | 9 | for obj in AccessAttempt.objects.all(): |
10 | 10 | self.stdout.write( |
11 | 11 | f"{obj.ip_address}\t{obj.username}\t{obj.failures_since_start}" |
5 | 5 | class Command(BaseCommand): |
6 | 6 | help = "Reset all access attempts and lockouts" |
7 | 7 | |
8 | def handle(self, *args, **options): # pylint: disable=unused-argument | |
8 | def handle(self, *args, **options): | |
9 | 9 | count = reset() |
10 | 10 | |
11 | 11 | if count: |
36 | 36 | |
37 | 37 | if settings.AXES_ENABLED: |
38 | 38 | if getattr(request, "axes_locked_out", None): |
39 | response = get_lockout_response(request) # type: ignore | |
39 | credentials = getattr(request, "axes_credentials", None) | |
40 | response = get_lockout_response(request, credentials) # type: ignore | |
40 | 41 | |
41 | 42 | return response |
0 | # Generated by Django 3.2.7 on 2021-09-13 15:16 | |
1 | ||
2 | from django.db import migrations | |
3 | from django.db.models import Count | |
4 | ||
5 | ||
6 | def deduplicate_attempts(apps, schema_editor): | |
7 | AccessAttempt = apps.get_model("axes", "AccessAttempt") | |
8 | duplicated_attempts = ( | |
9 | AccessAttempt.objects.values("username", "user_agent", "ip_address") | |
10 | .annotate(Count("id")) | |
11 | .order_by() | |
12 | .filter(id__count__gt=1) | |
13 | ) | |
14 | ||
15 | for attempt in duplicated_attempts: | |
16 | redundant_attempts = AccessAttempt.objects.filter( | |
17 | username=attempt["username"], | |
18 | user_agent=attempt["user_agent"], | |
19 | ip_address=attempt["ip_address"], | |
20 | )[1:] | |
21 | for redundant_attempt in redundant_attempts: | |
22 | redundant_attempt.delete() | |
23 | ||
24 | ||
25 | class Migration(migrations.Migration): | |
26 | ||
27 | dependencies = [ | |
28 | ("axes", "0006_remove_accesslog_trusted"), | |
29 | ] | |
30 | ||
31 | operations = [ | |
32 | migrations.RunPython(deduplicate_attempts), | |
33 | migrations.AlterUniqueTogether( | |
34 | name="accessattempt", | |
35 | unique_together={("username", "ip_address", "user_agent")}, | |
36 | ), | |
37 | ] |
33 | 33 | class Meta: |
34 | 34 | verbose_name = _("access attempt") |
35 | 35 | verbose_name_plural = _("access attempts") |
36 | unique_together = [["username", "ip_address", "user_agent"]] | |
36 | 37 | |
37 | 38 | |
38 | 39 | class AccessLog(AccessBase): |
0 | django-axes (5.31.0-1) UNRELEASED; urgency=low | |
1 | ||
2 | * New upstream release. | |
3 | ||
4 | -- Debian Janitor <janitor@jelmer.uk> Sat, 29 Jan 2022 19:26:57 -0000 | |
5 | ||
0 | 6 | django-axes (5.24.0-1) unstable; urgency=medium |
1 | 7 | |
2 | 8 | [ Edward Betts ] |
81 | 81 | * ``AXES_WHITELIST_CALLABLE``: A callable or a string path to callable that takes |
82 | 82 | two arguments for whitelisting determination and returns True, |
83 | 83 | if user should be whitelisted: |
84 | ``def is_whilisted(request: HttpRequest, credentials: dict) -> bool: ...``. | |
84 | ``def is_whitelisted(request: HttpRequest, credentials: dict) -> bool: ...``. | |
85 | 85 | This can be any callable similarly to ``AXES_USERNAME_CALLABLE``. |
86 | 86 | Default: ``None`` |
87 | 87 | * ``AXES_LOCKOUT_CALLABLE``: A callable or a string path to callable that takes |
125 | 125 | |
126 | 126 | Please check the Django signals documentation for more information: |
127 | 127 | |
128 | https://docs.djangoproject.com/en/3.1/topics/signals/ | |
128 | https://docs.djangoproject.com/en/3.2/topics/signals/ | |
129 | 129 | |
130 | 130 | When a user login fails a signal is emitted and PermissionDenied |
131 | 131 | raises a HTTP 403 reply which interrupts the login process. |
9 | 9 | legacy_tox_ini = """ |
10 | 10 | [tox] |
11 | 11 | envlist = |
12 | py{36,37,38,39,py3}-dj{22,31,32} | |
13 | py{38,39}-djmain | |
14 | py38-djqa | |
12 | py{37,38,39,310,py3}-dj32 | |
13 | py{38,39,310,py3}-dj40 | |
14 | py{38,39,310}-djmain | |
15 | py310-djqa | |
15 | 16 | |
16 | 17 | [gh-actions] |
17 | 18 | python = |
18 | 3.6: py36 | |
19 | 19 | 3.7: py37 |
20 | 20 | 3.8: py38 |
21 | 21 | 3.9: py39 |
22 | 3.10: py310 | |
22 | 23 | pypy3: pypy3 |
23 | 24 | |
24 | 25 | [gh-actions:env] |
25 | 26 | DJANGO = |
26 | 2.2: dj22 | |
27 | 3.1: dj31 | |
28 | 27 | 3.2: dj32 |
28 | 4.0: dj40 | |
29 | 29 | main: djmain |
30 | 30 | qa: djqa |
31 | 31 | |
33 | 33 | [testenv] |
34 | 34 | deps = |
35 | 35 | -r requirements-test.txt |
36 | dj22: django>=2.2,<2.3 | |
37 | dj31: django>=3.1,<3.2 | |
38 | 36 | dj32: django>=3.2,<3.3 |
37 | dj40: django>=4.0,<4.1 | |
39 | 38 | djmain: https://github.com/django/django/archive/main.tar.gz |
40 | 39 | usedevelop = true |
41 | 40 | commands = pytest |
42 | 41 | setenv = |
43 | 42 | PYTHONDONTWRITEBYTECODE=1 |
44 | ||
45 | 43 | # Django development version is allowed to fail the test matrix |
46 | [testenv:py{38,39,py3}-djmain] | |
47 | ignore_errors = true | |
48 | ignore_outcome = true | |
44 | ignore_outcome = | |
45 | djmain: True | |
46 | ignore_errors = | |
47 | djmain: True | |
49 | 48 | |
50 | 49 | # QA runs type checks, linting, and code formatting checks |
51 | [testenv:py38-djqa] | |
50 | [testenv:py310-djqa] | |
52 | 51 | deps = -r requirements-qa.txt |
53 | 52 | commands = |
54 | 53 | mypy axes |
55 | 54 | prospector |
56 | black -t py36 --check --diff axes | |
55 | black -t py38 --check --diff axes | |
57 | 56 | """ |
0 | black==21.8b0 | |
1 | mypy==0.910 | |
2 | prospector==1.3.1 | |
0 | black==21.12b0 | |
1 | mypy==0.930 | |
2 | prospector==1.5.3.1 | |
3 | 3 | types-pkg_resources # Type stub |
0 | 0 | -e . |
1 | coverage==5.5 | |
1 | coverage==6.2 | |
2 | 2 | pytest==6.2.5 |
3 | pytest-cov==2.12.1 | |
4 | pytest-django==4.4.0 | |
3 | pytest-cov==3.0.0 | |
4 | pytest-django==4.5.2 | |
5 | 5 | pytest-subtests==0.5.0 |
0 | 0 | -e . |
1 | 1 | -r requirements-qa.txt |
2 | 2 | -r requirements-test.txt |
3 | sphinx_rtd_theme==0.5.2 | |
4 | tox==3.24.3 | |
3 | sphinx_rtd_theme==1.0.0 | |
4 | tox==3.24.5 |
34 | 34 | package_dir={"axes": "axes"}, |
35 | 35 | use_scm_version=True, |
36 | 36 | setup_requires=["setuptools_scm"], |
37 | python_requires="~=3.6", | |
38 | install_requires=["django>=2.2", "django-ipware>=3,<5"], | |
37 | python_requires=">=3.7", | |
38 | install_requires=["django>=3.2", "django-ipware>=3", "setuptools"], | |
39 | 39 | include_package_data=True, |
40 | 40 | packages=find_packages(exclude=["tests"]), |
41 | 41 | classifiers=[ |
43 | 43 | "Environment :: Web Environment", |
44 | 44 | "Environment :: Plugins", |
45 | 45 | "Framework :: Django", |
46 | "Framework :: Django :: 2.2", | |
47 | "Framework :: Django :: 3.1", | |
48 | 46 | "Framework :: Django :: 3.2", |
47 | "Framework :: Django :: 4.0", | |
49 | 48 | "Intended Audience :: Developers", |
50 | 49 | "Intended Audience :: System Administrators", |
51 | 50 | "License :: OSI Approved :: MIT License", |
52 | 51 | "Operating System :: OS Independent", |
53 | 52 | "Programming Language :: Python", |
54 | 53 | "Programming Language :: Python :: 3", |
55 | "Programming Language :: Python :: 3.6", | |
56 | 54 | "Programming Language :: Python :: 3.7", |
57 | 55 | "Programming Language :: Python :: 3.8", |
58 | 56 | "Programming Language :: Python :: 3.9", |
57 | "Programming Language :: Python :: 3.10", | |
59 | 58 | "Programming Language :: Python :: Implementation :: CPython", |
60 | 59 | "Programming Language :: Python :: Implementation :: PyPy", |
61 | 60 | "Topic :: Internet :: Log Analysis", |
64 | 64 | |
65 | 65 | USE_I18N = False |
66 | 66 | |
67 | USE_L10N = False | |
68 | ||
69 | 67 | USE_TZ = False |
70 | 68 | |
71 | 69 | LOGIN_REDIRECT_URL = "/admin/" |
350 | 350 | IP_1 = "127.1.0.1" |
351 | 351 | IP_2 = "127.1.0.2" |
352 | 352 | |
353 | def set_up_login_attemtps(self): | |
353 | def set_up_login_attempts(self): | |
354 | 354 | """Set up the login attempts.""" |
355 | 355 | self.login(username=self.USERNAME_1, remote_addr=self.IP_1) |
356 | 356 | self.login(username=self.USERNAME_1, remote_addr=self.IP_2) |
378 | 378 | |
379 | 379 | @override_settings(AXES_ONLY_USER_FAILURES=True) |
380 | 380 | def test_handler_reset_attempts_username(self): |
381 | self.set_up_login_attemtps() | |
381 | self.set_up_login_attempts() | |
382 | 382 | self.assertEqual( |
383 | 383 | 2, |
384 | 384 | AxesProxyHandler.get_failures( |
406 | 406 | ) |
407 | 407 | |
408 | 408 | def test_handler_reset_attempts_ip(self): |
409 | self.set_up_login_attemtps() | |
409 | self.set_up_login_attempts() | |
410 | 410 | self.check_failures(2, ip_address=self.IP_1) |
411 | 411 | self.assertEqual(1, AxesProxyHandler.reset_attempts(ip_address=self.IP_1)) |
412 | 412 | self.check_failures(0, ip_address=self.IP_1) |
414 | 414 | |
415 | 415 | @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True) |
416 | 416 | def test_handler_reset_attempts_ip_and_username(self): |
417 | self.set_up_login_attemtps() | |
417 | self.set_up_login_attempts() | |
418 | 418 | self.check_failures(1, username=self.USERNAME_1, ip_address=self.IP_1) |
419 | 419 | self.check_failures(1, username=self.USERNAME_2, ip_address=self.IP_1) |
420 | 420 | self.check_failures(1, username=self.USERNAME_1, ip_address=self.IP_2) |
0 | from django.conf import settings | |
0 | 1 | from django.http import HttpResponse, HttpRequest |
1 | 2 | from django.test import override_settings |
2 | 3 | |
3 | 4 | from axes.middleware import AxesMiddleware |
4 | 5 | from tests.base import AxesTestCase |
6 | ||
7 | ||
8 | def get_username(request, credentials: dict) -> str: | |
9 | return credentials.get(settings.AXES_USERNAME_FORM_FIELD) | |
5 | 10 | |
6 | 11 | |
7 | 12 | class MiddlewareTestCase(AxesTestCase): |
27 | 32 | response = AxesMiddleware(get_response)(self.request) |
28 | 33 | self.assertEqual(response.status_code, self.STATUS_LOCKOUT) |
29 | 34 | |
35 | @override_settings(AXES_USERNAME_CALLABLE="tests.test_middleware.get_username") | |
36 | def test_lockout_response_with_axes_callable_username(self): | |
37 | def get_response(request): | |
38 | request.axes_locked_out = True | |
39 | request.axes_credentials = {settings.AXES_USERNAME_FORM_FIELD: 'username'} | |
40 | ||
41 | return HttpResponse() | |
42 | ||
43 | response = AxesMiddleware(get_response)(self.request) | |
44 | self.assertEqual(response.status_code, self.STATUS_LOCKOUT) | |
45 | ||
30 | 46 | @override_settings(AXES_ENABLED=False) |
31 | 47 | def test_respects_enabled_switch(self): |
32 | 48 | def get_response(request): |