Skip to content

Commit 981b5d2

Browse files
SOLR-16951: Add PKI Auth Caching for both generation and validation (#3334)
1 parent 0c4f7e1 commit 981b5d2

2 files changed

Lines changed: 51 additions & 7 deletions

File tree

solr/CHANGES.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,9 @@ Improvements
245245

246246
* SOLR-17750: S3 File downloads now handle connection issues more gracefully (Houston Putman, Mark Miller)
247247

248+
* SOLR-16951: Add PKI Auth Caching for both generation of the PKI Auth Tokens and validation of received tokens.
249+
The default PKI Auth validity TTL has been increased from 5 seconds to 10 seconds. (Houston Putman)
250+
248251
Optimizations
249252
---------------------
250253
* SOLR-17578: Remove ZkController internal core supplier, for slightly faster reconnection after Zookeeper session loss. (Pierre Salagnac)

solr/core/src/java/org/apache/solr/security/PKIAuthenticationPlugin.java

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
import static java.nio.charset.StandardCharsets.UTF_8;
2020
import static org.apache.solr.client.solrj.SolrRequest.METHOD.GET;
2121

22+
import com.github.benmanes.caffeine.cache.Caffeine;
23+
import com.github.benmanes.caffeine.cache.LoadingCache;
2224
import com.google.common.annotations.VisibleForTesting;
2325
import jakarta.servlet.FilterChain;
2426
import jakarta.servlet.http.HttpServletRequest;
@@ -37,6 +39,7 @@
3739
import java.util.Optional;
3840
import java.util.Set;
3941
import java.util.concurrent.ConcurrentHashMap;
42+
import java.util.concurrent.TimeUnit;
4043
import org.apache.http.HttpEntity;
4144
import org.apache.http.HttpException;
4245
import org.apache.http.HttpHeaders;
@@ -94,7 +97,10 @@ public static void withServerIdentity(final boolean enabled) {
9497
private final Map<String, PublicKey> keyCache = new ConcurrentHashMap<>();
9598
private final PublicKeyHandler publicKeyHandler;
9699
private final CoreContainer cores;
97-
private static final int MAX_VALIDITY = Integer.getInteger("pkiauth.ttl", 5000);
100+
private final LoadingCache<String, PKIHeaderData> validatedHeaderCache;
101+
private final LoadingCache<String, String> generatedV1TokenCache;
102+
private final LoadingCache<String, String> generatedV2TokenCache;
103+
private static final int MAX_VALIDITY = Integer.getInteger("pkiauth.ttl", 10000);
98104
private final String myNodeName;
99105
private final HttpHeaderClientInterceptor interceptor = new HttpHeaderClientInterceptor();
100106
private boolean interceptorRegistered = false;
@@ -114,6 +120,38 @@ public PKIAuthenticationPlugin(
114120
this.cores = cores;
115121
myNodeName = nodeName;
116122

123+
// Don't expire after read, because there is no reason to add the overhead of updating expiry
124+
// information after each read. The expiration time here doesn't matter too much, because we
125+
// still check the PKI Token TTL after fetching from the cache. We just want to make sure cache
126+
// entries are cleaned up regularly.
127+
validatedHeaderCache =
128+
Caffeine.newBuilder()
129+
.maximumSize(1000)
130+
.expireAfterWrite(MAX_VALIDITY, TimeUnit.MILLISECONDS)
131+
.build(this::decipherHeaderV2);
132+
// We must expire much earlier than the max validity, because these cached Auth tokens still
133+
// need to be sent to the server, which will validate the TTL. If we expire at maxValidity, the
134+
// TTL check will always fail before the cache entry is expired.
135+
long expireAfterTime = MAX_VALIDITY / 4;
136+
// Refreshing is done asynchronously, so we want to do it before expiration. This means that
137+
// requests are not synchronously blocked when generating new Auth tokens. However, the refresh
138+
// will only happen when the cached header is requested. Therefore, we want to give a long-ish
139+
// runway for requests to come in to trigger an asynchronous-refresh before expiry causes a
140+
// synchronous-refresh.
141+
long shouldRefreshTime = Math.max(1, expireAfterTime / 2);
142+
generatedV1TokenCache =
143+
Caffeine.newBuilder()
144+
.maximumSize(100)
145+
.refreshAfterWrite(shouldRefreshTime, TimeUnit.MILLISECONDS)
146+
.expireAfterWrite(expireAfterTime, TimeUnit.MILLISECONDS)
147+
.build(this::generateToken);
148+
generatedV2TokenCache =
149+
Caffeine.newBuilder()
150+
.maximumSize(100)
151+
.refreshAfterWrite(shouldRefreshTime, TimeUnit.MILLISECONDS)
152+
.expireAfterWrite(expireAfterTime, TimeUnit.MILLISECONDS)
153+
.build(this::generateTokenV2);
154+
117155
Set<String> knownPkiVersions = Set.of("v1", "v2");
118156
// We always accept v2 even if it is not specified
119157
String[] versions = System.getProperty(ACCEPT_VERSIONS, "v2").split(",");
@@ -155,7 +193,7 @@ public boolean doAuthenticate(
155193
return sendError(response, true, "Could not parse node name from SolrAuthV2 header.");
156194
}
157195

158-
headerData = decipherHeaderV2(headerV2, headerV2.substring(0, nodeNameEnd));
196+
headerData = validatedHeaderCache.get(headerV2);
159197
} else if (headerV1 != null && acceptPkiV1) {
160198
List<String> authInfo = StrUtils.splitWS(headerV1, false);
161199
if (authInfo.size() != 2) {
@@ -223,7 +261,8 @@ private PublicKey getOrFetchPublicKey(String nodeName) {
223261
return key;
224262
}
225263

226-
private PKIHeaderData decipherHeaderV2(String header, String nodeName) {
264+
private PKIHeaderData decipherHeaderV2(String header) {
265+
String nodeName = header.substring(0, header.indexOf(' '));
227266
PublicKey key = getOrFetchPublicKey(nodeName);
228267

229268
int sigStart = header.lastIndexOf(' ');
@@ -414,11 +453,11 @@ public void onBegin(Request request) {
414453
final Optional<String> preFetchedUser = getUserFromJettyRequest(request);
415454
if ("v1".equals(System.getProperty(SEND_VERSION))) {
416455
preFetchedUser
417-
.map(PKIAuthenticationPlugin.this::generateToken)
456+
.map(generatedV1TokenCache::get)
418457
.ifPresent(token -> request.headers(httpFields -> httpFields.add(HEADER, token)));
419458
} else {
420459
preFetchedUser
421-
.map(PKIAuthenticationPlugin.this::generateTokenV2)
460+
.map(generatedV2TokenCache::get)
422461
.ifPresent(
423462
token -> request.headers(httpFields -> httpFields.add(HEADER_V2, token)));
424463
}
@@ -520,10 +559,12 @@ private String generateTokenV2(String user) {
520559

521560
void setHeader(HttpRequest httpRequest) {
522561
if ("v1".equals(System.getProperty(SEND_VERSION))) {
523-
getUser().map(this::generateToken).ifPresent(token -> httpRequest.setHeader(HEADER, token));
562+
getUser()
563+
.map(generatedV1TokenCache::get)
564+
.ifPresent(token -> httpRequest.setHeader(HEADER, token));
524565
} else {
525566
getUser()
526-
.map(this::generateTokenV2)
567+
.map(generatedV2TokenCache::get)
527568
.ifPresent(token -> httpRequest.setHeader(HEADER_V2, token));
528569
}
529570
}

0 commit comments

Comments
 (0)