Skip to content

Commit e35e30d

Browse files
committed
Validate field permissions when creating a role
When creating a role, we do not check if the exceptions for the field permissions are a subset of granted fields. If such a role is assigned to a user then that user's authentication fails for this reason. We added a check to validate role query in elastic#46275 and on the same lines, this commit adds check if the exceptions for the field permissions is a subset of granted fields when parsing the index privileges from the role descriptor. Co-authored-by: Yogesh Gaikwad <[email protected]> Backport of: elastic#50212
1 parent eb8fd44 commit e35e30d

File tree

3 files changed

+46
-2
lines changed

3 files changed

+46
-2
lines changed

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

+11
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import org.elasticsearch.ElasticsearchParseException;
99
import org.elasticsearch.Version;
10+
import org.elasticsearch.ElasticsearchSecurityException;
1011
import org.elasticsearch.common.Nullable;
1112
import org.elasticsearch.common.ParseField;
1213
import org.elasticsearch.common.Strings;
@@ -24,6 +25,7 @@
2425
import org.elasticsearch.common.xcontent.XContentParser;
2526
import org.elasticsearch.common.xcontent.XContentType;
2627
import org.elasticsearch.common.xcontent.json.JsonXContent;
28+
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions;
2729
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege;
2830
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivileges;
2931
import org.elasticsearch.xpack.core.security.support.Validation;
@@ -539,6 +541,7 @@ private static RoleDescriptor.IndicesPrivileges parseIndex(String roleName, XCon
539541
throw new ElasticsearchParseException("failed to parse indices privileges for role [{}]. {} requires {} if {} is given",
540542
roleName, Fields.FIELD_PERMISSIONS, Fields.GRANT_FIELDS, Fields.EXCEPT_FIELDS);
541543
}
544+
checkIfExceptFieldsIsSubsetOfGrantedFields(roleName, grantedFields, deniedFields);
542545
return RoleDescriptor.IndicesPrivileges.builder()
543546
.indices(names)
544547
.privileges(privileges)
@@ -549,6 +552,14 @@ private static RoleDescriptor.IndicesPrivileges parseIndex(String roleName, XCon
549552
.build();
550553
}
551554

555+
private static void checkIfExceptFieldsIsSubsetOfGrantedFields(String roleName, String[] grantedFields, String[] deniedFields) {
556+
try {
557+
FieldPermissions.buildPermittedFieldsAutomaton(grantedFields, deniedFields);
558+
} catch (ElasticsearchSecurityException e) {
559+
throw new ElasticsearchParseException("failed to parse indices privileges for role [{}] - {}", e, roleName, e.getMessage());
560+
}
561+
}
562+
552563
private static ApplicationResourcePrivileges[] parseApplicationPrivileges(String roleName, XContentParser parser)
553564
throws IOException {
554565
if (parser.currentToken() != XContentParser.Token.START_ARRAY) {

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

+6-2
Original file line numberDiff line numberDiff line change
@@ -119,12 +119,16 @@ public static Automaton initializePermittedFieldsAutomaton(FieldPermissionsDefin
119119
assert groups.size() > 0 : "there must always be a single group for field inclusion/exclusion";
120120
List<Automaton> automatonList =
121121
groups.stream()
122-
.map(g -> FieldPermissions.initializePermittedFieldsAutomaton(g.getGrantedFields(), g.getExcludedFields()))
122+
.map(g -> FieldPermissions.buildPermittedFieldsAutomaton(g.getGrantedFields(), g.getExcludedFields()))
123123
.collect(Collectors.toList());
124124
return Automatons.unionAndMinimize(automatonList);
125125
}
126126

127-
private static Automaton initializePermittedFieldsAutomaton(final String[] grantedFields, final String[] deniedFields) {
127+
/**
128+
* Construct a single automaton to represent the set of {@code grantedFields} except for the {@code deniedFields}.
129+
* @throws ElasticsearchSecurityException If {@code deniedFields} is not a subset of {@code grantedFields}.
130+
*/
131+
public static Automaton buildPermittedFieldsAutomaton(final String[] grantedFields, final String[] deniedFields) {
128132
Automaton grantedFieldsAutomaton;
129133
if (grantedFields == null || Arrays.stream(grantedFields).anyMatch(Regex::isMatchAllPattern)) {
130134
grantedFieldsAutomaton = Automatons.MATCH_ALL;

x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authz/RoleDescriptorTests.java

+29
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
*/
66
package org.elasticsearch.xpack.security.authz;
77

8+
import org.elasticsearch.ElasticsearchParseException;
89
import org.elasticsearch.Version;
910
import org.elasticsearch.common.Strings;
1011
import org.elasticsearch.common.bytes.BytesArray;
@@ -19,6 +20,7 @@
1920
import org.elasticsearch.common.xcontent.XContentBuilder;
2021
import org.elasticsearch.common.xcontent.XContentType;
2122
import org.elasticsearch.test.ESTestCase;
23+
import org.elasticsearch.test.TestMatchers;
2224
import org.elasticsearch.test.VersionUtils;
2325
import org.elasticsearch.xpack.core.XPackClientPlugin;
2426
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
@@ -27,6 +29,7 @@
2729
import org.elasticsearch.xpack.core.security.support.MetadataUtils;
2830
import org.hamcrest.Matchers;
2931

32+
import java.io.IOException;
3033
import java.util.Arrays;
3134
import java.util.Collections;
3235
import java.util.LinkedHashSet;
@@ -296,4 +299,30 @@ public void testParseIgnoresTransientMetadata() throws Exception {
296299
assertEquals(true, parsed.getTransientMetadata().get("enabled"));
297300
}
298301

302+
public void testParseIndicesPrivilegesSucceedsWhenExceptFieldsIsSubsetOfGrantedFields() throws IOException {
303+
final boolean grantAll = randomBoolean();
304+
final String grant = grantAll ? "\"*\"" : "\"f1\",\"f2\"";
305+
final String except = grantAll ? "\"_fx\",\"f8\"" : "\"f1\"";
306+
307+
final String json = "{ \"indices\": [{\"names\": [\"idx1\",\"idx2\"], \"privileges\": [\"p1\", \"p2\"], \"field_security\" : { " +
308+
"\"grant\" : [" + grant + "], \"except\" : [" + except + "] } }] }";
309+
final RoleDescriptor rd = RoleDescriptor.parse("test",
310+
new BytesArray(json), false, XContentType.JSON);
311+
assertEquals("test", rd.getName());
312+
assertEquals(1, rd.getIndicesPrivileges().length);
313+
assertArrayEquals(new String[]{"idx1", "idx2"}, rd.getIndicesPrivileges()[0].getIndices());
314+
assertArrayEquals((grantAll) ? new String[]{"*"} : new String[]{"f1", "f2"}, rd.getIndicesPrivileges()[0].getGrantedFields());
315+
assertArrayEquals((grantAll) ? new String[]{"_fx", "f8"} : new String[]{"f1"}, rd.getIndicesPrivileges()[0].getDeniedFields());
316+
}
317+
318+
public void testParseIndicesPrivilegesFailsWhenExceptFieldsAreNotSubsetOfGrantedFields() {
319+
final String json = "{ \"indices\": [{\"names\": [\"idx1\",\"idx2\"], \"privileges\": [\"p1\", \"p2\"], \"field_security\" : { " +
320+
"\"grant\" : [\"f1\",\"f2\"], \"except\" : [\"f3\"] } }] }";
321+
final ElasticsearchParseException epe = expectThrows(ElasticsearchParseException.class, () -> RoleDescriptor.parse("test",
322+
new BytesArray(json), false, XContentType.JSON));
323+
assertThat(epe, TestMatchers.throwableWithMessage(containsString("must be a subset of the granted fields ")));
324+
assertThat(epe, TestMatchers.throwableWithMessage(containsString("f1")));
325+
assertThat(epe, TestMatchers.throwableWithMessage(containsString("f2")));
326+
assertThat(epe, TestMatchers.throwableWithMessage(containsString("f3")));
327+
}
299328
}

0 commit comments

Comments
 (0)