mirror of
https://github.com/jazzband/django-defender.git
synced 2026-03-16 22:10:32 +00:00
Use redis cache in get_approx_account_lockouts_from_login_attempts (#250)
Some checks failed
Test / build (3.10, 5) (push) Has been cancelled
Test / build (3.10, 6) (push) Has been cancelled
Test / build (3.10, 7) (push) Has been cancelled
Test / build (3.11, 5) (push) Has been cancelled
Test / build (3.11, 6) (push) Has been cancelled
Test / build (3.11, 7) (push) Has been cancelled
Test / build (3.12, 5) (push) Has been cancelled
Test / build (3.12, 6) (push) Has been cancelled
Test / build (3.12, 7) (push) Has been cancelled
Test / build (3.13, 5) (push) Has been cancelled
Test / build (3.13, 6) (push) Has been cancelled
Test / build (3.13, 7) (push) Has been cancelled
Test / build (3.9, 5) (push) Has been cancelled
Test / build (3.9, 6) (push) Has been cancelled
Test / build (3.9, 7) (push) Has been cancelled
Some checks failed
Test / build (3.10, 5) (push) Has been cancelled
Test / build (3.10, 6) (push) Has been cancelled
Test / build (3.10, 7) (push) Has been cancelled
Test / build (3.11, 5) (push) Has been cancelled
Test / build (3.11, 6) (push) Has been cancelled
Test / build (3.11, 7) (push) Has been cancelled
Test / build (3.12, 5) (push) Has been cancelled
Test / build (3.12, 6) (push) Has been cancelled
Test / build (3.12, 7) (push) Has been cancelled
Test / build (3.13, 5) (push) Has been cancelled
Test / build (3.13, 6) (push) Has been cancelled
Test / build (3.13, 7) (push) Has been cancelled
Test / build (3.9, 5) (push) Has been cancelled
Test / build (3.9, 6) (push) Has been cancelled
Test / build (3.9, 7) (push) Has been cancelled
* Use redis cache in `get_approx_account_lockouts_from_login_attempts` * use django_redis in ci * Add `django_redis` and `redis` to requirements.txt * Fix an issue detected by tests: clear redis cache upon block reset * Remove the unnecessary `if`
This commit is contained in:
parent
37e5dd3123
commit
289af19ce9
6 changed files with 57 additions and 10 deletions
|
|
@ -7,7 +7,7 @@ from celery import Celery
|
||||||
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:",}}
|
DATABASES = {"default": {"ENGINE": "django.db.backends.sqlite3", "NAME": ":memory:",}}
|
||||||
|
|
||||||
CACHES = {
|
CACHES = {
|
||||||
"default": {"BACKEND": "redis_cache.RedisCache", "LOCATION": "localhost:6379",}
|
"default": {"BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://localhost:6379",}
|
||||||
}
|
}
|
||||||
|
|
||||||
SITE_ID = 1
|
SITE_ID = 1
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
|
|
||||||
from defender import config
|
from defender import config
|
||||||
|
from defender.connection import get_redis_connection
|
||||||
from .models import AccessAttempt
|
from .models import AccessAttempt
|
||||||
from django.db.models import Q
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
@ -19,6 +20,14 @@ def store_login_attempt(
|
||||||
login_valid=login_valid,
|
login_valid=login_valid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_approx_lockouts_cache_key(ip_address, username):
|
||||||
|
"""get cache key for approximate number of account lockouts"""
|
||||||
|
return "{0}:approx_lockouts:ip:{1}:user:{2}".format(
|
||||||
|
config.CACHE_PREFIX, ip_address or "", username.lower() if username else ""
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def get_approx_account_lockouts_from_login_attempts(ip_address=None, username=None):
|
def get_approx_account_lockouts_from_login_attempts(ip_address=None, username=None):
|
||||||
"""Get the approximate number of account lockouts in a period of ACCESS_ATTEMPT_EXPIRATION hours.
|
"""Get the approximate number of account lockouts in a period of ACCESS_ATTEMPT_EXPIRATION hours.
|
||||||
This is approximate because we do not consider the time between these failed
|
This is approximate because we do not consider the time between these failed
|
||||||
|
|
@ -31,10 +40,6 @@ def get_approx_account_lockouts_from_login_attempts(ip_address=None, username=No
|
||||||
Returns:
|
Returns:
|
||||||
int: The minimum of the count of logged failure attempts and the length of the LOCKOUT_COOLOFF_TIMES - 1, or 0 dependant on either configuration or argument parameters (ie. both ip_address and username being None).
|
int: The minimum of the count of logged failure attempts and the length of the LOCKOUT_COOLOFF_TIMES - 1, or 0 dependant on either configuration or argument parameters (ie. both ip_address and username being None).
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# TODO: Possibly add logic to temporarily store this info in the cache
|
|
||||||
# to help mitigate any potential performance impact this could have.
|
|
||||||
|
|
||||||
if not config.STORE_ACCESS_ATTEMPTS or not (ip_address or username):
|
if not config.STORE_ACCESS_ATTEMPTS or not (ip_address or username):
|
||||||
# If we're not storing login attempts OR both ip_address and username are
|
# If we're not storing login attempts OR both ip_address and username are
|
||||||
# None we should return 0.
|
# None we should return 0.
|
||||||
|
|
@ -57,4 +62,15 @@ def get_approx_account_lockouts_from_login_attempts(ip_address=None, username=No
|
||||||
# conditions, we're in an inappropriate context.
|
# conditions, we're in an inappropriate context.
|
||||||
raise Exception("Invalid state requested")
|
raise Exception("Invalid state requested")
|
||||||
|
|
||||||
return AccessAttempt.objects.filter(q).count() // failure_limit
|
cache_key = get_approx_lockouts_cache_key(ip_address, username)
|
||||||
|
|
||||||
|
redis_client = get_redis_connection()
|
||||||
|
cached_value = redis_client.get(cache_key)
|
||||||
|
if cached_value is not None:
|
||||||
|
return int(cached_value)
|
||||||
|
|
||||||
|
lockouts = AccessAttempt.objects.filter(q).count() // failure_limit
|
||||||
|
|
||||||
|
redis_client.set(cache_key, int(lockouts), 60)
|
||||||
|
|
||||||
|
return lockouts
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ from django.urls import reverse
|
||||||
|
|
||||||
import redis
|
import redis
|
||||||
|
|
||||||
from defender.data import get_approx_account_lockouts_from_login_attempts
|
from defender.data import get_approx_account_lockouts_from_login_attempts, get_approx_lockouts_cache_key
|
||||||
|
|
||||||
from . import utils
|
from . import utils
|
||||||
from . import config
|
from . import config
|
||||||
|
|
@ -964,6 +964,19 @@ class AccessAttemptTest(DefenderTestCase):
|
||||||
def test_approx_account_lockout_count_default_case_invalid_args_pt2(self):
|
def test_approx_account_lockout_count_default_case_invalid_args_pt2(self):
|
||||||
with self.assertRaises(Exception):
|
with self.assertRaises(Exception):
|
||||||
get_approx_account_lockouts_from_login_attempts(username=VALID_USERNAME)
|
get_approx_account_lockouts_from_login_attempts(username=VALID_USERNAME)
|
||||||
|
|
||||||
|
def test_approx_account_lockout_uses_redis_cache(self):
|
||||||
|
get_approx_account_lockouts_from_login_attempts(
|
||||||
|
ip_address="127.0.0.1", username=VALID_USERNAME
|
||||||
|
)
|
||||||
|
|
||||||
|
redis_client = get_redis_connection()
|
||||||
|
cached_value = redis_client.get(
|
||||||
|
get_approx_lockouts_cache_key(
|
||||||
|
ip_address="127.0.0.1", username=VALID_USERNAME
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertIsNotNone(cached_value)
|
||||||
|
|
||||||
|
|
||||||
class SignalTest(DefenderTestCase):
|
class SignalTest(DefenderTestCase):
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,11 @@ from django.utils.module_loading import import_string
|
||||||
|
|
||||||
from .connection import get_redis_connection
|
from .connection import get_redis_connection
|
||||||
from . import config
|
from . import config
|
||||||
from .data import get_approx_account_lockouts_from_login_attempts, store_login_attempt
|
from .data import (
|
||||||
|
get_approx_account_lockouts_from_login_attempts,
|
||||||
|
get_approx_lockouts_cache_key,
|
||||||
|
store_login_attempt,
|
||||||
|
)
|
||||||
from .signals import (
|
from .signals import (
|
||||||
send_username_block_signal,
|
send_username_block_signal,
|
||||||
send_ip_block_signal,
|
send_ip_block_signal,
|
||||||
|
|
@ -331,6 +335,10 @@ def unblock_ip(ip_address, pipe=None):
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
send_ip_unblock_signal(ip_address)
|
send_ip_unblock_signal(ip_address)
|
||||||
|
|
||||||
|
redis_cache_key = get_approx_lockouts_cache_key(ip_address, None)
|
||||||
|
redis_client = get_redis_connection()
|
||||||
|
redis_client.delete(redis_cache_key)
|
||||||
|
|
||||||
|
|
||||||
def unblock_username(username, pipe=None):
|
def unblock_username(username, pipe=None):
|
||||||
""" unblock the given Username """
|
""" unblock the given Username """
|
||||||
|
|
@ -345,6 +353,10 @@ def unblock_username(username, pipe=None):
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
send_username_unblock_signal(username)
|
send_username_unblock_signal(username)
|
||||||
|
|
||||||
|
redis_cache_key = get_approx_lockouts_cache_key(None, username)
|
||||||
|
redis_client = get_redis_connection()
|
||||||
|
redis_client.delete(redis_cache_key)
|
||||||
|
|
||||||
|
|
||||||
def reset_failed_attempts(ip_address=None, username=None):
|
def reset_failed_attempts(ip_address=None, username=None):
|
||||||
""" reset the failed attempts for these ip's and usernames
|
""" reset the failed attempts for these ip's and usernames
|
||||||
|
|
@ -357,6 +369,10 @@ def reset_failed_attempts(ip_address=None, username=None):
|
||||||
unblock_ip(ip_address, pipe=pipe)
|
unblock_ip(ip_address, pipe=pipe)
|
||||||
unblock_username(username, pipe=pipe)
|
unblock_username(username, pipe=pipe)
|
||||||
|
|
||||||
|
redis_cache_key = get_approx_lockouts_cache_key(ip_address, username)
|
||||||
|
redis_client = get_redis_connection()
|
||||||
|
redis_client.delete(redis_cache_key)
|
||||||
|
|
||||||
pipe.execute()
|
pipe.execute()
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,8 @@
|
||||||
-e .
|
-e .
|
||||||
coverage
|
coverage
|
||||||
mockredispy
|
mockredispy
|
||||||
django-redis-cache
|
django-redis>=5,<6
|
||||||
|
redis>=5,<6
|
||||||
importlib-metadata<5.0
|
importlib-metadata<5.0
|
||||||
celery
|
celery
|
||||||
sphinx_rtd_theme==2.0.0
|
sphinx_rtd_theme==2.0.0
|
||||||
|
|
|
||||||
3
tox.ini
3
tox.ini
|
|
@ -2,7 +2,8 @@
|
||||||
envlist =
|
envlist =
|
||||||
# list of supported Django/Python versions:
|
# list of supported Django/Python versions:
|
||||||
py{37,38,39,py3}-dj{32}
|
py{37,38,39,py3}-dj{32}
|
||||||
py{38,39,310,311}-dj{40,41,42,main}
|
py{38,39,310,311}-dj{40,41,42}
|
||||||
|
py{311}-dj{main}
|
||||||
py38-{lint,docs}
|
py38-{lint,docs}
|
||||||
|
|
||||||
[gh-actions]
|
[gh-actions]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue