Skip to content

Commit a1f4f7c

Browse files
committed
Add ProjectScopedCache and update ReloadablePlugin
1 parent b663616 commit a1f4f7c

File tree

5 files changed

+266
-19
lines changed

5 files changed

+266
-19
lines changed

server/src/main/java/org/elasticsearch/node/NodeConstruction.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
import org.elasticsearch.cluster.metadata.MetadataDataStreamsService;
5353
import org.elasticsearch.cluster.metadata.MetadataIndexTemplateService;
5454
import org.elasticsearch.cluster.metadata.MetadataUpdateSettingsService;
55+
import org.elasticsearch.cluster.metadata.ProjectId;
5556
import org.elasticsearch.cluster.metadata.ProjectMetadata;
5657
import org.elasticsearch.cluster.metadata.SystemIndexMetadataUpgradeService;
5758
import org.elasticsearch.cluster.metadata.TemplateUpgradeService;
@@ -1517,12 +1518,26 @@ private CircuitBreakerService createCircuitBreakerService(
15171518
* @return A single ReloadablePlugin that, upon reload, reloads the plugins it wraps
15181519
*/
15191520
private static ReloadablePlugin wrapPlugins(List<ReloadablePlugin> reloadablePlugins) {
1520-
return settings -> {
1521-
for (ReloadablePlugin plugin : reloadablePlugins) {
1522-
try {
1523-
plugin.reload(settings);
1524-
} catch (IOException e) {
1525-
throw new UncheckedIOException(e);
1521+
return new ReloadablePlugin() {
1522+
@Override
1523+
public void reload(Settings settings) throws Exception {
1524+
for (ReloadablePlugin plugin : reloadablePlugins) {
1525+
try {
1526+
plugin.reload(settings);
1527+
} catch (IOException e) {
1528+
throw new UncheckedIOException(e);
1529+
}
1530+
}
1531+
}
1532+
1533+
@Override
1534+
public void reload(ProjectId projectId, Settings settings) throws Exception {
1535+
for (ReloadablePlugin plugin : reloadablePlugins) {
1536+
try {
1537+
plugin.reload(projectId, settings);
1538+
} catch (IOException e) {
1539+
throw new UncheckedIOException(e);
1540+
}
15261541
}
15271542
}
15281543
};

server/src/main/java/org/elasticsearch/plugins/ReloadablePlugin.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
package org.elasticsearch.plugins;
1111

12+
import org.elasticsearch.cluster.metadata.ProjectId;
1213
import org.elasticsearch.common.settings.Settings;
1314

1415
/**
@@ -41,4 +42,6 @@ public interface ReloadablePlugin {
4142
* if the offending call didn't happen.
4243
*/
4344
void reload(Settings settings) throws Exception;
45+
46+
default void reload(ProjectId projectId, Settings settings) throws Exception {}
4447
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealm.java

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import org.elasticsearch.action.ActionListener;
1010
import org.elasticsearch.common.cache.Cache;
1111
import org.elasticsearch.common.cache.CacheBuilder;
12+
import org.elasticsearch.common.cache.CacheLoader;
1213
import org.elasticsearch.common.settings.SecureString;
1314
import org.elasticsearch.common.util.concurrent.ListenableFuture;
1415
import org.elasticsearch.common.util.concurrent.ThreadContext;
@@ -32,25 +33,62 @@
3233

3334
public abstract class CachingUsernamePasswordRealm extends UsernamePasswordRealm implements CachingRealm {
3435

35-
private final Cache<String, ListenableFuture<CachedResult>> cache;
36+
private final UserAuthenticationCache cache;
3637
private final ThreadPool threadPool;
3738
private final boolean authenticationEnabled;
38-
final Hasher cacheHasher;
39+
protected final Hasher credentialHasher;
3940

4041
protected CachingUsernamePasswordRealm(RealmConfig config, ThreadPool threadPool) {
42+
this(config, threadPool, buildDefaultCache(config));
43+
}
44+
45+
protected CachingUsernamePasswordRealm(RealmConfig config, ThreadPool threadPool, UserAuthenticationCache cache) {
4146
super(config);
42-
cacheHasher = Hasher.resolve(this.config.getSetting(CachingUsernamePasswordRealmSettings.CACHE_HASH_ALGO_SETTING));
47+
credentialHasher = Hasher.resolve(this.config.getSetting(CachingUsernamePasswordRealmSettings.CACHE_HASH_ALGO_SETTING));
4348
this.threadPool = threadPool;
44-
final TimeValue ttl = this.config.getSetting(CachingUsernamePasswordRealmSettings.CACHE_TTL_SETTING);
49+
this.authenticationEnabled = config.getSetting(CachingUsernamePasswordRealmSettings.AUTHC_ENABLED_SETTING);
50+
this.cache = cache;
51+
}
52+
53+
private static UserAuthenticationCache buildDefaultCache(RealmConfig config) {
54+
final TimeValue ttl = config.getSetting(CachingUsernamePasswordRealmSettings.CACHE_TTL_SETTING);
4555
if (ttl.getNanos() > 0) {
46-
cache = CacheBuilder.<String, ListenableFuture<CachedResult>>builder()
56+
final Cache<String, ListenableFuture<CachedResult>> cache = CacheBuilder.<String, ListenableFuture<CachedResult>>builder()
4757
.setExpireAfterWrite(ttl)
48-
.setMaximumWeight(this.config.getSetting(CachingUsernamePasswordRealmSettings.CACHE_MAX_USERS_SETTING))
58+
.setMaximumWeight(config.getSetting(CachingUsernamePasswordRealmSettings.CACHE_MAX_USERS_SETTING))
4959
.build();
50-
} else {
51-
cache = null;
60+
return new UserAuthenticationCache() {
61+
@Override
62+
public void invalidate(String key) {
63+
cache.invalidate(key);
64+
}
65+
66+
@Override
67+
public void invalidate(String key, ListenableFuture<CachedResult> value) {
68+
cache.invalidate(key, value);
69+
}
70+
71+
@Override
72+
public void invalidateAll() {
73+
cache.invalidateAll();
74+
}
75+
76+
@Override
77+
public int count() {
78+
return cache.count();
79+
}
80+
81+
@Override
82+
public ListenableFuture<CachedResult> computeIfAbsent(
83+
String key,
84+
CacheLoader<String, ListenableFuture<CachedResult>> loader
85+
) throws ExecutionException {
86+
return cache.computeIfAbsent(key, loader);
87+
}
88+
};
5289
}
53-
this.authenticationEnabled = config.getSetting(CachingUsernamePasswordRealmSettings.AUTHC_ENABLED_SETTING);
90+
91+
return null;
5492
}
5593

5694
@Override
@@ -217,7 +255,9 @@ private void authenticateWithCache(UsernamePasswordToken token, ActionListener<A
217255
// notify any forestalled request listeners; they will not reach to the
218256
// authentication request and instead will use this result if they contain
219257
// the same credentials
220-
listenableCacheEntry.onResponse(new CachedResult(authResult, cacheHasher, authResult.getValue(), token.credentials()));
258+
listenableCacheEntry.onResponse(
259+
new CachedResult(authResult, credentialHasher, authResult.getValue(), token.credentials())
260+
);
221261
listener.onResponse(authResult);
222262
}, e -> {
223263
cache.invalidate(token.principal(), listenableCacheEntry);
@@ -282,7 +322,7 @@ private void lookupWithCache(String username, ActionListener<User> listener) {
282322
if (false == lookupInCache.get()) {
283323
// attempt lookup against the user directory
284324
doLookupUser(username, ActionListener.wrap(user -> {
285-
final CachedResult result = new CachedResult(AuthenticationResult.notHandled(), cacheHasher, user, null);
325+
final CachedResult result = new CachedResult(AuthenticationResult.notHandled(), credentialHasher, user, null);
286326
if (user == null) {
287327
// user not found, invalidate cache so that subsequent requests are forwarded to
288328
// the user directory
@@ -311,7 +351,20 @@ private void lookupWithCache(String username, ActionListener<User> listener) {
311351

312352
protected abstract void doLookupUser(String username, ActionListener<User> listener);
313353

314-
private static class CachedResult {
354+
protected interface UserAuthenticationCache {
355+
void invalidate(String key);
356+
357+
void invalidate(String key, ListenableFuture<CachedResult> value);
358+
359+
void invalidateAll();
360+
361+
int count();
362+
363+
ListenableFuture<CachedResult> computeIfAbsent(String key, CacheLoader<String, ListenableFuture<CachedResult>> loader)
364+
throws ExecutionException;
365+
}
366+
367+
protected static class CachedResult {
315368
private final AuthenticationResult<User> authenticationResult;
316369
private final User user;
317370
private final char[] hash;
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.security.support;
9+
10+
import org.elasticsearch.cluster.metadata.ProjectId;
11+
import org.elasticsearch.cluster.project.ProjectResolver;
12+
import org.elasticsearch.common.cache.Cache;
13+
import org.elasticsearch.common.cache.CacheBuilder;
14+
import org.elasticsearch.common.cache.CacheLoader;
15+
import org.elasticsearch.common.cache.RemovalListener;
16+
import org.elasticsearch.common.cache.RemovalNotification;
17+
import org.elasticsearch.common.util.concurrent.ReleasableLock;
18+
import org.elasticsearch.core.TimeValue;
19+
import org.elasticsearch.xpack.core.security.support.CacheIteratorHelper;
20+
21+
import java.util.Objects;
22+
import java.util.concurrent.ExecutionException;
23+
import java.util.function.ToLongBiFunction;
24+
25+
/**
26+
* Wrapper around a {@link Cache} instance where a composite key of the original key and the current project id, resolved through a
27+
* {@link ProjectResolver}, is used to write to and read from the cache.
28+
* <p>
29+
* During invalidation the cache is protected through locking in the {@link CacheIteratorHelper} because the result of iteration under any
30+
* mutation other than Cache.CacheIterator#remove() is undefined. Concurrent writes are allowed as long as there is no active
31+
* invalidation, the cache is protected against that by acquiring a read lock (blocking if invalidation in progress) before writing.
32+
*
33+
* @param <K> key type
34+
* @param <V> value type
35+
*/
36+
public class ProjectScopedCache<K, V> {
37+
private final Cache<ProjectScoped<K>, V> cache;
38+
private final ProjectResolver projectResolver;
39+
private final CacheIteratorHelper<ProjectScoped<K>, V> cacheIteratorHelper;
40+
41+
public ProjectScopedCache(ProjectResolver projectResolver, Cache<ProjectScoped<K>, V> cache) {
42+
this.projectResolver = projectResolver;
43+
this.cache = cache;
44+
cacheIteratorHelper = new CacheIteratorHelper<>(cache);
45+
}
46+
47+
public void invalidateProject() {
48+
if (projectResolver.supportsMultipleProjects()) {
49+
invalidateProject(projectResolver.getProjectId());
50+
} else {
51+
cache.invalidateAll();
52+
}
53+
}
54+
55+
public void invalidateProject(ProjectId projectId) {
56+
cacheIteratorHelper.removeKeysIf(key -> key.projectId().equals(projectId));
57+
}
58+
59+
public void invalidate(K key) {
60+
invalidate(projectResolver.getProjectId(), key);
61+
}
62+
63+
public void invalidate(ProjectId projectId, K key) {
64+
try (ReleasableLock ignored = cacheIteratorHelper.acquireUpdateLock()) {
65+
cache.invalidate(new ProjectScoped<>(projectId, key));
66+
}
67+
}
68+
69+
public void invalidate(K key, V value) {
70+
try (ReleasableLock ignored = cacheIteratorHelper.acquireUpdateLock()) {
71+
cache.invalidate(new ProjectScoped<>(projectResolver.getProjectId(), key), value);
72+
}
73+
}
74+
75+
public void invalidateAll() {
76+
try (ReleasableLock ignored = cacheIteratorHelper.acquireUpdateLock()) {
77+
cache.invalidateAll();
78+
}
79+
}
80+
81+
public V computeIfAbsent(K key, CacheLoader<K, V> loader) throws ExecutionException {
82+
try (var ignored = cacheIteratorHelper.acquireUpdateLock()) {
83+
return cache.computeIfAbsent(new ProjectScoped<>(projectResolver.getProjectId(), key), k -> loader.load(k.value));
84+
}
85+
}
86+
87+
public int count() {
88+
return cache.count();
89+
}
90+
91+
public static class Builder<K, V> {
92+
private long maximumWeight;
93+
private TimeValue expireAfterAccessNanos;
94+
private TimeValue expireAfterWrite;
95+
private ToLongBiFunction<K, V> weigher;
96+
private RemovalListener<K, V> removalListener;
97+
98+
public static <K, V> Builder<K, V> builder() {
99+
return new Builder<>();
100+
}
101+
102+
private Builder() {}
103+
104+
public Builder<K, V> setMaximumWeight(long maximumWeight) {
105+
if (maximumWeight < 0) {
106+
throw new IllegalArgumentException("maximumWeight < 0");
107+
}
108+
this.maximumWeight = maximumWeight;
109+
return this;
110+
}
111+
112+
public Builder<K, V> setExpireAfterAccess(TimeValue expireAfterAccess) {
113+
Objects.requireNonNull(expireAfterAccess);
114+
final long expireAfterAccessNanos = expireAfterAccess.getNanos();
115+
if (expireAfterAccessNanos <= 0) {
116+
throw new IllegalArgumentException("expireAfterAccess <= 0");
117+
}
118+
this.expireAfterAccessNanos = expireAfterAccess;
119+
return this;
120+
}
121+
122+
public Builder<K, V> setExpireAfterWrite(TimeValue expireAfterWrite) {
123+
Objects.requireNonNull(expireAfterWrite);
124+
final long expireAfterWriteNanos = expireAfterWrite.getNanos();
125+
if (expireAfterWriteNanos <= 0) {
126+
throw new IllegalArgumentException("expireAfterWrite <= 0");
127+
}
128+
this.expireAfterWrite = expireAfterWrite;
129+
return this;
130+
}
131+
132+
public Builder<K, V> weigher(ToLongBiFunction<K, V> weigher) {
133+
Objects.requireNonNull(weigher);
134+
this.weigher = weigher;
135+
return this;
136+
}
137+
138+
public Builder<K, V> removalListener(RemovalListener<K, V> removalListener) {
139+
Objects.requireNonNull(removalListener);
140+
this.removalListener = removalListener;
141+
return this;
142+
}
143+
144+
public ProjectScopedCache<K, V> build(ProjectResolver projectResolver) {
145+
CacheBuilder<ProjectScoped<K>, V> cacheBuilder = CacheBuilder.builder();
146+
147+
if (maximumWeight != -1) {
148+
cacheBuilder.setMaximumWeight(maximumWeight);
149+
}
150+
if (expireAfterAccessNanos != null) {
151+
cacheBuilder.setExpireAfterAccess(expireAfterAccessNanos);
152+
}
153+
if (expireAfterWrite != null) {
154+
cacheBuilder.setExpireAfterWrite(expireAfterWrite);
155+
}
156+
if (weigher != null) {
157+
cacheBuilder.weigher((key, value) -> weigher.applyAsLong(key.value, value));
158+
}
159+
if (removalListener != null) {
160+
cacheBuilder.removalListener((notification) -> {
161+
removalListener.onRemoval(
162+
new RemovalNotification<>(notification.getKey().value, notification.getValue(), notification.getRemovalReason())
163+
);
164+
});
165+
}
166+
return new ProjectScopedCache<>(projectResolver, cacheBuilder.build());
167+
}
168+
}
169+
170+
private record ProjectScoped<T>(ProjectId projectId, T value) {
171+
private ProjectScoped(ProjectId projectId, T value) {
172+
this.projectId = Objects.requireNonNull(projectId);
173+
this.value = value;
174+
}
175+
}
176+
}

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/support/CachingUsernamePasswordRealmTests.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ protected void doLookupUser(String username, ActionListener<User> listener) {
102102
listener.onFailure(new UnsupportedOperationException("this method should not be called"));
103103
}
104104
};
105-
assertThat(realm.cacheHasher, sameInstance(Hasher.resolve(cachingHashAlgo)));
105+
assertThat(realm.credentialHasher, sameInstance(Hasher.resolve(cachingHashAlgo)));
106106
}
107107

108108
public void testCacheSizeWhenCacheDisabled() {

0 commit comments

Comments
 (0)