Skip to content

Commit 3200d12

Browse files
committed
Optionally provide hash key of current DLS/FLS config in thread context
Closes #324 See merge request search-guard/search-guard-suite-enterprise!890
1 parent 11db3a0 commit 3200d12

File tree

11 files changed

+660
-23
lines changed

11 files changed

+660
-23
lines changed

dlic-dlsfls/src/main/java/com/floragunn/searchguard/enterprise/dlsfls/DlsFlsModule.java

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@
5656
import com.floragunn.searchguard.enterprise.dlsfls.lucene.DlsFlsDirectoryReaderWrapper;
5757
import com.floragunn.searchguard.license.SearchGuardLicense;
5858
import com.floragunn.searchguard.license.SearchGuardLicense.Feature;
59+
import com.floragunn.searchsupport.StaticSettings;
5960
import com.floragunn.searchsupport.cstate.ComponentState;
6061
import com.floragunn.searchsupport.cstate.ComponentStateProvider;
6162
import com.floragunn.searchsupport.cstate.metrics.TimeAggregation;
@@ -67,6 +68,9 @@ public class DlsFlsModule implements SearchGuardModule, ComponentStateProvider {
6768
// XXX Hack to trigger early initialization of DlsFlsConfig
6869
@SuppressWarnings("unused")
6970
private static final CType<DlsFlsConfig> TYPE = DlsFlsConfig.TYPE;
71+
72+
static final StaticSettings.Attribute<Boolean> PROVIDE_THREAD_CONTEXT_AUTHZ_HASH = StaticSettings.Attribute
73+
.define("searchguard.dls_fls.provide_thread_context_authz_hash").withDefault(false).asBoolean();
7074

7175
private final ComponentState componentState = new ComponentState(1000, null, "dlsfls", DlsFlsModule.class).requiresEnterpriseLicense();
7276
/**
@@ -104,7 +108,7 @@ public Collection<Object> createComponents(BaseDependencies baseDependencies) {
104108

105109
this.dlsFlsValve = new DlsFlsValve(baseDependencies.getLocalClient(), baseDependencies.getClusterService(),
106110
baseDependencies.getIndexNameExpressionResolver(), baseDependencies.getGuiceDependencies(),
107-
baseDependencies.getThreadPool().getThreadContext(), config);
111+
baseDependencies.getThreadPool().getThreadContext(), config, baseDependencies.getStaticSettings());
108112

109113
this.dlsFlsSearchOperationListener = new DlsFlsSearchOperationListener(this.dlsFlsBaseContext, config);
110114

@@ -190,4 +194,9 @@ public List<RestHandler> getRestHandlers(Settings settings, RestController restC
190194
public List<ActionHandler<? extends ActionRequest, ? extends ActionResponse>> getActions() {
191195
return DlsFlsConfigApi.ACTION_HANDLERS;
192196
}
197+
198+
@Override
199+
public StaticSettings.AttributeSet getSettings() {
200+
return StaticSettings.AttributeSet.of(PROVIDE_THREAD_CONTEXT_AUTHZ_HASH);
201+
}
193202
}

dlic-dlsfls/src/main/java/com/floragunn/searchguard/enterprise/dlsfls/DlsFlsProcessedConfig.java

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
public class DlsFlsProcessedConfig {
4343
private static final Logger log = LogManager.getLogger(DlsFlsProcessedConfig.class);
4444

45-
public static final DlsFlsProcessedConfig DEFAULT = new DlsFlsProcessedConfig(DlsFlsConfig.DEFAULT, null, null, null, null, null);
45+
public static final DlsFlsProcessedConfig DEFAULT = new DlsFlsProcessedConfig(DlsFlsConfig.DEFAULT, null, null, null, SgDynamicConfiguration.empty(CType.ROLES), null, null);
4646

4747
private final DlsFlsConfig dlsFlsConfig;
4848
private final RoleBasedDocumentAuthorization documentAuthorization;
@@ -51,21 +51,23 @@ public class DlsFlsProcessedConfig {
5151
private final boolean validationErrorsPresent;
5252
private final String validationErrorDescription;
5353
private final String uniqueValidationErrorToken;
54+
private final SgDynamicConfiguration<Role> roleConfig;
5455
private Future<?> updateFuture;
5556
private long metadataVersionEffective;
5657

5758
DlsFlsProcessedConfig(DlsFlsConfig dlsFlsConfig, RoleBasedDocumentAuthorization documentAuthorization,
58-
RoleBasedFieldAuthorization fieldAuthorization, RoleBasedFieldMasking fieldMasking, ValidationErrors rolesValidationErrors,
59-
ValidationErrors rolesMappingValidationErrors) {
59+
RoleBasedFieldAuthorization fieldAuthorization, RoleBasedFieldMasking fieldMasking, SgDynamicConfiguration<Role> roleConfig, ValidationErrors rolesValidationErrors,
60+
ValidationErrors rolesMappingValidationErrors) {
6061
this.dlsFlsConfig = dlsFlsConfig;
6162
this.documentAuthorization = documentAuthorization;
6263
this.fieldAuthorization = fieldAuthorization;
6364
this.fieldMasking = fieldMasking;
6465
this.validationErrorsPresent = ((rolesValidationErrors != null) && (rolesValidationErrors.hasErrors()))//
6566
|| ((rolesMappingValidationErrors != null) && (rolesMappingValidationErrors.hasErrors()));
6667
this.uniqueValidationErrorToken = UUID.randomUUID().toString();
67-
this.validationErrorDescription = describeValidationErrors(uniqueValidationErrorToken, rolesValidationErrors, //
68-
rolesMappingValidationErrors);
68+
this.validationErrorDescription = describeValidationErrors(uniqueValidationErrorToken, rolesValidationErrors,//
69+
rolesMappingValidationErrors);
70+
this.roleConfig = roleConfig;
6971
}
7072

7173
static DlsFlsProcessedConfig createFrom(ConfigMap configMap, ComponentState componentState, Meta indexMetadata) {
@@ -100,9 +102,10 @@ static DlsFlsProcessedConfig createFrom(ConfigMap configMap, ComponentState comp
100102
componentState.setState(State.INITIALIZED);
101103
ValidationErrors rolesValidationErrors = roleConfig.getValidationErrors();
102104
ValidationErrors rolesMappingsValidationErrors = Optional.ofNullable(configMap.get(CType.ROLESMAPPING))
103-
.map(SgDynamicConfiguration::getValidationErrors).orElse(null);
104-
return new DlsFlsProcessedConfig(dlsFlsConfig, documentAuthorization, fieldAuthorization, fieldMasking, rolesValidationErrors,
105-
rolesMappingsValidationErrors);
105+
.map(SgDynamicConfiguration::getValidationErrors)
106+
.orElse(null);
107+
return new DlsFlsProcessedConfig(dlsFlsConfig, documentAuthorization, fieldAuthorization, fieldMasking, roleConfig, rolesValidationErrors,
108+
rolesMappingsValidationErrors);
106109
} catch (Exception e) {
107110
log.error("Error while updating DLS/FLS config", e);
108111
componentState.setFailed(e);
@@ -130,6 +133,10 @@ public MetricsLevel getMetricsLevel() {
130133
return dlsFlsConfig.getMetricsLevel();
131134
}
132135

136+
public SgDynamicConfiguration<Role> getRoleConfig() {
137+
return roleConfig;
138+
}
139+
133140
private void updateIndices(Meta indexMetadata) {
134141
if (documentAuthorization != null) {
135142
documentAuthorization.updateIndices(indexMetadata);

dlic-dlsfls/src/main/java/com/floragunn/searchguard/enterprise/dlsfls/DlsFlsValve.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,12 +63,14 @@
6363
import com.floragunn.searchguard.enterprise.dlsfls.DlsFlsConfig.Mode;
6464
import com.floragunn.searchguard.enterprise.dlsfls.filter.DlsFilterLevelActionHandler;
6565
import com.floragunn.searchguard.support.ConfigConstants;
66+
import com.floragunn.searchsupport.StaticSettings;
6667
import com.floragunn.searchsupport.cstate.ComponentState;
6768
import com.floragunn.searchsupport.cstate.ComponentStateProvider;
6869
import com.floragunn.searchsupport.cstate.metrics.Meter;
6970
import com.floragunn.searchsupport.cstate.metrics.TimeAggregation;
7071
import com.floragunn.searchsupport.meta.Meta;
7172

73+
7274
public class DlsFlsValve implements SyncAuthorizationFilter, ComponentStateProvider {
7375
private static final String MAP_EXECUTION_HINT = "map";
7476
private static final String DIRECT_EXECUTION_HINT = "direct";
@@ -82,16 +84,18 @@ public class DlsFlsValve implements SyncAuthorizationFilter, ComponentStateProvi
8284
private final AtomicReference<DlsFlsProcessedConfig> config;
8385
private final ComponentState componentState = new ComponentState(0, null, "dls_fls_valve", DlsFlsValve.class).initialized();
8486
private final TimeAggregation applyTimeAggregation = new TimeAggregation.Nanoseconds();
87+
private final ThreadContextAuthzHashProvider authzHashProvider;
8588

8689
public DlsFlsValve(Client nodeClient, ClusterService clusterService, IndexNameExpressionResolver resolver, GuiceDependencies guiceDependencies,
87-
ThreadContext threadContext, AtomicReference<DlsFlsProcessedConfig> config) {
90+
ThreadContext threadContext, AtomicReference<DlsFlsProcessedConfig> config, StaticSettings staticSettings) {
8891
this.nodeClient = nodeClient;
8992
this.clusterService = clusterService;
9093
this.resolver = resolver;
9194
this.guiceDependencies = guiceDependencies;
9295
this.threadContext = threadContext;
9396
this.config = config;
9497
this.componentState.addMetrics("filter_request", applyTimeAggregation);
98+
this.authzHashProvider = new ThreadContextAuthzHashProvider(staticSettings, threadContext);
9599
}
96100

97101
@Override
@@ -134,6 +138,7 @@ public SyncAuthorizationFilter.Result apply(PrivilegesEvaluationContext context,
134138
boolean hasFieldMasking = fieldMasking.hasRestrictions(context, resolvedIndices, meter);
135139

136140
if (!hasDlsRestrictions && !hasFlsRestrictions && !hasFieldMasking) {
141+
authzHashProvider.noRestrictions();
137142
return SyncAuthorizationFilter.Result.OK;
138143
}
139144

@@ -169,6 +174,8 @@ public SyncAuthorizationFilter.Result apply(PrivilegesEvaluationContext context,
169174
}
170175
}
171176

177+
authzHashProvider.restrictions(context, config);
178+
172179
Object request = context.getRequest();
173180

174181
if (request instanceof RealtimeRequest) {
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
/*
2+
* Copyright 2024 by floragunn GmbH - All rights reserved
3+
*
4+
*
5+
* Unless required by applicable law or agreed to in writing, software
6+
* distributed here is distributed on an "AS IS" BASIS,
7+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
8+
*
9+
* This software is free of charge for non-commercial and academic use.
10+
* For commercial use in a production environment you have to obtain a license
11+
* from https://floragunn.com
12+
*
13+
*/
14+
package com.floragunn.searchguard.enterprise.dlsfls;
15+
16+
import java.nio.charset.StandardCharsets;
17+
import java.util.Map;
18+
import java.util.Set;
19+
import java.util.TreeMap;
20+
import java.util.TreeSet;
21+
22+
import org.elasticsearch.common.util.concurrent.ThreadContext;
23+
24+
import com.floragunn.codova.config.templates.Template;
25+
import com.floragunn.codova.documents.DocNode;
26+
import com.floragunn.searchguard.authz.PrivilegesEvaluationContext;
27+
import com.floragunn.searchguard.authz.config.Role;
28+
import com.floragunn.searchguard.configuration.SgDynamicConfiguration;
29+
import com.floragunn.searchsupport.StaticSettings;
30+
import com.google.common.hash.Hashing;
31+
32+
class ThreadContextAuthzHashProvider {
33+
static final String THREAD_CONTEXT_HEADER = "_sg_dls_fls_authz";
34+
35+
private final boolean active;
36+
private final ThreadContext threadContext;
37+
38+
ThreadContextAuthzHashProvider(StaticSettings staticSettings, ThreadContext threadContext) {
39+
this.active = staticSettings.get(DlsFlsModule.PROVIDE_THREAD_CONTEXT_AUTHZ_HASH);
40+
this.threadContext = threadContext;
41+
}
42+
43+
void noRestrictions() {
44+
if (this.active && this.threadContext.getHeader(THREAD_CONTEXT_HEADER) == null) {
45+
this.threadContext.putHeader(THREAD_CONTEXT_HEADER, "0");
46+
}
47+
}
48+
49+
void restrictions(PrivilegesEvaluationContext context, DlsFlsProcessedConfig config) {
50+
if (this.active && this.threadContext.getHeader(THREAD_CONTEXT_HEADER) == null) {
51+
String restrictionsInfo = restrictionsInfo(context, config);
52+
this.threadContext.putHeader(THREAD_CONTEXT_HEADER, Hashing.sha256().hashString(restrictionsInfo, StandardCharsets.UTF_8).toString());
53+
}
54+
55+
}
56+
57+
String restrictionsInfo(PrivilegesEvaluationContext context, DlsFlsProcessedConfig config) {
58+
SgDynamicConfiguration<Role> rolesConfig;
59+
60+
if (context.getSpecialPrivilegesEvaluationContext() != null) {
61+
rolesConfig = context.getSpecialPrivilegesEvaluationContext().getRolesConfig();
62+
} else {
63+
rolesConfig = config.getRoleConfig();
64+
}
65+
66+
return restrictionsInfo(context, rolesConfig);
67+
}
68+
69+
String restrictionsInfo(PrivilegesEvaluationContext context, SgDynamicConfiguration<Role> rolesConfig) {
70+
if (usesTemplates(context, rolesConfig)) {
71+
return userBasedRestrictionsInfo(context);
72+
} else {
73+
return roleBasedRestrictionInfo(context, rolesConfig);
74+
}
75+
}
76+
77+
boolean usesTemplates(PrivilegesEvaluationContext context, SgDynamicConfiguration<Role> rolesConfig) {
78+
for (String roleName : context.getMappedRoles()) {
79+
Role role = rolesConfig.getCEntry(roleName);
80+
81+
if (role != null) {
82+
for (Role.Index indexPermissions : role.getIndexPermissions()) {
83+
if (indexPermissions.usesTemplates()) {
84+
return true;
85+
}
86+
}
87+
}
88+
}
89+
90+
return false;
91+
}
92+
93+
String userBasedRestrictionsInfo(PrivilegesEvaluationContext context) {
94+
return context.getUser().getName() + "::" + DocNode.wrap(context.getUser().getStructuredAttributes()).toJsonString() + "::" + context.getMappedRoles();
95+
}
96+
97+
String roleBasedRestrictionInfo(PrivilegesEvaluationContext context, SgDynamicConfiguration<Role> rolesConfig) {
98+
Set<String> unprotectedIndices = new TreeSet<>();
99+
Map<String, Set<String>> protectedIndices = new TreeMap<>();
100+
101+
for (String roleName : context.getMappedRoles()) {
102+
Role role = rolesConfig.getCEntry(roleName);
103+
104+
if (role != null) {
105+
for (Role.Index indexPermissions : role.getIndexPermissions()) {
106+
if (indexPermissions.getDls() == null && (indexPermissions.getFls() == null || indexPermissions.getFls().isEmpty())
107+
&& (indexPermissions.getMaskedFields() == null || indexPermissions.getMaskedFields().isEmpty())) {
108+
unprotectedIndices.addAll(indexPermissions.getIndexPatterns().getSource().map(Template::toString));
109+
} else {
110+
for (String indexPattern : indexPermissions.getIndexPatterns().getSource().map(Template::toString)) {
111+
Set<String> rules = protectedIndices.computeIfAbsent(indexPattern, k -> new TreeSet<>());
112+
rules.add("dls:" + indexPermissions.getDls());
113+
rules.add("fls: " + indexPermissions.getFls());
114+
rules.add("fm: " + indexPermissions.getMaskedFields());
115+
}
116+
}
117+
}
118+
}
119+
}
120+
121+
for (String unprotectedIndex : unprotectedIndices) {
122+
protectedIndices.remove(unprotectedIndex);
123+
}
124+
125+
return unprotectedIndices + "::" + protectedIndices;
126+
}
127+
}

dlic-dlsfls/src/test/java/com/floragunn/searchguard/enterprise/dlsfls/DlsFlsProcessedConfigTest.java

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -49,17 +49,16 @@ public class DlsFlsProcessedConfigTest {
4949

5050
@Test
5151
public void shouldSupportNullValidationErrors() {
52-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
53-
null, null);
52+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking, null, null, null);
5453

5554
assertThat(config.containsValidationError(), equalTo(false));
5655
assertThat(config.getValidationErrorDescription(), isEmptyOrNullString());
5756
}
5857

5958
@Test
6059
public void shouldNotContainValidationErrors() {
61-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
62-
rolesValidationErrors, rolesMapingdValidationErrors);
60+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking, null, rolesValidationErrors,
61+
rolesMapingdValidationErrors);
6362

6463
assertThat(config.containsValidationError(), equalTo(false));
6564
assertThat(config.getValidationErrorDescription(), isEmptyOrNullString());
@@ -70,7 +69,7 @@ public void shouldReturnRoleValidationErrorForRoles() {
7069
when(rolesValidationErrors.hasErrors()).thenReturn(true);
7170
when(rolesValidationErrors.getErrors()).thenReturn(ImmutableMap.of(INVALID_ROLE_1, singleton(VALIDATION_ERROR_1)));
7271

73-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
72+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,null,
7473
rolesValidationErrors, rolesMapingdValidationErrors);
7574

7675
assertThat(config.containsValidationError(), equalTo(true));
@@ -86,7 +85,7 @@ public void shouldReturnMultipleRoleValidationError() {
8685
when(rolesValidationErrors.getErrors()).thenReturn(ImmutableMap.of(INVALID_ROLE_1, singleton(VALIDATION_ERROR_1),//
8786
INVALID_ROLE_2, singleton(VALIDATION_ERROR_2)));
8887

89-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
88+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,null,
9089
rolesValidationErrors, rolesMapingdValidationErrors);
9190

9291
assertThat(config.containsValidationError(), equalTo(true));
@@ -103,7 +102,7 @@ public void shouldReturnMultipleValidationErrorForSameRole() {
103102
when(rolesValidationErrors.hasErrors()).thenReturn(true);
104103
when(rolesValidationErrors.getErrors()).thenReturn(ImmutableMap.of(INVALID_ROLE_1, asList(VALIDATION_ERROR_1, VALIDATION_ERROR_2)));
105104

106-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
105+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,null,
107106
rolesValidationErrors, rolesMapingdValidationErrors);
108107

109108
assertThat(config.containsValidationError(), equalTo(true));
@@ -119,7 +118,7 @@ public void shouldReturnRoleValidationErrorForRolesMappings() {
119118
when(rolesMapingdValidationErrors.hasErrors()).thenReturn(true);
120119
when(rolesMapingdValidationErrors.getErrors()).thenReturn(ImmutableMap.of(INVALID_MAPPING_1, singleton(VALIDATION_ERROR_1)));
121120

122-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
121+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,null,
123122
rolesValidationErrors, rolesMapingdValidationErrors);
124123

125124
assertThat(config.containsValidationError(), equalTo(true));
@@ -135,7 +134,7 @@ public void shouldReturnMultipleRoleMappingValidationError() {
135134
when(rolesMapingdValidationErrors.getErrors()).thenReturn(ImmutableMap.of(INVALID_MAPPING_1, singleton(VALIDATION_ERROR_1),//
136135
INVALID_MAPPING_2, singleton(VALIDATION_ERROR_2)));
137136

138-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
137+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,null,
139138
rolesValidationErrors, rolesMapingdValidationErrors);
140139

141140
assertThat(config.containsValidationError(), equalTo(true));
@@ -152,7 +151,7 @@ public void shouldReturnMultipleValidationErrorForSameMappingsRole() {
152151
when(rolesMapingdValidationErrors.hasErrors()).thenReturn(true);
153152
when(rolesMapingdValidationErrors.getErrors()).thenReturn(Map.of(INVALID_MAPPING_1, asList(VALIDATION_ERROR_1, VALIDATION_ERROR_2)));
154153

155-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
154+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,null,
156155
rolesValidationErrors, rolesMapingdValidationErrors);
157156

158157
assertThat(config.containsValidationError(), equalTo(true));
@@ -170,7 +169,7 @@ public void shouldReturnValidationErrorsForRolesAndMappings() {
170169
when(rolesMapingdValidationErrors.hasErrors()).thenReturn(true);
171170
when(rolesMapingdValidationErrors.getErrors()).thenReturn(ImmutableMap.of(INVALID_MAPPING_2, singleton(VALIDATION_ERROR_2)));
172171

173-
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking,//
172+
this.config = new DlsFlsProcessedConfig(dlsDlsConfig, documentAuthorization, fieldAuthorization, fieldMasking, null,
174173
rolesValidationErrors, rolesMapingdValidationErrors);
175174

176175
assertThat(config.containsValidationError(), equalTo(true));

0 commit comments

Comments
 (0)