Skip to content

Commit 63c66c5

Browse files
author
ly
committed
Support chained IndexReader wrappers for extensible Field-Level Security, Document-Level Security, and custom features like Field Masking
1 parent 9b6a28f commit 63c66c5

File tree

2 files changed

+81
-20
lines changed

2 files changed

+81
-20
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/SecurityExtension.java

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
*/
77
package org.elasticsearch.xpack.core.security;
88

9+
import org.apache.lucene.index.DirectoryReader;
910
import org.elasticsearch.action.ActionListener;
1011
import org.elasticsearch.client.Client;
1112
import org.elasticsearch.cluster.service.ClusterService;
1213
import org.elasticsearch.common.settings.Settings;
14+
import org.elasticsearch.core.CheckedFunction;
1315
import org.elasticsearch.env.Environment;
1416
import org.elasticsearch.threadpool.ThreadPool;
1517
import org.elasticsearch.watcher.ResourceWatcherService;
@@ -20,6 +22,7 @@
2022
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
2123
import org.elasticsearch.xpack.core.security.authz.store.RoleRetrievalResult;
2224

25+
import java.io.IOException;
2326
import java.util.Collections;
2427
import java.util.List;
2528
import java.util.Map;
@@ -122,6 +125,23 @@ default AuthorizationEngine getAuthorizationEngine(Settings settings) {
122125
return null;
123126
}
124127

128+
/**
129+
* Provides an optional {@link DirectoryReader} wrapper to be applied to each index.
130+
* <p>
131+
* This allows security plugins or extensions to inject custom logic for transforming
132+
* Lucene readers — such as for field value masking.
133+
* <p>
134+
* The default implementation returns {@code null}, indicating no additional wrapping is applied.
135+
* Implementations may return a non-null {@link CheckedFunction} to participate in the
136+
* {@code IndexModule.setReaderWrapper()} chain.
137+
*
138+
* @param securityContext the current {@link SecurityContext} providing user and request context
139+
* @return a {@link CheckedFunction} to wrap a {@link DirectoryReader}, or {@code null} if none
140+
*/
141+
default CheckedFunction<DirectoryReader, DirectoryReader, IOException> getIndexReaderWrapper(SecurityContext securityContext) {
142+
return null;
143+
}
144+
125145
default String extensionName() {
126146
return getClass().getName();
127147
}

x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/Security.java

Lines changed: 61 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
import org.apache.logging.log4j.LogManager;
1414
import org.apache.logging.log4j.Logger;
15+
import org.apache.lucene.index.DirectoryReader;
1516
import org.apache.lucene.util.SetOnce;
1617
import org.elasticsearch.ElasticsearchSecurityException;
1718
import org.elasticsearch.ElasticsearchStatusException;
@@ -49,6 +50,7 @@
4950
import org.elasticsearch.common.util.concurrent.EsExecutors;
5051
import org.elasticsearch.common.util.concurrent.ThreadContext;
5152
import org.elasticsearch.common.util.set.Sets;
53+
import org.elasticsearch.core.CheckedFunction;
5254
import org.elasticsearch.core.Nullable;
5355
import org.elasticsearch.env.Environment;
5456
import org.elasticsearch.env.NodeEnvironment;
@@ -58,6 +60,7 @@
5860
import org.elasticsearch.http.netty4.internal.HttpHeadersAuthenticatorUtils;
5961
import org.elasticsearch.http.netty4.internal.HttpValidator;
6062
import org.elasticsearch.index.IndexModule;
63+
import org.elasticsearch.index.IndexService;
6164
import org.elasticsearch.indices.ExecutorNames;
6265
import org.elasticsearch.indices.SystemIndexDescriptor;
6366
import org.elasticsearch.indices.breaker.CircuitBreakerService;
@@ -1159,33 +1162,71 @@ public List<BootstrapCheck> getBootstrapChecks() {
11591162
return bootstrapChecks.get();
11601163
}
11611164

1165+
/**
1166+
* Constructs a composed {@link CheckedFunction} chain that wraps a {@link DirectoryReader}
1167+
* with multiple reader-level security layers, including built-in DLS/FLS support and
1168+
* pluggable extensions (e.g., field masking).
1169+
*
1170+
* <p>This method is called per index and returns a function that applies each
1171+
* {@link DirectoryReader} wrapper in order, ensuring all registeredregistered security logic
1172+
* is applied consistently.
1173+
*
1174+
* @param indexService the {@link IndexService} associated with the index
1175+
* @return a composed function that wraps a {@link DirectoryReader} with all applicable security wrappers
1176+
*/
1177+
private CheckedFunction<DirectoryReader, DirectoryReader, IOException> buildReaderWrapperChain(IndexService indexService){
1178+
// Create the core SecurityIndexReaderWrapper which enforces DLS/FLS
1179+
SecurityIndexReaderWrapper securityWrapper = new SecurityIndexReaderWrapper(
1180+
shardId -> indexService.newSearchExecutionContext(
1181+
shardId.id(),
1182+
0,
1183+
// we pass a null index reader, which is legal and will disable rewrite optimizations
1184+
// based on index statistics, which is probably safer...
1185+
null,
1186+
() -> {
1187+
throw new IllegalArgumentException("permission filters are not allowed to use the current timestamp");
1188+
1189+
},
1190+
null,
1191+
// Don't use runtime mappings in the security query
1192+
emptyMap()
1193+
),
1194+
dlsBitsetCache.get(),
1195+
securityContext.get(),
1196+
getLicenseState(),
1197+
indexService.getScriptService()
1198+
);
1199+
1200+
// Initialize wrapper chain with core security logic
1201+
List<CheckedFunction<DirectoryReader, DirectoryReader, IOException>> wrappers = new ArrayList<>();
1202+
wrappers.add(securityWrapper);
1203+
1204+
// Add any additional reader wrappers provided by security extensions (e.g., field masking)
1205+
for (SecurityExtension securityExtension : securityExtensions) {
1206+
CheckedFunction<DirectoryReader, DirectoryReader, IOException> wrapper = securityExtension.getIndexReaderWrapper(securityContext.get());
1207+
if(wrapper !=null ){
1208+
wrappers.add(wrapper);
1209+
}
1210+
}
1211+
1212+
// Return a composed function that applies all wrappers in sequence
1213+
return reader -> {
1214+
DirectoryReader current = reader;
1215+
for (CheckedFunction<DirectoryReader, DirectoryReader, IOException> wrapper : wrappers) {
1216+
current = wrapper.apply(current);
1217+
}
1218+
return current;
1219+
};
1220+
}
1221+
11621222
@Override
11631223
public void onIndexModule(IndexModule module) {
11641224
if (enabled) {
11651225
assert getLicenseState() != null;
11661226
if (XPackSettings.DLS_FLS_ENABLED.get(settings)) {
11671227
assert dlsBitsetCache.get() != null;
11681228
module.setReaderWrapper(
1169-
indexService -> new SecurityIndexReaderWrapper(
1170-
shardId -> indexService.newSearchExecutionContext(
1171-
shardId.id(),
1172-
0,
1173-
// we pass a null index reader, which is legal and will disable rewrite optimizations
1174-
// based on index statistics, which is probably safer...
1175-
null,
1176-
() -> {
1177-
throw new IllegalArgumentException("permission filters are not allowed to use the current timestamp");
1178-
1179-
},
1180-
null,
1181-
// Don't use runtime mappings in the security query
1182-
emptyMap()
1183-
),
1184-
dlsBitsetCache.get(),
1185-
securityContext.get(),
1186-
getLicenseState(),
1187-
indexService.getScriptService()
1188-
)
1229+
indexService -> buildReaderWrapperChain(indexService)
11891230
);
11901231
/*
11911232
* We need to forcefully overwrite the query cache implementation to use security's opt-out query cache implementation. This

0 commit comments

Comments
 (0)