Codebase list django-axes / 5e747a6
New upstream release. Debian Janitor 2 years ago
27 changed file(s) with 247 addition(s) and 74 deletion(s). Raw diff Collapse all Expand all
77 runs-on: ubuntu-latest
88 strategy:
99 fail-fast: false
10 max-parallel: 5
1011 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']
1414 include:
15 # Tox configuration for QA environment
1516 - python-version: '3.8'
1617 django-version: 'qa'
17 # Django > 3.2 only supports >= Python 3.8
18 # Django main
1819 - python-version: '3.8'
1920 django-version: 'main'
2021 experimental: true
2122 - python-version: '3.9'
2223 django-version: 'main'
2324 experimental: true
24 - python-version: 'pypy3'
25 - python-version: '3.10'
2526 django-version: 'main'
2627 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'
2738
2839 steps:
2940 - uses: actions/checkout@v2
00
11 Changes
22 =======
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]
362
463
564 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/
00 from pkg_resources import get_distribution
11
2 import django
3
4 if django.VERSION < (3, 2):
5 default_app_config = "axes.apps.AppConfig"
62
73 __version__ = get_distribution("django-axes").version
141141 admin_url = reverse("admin:index")
142142 except NoReverseMatch:
143143 return True
144 return not re.match("^%s" % admin_url, request.path)
144 return not re.match(f"^{admin_url}", request.path)
145145
146146 return False
147147
6363 )
6464 return failure_count
6565
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):
6967 """
7068 When user login fails, save attempt record in cache and lock user out if necessary.
7169
127125 )
128126
129127 request.axes_locked_out = True
128 request.axes_credentials = credentials
130129 user_locked_out.send(
131130 "axes",
132131 request=request,
134133 ip_address=request.axes_ip_address,
135134 )
136135
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):
140137 """
141138 When user logs in, update the AccessLog related to the user.
142139 """
8282 )
8383 return attempt_count
8484
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):
8886 """
8987 When user login fails, save AccessAttempt record in database, mark request with lockout attribute and emit lockout signal.
9088 """
185183 )
186184
187185 request.axes_locked_out = True
186 request.axes_credentials = credentials
188187 user_locked_out.send(
189188 "axes",
190189 request=request,
192191 ip_address=request.axes_ip_address,
193192 )
194193
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):
198195 """
199196 When user logs in, update the AccessLog related to the user.
200197 """
234231 client_str,
235232 )
236233
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):
240235 """
241236 When user logs out, update the AccessLog related to the user.
242237 """
7777 request.axes_http_accept = get_client_http_accept(request)
7878 request.axes_failures_since_start = None
7979 request.axes_updated = True
80 request.axes_credentials = None
8081
8182 @classmethod
8283 def is_locked(cls, request, credentials: dict = None) -> bool:
6161 if isinstance(cool_off, str):
6262 return import_string(cool_off)()
6363 if callable(cool_off):
64 return cool_off()
64 return cool_off() # pylint: disable=not-callable
6565
6666 return cool_off
6767
120120 log.debug("Using settings.AXES_USERNAME_CALLABLE to get username")
121121
122122 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 )
124126 if isinstance(settings.AXES_USERNAME_CALLABLE, str):
125127 return import_string(settings.AXES_USERNAME_CALLABLE)(request, credentials)
126128 raise TypeError(
257259 log.debug("Using settings.AXES_CLIENT_STR_CALLABLE to get client string.")
258260
259261 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
261263 username, ip_address, user_agent, path_info, request
262264 )
263265 if isinstance(settings.AXES_CLIENT_STR_CALLABLE, str):
268270 "settings.AXES_CLIENT_STR_CALLABLE needs to be a string, callable or None."
269271 )
270272
271 client_dict = dict()
273 client_dict = {}
272274
273275 if settings.AXES_VERBOSE:
274276 # Verbose mode logs every attribute that is available
341343
342344 def get_failure_limit(request, credentials) -> int:
343345 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 )
345349 if isinstance(settings.AXES_FAILURE_LIMIT, str):
346350 return import_string(settings.AXES_FAILURE_LIMIT)(request, credentials)
347351 if isinstance(settings.AXES_FAILURE_LIMIT, int):
358362 def get_lockout_response(request, credentials: dict = None) -> HttpResponse:
359363 if settings.AXES_LOCKOUT_CALLABLE:
360364 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 )
362368 if isinstance(settings.AXES_LOCKOUT_CALLABLE, str):
363369 return import_string(settings.AXES_LOCKOUT_CALLABLE)(request, credentials)
364370 raise TypeError(
399405 if settings.AXES_LOCKOUT_URL:
400406 lockout_url = settings.AXES_LOCKOUT_URL
401407 query_string = urlencode({"username": context["username"]})
402 url = "{}?{}".format(lockout_url, query_string)
408 url = f"{lockout_url}?{query_string}"
403409 return redirect(url)
404410
405411 return HttpResponse(get_lockout_message(), status=status)
409415 if not settings.AXES_IP_WHITELIST:
410416 return False
411417
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 )
413421
414422
415423 def is_ip_address_in_blacklist(ip_address: str) -> bool:
416424 if not settings.AXES_IP_BLACKLIST:
417425 return False
418426
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 )
420430
421431
422432 def is_client_ip_address_whitelisted(request):
493503 if whitelist_callable is None:
494504 return False
495505 if callable(whitelist_callable):
496 return whitelist_callable(request, credentials)
506 return whitelist_callable(request, credentials) # pylint: disable=not-callable
497507 if isinstance(whitelist_callable, str):
498508 return import_string(whitelist_callable)(request, credentials)
499509
55 class Command(BaseCommand):
66 help = "List access attempts"
77
8 def handle(self, *args, **options): # pylint: disable=unused-argument
8 def handle(self, *args, **options):
99 for obj in AccessAttempt.objects.all():
1010 self.stdout.write(
1111 f"{obj.ip_address}\t{obj.username}\t{obj.failures_since_start}"
55 class Command(BaseCommand):
66 help = "Reset all access attempts and lockouts"
77
8 def handle(self, *args, **options): # pylint: disable=unused-argument
8 def handle(self, *args, **options):
99 count = reset()
1010
1111 if count:
+0
-1
axes/management/commands/axes_reset_user.py less more
0 axes_reset_username.py
3636
3737 if settings.AXES_ENABLED:
3838 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
4041
4142 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 ]
3333 class Meta:
3434 verbose_name = _("access attempt")
3535 verbose_name_plural = _("access attempts")
36 unique_together = [["username", "ip_address", "user_agent"]]
3637
3738
3839 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
06 django-axes (5.24.0-1) unstable; urgency=medium
17
28 [ Edward Betts ]
8181 * ``AXES_WHITELIST_CALLABLE``: A callable or a string path to callable that takes
8282 two arguments for whitelisting determination and returns True,
8383 if user should be whitelisted:
84 ``def is_whilisted(request: HttpRequest, credentials: dict) -> bool: ...``.
84 ``def is_whitelisted(request: HttpRequest, credentials: dict) -> bool: ...``.
8585 This can be any callable similarly to ``AXES_USERNAME_CALLABLE``.
8686 Default: ``None``
8787 * ``AXES_LOCKOUT_CALLABLE``: A callable or a string path to callable that takes
125125
126126 Please check the Django signals documentation for more information:
127127
128 https://docs.djangoproject.com/en/3.1/topics/signals/
128 https://docs.djangoproject.com/en/3.2/topics/signals/
129129
130130 When a user login fails a signal is emitted and PermissionDenied
131131 raises a HTTP 403 reply which interrupts the login process.
99 legacy_tox_ini = """
1010 [tox]
1111 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
1516
1617 [gh-actions]
1718 python =
18 3.6: py36
1919 3.7: py37
2020 3.8: py38
2121 3.9: py39
22 3.10: py310
2223 pypy3: pypy3
2324
2425 [gh-actions:env]
2526 DJANGO =
26 2.2: dj22
27 3.1: dj31
2827 3.2: dj32
28 4.0: dj40
2929 main: djmain
3030 qa: djqa
3131
3333 [testenv]
3434 deps =
3535 -r requirements-test.txt
36 dj22: django>=2.2,<2.3
37 dj31: django>=3.1,<3.2
3836 dj32: django>=3.2,<3.3
37 dj40: django>=4.0,<4.1
3938 djmain: https://github.com/django/django/archive/main.tar.gz
4039 usedevelop = true
4140 commands = pytest
4241 setenv =
4342 PYTHONDONTWRITEBYTECODE=1
44
4543 # 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
4948
5049 # QA runs type checks, linting, and code formatting checks
51 [testenv:py38-djqa]
50 [testenv:py310-djqa]
5251 deps = -r requirements-qa.txt
5352 commands =
5453 mypy axes
5554 prospector
56 black -t py36 --check --diff axes
55 black -t py38 --check --diff axes
5756 """
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
33 types-pkg_resources # Type stub
00 -e .
1 coverage==5.5
1 coverage==6.2
22 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
55 pytest-subtests==0.5.0
00 -e .
11 -r requirements-qa.txt
22 -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
3434 package_dir={"axes": "axes"},
3535 use_scm_version=True,
3636 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"],
3939 include_package_data=True,
4040 packages=find_packages(exclude=["tests"]),
4141 classifiers=[
4343 "Environment :: Web Environment",
4444 "Environment :: Plugins",
4545 "Framework :: Django",
46 "Framework :: Django :: 2.2",
47 "Framework :: Django :: 3.1",
4846 "Framework :: Django :: 3.2",
47 "Framework :: Django :: 4.0",
4948 "Intended Audience :: Developers",
5049 "Intended Audience :: System Administrators",
5150 "License :: OSI Approved :: MIT License",
5251 "Operating System :: OS Independent",
5352 "Programming Language :: Python",
5453 "Programming Language :: Python :: 3",
55 "Programming Language :: Python :: 3.6",
5654 "Programming Language :: Python :: 3.7",
5755 "Programming Language :: Python :: 3.8",
5856 "Programming Language :: Python :: 3.9",
57 "Programming Language :: Python :: 3.10",
5958 "Programming Language :: Python :: Implementation :: CPython",
6059 "Programming Language :: Python :: Implementation :: PyPy",
6160 "Topic :: Internet :: Log Analysis",
6464
6565 USE_I18N = False
6666
67 USE_L10N = False
68
6967 USE_TZ = False
7068
7169 LOGIN_REDIRECT_URL = "/admin/"
350350 IP_1 = "127.1.0.1"
351351 IP_2 = "127.1.0.2"
352352
353 def set_up_login_attemtps(self):
353 def set_up_login_attempts(self):
354354 """Set up the login attempts."""
355355 self.login(username=self.USERNAME_1, remote_addr=self.IP_1)
356356 self.login(username=self.USERNAME_1, remote_addr=self.IP_2)
378378
379379 @override_settings(AXES_ONLY_USER_FAILURES=True)
380380 def test_handler_reset_attempts_username(self):
381 self.set_up_login_attemtps()
381 self.set_up_login_attempts()
382382 self.assertEqual(
383383 2,
384384 AxesProxyHandler.get_failures(
406406 )
407407
408408 def test_handler_reset_attempts_ip(self):
409 self.set_up_login_attemtps()
409 self.set_up_login_attempts()
410410 self.check_failures(2, ip_address=self.IP_1)
411411 self.assertEqual(1, AxesProxyHandler.reset_attempts(ip_address=self.IP_1))
412412 self.check_failures(0, ip_address=self.IP_1)
414414
415415 @override_settings(AXES_LOCK_OUT_BY_COMBINATION_USER_AND_IP=True)
416416 def test_handler_reset_attempts_ip_and_username(self):
417 self.set_up_login_attemtps()
417 self.set_up_login_attempts()
418418 self.check_failures(1, username=self.USERNAME_1, ip_address=self.IP_1)
419419 self.check_failures(1, username=self.USERNAME_2, ip_address=self.IP_1)
420420 self.check_failures(1, username=self.USERNAME_1, ip_address=self.IP_2)
0 from django.conf import settings
01 from django.http import HttpResponse, HttpRequest
12 from django.test import override_settings
23
34 from axes.middleware import AxesMiddleware
45 from tests.base import AxesTestCase
6
7
8 def get_username(request, credentials: dict) -> str:
9 return credentials.get(settings.AXES_USERNAME_FORM_FIELD)
510
611
712 class MiddlewareTestCase(AxesTestCase):
2732 response = AxesMiddleware(get_response)(self.request)
2833 self.assertEqual(response.status_code, self.STATUS_LOCKOUT)
2934
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
3046 @override_settings(AXES_ENABLED=False)
3147 def test_respects_enabled_switch(self):
3248 def get_response(request):