add chache cleanup and locking

This commit is contained in:
Peter Bieringer 2024-12-31 16:13:52 +01:00
parent 79ba07e16b
commit 5ce0cee8bf

View file

@ -31,6 +31,7 @@ Take a look at the class ``BaseAuth`` if you want to implement your own.
import hashlib import hashlib
import time import time
import threading
from typing import Sequence, Set, Tuple, Union, final from typing import Sequence, Set, Tuple, Union, final
from radicale import config, types, utils from radicale import config, types, utils
@ -63,9 +64,10 @@ class BaseAuth:
_cache_logins: bool _cache_logins: bool
_cache_successful: dict # login -> (digest, time_ns) _cache_successful: dict # login -> (digest, time_ns)
_cache_successful_logins_expiry: int _cache_successful_logins_expiry: int
_cache_failed: dict # digest_failed -> (time_ns) _cache_failed: dict # digest_failed -> (time_ns, login)
_cache_failed_logins_expiry: int _cache_failed_logins_expiry: int
_cache_failed_logins_salt_ns: int # persistent over runtime _cache_failed_logins_salt_ns: int # persistent over runtime
_lock: threading.Lock
def __init__(self, configuration: "config.Configuration") -> None: def __init__(self, configuration: "config.Configuration") -> None:
"""Initialize BaseAuth. """Initialize BaseAuth.
@ -105,6 +107,7 @@ class BaseAuth:
self._cache_successful = dict() self._cache_successful = dict()
self._cache_failed = dict() self._cache_failed = dict()
self._cache_failed_logins_salt_ns = time.time_ns() self._cache_failed_logins_salt_ns = time.time_ns()
self._lock = threading.Lock()
def _cache_digest(self, login: str, password: str, salt: str) -> str: def _cache_digest(self, login: str, password: str, salt: str) -> str:
h = hashlib.sha3_512() h = hashlib.sha3_512()
@ -152,19 +155,34 @@ class BaseAuth:
result = "" result = ""
digest = "" digest = ""
time_ns = time.time_ns() time_ns = time.time_ns()
# cleanup failed login cache to avoid out-of-memory
cache_failed_entries = len(self._cache_failed)
if cache_failed_entries > 0:
logger.debug("Login failed cache investigation start (entries: %d)", cache_failed_entries)
self._lock.acquire()
cache_failed_cleanup = dict()
for digest in self._cache_failed:
(time_ns_cache, login_cache) = self._cache_failed[digest]
age_failed = int((time_ns - time_ns_cache) / 1000 / 1000 / 1000)
if age_failed > self._cache_failed_logins_expiry:
cache_failed_cleanup[digest] = (login_cache, age_failed)
cache_failed_cleanup_entries = len(cache_failed_cleanup)
logger.debug("Login failed cache cleanup start (entries: %d)", cache_failed_cleanup_entries)
if cache_failed_cleanup_entries > 0:
for digest in cache_failed_cleanup:
(login, age_failed) = cache_failed_cleanup[digest]
logger.debug("Login failed cache entry for user+password expired: '%s' (age: %d > %d sec)", login_cache, age_failed, self._cache_failed_logins_expiry)
del self._cache_failed[digest]
self._lock.release()
logger.debug("Login failed cache investigation finished")
# check for cache failed login
digest_failed = login + ":" + self._cache_digest(login, password, str(self._cache_failed_logins_salt_ns)) digest_failed = login + ":" + self._cache_digest(login, password, str(self._cache_failed_logins_salt_ns))
if self._cache_failed.get(digest_failed): if self._cache_failed.get(digest_failed):
# login+password found in cache "failed" # login+password found in cache "failed" -> shortcut return
time_ns_cache = self._cache_failed[digest_failed] (time_ns_cache, login_cache) = self._cache_failed[digest]
age_failed = int((time_ns - time_ns_cache) / 1000 / 1000 / 1000) age_failed = int((time_ns - time_ns_cache) / 1000 / 1000 / 1000)
if age_failed > self._cache_failed_logins_expiry: logger.debug("Login failed cache entry for user+password found: '%s' (age: %d sec)", login_cache, age_failed)
logger.debug("Login failed cache entry for user+password found but expired: '%s' (age: %d > %d sec)", login, age_failed, self._cache_failed_logins_expiry) return ""
# delete expired failed from cache
del self._cache_failed[digest_failed]
else:
# shortcut return
logger.debug("Login failed cache entry for user+password found: '%s' (age: %d sec)", login, age_failed)
return ""
if self._cache_successful.get(login): if self._cache_successful.get(login):
# login found in cache "successful" # login found in cache "successful"
(digest_cache, time_ns_cache) = self._cache_successful[login] (digest_cache, time_ns_cache) = self._cache_successful[login]
@ -194,14 +212,18 @@ class BaseAuth:
# successful login, but expired, digest must be recalculated # successful login, but expired, digest must be recalculated
digest = self._cache_digest(login, password, str(time_ns)) digest = self._cache_digest(login, password, str(time_ns))
# store successful login in cache # store successful login in cache
self._lock.acquire()
self._cache_successful[login] = (digest, time_ns) self._cache_successful[login] = (digest, time_ns)
self._lock.release()
logger.debug("Login successful cache for user set: '%s'", login) logger.debug("Login successful cache for user set: '%s'", login)
if self._cache_failed.get(digest_failed): if self._cache_failed.get(digest_failed):
logger.debug("Login failed cache for user cleared: '%s'", login) logger.debug("Login failed cache for user cleared: '%s'", login)
del self._cache_failed[digest_failed] del self._cache_failed[digest_failed]
else: else:
logger.debug("Login failed for user+password via backend: '%s'", login) logger.debug("Login failed for user+password via backend: '%s'", login)
self._cache_failed[digest_failed] = time_ns self._lock.acquire()
self._cache_failed[digest_failed] = (time_ns, login)
self._lock.release()
logger.debug("Login failed cache for user set: '%s'", login) logger.debug("Login failed cache for user set: '%s'", login)
return result return result
else: else: