-
Notifications
You must be signed in to change notification settings - Fork 25.2k
Support multi-intersection for FieldPermissions #91169
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
8814909
28eb65d
970540b
e4213f1
e09ef68
49cf6b2
e9b149a
cf27cca
ac19092
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -18,7 +18,7 @@ | |
import org.elasticsearch.common.Strings; | ||
import org.elasticsearch.common.io.stream.StreamOutput; | ||
import org.elasticsearch.common.regex.Regex; | ||
import org.elasticsearch.core.Nullable; | ||
import org.elasticsearch.common.util.CollectionUtils; | ||
import org.elasticsearch.xpack.core.security.authz.accesscontrol.FieldSubsetReader; | ||
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissionsDefinition.FieldGrantExcludeGroup; | ||
import org.elasticsearch.xpack.core.security.authz.support.SecurityQueryTemplateEvaluator.DlsQueryEvaluationContext; | ||
|
@@ -61,9 +61,8 @@ public final class FieldPermissions implements Accountable, CacheKey { | |
BASE_HASHSET_ENTRY_SIZE = mapEntryShallowSize + 2 * RamUsageEstimator.NUM_BYTES_OBJECT_REF; | ||
} | ||
|
||
private final FieldPermissionsDefinition fieldPermissionsDefinition; | ||
@Nullable | ||
private final FieldPermissionsDefinition limitedByFieldPermissionsDefinition; | ||
private final List<FieldPermissionsDefinition> fieldPermissionsDefinitions; | ||
|
||
// an automaton that represents a union of one more sets of permitted and denied fields | ||
private final CharacterRunAutomaton permittedFieldsAutomaton; | ||
private final boolean permittedFieldsAutomatonIsTotal; | ||
|
@@ -72,7 +71,7 @@ public final class FieldPermissions implements Accountable, CacheKey { | |
private final long ramBytesUsed; | ||
|
||
/** Constructor that does not enable field-level security: all fields are accepted. */ | ||
public FieldPermissions() { | ||
private FieldPermissions() { | ||
this(new FieldPermissionsDefinition(null, null), Automatons.MATCH_ALL); | ||
} | ||
Comment on lines
-75
to
76
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Make this constructor private since it is only really useful for building the static |
||
|
||
|
@@ -85,33 +84,33 @@ public FieldPermissions(FieldPermissionsDefinition fieldPermissionsDefinition) { | |
/** Constructor that enables field-level security based on include/exclude rules. Exclude rules | ||
* have precedence over include rules. */ | ||
FieldPermissions(FieldPermissionsDefinition fieldPermissionsDefinition, Automaton permittedFieldsAutomaton) { | ||
this(fieldPermissionsDefinition, null, permittedFieldsAutomaton); | ||
this( | ||
List.of(Objects.requireNonNull(fieldPermissionsDefinition, "field permission definition cannot be null")), | ||
permittedFieldsAutomaton | ||
); | ||
} | ||
|
||
/** Constructor that enables field-level security based on include/exclude rules. Exclude rules | ||
* have precedence over include rules. */ | ||
private FieldPermissions( | ||
FieldPermissionsDefinition fieldPermissionsDefinition, | ||
@Nullable FieldPermissionsDefinition limitedByFieldPermissionsDefinition, | ||
Automaton permittedFieldsAutomaton | ||
) { | ||
private FieldPermissions(List<FieldPermissionsDefinition> fieldPermissionsDefinitions, Automaton permittedFieldsAutomaton) { | ||
if (permittedFieldsAutomaton.isDeterministic() == false && permittedFieldsAutomaton.getNumStates() > 1) { | ||
// we only accept deterministic automata so that the CharacterRunAutomaton constructor | ||
// directly wraps the provided automaton | ||
throw new IllegalArgumentException("Only accepts deterministic automata"); | ||
} | ||
this.fieldPermissionsDefinition = Objects.requireNonNull(fieldPermissionsDefinition, "field permission definition cannot be null"); | ||
this.limitedByFieldPermissionsDefinition = limitedByFieldPermissionsDefinition; | ||
this.fieldPermissionsDefinitions = Objects.requireNonNull( | ||
fieldPermissionsDefinitions, | ||
"field permission definitions cannot be null" | ||
); | ||
this.originalAutomaton = permittedFieldsAutomaton; | ||
this.permittedFieldsAutomaton = new CharacterRunAutomaton(permittedFieldsAutomaton); | ||
// we cache the result of isTotal since this might be a costly operation | ||
this.permittedFieldsAutomatonIsTotal = Operations.isTotal(permittedFieldsAutomaton); | ||
|
||
long ramBytesUsed = BASE_FIELD_PERM_DEF_BYTES; | ||
ramBytesUsed += ramBytesUsedForFieldPermissionsDefinition(this.fieldPermissionsDefinition); | ||
if (this.limitedByFieldPermissionsDefinition != null) { | ||
ramBytesUsed += ramBytesUsedForFieldPermissionsDefinition(this.limitedByFieldPermissionsDefinition); | ||
} | ||
ramBytesUsed += this.fieldPermissionsDefinitions.stream() | ||
.mapToLong(FieldPermissions::ramBytesUsedForFieldPermissionsDefinition) | ||
.sum(); | ||
ramBytesUsed += permittedFieldsAutomaton.ramBytesUsed(); | ||
ramBytesUsed += runAutomatonRamBytesUsed(permittedFieldsAutomaton); | ||
this.ramBytesUsed = ramBytesUsed; | ||
|
@@ -199,12 +198,16 @@ public static Automaton buildPermittedFieldsAutomaton(final String[] grantedFiel | |
*/ | ||
public FieldPermissions limitFieldPermissions(FieldPermissions limitedBy) { | ||
if (hasFieldLevelSecurity() && limitedBy != null && limitedBy.hasFieldLevelSecurity()) { | ||
// TODO: the automaton computation is not cached | ||
Automaton _permittedFieldsAutomaton = Automatons.intersectAndMinimize(getIncludeAutomaton(), limitedBy.getIncludeAutomaton()); | ||
jakelandis marked this conversation as resolved.
Show resolved
Hide resolved
|
||
return new FieldPermissions(fieldPermissionsDefinition, limitedBy.fieldPermissionsDefinition, _permittedFieldsAutomaton); | ||
return new FieldPermissions( | ||
CollectionUtils.concatLists(fieldPermissionsDefinitions, limitedBy.fieldPermissionsDefinitions), | ||
_permittedFieldsAutomaton | ||
); | ||
} else if (limitedBy != null && limitedBy.hasFieldLevelSecurity()) { | ||
return new FieldPermissions(limitedBy.getFieldPermissionsDefinition(), limitedBy.getIncludeAutomaton()); | ||
return new FieldPermissions(limitedBy.fieldPermissionsDefinitions, limitedBy.getIncludeAutomaton()); | ||
} else if (hasFieldLevelSecurity()) { | ||
return new FieldPermissions(this.getFieldPermissionsDefinition(), getIncludeAutomaton()); | ||
return new FieldPermissions(fieldPermissionsDefinitions, getIncludeAutomaton()); | ||
} | ||
return FieldPermissions.DEFAULT; | ||
} | ||
|
@@ -217,23 +220,13 @@ public boolean grantsAccessTo(String fieldName) { | |
return permittedFieldsAutomatonIsTotal || permittedFieldsAutomaton.run(fieldName); | ||
} | ||
|
||
public FieldPermissionsDefinition getFieldPermissionsDefinition() { | ||
return fieldPermissionsDefinition; | ||
} | ||
|
||
public FieldPermissionsDefinition getLimitedByFieldPermissionsDefinition() { | ||
return limitedByFieldPermissionsDefinition; | ||
public List<FieldPermissionsDefinition> getFieldPermissionsDefinitions() { | ||
return fieldPermissionsDefinitions; | ||
} | ||
|
||
@Override | ||
public void buildCacheKey(StreamOutput out, DlsQueryEvaluationContext context) throws IOException { | ||
fieldPermissionsDefinition.buildCacheKey(out, context); | ||
if (limitedByFieldPermissionsDefinition != null) { | ||
out.writeBoolean(true); | ||
limitedByFieldPermissionsDefinition.buildCacheKey(out, context); | ||
} else { | ||
out.writeBoolean(false); | ||
} | ||
out.writeCollection(fieldPermissionsDefinitions, (o, fpd) -> fpd.buildCacheKey(o, context)); | ||
} | ||
|
||
/** Return whether field-level security is enabled, ie. whether any field might be filtered out. */ | ||
|
@@ -259,13 +252,12 @@ public boolean equals(Object o) { | |
if (o == null || getClass() != o.getClass()) return false; | ||
FieldPermissions that = (FieldPermissions) o; | ||
return permittedFieldsAutomatonIsTotal == that.permittedFieldsAutomatonIsTotal | ||
&& fieldPermissionsDefinition.equals(that.fieldPermissionsDefinition) | ||
&& Objects.equals(limitedByFieldPermissionsDefinition, that.limitedByFieldPermissionsDefinition); | ||
&& fieldPermissionsDefinitions.equals(that.fieldPermissionsDefinitions); | ||
} | ||
|
||
@Override | ||
public int hashCode() { | ||
return Objects.hash(fieldPermissionsDefinition, limitedByFieldPermissionsDefinition, permittedFieldsAutomatonIsTotal); | ||
return Objects.hash(fieldPermissionsDefinitions, permittedFieldsAutomatonIsTotal); | ||
} | ||
|
||
@Override | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -72,26 +72,25 @@ public FieldPermissions getFieldPermissions(FieldPermissionsDefinition fieldPerm | |
} | ||
|
||
/** | ||
* Returns a field permissions object that corresponds to the merging of the given field permissions and caches the instance if one was | ||
* not found in the cache. | ||
* Returns a field permissions object that corresponds to the union of the given field permissions. | ||
* Union means a field is granted if it is granted by any of the FieldPermissions from the given | ||
* collection. | ||
* The returned instance is cached if one was not found in the cache. | ||
*/ | ||
FieldPermissions getFieldPermissions(Collection<FieldPermissions> fieldPermissionsCollection) { | ||
FieldPermissions union(Collection<FieldPermissions> fieldPermissionsCollection) { | ||
Comment on lines
-78
to
+80
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rename this method because it should only be used for union field permissions within the same role. It must not be used for multiple limiting (intersecting) roles. |
||
Optional<FieldPermissions> allowAllFieldPermissions = fieldPermissionsCollection.stream() | ||
.filter(((Predicate<FieldPermissions>) (FieldPermissions::hasFieldLevelSecurity)).negate()) | ||
.findFirst(); | ||
return allowAllFieldPermissions.orElseGet(() -> { | ||
final Set<FieldGrantExcludeGroup> fieldGrantExcludeGroups = new HashSet<>(); | ||
for (FieldPermissions fieldPermissions : fieldPermissionsCollection) { | ||
final FieldPermissionsDefinition definition = fieldPermissions.getFieldPermissionsDefinition(); | ||
final FieldPermissionsDefinition limitedByDefinition = fieldPermissions.getLimitedByFieldPermissionsDefinition(); | ||
if (definition == null) { | ||
throw new IllegalArgumentException("Expected field permission definition, but found null"); | ||
} else if (limitedByDefinition != null) { | ||
final List<FieldPermissionsDefinition> fieldPermissionsDefinitions = fieldPermissions.getFieldPermissionsDefinitions(); | ||
if (fieldPermissionsDefinitions.size() != 1) { | ||
throw new IllegalArgumentException( | ||
"Expected no limited-by field permission definition, but found [" + limitedByDefinition + "]" | ||
"Expected a single field permission definition, but found [" + fieldPermissionsDefinitions + "]" | ||
); | ||
} | ||
fieldGrantExcludeGroups.addAll(definition.getFieldGrantExcludeGroups()); | ||
fieldGrantExcludeGroups.addAll(fieldPermissionsDefinitions.get(0).getFieldGrantExcludeGroups()); | ||
} | ||
final FieldPermissionsDefinition combined = new FieldPermissionsDefinition(fieldGrantExcludeGroups); | ||
try { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is the critical change of this PR: replacing two hard coded variables with a List (similar to that of #91151). Everything else revolves around it.