Skip to content

Commit 954838d

Browse files
committed
Customize rate limit settings in kustvakt.conf
[AI assisted] Change-Id: I5907dbd839151b00bab2dfea9dedc3d5ffb11765
1 parent b7d299b commit 954838d

6 files changed

Lines changed: 132 additions & 30 deletions

File tree

Changes

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
- Change Userdata to use String username instead of integer userId
77
- Allow admin to create groups with name length less than 3 characters
88
to support existing groups from C2
9-
- Implemented rate limit for authenticated users (with AI assistance)
9+
- Implemented rate limit for authenticated users (AI assisted)
10+
- Customize rate limit settings in kustvakt.conf (AI assisted)
1011

1112
# version 1.1
1213

src/main/java/de/ids_mannheim/korap/web/filter/RateLimitFilter.java

Lines changed: 87 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,34 @@
55
import java.time.Duration;
66
import java.util.Base64;
77
import java.util.Objects;
8+
import java.util.Properties;
89
import java.util.concurrent.ConcurrentHashMap;
910

1011
import org.apache.logging.log4j.LogManager;
1112
import org.apache.logging.log4j.Logger;
13+
import org.springframework.beans.factory.annotation.Autowired;
1214
import org.springframework.stereotype.Component;
1315

16+
import de.ids_mannheim.korap.config.KustvaktConfiguration;
17+
import de.ids_mannheim.korap.utils.TimeUtils;
18+
import jakarta.annotation.PostConstruct;
1419
import jakarta.annotation.Priority;
1520
import jakarta.ws.rs.Priorities;
1621
import jakarta.ws.rs.WebApplicationException;
1722
import jakarta.ws.rs.container.ContainerRequestContext;
1823
import jakarta.ws.rs.container.ContainerRequestFilter;
1924
import jakarta.ws.rs.core.HttpHeaders;
2025
import jakarta.ws.rs.core.Response;
26+
import lombok.Getter;
2127

22-
/** Implemented with AI assistance
23-
*
28+
/**
2429
* Simple in-memory rate limitation for authenticated users.
2530
* <p>
2631
* Keyed by bearer token (preferred) or username (fallback).
2732
* <p>
2833
* Note: In-memory means per-JVM only. For clustered deployments, use Redis/etc.
34+
*
35+
* Implemented with AI assistance
2936
*/
3037
@Component
3138
@Priority(Priorities.AUTHORIZATION)
@@ -34,53 +41,96 @@ public class RateLimitFilter implements ContainerRequestFilter {
3441
private static final Logger jlog = LogManager
3542
.getLogger(RateLimitFilter.class);
3643

37-
// Defaults: 60 requests per minute per key
38-
// Keep these conservative and easy to change later via config injection.
39-
private static final long REFILL_TOKENS = 60;
40-
private static final Duration REFILL_PERIOD = Duration.ofMinutes(1);
41-
public static final long BURST_CAPACITY = 60;
42-
43-
/**
44-
* Prevent unbounded growth: keep at most this many distinct keys in-memory.
45-
*/
46-
private static final int MAX_BUCKETS = 10_000;
44+
@Autowired
45+
private KustvaktConfiguration config;
4746

48-
/**
49-
* Evict buckets that haven't been seen for this long.
50-
*/
51-
private static final Duration BUCKET_TTL = Duration.ofHours(6);
47+
// Rate limiting configuration (loaded from kustvakt.conf)
48+
private long refillTokens;
49+
private Duration refillPeriod;
50+
@Getter
51+
private long burstCapacity;
52+
private int maxBuckets;
53+
private Duration bucketTTL;
5254

5355
private final ConcurrentHashMap<String, BucketEntry> buckets = new ConcurrentHashMap<>();
5456

57+
@PostConstruct
58+
private void initializeConfiguration() {
59+
try {
60+
Properties props = config.getProperties();
61+
62+
// Handle case where properties might not be initialized yet
63+
if (props == null) {
64+
jlog.warn("KustvaktConfiguration properties not available, using default rate limiting values");
65+
setDefaultValues();
66+
return;
67+
}
68+
69+
// Load rate limiting settings from kustvakt.conf with sensible defaults
70+
String refillTokensStr = props.getProperty("ratelimit.refill.tokens", "60");
71+
this.refillTokens = Long.parseLong(refillTokensStr);
72+
73+
String refillPeriodStr = props.getProperty("ratelimit.refill.period", "1M");
74+
this.refillPeriod = Duration.ofSeconds(TimeUtils.convertTimeToSeconds(refillPeriodStr));
75+
76+
String burstCapacityStr = props.getProperty("ratelimit.burst.capacity", "60");
77+
this.burstCapacity = Long.parseLong(burstCapacityStr);
78+
79+
String maxBucketsStr = props.getProperty("ratelimit.max.buckets", "10000");
80+
this.maxBuckets = Integer.parseInt(maxBucketsStr);
81+
82+
String bucketTTLStr = props.getProperty("ratelimit.bucket.ttl", "6H");
83+
this.bucketTTL = Duration.ofSeconds(TimeUtils.convertTimeToSeconds(bucketTTLStr));
84+
85+
jlog.info("Rate limiting initialized: refillTokens={}, refillPeriod={}, burstCapacity={}, maxBuckets={}, bucketTTL={}",
86+
refillTokens, refillPeriod, burstCapacity, maxBuckets, bucketTTL);
87+
} catch (Exception e) {
88+
jlog.error("Failed to initialize rate limiting configuration, using defaults", e);
89+
setDefaultValues();
90+
}
91+
}
92+
93+
private void setDefaultValues() {
94+
this.refillTokens = 60;
95+
this.refillPeriod = Duration.ofMinutes(1);
96+
this.burstCapacity = 60;
97+
this.maxBuckets = 10000;
98+
this.bucketTTL = Duration.ofHours(6);
99+
jlog.info("Rate limiting initialized with defaults: refillTokens={}, refillPeriod={}, burstCapacity={}, maxBuckets={}, bucketTTL={}",
100+
refillTokens, refillPeriod, burstCapacity, maxBuckets, bucketTTL);
101+
}
102+
55103
@Override
56104
public void filter (ContainerRequestContext request) {
57105
// Only apply to authenticated requests
58106
if (request.getSecurityContext() == null
59107
|| request.getSecurityContext().getUserPrincipal() == null) {
108+
jlog.debug("Skipping rate limiting - no SecurityContext or UserPrincipal");
60109
return;
61110
}
62111

63112
String key = resolveKey(request);
64113
if (key == null) {
114+
jlog.debug("Skipping rate limiting - could not resolve key");
65115
return;
66116
}
67117

118+
jlog.debug("Applying rate limiting for key: {}", key);
119+
68120
long now = System.currentTimeMillis();
69121

70122
// Opportunistic cleanup to avoid memory growth.
71-
// Do it only on inserts or if we grow too large.
72-
if (buckets.size() > MAX_BUCKETS) {
123+
if (buckets.size() > maxBuckets) {
73124
cleanupOldEntries(now);
74125
}
75126

76127
BucketEntry entry = buckets.compute(key, (k, existing) -> {
77128
if (existing == null) {
78-
// If we're still too large, try another cleanup pass before adding.
79-
if (buckets.size() > MAX_BUCKETS) {
129+
if (buckets.size() > maxBuckets) {
80130
cleanupOldEntries(now);
81131
}
82-
return new BucketEntry(new TokenBucket(BURST_CAPACITY,
83-
REFILL_TOKENS, REFILL_PERIOD.toMillis()), now);
132+
return new BucketEntry(new TokenBucket(burstCapacity,
133+
refillTokens, refillPeriod.toMillis()), now);
84134
}
85135
existing.lastSeenAtMillis = now;
86136
return existing;
@@ -90,20 +140,29 @@ public void filter (ContainerRequestContext request) {
90140
long retryAfterSeconds = Math
91141
.max(1, entry.bucket.millisUntilNextToken() / 1000);
92142

143+
jlog.info("Rate limit exceeded for key: {}, retry after {} seconds", key, retryAfterSeconds);
93144
throw new WebApplicationException(Response.status(429)
94145
.header("Retry-After", String.valueOf(retryAfterSeconds))
95146
.entity("Rate limit exceeded")
96147
.build());
97148
}
98149
}
99150

151+
/**
152+
* Clear all rate limit buckets. For testing purposes only.
153+
*/
154+
public void clearBuckets() {
155+
buckets.clear();
156+
jlog.info("Rate limit buckets cleared");
157+
}
158+
100159
private void cleanupOldEntries (long nowMillis) {
101-
final long cutoff = nowMillis - BUCKET_TTL.toMillis();
160+
final long cutoff = nowMillis - bucketTTL.toMillis();
102161
buckets.entrySet().removeIf(e -> e.getValue().lastSeenAtMillis < cutoff);
103162

104163
// Still too big? Remove arbitrary entries (best-effort bound).
105-
if (buckets.size() > MAX_BUCKETS) {
106-
int toRemove = buckets.size() - MAX_BUCKETS;
164+
if (buckets.size() > maxBuckets) {
165+
int toRemove = buckets.size() - maxBuckets;
107166
for (String k : buckets.keySet()) {
108167
buckets.remove(k);
109168
if (--toRemove <= 0)
@@ -123,6 +182,8 @@ private String resolveKey (ContainerRequestContext request) {
123182
}
124183
}
125184

185+
// Skip unauthenticated requests. DemoUserFilter sets username guest
186+
// for such requests.
126187
// Fallback to username/principal name
127188
// String name = request.getSecurityContext().getUserPrincipal().getName();
128189
// if (name != null && !name.isBlank()) {
@@ -208,4 +269,4 @@ private static final class BucketEntry {
208269
this.lastSeenAtMillis = lastSeenAtMillis;
209270
}
210271
}
211-
}
272+
}

src/main/resources/kustvakt.conf

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,19 @@ availability.regex.free = CC.*
5959
availability.regex.public = ACA.*|QAO-NC
6060
availability.regex.all = QAO.*
6161

62+
# Rate limiting for authenticated users
63+
#
64+
# Number of requests allowed per time period
65+
ratelimit.refill.tokens = 60
66+
# Time period for token refill (format: 1S, 30M, 1H, 1D)
67+
ratelimit.refill.period = 1M
68+
# Maximum burst capacity (tokens that can be consumed immediately)
69+
ratelimit.burst.capacity = 60
70+
# Maximum number of rate limit buckets to keep in memory
71+
ratelimit.max.buckets = 10000
72+
# Time to live for unused rate limit buckets
73+
ratelimit.bucket.ttl = 6H
74+
6275
# options referring to the security module!
6376

6477
# OAuth

src/test/java/de/ids_mannheim/korap/web/controller/RateLimitTest.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import static org.junit.jupiter.api.Assertions.assertEquals;
44

5+
import org.junit.jupiter.api.BeforeEach;
56
import org.junit.jupiter.api.Test;
7+
import org.springframework.beans.factory.annotation.Autowired;
68

79
import com.fasterxml.jackson.databind.JsonNode;
810

@@ -13,12 +15,23 @@
1315
import jakarta.ws.rs.core.Response;
1416
import jakarta.ws.rs.core.Response.Status;
1517

16-
/**
18+
/**
1719
* Verifies authenticated rate limiting (HTTP 429) is applied after
1820
* auth.
21+
*
22+
* Implemented with AI assistance
1923
*/
2024
public class RateLimitTest extends OAuth2TestBase {
25+
@Autowired
26+
private RateLimitFilter rateLimitFilter;
2127

28+
@BeforeEach
29+
public void clearRateLimitState() {
30+
// Clear rate limit state before each test
31+
if (rateLimitFilter != null) {
32+
rateLimitFilter.clearBuckets();
33+
}
34+
}
2235
@Test
2336
public void testAuthenticatedRateLimitBearerToken ()
2437
throws KustvaktException {
@@ -27,7 +40,7 @@ public void testAuthenticatedRateLimitBearerToken ()
2740
JsonNode node = JsonUtils.readTree(response.readEntity(String.class));
2841
String accessToken = node.at("/access_token").asText();
2942

30-
for (long i = 0; i < RateLimitFilter.BURST_CAPACITY; i++) {
43+
for (long i = 0; i < rateLimitFilter.getBurstCapacity(); i++) {
3144
Response r = searchWithAccessToken(accessToken);
3245
assertEquals(Status.OK.getStatusCode(), r.getStatus(),
3346
"request " + i);

src/test/java/de/ids_mannheim/korap/web/lite/RateLimitAnonymousTest.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
import jakarta.ws.rs.core.Response;
99
import jakarta.ws.rs.core.Response.Status;
1010

11-
/**
11+
/**
1212
* Verifies unauthenticated requests are not rate-limited.
13+
*
14+
* Implemented with AI assistance
1315
*/
1416
public class RateLimitAnonymousTest extends LiteJerseyTest {
1517

src/test/resources/kustvakt-test.conf

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,18 @@ availability.regex.public = ACA.*|QAO-NC
6161
# availability.regex.all = QAO.*
6262
availability.regex.all = QAO-NC-LOC:ids.*
6363

64+
# Rate limiting for authenticated users
65+
#
66+
# Number of requests allowed per time period
67+
ratelimit.refill.tokens = 5
68+
# Time period for token refill (format: 1S, 30M, 1H, 1D)
69+
ratelimit.refill.period = 1M
70+
# Maximum burst capacity (tokens that can be consumed immediately)
71+
ratelimit.burst.capacity = 5
72+
# Maximum number of rate limit buckets to keep in memory
73+
ratelimit.max.buckets = 10000
74+
# Time to live for unused rate limit buckets
75+
ratelimit.bucket.ttl = 6H
6476

6577
# options referring to the security module!
6678

0 commit comments

Comments
 (0)