Skip to content

Commit ecae222

Browse files
authored
Support multi-intersection for FieldPermissions (#91169)
This PR is the 2nd half of updating DocumentPermissions and FieldPermissions to support multi-level of limiting similar to LimitedRole (since #81403). Instead of hard coding fieldsDefinition and limitedByFieldsDefinition, this PR replaces them with a list of fieldsDefinitions which can accomodate multiple of them (more than 2). Relates: #91151
1 parent 1465b99 commit ecae222

File tree

12 files changed

+146
-92
lines changed

12 files changed

+146
-92
lines changed

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java

Lines changed: 28 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
import org.elasticsearch.common.Strings;
1919
import org.elasticsearch.common.io.stream.StreamOutput;
2020
import org.elasticsearch.common.regex.Regex;
21-
import org.elasticsearch.core.Nullable;
21+
import org.elasticsearch.common.util.CollectionUtils;
2222
import org.elasticsearch.xpack.core.security.authz.accesscontrol.FieldSubsetReader;
2323
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition.FieldGrantExcludeGroup;
2424
import org.elasticsearch.xpack.core.security.authz.support.SecurityQueryTemplateEvaluator.DlsQueryEvaluationContext;
@@ -61,9 +61,8 @@ public final class FieldPermissions implements Accountable, CacheKey {
6161
BASE_HASHSET_ENTRY_SIZE = mapEntryShallowSize + 2 * RamUsageEstimator.NUM_BYTES_OBJECT_REF;
6262
}
6363

64-
private final FieldPermissionsDefinition fieldPermissionsDefinition;
65-
@Nullable
66-
private final FieldPermissionsDefinition limitedByFieldPermissionsDefinition;
64+
private final List<FieldPermissionsDefinition> fieldPermissionsDefinitions;
65+
6766
// an automaton that represents a union of one more sets of permitted and denied fields
6867
private final CharacterRunAutomaton permittedFieldsAutomaton;
6968
private final boolean permittedFieldsAutomatonIsTotal;
@@ -72,7 +71,7 @@ public final class FieldPermissions implements Accountable, CacheKey {
7271
private final long ramBytesUsed;
7372

7473
/** Constructor that does not enable field-level security: all fields are accepted. */
75-
public FieldPermissions() {
74+
private FieldPermissions() {
7675
this(new FieldPermissionsDefinition(null, null), Automatons.MATCH_ALL);
7776
}
7877

@@ -85,33 +84,33 @@ public FieldPermissions(FieldPermissionsDefinition fieldPermissionsDefinition) {
8584
/** Constructor that enables field-level security based on include/exclude rules. Exclude rules
8685
* have precedence over include rules. */
8786
FieldPermissions(FieldPermissionsDefinition fieldPermissionsDefinition, Automaton permittedFieldsAutomaton) {
88-
this(fieldPermissionsDefinition, null, permittedFieldsAutomaton);
87+
this(
88+
List.of(Objects.requireNonNull(fieldPermissionsDefinition, "field permission definition cannot be null")),
89+
permittedFieldsAutomaton
90+
);
8991
}
9092

9193
/** Constructor that enables field-level security based on include/exclude rules. Exclude rules
9294
* have precedence over include rules. */
93-
private FieldPermissions(
94-
FieldPermissionsDefinition fieldPermissionsDefinition,
95-
@Nullable FieldPermissionsDefinition limitedByFieldPermissionsDefinition,
96-
Automaton permittedFieldsAutomaton
97-
) {
95+
private FieldPermissions(List<FieldPermissionsDefinition> fieldPermissionsDefinitions, Automaton permittedFieldsAutomaton) {
9896
if (permittedFieldsAutomaton.isDeterministic() == false && permittedFieldsAutomaton.getNumStates() > 1) {
9997
// we only accept deterministic automata so that the CharacterRunAutomaton constructor
10098
// directly wraps the provided automaton
10199
throw new IllegalArgumentException("Only accepts deterministic automata");
102100
}
103-
this.fieldPermissionsDefinition = Objects.requireNonNull(fieldPermissionsDefinition, "field permission definition cannot be null");
104-
this.limitedByFieldPermissionsDefinition = limitedByFieldPermissionsDefinition;
101+
this.fieldPermissionsDefinitions = Objects.requireNonNull(
102+
fieldPermissionsDefinitions,
103+
"field permission definitions cannot be null"
104+
);
105105
this.originalAutomaton = permittedFieldsAutomaton;
106106
this.permittedFieldsAutomaton = new CharacterRunAutomaton(permittedFieldsAutomaton);
107107
// we cache the result of isTotal since this might be a costly operation
108108
this.permittedFieldsAutomatonIsTotal = Operations.isTotal(permittedFieldsAutomaton);
109109

110110
long ramBytesUsed = BASE_FIELD_PERM_DEF_BYTES;
111-
ramBytesUsed += ramBytesUsedForFieldPermissionsDefinition(this.fieldPermissionsDefinition);
112-
if (this.limitedByFieldPermissionsDefinition != null) {
113-
ramBytesUsed += ramBytesUsedForFieldPermissionsDefinition(this.limitedByFieldPermissionsDefinition);
114-
}
111+
ramBytesUsed += this.fieldPermissionsDefinitions.stream()
112+
.mapToLong(FieldPermissions::ramBytesUsedForFieldPermissionsDefinition)
113+
.sum();
115114
ramBytesUsed += permittedFieldsAutomaton.ramBytesUsed();
116115
ramBytesUsed += runAutomatonRamBytesUsed(permittedFieldsAutomaton);
117116
this.ramBytesUsed = ramBytesUsed;
@@ -199,12 +198,16 @@ public static Automaton buildPermittedFieldsAutomaton(final String[] grantedFiel
199198
*/
200199
public FieldPermissions limitFieldPermissions(FieldPermissions limitedBy) {
201200
if (hasFieldLevelSecurity() && limitedBy != null && limitedBy.hasFieldLevelSecurity()) {
201+
// TODO: cache the automaton computation with FieldPermissionsCache
202202
Automaton _permittedFieldsAutomaton = Automatons.intersectAndMinimize(getIncludeAutomaton(), limitedBy.getIncludeAutomaton());
203-
return new FieldPermissions(fieldPermissionsDefinition, limitedBy.fieldPermissionsDefinition, _permittedFieldsAutomaton);
203+
return new FieldPermissions(
204+
CollectionUtils.concatLists(fieldPermissionsDefinitions, limitedBy.fieldPermissionsDefinitions),
205+
_permittedFieldsAutomaton
206+
);
204207
} else if (limitedBy != null && limitedBy.hasFieldLevelSecurity()) {
205-
return new FieldPermissions(limitedBy.getFieldPermissionsDefinition(), limitedBy.getIncludeAutomaton());
208+
return new FieldPermissions(limitedBy.fieldPermissionsDefinitions, limitedBy.getIncludeAutomaton());
206209
} else if (hasFieldLevelSecurity()) {
207-
return new FieldPermissions(this.getFieldPermissionsDefinition(), getIncludeAutomaton());
210+
return new FieldPermissions(fieldPermissionsDefinitions, getIncludeAutomaton());
208211
}
209212
return FieldPermissions.DEFAULT;
210213
}
@@ -217,23 +220,13 @@ public boolean grantsAccessTo(String fieldName) {
217220
return permittedFieldsAutomatonIsTotal || permittedFieldsAutomaton.run(fieldName);
218221
}
219222

220-
public FieldPermissionsDefinition getFieldPermissionsDefinition() {
221-
return fieldPermissionsDefinition;
222-
}
223-
224-
public FieldPermissionsDefinition getLimitedByFieldPermissionsDefinition() {
225-
return limitedByFieldPermissionsDefinition;
223+
public List<FieldPermissionsDefinition> getFieldPermissionsDefinitions() {
224+
return fieldPermissionsDefinitions;
226225
}
227226

228227
@Override
229228
public void buildCacheKey(StreamOutput out, DlsQueryEvaluationContext context) throws IOException {
230-
fieldPermissionsDefinition.buildCacheKey(out, context);
231-
if (limitedByFieldPermissionsDefinition != null) {
232-
out.writeBoolean(true);
233-
limitedByFieldPermissionsDefinition.buildCacheKey(out, context);
234-
} else {
235-
out.writeBoolean(false);
236-
}
229+
out.writeCollection(fieldPermissionsDefinitions, (o, fpd) -> fpd.buildCacheKey(o, context));
237230
}
238231

239232
/** Return whether field-level security is enabled, ie. whether any field might be filtered out. */
@@ -259,13 +252,12 @@ public boolean equals(Object o) {
259252
if (o == null || getClass() != o.getClass()) return false;
260253
FieldPermissions that = (FieldPermissions) o;
261254
return permittedFieldsAutomatonIsTotal == that.permittedFieldsAutomatonIsTotal
262-
&& fieldPermissionsDefinition.equals(that.fieldPermissionsDefinition)
263-
&& Objects.equals(limitedByFieldPermissionsDefinition, that.limitedByFieldPermissionsDefinition);
255+
&& fieldPermissionsDefinitions.equals(that.fieldPermissionsDefinitions);
264256
}
265257

266258
@Override
267259
public int hashCode() {
268-
return Objects.hash(fieldPermissionsDefinition, limitedByFieldPermissionsDefinition, permittedFieldsAutomatonIsTotal);
260+
return Objects.hash(fieldPermissionsDefinitions, permittedFieldsAutomatonIsTotal);
269261
}
270262

271263
@Override

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissionsCache.java

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -72,26 +72,25 @@ public FieldPermissions getFieldPermissions(FieldPermissionsDefinition fieldPerm
7272
}
7373

7474
/**
75-
* Returns a field permissions object that corresponds to the merging of the given field permissions and caches the instance if one was
76-
* not found in the cache.
75+
* Returns a field permissions object that corresponds to the union of the given field permissions.
76+
* Union means a field is granted if it is granted by any of the FieldPermissions from the given
77+
* collection.
78+
* The returned instance is cached if one was not found in the cache.
7779
*/
78-
FieldPermissions getFieldPermissions(Collection<FieldPermissions> fieldPermissionsCollection) {
80+
FieldPermissions union(Collection<FieldPermissions> fieldPermissionsCollection) {
7981
Optional<FieldPermissions> allowAllFieldPermissions = fieldPermissionsCollection.stream()
8082
.filter(((Predicate<FieldPermissions>) (FieldPermissions::hasFieldLevelSecurity)).negate())
8183
.findFirst();
8284
return allowAllFieldPermissions.orElseGet(() -> {
8385
final Set<FieldGrantExcludeGroup> fieldGrantExcludeGroups = new HashSet<>();
8486
for (FieldPermissions fieldPermissions : fieldPermissionsCollection) {
85-
final FieldPermissionsDefinition definition = fieldPermissions.getFieldPermissionsDefinition();
86-
final FieldPermissionsDefinition limitedByDefinition = fieldPermissions.getLimitedByFieldPermissionsDefinition();
87-
if (definition == null) {
88-
throw new IllegalArgumentException("Expected field permission definition, but found null");
89-
} else if (limitedByDefinition != null) {
87+
final List<FieldPermissionsDefinition> fieldPermissionsDefinitions = fieldPermissions.getFieldPermissionsDefinitions();
88+
if (fieldPermissionsDefinitions.size() != 1) {
9089
throw new IllegalArgumentException(
91-
"Expected no limited-by field permission definition, but found [" + limitedByDefinition + "]"
90+
"Expected a single field permission definition, but found [" + fieldPermissionsDefinitions + "]"
9291
);
9392
}
94-
fieldGrantExcludeGroups.addAll(definition.getFieldGrantExcludeGroups());
93+
fieldGrantExcludeGroups.addAll(fieldPermissionsDefinitions.get(0).getFieldGrantExcludeGroups());
9594
}
9695
final FieldPermissionsDefinition combined = new FieldPermissionsDefinition(fieldGrantExcludeGroups);
9796
try {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/IndicesPermission.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ && containsPrivilegeThatGrantsMappingUpdatesForBwc(group))) {
494494
if (indexFieldPermissions != null && indexFieldPermissions.isEmpty() == false) {
495495
fieldPermissions = indexFieldPermissions.size() == 1
496496
? indexFieldPermissions.iterator().next()
497-
: fieldPermissionsCache.getFieldPermissions(indexFieldPermissions);
497+
: fieldPermissionsCache.union(indexFieldPermissions);
498498
} else {
499499
fieldPermissions = FieldPermissions.DEFAULT;
500500
}
@@ -609,6 +609,7 @@ public static class Group {
609609
private final String[] indices;
610610
private final StringMatcher indexNameMatcher;
611611
private final Supplier<Automaton> indexNameAutomaton;
612+
// TODO: Use FieldPermissionsDefinition instead of FieldPermissions. The former is a better counterpart to query
612613
private final FieldPermissions fieldPermissions;
613614
private final Set<BytesReference> query;
614615
// by default certain restricted indices are exempted when granting privileges, as they should generally be hidden for ordinary

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/accesscontrol/SecurityIndexReaderWrapperIntegrationTests.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ public void testDLS() throws Exception {
159159
for (int i = 0; i < numValues; i++) {
160160
String termQuery = "{\"term\": {\"field\": \"" + values[i] + "\"} }";
161161
IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(
162-
new FieldPermissions(),
162+
FieldPermissions.DEFAULT,
163163
DocumentPermissions.filteredBy(singleton(new BytesArray(termQuery)))
164164
);
165165
SecurityIndexReaderWrapper wrapper = new SecurityIndexReaderWrapper(
@@ -224,15 +224,15 @@ public void testDLSWithLimitedPermissions() throws Exception {
224224
queries.add(new BytesArray("{\"terms\" : { \"f2\" : [\"fv22\"] } }"));
225225
queries.add(new BytesArray("{\"terms\" : { \"f2\" : [\"fv32\"] } }"));
226226
IndicesAccessControl.IndexAccessControl indexAccessControl = new IndicesAccessControl.IndexAccessControl(
227-
new FieldPermissions(),
227+
FieldPermissions.DEFAULT,
228228
DocumentPermissions.filteredBy(queries)
229229
);
230230
queries = singleton(new BytesArray("{\"terms\" : { \"f1\" : [\"fv11\", \"fv21\", \"fv31\"] } }"));
231231
if (restrictiveLimitedIndexPermissions) {
232232
queries = singleton(new BytesArray("{\"terms\" : { \"f1\" : [\"fv11\", \"fv31\"] } }"));
233233
}
234234
IndicesAccessControl.IndexAccessControl limitedIndexAccessControl = new IndicesAccessControl.IndexAccessControl(
235-
new FieldPermissions(),
235+
FieldPermissions.DEFAULT,
236236
DocumentPermissions.filteredBy(queries)
237237
);
238238
IndexSettings indexSettings = IndexSettingsModule.newIndexSettings(

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissionsCacheTests.java

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -44,9 +44,7 @@ public void testMergeFieldPermissions() {
4444
FieldPermissions fieldPermissions2 = randomBoolean()
4545
? fieldPermissionsCache.getFieldPermissions(allowed2, denied2)
4646
: new FieldPermissions(fieldPermissionDef(allowed2, denied2));
47-
FieldPermissions mergedFieldPermissions = fieldPermissionsCache.getFieldPermissions(
48-
Arrays.asList(fieldPermissions1, fieldPermissions2)
49-
);
47+
FieldPermissions mergedFieldPermissions = fieldPermissionsCache.union(Arrays.asList(fieldPermissions1, fieldPermissions2));
5048
assertTrue(mergedFieldPermissions.grantsAccessTo(allowedPrefix1 + "b"));
5149
assertTrue(mergedFieldPermissions.grantsAccessTo(allowedPrefix2 + "b"));
5250
assertFalse(mergedFieldPermissions.grantsAccessTo(denied1[0]));
@@ -62,7 +60,7 @@ public void testMergeFieldPermissions() {
6260
fieldPermissions2 = randomBoolean()
6361
? fieldPermissionsCache.getFieldPermissions(allowed2, denied2)
6462
: new FieldPermissions(fieldPermissionDef(allowed2, denied2));
65-
mergedFieldPermissions = fieldPermissionsCache.getFieldPermissions(Arrays.asList(fieldPermissions1, fieldPermissions2));
63+
mergedFieldPermissions = fieldPermissionsCache.union(Arrays.asList(fieldPermissions1, fieldPermissions2));
6664
assertFalse(mergedFieldPermissions.hasFieldLevelSecurity());
6765

6866
allowed1 = new String[] {};
@@ -75,7 +73,7 @@ public void testMergeFieldPermissions() {
7573
fieldPermissions2 = randomBoolean()
7674
? fieldPermissionsCache.getFieldPermissions(allowed2, denied2)
7775
: new FieldPermissions(fieldPermissionDef(allowed2, denied2));
78-
mergedFieldPermissions = fieldPermissionsCache.getFieldPermissions(Arrays.asList(fieldPermissions1, fieldPermissions2));
76+
mergedFieldPermissions = fieldPermissionsCache.union(Arrays.asList(fieldPermissions1, fieldPermissions2));
7977
for (String field : allowed2) {
8078
assertTrue(mergedFieldPermissions.grantsAccessTo(field));
8179
}
@@ -93,7 +91,7 @@ public void testMergeFieldPermissions() {
9391
fieldPermissions2 = randomBoolean()
9492
? fieldPermissionsCache.getFieldPermissions(allowed2, denied2)
9593
: new FieldPermissions(fieldPermissionDef(allowed2, denied2));
96-
mergedFieldPermissions = fieldPermissionsCache.getFieldPermissions(Arrays.asList(fieldPermissions1, fieldPermissions2));
94+
mergedFieldPermissions = fieldPermissionsCache.union(Arrays.asList(fieldPermissions1, fieldPermissions2));
9795
assertTrue(mergedFieldPermissions.grantsAccessTo("a"));
9896
assertTrue(mergedFieldPermissions.grantsAccessTo("b"));
9997

@@ -107,7 +105,7 @@ public void testMergeFieldPermissions() {
107105
fieldPermissions2 = randomBoolean()
108106
? fieldPermissionsCache.getFieldPermissions(allowed2, denied2)
109107
: new FieldPermissions(fieldPermissionDef(allowed2, denied2));
110-
mergedFieldPermissions = fieldPermissionsCache.getFieldPermissions(Arrays.asList(fieldPermissions1, fieldPermissions2));
108+
mergedFieldPermissions = fieldPermissionsCache.union(Arrays.asList(fieldPermissions1, fieldPermissions2));
111109
assertTrue(mergedFieldPermissions.grantsAccessTo("a"));
112110
assertTrue(mergedFieldPermissions.grantsAccessTo("b"));
113111
assertFalse(mergedFieldPermissions.grantsAccessTo("aa"));
@@ -126,7 +124,7 @@ public void testNonFlsAndFlsMerging() {
126124
FieldPermissionsCache cache = new FieldPermissionsCache(Settings.EMPTY);
127125
for (int i = 0; i < scaledRandomIntBetween(1, 12); i++) {
128126
Collections.shuffle(permissionsList, random());
129-
FieldPermissions result = cache.getFieldPermissions(permissionsList);
127+
FieldPermissions result = cache.union(permissionsList);
130128
assertFalse(result.hasFieldLevelSecurity());
131129
}
132130
}

0 commit comments

Comments
 (0)