This is an automated email from the ASF dual-hosted git repository.
houston pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/branch_9x by this push:
new 1b51c6e8996 SOLR-16951: Add PKI Auth Caching for both generation and
validation (#3334)
1b51c6e8996 is described below
commit 1b51c6e899621b0ea3cbcb1aaab1e549d0a00614
Author: Houston Putman <[email protected]>
AuthorDate: Tue Jun 10 12:25:26 2025 -0500
SOLR-16951: Add PKI Auth Caching for both generation and validation (#3334)
(cherry picked from commit 981b5d2b908d635788711a6b52df3fcdc7883624)
---
solr/CHANGES.txt | 3 ++
.../solr/security/PKIAuthenticationPlugin.java | 58 ++++++++++++++++++----
2 files changed, 51 insertions(+), 10 deletions(-)
diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt
index 6bb6df5e1e0..0b265ebe8ae 100644
--- a/solr/CHANGES.txt
+++ b/solr/CHANGES.txt
@@ -58,6 +58,9 @@ Improvements
* SOLR-17750: S3 File downloads now handle connection issues more gracefully
(Houston Putman, Mark Miller)
+* SOLR-16951: Add PKI Auth Caching for both generation of the PKI Auth Tokens
and validation of received tokens.
+ The default PKI Auth validity TTL has been increased from 5 seconds to 10
seconds. (Houston Putman)
+
Optimizations
---------------------
* SOLR-17578: Remove ZkController internal core supplier, for slightly faster
reconnection after Zookeeper session loss. (Pierre Salagnac)
diff --git
a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
index e23c90e9b23..ff01947cbdf 100644
--- a/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
+++ b/solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java
@@ -19,6 +19,8 @@ package org.apache.solr.security;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
+import com.github.benmanes.caffeine.cache.Caffeine;
+import com.github.benmanes.caffeine.cache.LoadingCache;
import com.google.common.annotations.VisibleForTesting;
import java.io.IOException;
import java.lang.invoke.MethodHandles;
@@ -34,9 +36,7 @@ import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
-import javax.servlet.FilterChain;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+import java.util.concurrent.TimeUnit;
import org.apache.http.HttpEntity;
import org.apache.http.HttpException;
import org.apache.http.HttpHeaders;
@@ -94,7 +94,10 @@ public class PKIAuthenticationPlugin extends
AuthenticationPlugin
private final Map<String, PublicKey> keyCache = new ConcurrentHashMap<>();
private final PublicKeyHandler publicKeyHandler;
private final CoreContainer cores;
- private static final int MAX_VALIDITY = Integer.getInteger("pkiauth.ttl",
5000);
+ private final LoadingCache<String, PKIHeaderData> validatedHeaderCache;
+ private final LoadingCache<String, String> generatedV1TokenCache;
+ private final LoadingCache<String, String> generatedV2TokenCache;
+ private static final int MAX_VALIDITY = Integer.getInteger("pkiauth.ttl",
10000);
private final String myNodeName;
private final HttpHeaderClientInterceptor interceptor = new
HttpHeaderClientInterceptor();
private boolean interceptorRegistered = false;
@@ -114,6 +117,38 @@ public class PKIAuthenticationPlugin extends
AuthenticationPlugin
this.cores = cores;
myNodeName = nodeName;
+ // Don't expire after read, because there is no reason to add the overhead
of updating expiry
+ // information after each read. The expiration time here doesn't matter
too much, because we
+ // still check the PKI Token TTL after fetching from the cache. We just
want to make sure cache
+ // entries are cleaned up regularly.
+ validatedHeaderCache =
+ Caffeine.newBuilder()
+ .maximumSize(1000)
+ .expireAfterWrite(MAX_VALIDITY, TimeUnit.MILLISECONDS)
+ .build(this::decipherHeaderV2);
+ // We must expire much earlier than the max validity, because these cached
Auth tokens still
+ // need to be sent to the server, which will validate the TTL. If we
expire at maxValidity, the
+ // TTL check will always fail before the cache entry is expired.
+ long expireAfterTime = MAX_VALIDITY / 4;
+ // Refreshing is done asynchronously, so we want to do it before
expiration. This means that
+ // requests are not synchronously blocked when generating new Auth tokens.
However, the refresh
+ // will only happen when the cached header is requested. Therefore, we
want to give a long-ish
+ // runway for requests to come in to trigger an asynchronous-refresh
before expiry causes a
+ // synchronous-refresh.
+ long shouldRefreshTime = Math.max(1, expireAfterTime / 2);
+ generatedV1TokenCache =
+ Caffeine.newBuilder()
+ .maximumSize(100)
+ .refreshAfterWrite(shouldRefreshTime, TimeUnit.MILLISECONDS)
+ .expireAfterWrite(expireAfterTime, TimeUnit.MILLISECONDS)
+ .build(this::generateToken);
+ generatedV2TokenCache =
+ Caffeine.newBuilder()
+ .maximumSize(100)
+ .refreshAfterWrite(shouldRefreshTime, TimeUnit.MILLISECONDS)
+ .expireAfterWrite(expireAfterTime, TimeUnit.MILLISECONDS)
+ .build(this::generateTokenV2);
+
Set<String> knownPkiVersions = Set.of("v1", "v2");
// We always accept v2 even if it is not specified
String[] versions = System.getProperty(ACCEPT_VERSIONS, "v2").split(",");
@@ -155,7 +190,7 @@ public class PKIAuthenticationPlugin extends
AuthenticationPlugin
return sendError(response, true, "Could not parse node name from
SolrAuthV2 header.");
}
- headerData = decipherHeaderV2(headerV2, headerV2.substring(0,
nodeNameEnd));
+ headerData = validatedHeaderCache.get(headerV2);
} else if (headerV1 != null && acceptPkiV1) {
List<String> authInfo = StrUtils.splitWS(headerV1, false);
if (authInfo.size() != 2) {
@@ -223,7 +258,8 @@ public class PKIAuthenticationPlugin extends
AuthenticationPlugin
return key;
}
- private PKIHeaderData decipherHeaderV2(String header, String nodeName) {
+ private PKIHeaderData decipherHeaderV2(String header) {
+ String nodeName = header.substring(0, header.indexOf(' '));
PublicKey key = getOrFetchPublicKey(nodeName);
int sigStart = header.lastIndexOf(' ');
@@ -414,11 +450,11 @@ public class PKIAuthenticationPlugin extends
AuthenticationPlugin
final Optional<String> preFetchedUser =
getUserFromJettyRequest(request);
if ("v1".equals(System.getProperty(SEND_VERSION))) {
preFetchedUser
- .map(PKIAuthenticationPlugin.this::generateToken)
+ .map(generatedV1TokenCache::get)
.ifPresent(token -> request.header(HEADER, token));
} else {
preFetchedUser
- .map(PKIAuthenticationPlugin.this::generateTokenV2)
+ .map(generatedV2TokenCache::get)
.ifPresent(token -> request.header(HEADER_V2, token));
}
}
@@ -519,10 +555,12 @@ public class PKIAuthenticationPlugin extends
AuthenticationPlugin
void setHeader(HttpRequest httpRequest) {
if ("v1".equals(System.getProperty(SEND_VERSION))) {
- getUser().map(this::generateToken).ifPresent(token ->
httpRequest.setHeader(HEADER, token));
+ getUser()
+ .map(generatedV1TokenCache::get)
+ .ifPresent(token -> httpRequest.setHeader(HEADER, token));
} else {
getUser()
- .map(this::generateTokenV2)
+ .map(generatedV2TokenCache::get)
.ifPresent(token -> httpRequest.setHeader(HEADER_V2, token));
}
}