Make rate-limit optional [AI-assisted]

Enabled by default for kustvakt.conf and kustvakt-test.conf

The RateLimitFilter is now registered as a @Provider and has been
removed from @ResourceFilters.

Change-Id: I281e42614c2bb58cb84766969e6b3cae1f89c172
diff --git a/Changes b/Changes
index 48eda79..000f317 100644
--- a/Changes
+++ b/Changes
@@ -5,6 +5,7 @@
 - Fix handling non-existent large context group
 - Make large context group optional and disabled by default [AI-assisted]
 - Add tests for large context group config [AI-assisted]
+- Make rate-limit optional and enabled by default [AI-assisted]
 
 # version 1.2-SNAPSHOT
 
diff --git a/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java b/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
index 4692cd8..37fff2d 100644
--- a/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
+++ b/src/main/java/de/ids_mannheim/korap/core/web/controller/SearchController.java
@@ -28,7 +28,6 @@
 import de.ids_mannheim.korap.web.filter.APIDeprecationFilter;
 import de.ids_mannheim.korap.web.filter.APIVersionFilter;
 import de.ids_mannheim.korap.web.filter.AdminFilter;
-import de.ids_mannheim.korap.web.filter.RateLimitFilter;
 import de.ids_mannheim.korap.web.filter.AuthenticationFilter;
 import de.ids_mannheim.korap.web.filter.DemoUserFilter;
 import de.ids_mannheim.korap.web.utils.ResourceFilters;
@@ -59,7 +58,7 @@
 @Controller
 @Path("/")
 @ResourceFilters({ APIVersionFilter.class, AuthenticationFilter.class,
-        DemoUserFilter.class, RateLimitFilter.class })
+        DemoUserFilter.class })
 public class SearchController {
 
     private static final boolean DEBUG = false;
diff --git a/src/main/java/de/ids_mannheim/korap/web/filter/RateLimitFilter.java b/src/main/java/de/ids_mannheim/korap/web/filter/RateLimitFilter.java
index 7a09360..fed616d 100644
--- a/src/main/java/de/ids_mannheim/korap/web/filter/RateLimitFilter.java
+++ b/src/main/java/de/ids_mannheim/korap/web/filter/RateLimitFilter.java
@@ -23,6 +23,7 @@
 import jakarta.ws.rs.container.ContainerRequestFilter;
 import jakarta.ws.rs.core.HttpHeaders;
 import jakarta.ws.rs.core.Response;
+import jakarta.ws.rs.ext.Provider;
 import lombok.Getter;
 
 /**
@@ -34,6 +35,7 @@
  * 
  * Implemented with AI assistance
  */
+@Provider
 @Component
 @Priority(Priorities.AUTHORIZATION)
 public class RateLimitFilter implements ContainerRequestFilter {
@@ -45,6 +47,7 @@
     private KustvaktConfiguration config;
 
     // Rate limiting configuration (loaded from kustvakt.conf)
+    private boolean enabled;
     private long refillTokens;
     private Duration refillPeriod;
     @Getter
@@ -67,6 +70,14 @@
             }
             
             // Load rate limiting settings from kustvakt.conf with sensible defaults
+            String enabledStr = props.getProperty("ratelimit.enabled", "true");
+            this.enabled = Boolean.parseBoolean(enabledStr);
+
+            if (!enabled) {
+                jlog.info("Rate limiting is disabled (ratelimit.enabled=false)");
+                return;
+            }
+
             String refillTokensStr = props.getProperty("ratelimit.refill.tokens", "60");
             this.refillTokens = Long.parseLong(refillTokensStr);
 
@@ -91,6 +102,7 @@
     }
     
     private void setDefaultValues() {
+        this.enabled = true;
         this.refillTokens = 60;
         this.refillPeriod = Duration.ofMinutes(1);
         this.burstCapacity = 60;
@@ -102,6 +114,9 @@
 
     @Override
     public void filter (ContainerRequestContext request) {
+        if (!enabled) {
+            return;
+        }
         // Only apply to authenticated requests
         if (request.getSecurityContext() == null
                 || request.getSecurityContext().getUserPrincipal() == null) {
@@ -156,6 +171,18 @@
         jlog.info("Rate limit buckets cleared");
     }
 
+    /**
+     * Enable or disable rate limiting at runtime. For testing purposes only.
+     */
+    public void setEnabled (boolean enabled) {
+        this.enabled = enabled;
+        jlog.info("Rate limiting enabled set to: {}", enabled);
+    }
+
+    public boolean isEnabled () {
+        return enabled;
+    }
+
     private void cleanupOldEntries (long nowMillis) {
         final long cutoff = nowMillis - bucketTTL.toMillis();
         buckets.entrySet().removeIf(e -> e.getValue().lastSeenAtMillis < cutoff);
diff --git a/src/main/resources/kustvakt.conf b/src/main/resources/kustvakt.conf
index 8bd0ff9..497efd4 100644
--- a/src/main/resources/kustvakt.conf
+++ b/src/main/resources/kustvakt.conf
@@ -62,6 +62,8 @@
 
 # Rate limiting for authenticated users
 #
+# Enable or disable rate limiting (true/false)
+ratelimit.enabled = true
 # Number of requests allowed per time period
 ratelimit.refill.tokens = 60
 # Time period for token refill (format: 1S, 30M, 1H, 1D)
diff --git a/src/test/java/de/ids_mannheim/korap/web/controller/RateLimitTest.java b/src/test/java/de/ids_mannheim/korap/web/controller/RateLimitTest.java
index 7c74082..fdcdc3c 100644
--- a/src/test/java/de/ids_mannheim/korap/web/controller/RateLimitTest.java
+++ b/src/test/java/de/ids_mannheim/korap/web/controller/RateLimitTest.java
@@ -4,6 +4,7 @@
 
 import org.junit.jupiter.api.BeforeEach;
 import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.parallel.Isolated;
 import org.springframework.beans.factory.annotation.Autowired;
 
 import com.fasterxml.jackson.databind.JsonNode;
@@ -21,6 +22,7 @@
  * 
  * Implemented with AI assistance
  */
+@Isolated
 public class RateLimitTest extends OAuth2TestBase {
 	@Autowired
     private RateLimitFilter rateLimitFilter;
@@ -51,4 +53,29 @@
 		assertEquals(429, limited.getStatus());
 		limited.close();
 	}
+
+	@Test
+	public void testRateLimitDisabled () throws KustvaktException {
+		rateLimitFilter.setEnabled(false);
+		try {
+			Response response = requestTokenWithDoryPassword(superClientId,
+					clientSecret);
+			JsonNode node = JsonUtils
+					.readTree(response.readEntity(String.class));
+			String accessToken = node.at("/access_token").asText();
+
+			// Exceed burst capacity – all requests should still succeed
+			long overLimit = rateLimitFilter.getBurstCapacity() + 5;
+			for (long i = 0; i < overLimit; i++) {
+				Response r = searchWithAccessToken(accessToken);
+				assertEquals(Status.OK.getStatusCode(), r.getStatus(),
+						"request " + i + " should succeed when rate limiting is disabled");
+				r.close();
+			}
+		}
+		finally {
+			// Always re-enable so other tests are not affected
+			rateLimitFilter.setEnabled(true);
+		}
+	}
 }
\ No newline at end of file
diff --git a/src/test/resources/kustvakt-test.conf b/src/test/resources/kustvakt-test.conf
index 8f7cf6a..18c4a9f 100644
--- a/src/test/resources/kustvakt-test.conf
+++ b/src/test/resources/kustvakt-test.conf
@@ -66,6 +66,8 @@
 
 # Rate limiting for authenticated users
 #
+# Enable or disable rate limiting (true/false)
+ratelimit.enabled = true
 # Number of requests allowed per time period
 ratelimit.refill.tokens = 5
 # Time period for token refill (format: 1S, 30M, 1H, 1D)