Skip to content

Commit 510c8d1

Browse files
browniebrokealeksihakli
authored andcommitted
feat: pass the request to get dynamic cool off period
1 parent 3f4526e commit 510c8d1

File tree

4 files changed

+24
-18
lines changed

4 files changed

+24
-18
lines changed

axes/handlers/cache.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ def user_login_failed(self, sender, credentials: dict, request=None, **kwargs):
113113
return
114114

115115
cache_keys = get_client_cache_keys(request, credentials)
116-
cache_timeout = get_cache_timeout(username)
116+
cache_timeout = get_cache_timeout(request)
117117
failures = []
118118
for cache_key in cache_keys:
119119
added = self.cache.add(key=cache_key, value=1, timeout=cache_timeout)

axes/helpers.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def get_cache() -> BaseCache:
3434
return caches[getattr(settings, "AXES_CACHE", "default")]
3535

3636

37-
def get_cache_timeout(username: Optional[str] = None) -> Optional[int]:
37+
def get_cache_timeout(request: Optional[HttpRequest] = None) -> Optional[int]:
3838
"""
3939
Return the cache timeout interpreted from settings.AXES_COOLOFF_TIME.
4040
@@ -45,20 +45,20 @@ def get_cache_timeout(username: Optional[str] = None) -> Optional[int]:
4545
for use with the Django cache backends.
4646
"""
4747

48-
cool_off = get_cool_off(username)
48+
cool_off = get_cool_off(request)
4949
if cool_off is None:
5050
return None
5151
return int(cool_off.total_seconds())
5252

5353

54-
def get_cool_off(username: Optional[str] = None) -> Optional[timedelta]:
54+
def get_cool_off(request: Optional[HttpRequest] = None) -> Optional[timedelta]:
5555
"""
5656
Return the login cool off time interpreted from settings.AXES_COOLOFF_TIME.
5757
5858
The return value is either None or timedelta.
5959
6060
Notice that the settings.AXES_COOLOFF_TIME is either None, timedelta, integer/float of hours,
61-
a path to a callable or a callable taking zero or 1 argument (the username). This function
61+
a path to a callable or a callable taking zero or 1 argument (the request). This function
6262
offers a unified _timedelta or None_ representation of that configuration for use with the
6363
Axes internal implementations.
6464
@@ -73,18 +73,18 @@ def get_cool_off(username: Optional[str] = None) -> Optional[timedelta]:
7373
return timedelta(minutes=cool_off * 60)
7474
if isinstance(cool_off, str):
7575
cool_off_func = import_string(cool_off)
76-
return _maybe_partial(cool_off_func, username)()
76+
return _maybe_partial(cool_off_func, request)()
7777
if callable(cool_off):
78-
return _maybe_partial(cool_off, username)() # pylint: disable=not-callable
78+
return _maybe_partial(cool_off, request)() # pylint: disable=not-callable
7979

8080
return cool_off
8181

8282

83-
def _maybe_partial(func: Callable, username: Optional[str] = None):
84-
"""Bind the given username to the function if it accepts a single argument."""
83+
def _maybe_partial(func: Callable, request: Optional[HttpRequest] = None):
84+
"""Bind the given request to the function if it accepts a single argument."""
8585
sig = inspect.signature(func)
8686
if len(sig.parameters) == 1:
87-
return functools.partial(func, username)
87+
return functools.partial(func, request)
8888
return func
8989

9090

docs/4_configuration.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ The following ``settings.py`` options are available for customizing Axes behavio
2323
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2424
| AXES_LOCK_OUT_AT_FAILURE | True | After the number of allowed login attempts are exceeded, should we lock out this IP (and optional user agent)? |
2525
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
26-
| AXES_COOLOFF_TIME | None | If set, defines a period of inactivity after which old failed login attempts will be cleared. Can be set to a Python timedelta object, an integer, a float, a callable, or a string path to a callable which takes the username as argument. If an integer or float, will be interpreted as a number of hours: ``AXES_COOLOFF_TIME = 2`` 2 hours, ``AXES_COOLOFF_TIME = 2.0`` 2 hours, 120 minutes, ``AXES_COOLOFF_TIME = 1.7`` 1.7 hours, 102 minutes, 6120 seconds |
26+
| AXES_COOLOFF_TIME | None | If set, defines a period of inactivity after which old failed login attempts will be cleared. Can be set to a Python timedelta object, an integer, a float, a callable, or a string path to a callable which takes the request as argument. If an integer or float, will be interpreted as a number of hours: ``AXES_COOLOFF_TIME = 2`` 2 hours, ``AXES_COOLOFF_TIME = 2.0`` 2 hours, 120 minutes, ``AXES_COOLOFF_TIME = 1.7`` 1.7 hours, 102 minutes, 6120 seconds |
2727
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
2828
| AXES_ONLY_ADMIN_SITE | False | If ``True``, lock is only enabled for admin site. Admin site is determined by checking request path against the path of ``"admin:index"`` view. If admin urls are not registered in current urlconf, all requests will not be locked. |
2929
+------------------------------------------------------+----------------------------------------------+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+

tests/test_helpers.py

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -59,31 +59,37 @@ def test_get_cache_timeout_timedelta(self):
5959
def test_get_cache_timeout_none(self):
6060
self.assertEqual(get_cache_timeout(), None)
6161

62-
def test_get_increasing_cache_timeout(self):
62+
def test_get_increasing_cache_timeout_by_username(self):
6363
user_durations = {
6464
"ben": timedelta(minutes=5),
6565
"jen": timedelta(minutes=10),
6666
}
6767

68-
def _callback(username):
68+
def _callback(request):
69+
username = request.POST["username"] if request else object()
6970
previous_duration = user_durations.get(username, timedelta())
7071
user_durations[username] = previous_duration + timedelta(minutes=5)
7172
return user_durations[username]
7273

74+
rf = RequestFactory()
75+
ben_req = rf.post("/", data={"username": "ben"})
76+
jen_req = rf.post("/", data={"username": "jen"})
77+
james_req = rf.post("/", data={"username": "james"})
78+
7379
with override_settings(AXES_COOLOFF_TIME=_callback):
7480
with self.subTest("no username"):
7581
self.assertEqual(get_cache_timeout(), 300)
7682

7783
with self.subTest("ben"):
78-
self.assertEqual(get_cache_timeout("ben"), 600)
79-
self.assertEqual(get_cache_timeout("ben"), 900)
80-
self.assertEqual(get_cache_timeout("ben"), 1200)
84+
self.assertEqual(get_cache_timeout(ben_req), 600)
85+
self.assertEqual(get_cache_timeout(ben_req), 900)
86+
self.assertEqual(get_cache_timeout(ben_req), 1200)
8187

8288
with self.subTest("jen"):
83-
self.assertEqual(get_cache_timeout("jen"), 900)
89+
self.assertEqual(get_cache_timeout(jen_req), 900)
8490

8591
with self.subTest("james"):
86-
self.assertEqual(get_cache_timeout("james"), 300)
92+
self.assertEqual(get_cache_timeout(james_req), 300)
8793

8894

8995
class TimestampTestCase(AxesTestCase):

0 commit comments

Comments
 (0)