Skip to content

Commit 6b56b51

Browse files
Make key_getter mandatory for RateLimiter
1 parent bad2fde commit 6b56b51

3 files changed

Lines changed: 5 additions & 20 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1414
can raise `InvalidCredentialsError` when invalid credentials are provided, to
1515
enable automatic logging and, if enabled, brute-force protection.
1616
- Add `RateLimiter` class that can block authentication attempts after a configurable
17-
threshold is exceeded. By default stores failed attempts in-memory. This feature is
18-
disabled unless a `key_getter` function is provided (a function to obtain a context
19-
key from the user-defined authentication context, like a client IP).
17+
threshold is exceeded. By default stores failed attempts in-memory.
2018
- Integrate `RateLimiter` into `AuthenticationStrategy` with automatic tracking of
2119
failed authentication attempts and support for blocking excessive requests.
2220
- Add Python `3.14` and remove `3.9` from the build matrix.

guardpost/protection.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,7 @@ class RateLimiter:
137137

138138
def __init__(
139139
self,
140-
key_getter: Optional[Callable[[Any], str]] = None,
140+
key_getter: Callable[[Any], str],
141141
threshold: int = 20,
142142
block_time: int = 60,
143143
store: Optional[AuthenticationAttemptsStore] = None,
@@ -148,10 +148,8 @@ def __init__(
148148
Initialize a RateLimiter instance for brute-force protection.
149149
150150
Args:
151-
key_getter: Optional callable that extracts a unique key from the
151+
key_getter: Callable that extracts a unique key from the
152152
authentication context (e.g., client IP address, username).
153-
If None, brute-force protection is disabled and a deprecation
154-
warning is issued.
155153
threshold: Maximum number of failed authentication attempts allowed
156154
before blocking. Must be a positive integer. Defaults to 20.
157155
block_time: Duration in seconds to block further attempts after
@@ -173,6 +171,8 @@ def __init__(
173171
self._store = store or SelfCleaningInMemoryAuthenticationAttemptsStore(
174172
max_entry_age=self._block_time + 5
175173
)
174+
if not key_getter:
175+
raise TypeError("Missing key_getter")
176176
self._key_getter = key_getter
177177
self._logger = logger or logging.getLogger("guardpost")
178178

@@ -182,8 +182,6 @@ def get_context_key(self, context: Any) -> str:
182182
If a key_getter is not configured, rate limiting is disabled and
183183
returns an empty string.
184184
"""
185-
if not self._key_getter:
186-
return ""
187185
return self._key_getter(context)
188186

189187
async def validate_authentication_attempt(self, context: Any) -> None:

tests/test_protection.py

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -149,17 +149,6 @@ def test_get_context_key(self, basic_limiter):
149149
key = basic_limiter.get_context_key(context)
150150
assert key == "192.168.1.1"
151151

152-
def test_get_context_key_no_extractor(self):
153-
limiter = RateLimiter(key_getter=None)
154-
key = limiter.get_context_key({"ip": "192.168.1.1"})
155-
assert key == ""
156-
157-
@pytest.mark.asyncio
158-
async def test_allow_authentication_attempt_no_key(self):
159-
limiter = RateLimiter(key_getter=None)
160-
result = await limiter.allow_authentication_attempt({})
161-
assert result is True
162-
163152
@pytest.mark.asyncio
164153
async def test_allow_authentication_attempt_trusted_key(self):
165154
limiter = RateLimiter(key_getter=self.key_getter, trusted_keys=["192.168.1.1"])

0 commit comments

Comments
 (0)