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..832e9c9365e94 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 @@ -6,6 +6,7 @@ package org.elasticsearch.xpack.core.security.authz; import org.elasticsearch.ElasticsearchParseException; +import org.elasticsearch.ElasticsearchSecurityException; import org.elasticsearch.common.Nullable; import org.elasticsearch.common.ParseField; import org.elasticsearch.common.Strings; @@ -23,6 +24,7 @@ import org.elasticsearch.common.xcontent.XContentParser; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions; 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.Validation; @@ -532,6 +534,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 +545,14 @@ private static RoleDescriptor.IndicesPrivileges parseIndex(String roleName, XCon .build(); } + private static void checkIfExceptFieldsIsSubsetOfGrantedFields(String roleName, String[] grantedFields, String[] deniedFields) { + try { + FieldPermissions.buildPermittedFieldsAutomaton(grantedFields, deniedFields); + } catch (ElasticsearchSecurityException e) { + throw new ElasticsearchParseException("failed to parse indices privileges for role [{}] - {}", e, roleName, e.getMessage()); + } + } + private static ApplicationResourcePrivileges[] parseApplicationPrivileges(String roleName, XContentParser parser) throws IOException { if (parser.currentToken() != XContentParser.Token.START_ARRAY) { diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java index f58367dc43886..f745629b60199 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/authz/permission/FieldPermissions.java @@ -119,12 +119,16 @@ public static Automaton initializePermittedFieldsAutomaton(FieldPermissionsDefin assert groups.size() > 0 : "there must always be a single group for field inclusion/exclusion"; List automatonList = groups.stream() - .map(g -> FieldPermissions.initializePermittedFieldsAutomaton(g.getGrantedFields(), g.getExcludedFields())) + .map(g -> FieldPermissions.buildPermittedFieldsAutomaton(g.getGrantedFields(), g.getExcludedFields())) .collect(Collectors.toList()); return Automatons.unionAndMinimize(automatonList); } - private static Automaton initializePermittedFieldsAutomaton(final String[] grantedFields, final String[] deniedFields) { + /** + * Construct a single automaton to represent the set of {@code grantedFields} except for the {@code deniedFields}. + * @throws ElasticsearchSecurityException If {@code deniedFields} is not a subset of {@code grantedFields}. + */ + public static Automaton buildPermittedFieldsAutomaton(final String[] grantedFields, final String[] deniedFields) { Automaton grantedFieldsAutomaton; if (grantedFields == null || Arrays.stream(grantedFields).anyMatch(Regex::isMatchAllPattern)) { grantedFieldsAutomaton = Automatons.MATCH_ALL; 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..66161ee27ec19 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; @@ -19,6 +20,7 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.TestMatchers; import org.elasticsearch.test.VersionUtils; import org.elasticsearch.xpack.core.XPackClientPlugin; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; @@ -27,6 +29,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 +299,30 @@ 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, TestMatchers.throwableWithMessage(containsString("must be a subset of the granted fields "))); + assertThat(epe, TestMatchers.throwableWithMessage(containsString("f1"))); + assertThat(epe, TestMatchers.throwableWithMessage(containsString("f2"))); + assertThat(epe, TestMatchers.throwableWithMessage(containsString("f3"))); + } }