diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java index 6dfc2204c008c..9a8a9bff2b6b5 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/RoleDescriptor.java @@ -5,6 +5,10 @@ */ package org.elasticsearch.xpack.core.security.authz; +import org.apache.lucene.util.automaton.Automata; +import org.apache.lucene.util.automaton.Automaton; +import org.apache.lucene.util.automaton.MinimizationOperations; +import org.apache.lucene.util.automaton.Operations; import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; @@ -15,6 +19,7 @@ import org.elasticsearch.common.io.stream.StreamInput; import org.elasticsearch.common.io.stream.StreamOutput; import org.elasticsearch.common.io.stream.Writeable; +import org.elasticsearch.common.regex.Regex; import org.elasticsearch.common.xcontent.LoggingDeprecationHandler; import org.elasticsearch.common.xcontent.NamedXContentRegistry; import org.elasticsearch.common.xcontent.ObjectParser; @@ -25,6 +30,7 @@ import org.elasticsearch.common.xcontent.json.JsonXContent; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege; import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges; +import org.elasticsearch.xpack.core.security.support.Automatons; import org.elasticsearch.xpack.core.security.support.Validation; import org.elasticsearch.xpack.core.security.xcontent.XContentUtils; @@ -38,6 +44,8 @@ import java.util.Map; import java.util.Objects; +import static org.apache.lucene.util.automaton.Operations.subsetOf; + /** * A holder for a Role that contains user-readable information about the Role * without containing the actual Role object. @@ -532,6 +540,7 @@ private static RoleDescriptor.IndicesPrivileges parseIndex(String roleName, XCon throw new ElasticsearchParseException("failed to parse indices privileges for role [{}]. {} requires {} if {} is given", roleName, Fields.FIELD_PERMISSIONS, Fields.GRANT_FIELDS, Fields.EXCEPT_FIELDS); } + checkIfExceptFieldsIsSubsetOfGrantedFields(roleName, grantedFields, deniedFields); return RoleDescriptor.IndicesPrivileges.builder() .indices(names) .privileges(privileges) @@ -542,6 +551,33 @@ private static RoleDescriptor.IndicesPrivileges parseIndex(String roleName, XCon .build(); } + private static void checkIfExceptFieldsIsSubsetOfGrantedFields(String roleName, String[] grantedFields, String[] deniedFields) { + Automaton grantedFieldsAutomaton; + if (grantedFields == null || Arrays.stream(grantedFields).anyMatch(Regex::isMatchAllPattern)) { + grantedFieldsAutomaton = Automatons.MATCH_ALL; + } else { + // an automaton that includes metadata fields, including join fields created by the _parent field such + // as _parent#type + Automaton metaFieldsAutomaton = Operations.concatenate(Automata.makeChar('_'), Automata.makeAnyString()); + grantedFieldsAutomaton = Operations.union(Automatons.patterns(grantedFields), metaFieldsAutomaton); + } + Automaton deniedFieldsAutomaton; + if (deniedFields == null || deniedFields.length == 0) { + deniedFieldsAutomaton = Automatons.EMPTY; + } else { + deniedFieldsAutomaton = Automatons.patterns(deniedFields); + } + grantedFieldsAutomaton = MinimizationOperations.minimize(grantedFieldsAutomaton, + Operations.DEFAULT_MAX_DETERMINIZED_STATES); + deniedFieldsAutomaton = MinimizationOperations.minimize(deniedFieldsAutomaton, + Operations.DEFAULT_MAX_DETERMINIZED_STATES); + if (subsetOf(deniedFieldsAutomaton, grantedFieldsAutomaton) == false) { + throw new ElasticsearchParseException("failed to parse indices privileges for role [{}]. [{}] field values [{}] must be a " + + "subset of [{}] field values [{}]", roleName, Fields.EXCEPT_FIELDS, Strings.arrayToCommaDelimitedString(deniedFields), + Fields.GRANT_FIELDS, Strings.arrayToCommaDelimitedString(grantedFields)); + } + } + private static ApplicationResourcePrivileges[] parseApplicationPrivileges(String roleName, XContentParser parser) throws IOException { if (parser.currentToken() != XContentParser.Token.START_ARRAY) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java index 0c20a7c20d09c..b872c2f64dd3e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java @@ -5,6 +5,7 @@ */ package org.elasticsearch.xpack.security.authz; +import org.elasticsearch.ElasticsearchParseException; import org.elasticsearch.Version; import org.elasticsearch.common.Strings; import org.elasticsearch.common.bytes.BytesArray; @@ -27,6 +28,7 @@ import org.elasticsearch.xpack.core.security.support.MetadataUtils; import org.hamcrest.Matchers; +import java.io.IOException; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; @@ -296,4 +298,28 @@ public void testParseIgnoresTransientMetadata() throws Exception { assertEquals(true, parsed.getTransientMetadata().get("enabled")); } + public void testParseIndicesPrivilegesSucceedsWhenExceptFieldsIsSubsetOfGrantedFields() throws IOException { + final boolean grantAll = randomBoolean(); + final String grant = grantAll ? "\"*\"" : "\"f1\",\"f2\""; + final String except = grantAll ? "\"_fx\",\"f8\"" : "\"f1\""; + + final String json = "{ \"indices\": [{\"names\": [\"idx1\",\"idx2\"], \"privileges\": [\"p1\", \"p2\"], \"field_security\" : { " + + "\"grant\" : [" + grant + "], \"except\" : [" + except + "] } }] }"; + final RoleDescriptor rd = RoleDescriptor.parse("test", + new BytesArray(json), false, XContentType.JSON); + assertEquals("test", rd.getName()); + assertEquals(1, rd.getIndicesPrivileges().length); + assertArrayEquals(new String[]{"idx1", "idx2"}, rd.getIndicesPrivileges()[0].getIndices()); + assertArrayEquals((grantAll) ? new String[]{"*"} : new String[]{"f1", "f2"}, rd.getIndicesPrivileges()[0].getGrantedFields()); + assertArrayEquals((grantAll) ? new String[]{"_fx", "f8"} : new String[]{"f1"}, rd.getIndicesPrivileges()[0].getDeniedFields()); + } + + public void testParseIndicesPrivilegesFailsWhenExceptFieldsAreNotSubsetOfGrantedFields() { + final String json = "{ \"indices\": [{\"names\": [\"idx1\",\"idx2\"], \"privileges\": [\"p1\", \"p2\"], \"field_security\" : { " + + "\"grant\" : [\"f1\",\"f2\"], \"except\" : [\"f3\"] } }] }"; + final ElasticsearchParseException epe = expectThrows(ElasticsearchParseException.class, () -> RoleDescriptor.parse("test", + new BytesArray(json), false, XContentType.JSON)); + assertThat(epe.getMessage(), containsString("must be a " + + "subset of [grant] field values ")); + } }