Skip to content

Commit 36ad069

Browse files
Fix Has Privilege API check on restricted indices (#41226)
The Has Privileges API allows to tap into the authorization process, to validate privileges without actually running the operations to be authorized. This commit fixes a bug, in which the Has Privilege API returned spurious results when checking for index privileges over restricted indices (currently .security, .security-6, .security-7). The actual authorization process is not affected by the bug.
1 parent 6b58cee commit 36ad069

File tree

4 files changed

+164
-25
lines changed

4 files changed

+164
-25
lines changed

x-pack/docs/en/rest-api/security/has-privileges.asciidoc

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,13 @@ privilege is assigned to the user.
2929

3030
`index`::
3131
`names`::: (list) A list of indices.
32-
`allow_restricted_indices`::: (boolean) If `names` contains internal restricted
33-
that also have to be covered by the has-privilege check, then this has to be
34-
set to `true`. By default this is `false` because restricted indices should
35-
generaly not be "visible" to APIs. For most use cases it is safe to ignore
36-
this parameter.
32+
`allow_restricted_indices`::: (boolean) This needs to be set to `true` (default
33+
is `false`) if using wildcards or regexps for patterns that cover restricted
34+
indices. Implicitly, restricted indices do not match index patterns because
35+
restricted indices usually have limited privileges and including them in
36+
pattern tests would render most such tests `false`. If restricted indices are
37+
explicitly included in the `names` list, privileges will be checked against
38+
them regardless of the value of `allow_restricted_indices`.
3739
`privileges`::: (list) A list of the privileges that you want to check for the
3840
specified indices.
3941

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

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -138,27 +138,40 @@ public ResourcePrivilegesMap checkResourcePrivileges(Set<String> checkForIndexPa
138138
final ResourcePrivilegesMap.Builder resourcePrivilegesMapBuilder = ResourcePrivilegesMap.builder();
139139
final Map<IndicesPermission.Group, Automaton> predicateCache = new HashMap<>();
140140
for (String forIndexPattern : checkForIndexPatterns) {
141-
final Automaton checkIndexAutomaton = IndicesPermission.Group.buildIndexMatcherAutomaton(allowRestrictedIndices,
142-
forIndexPattern);
143-
Automaton allowedIndexPrivilegesAutomaton = null;
144-
for (Group group : groups) {
145-
final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group,
146-
g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices()));
147-
if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) {
148-
if (allowedIndexPrivilegesAutomaton != null) {
149-
allowedIndexPrivilegesAutomaton = Automatons
150-
.unionAndMinimize(Arrays.asList(allowedIndexPrivilegesAutomaton, group.privilege().getAutomaton()));
141+
Automaton checkIndexAutomaton = Automatons.patterns(forIndexPattern);
142+
if (false == allowRestrictedIndices && false == RestrictedIndicesNames.RESTRICTED_NAMES.contains(forIndexPattern)) {
143+
checkIndexAutomaton = Automatons.minusAndMinimize(checkIndexAutomaton, RestrictedIndicesNames.NAMES_AUTOMATON);
144+
}
145+
if (false == Operations.isEmpty(checkIndexAutomaton)) {
146+
Automaton allowedIndexPrivilegesAutomaton = null;
147+
for (Group group : groups) {
148+
final Automaton groupIndexAutomaton = predicateCache.computeIfAbsent(group,
149+
g -> IndicesPermission.Group.buildIndexMatcherAutomaton(g.allowRestrictedIndices(), g.indices()));
150+
if (Operations.subsetOf(checkIndexAutomaton, groupIndexAutomaton)) {
151+
if (allowedIndexPrivilegesAutomaton != null) {
152+
allowedIndexPrivilegesAutomaton = Automatons
153+
.unionAndMinimize(Arrays.asList(allowedIndexPrivilegesAutomaton, group.privilege().getAutomaton()));
154+
} else {
155+
allowedIndexPrivilegesAutomaton = group.privilege().getAutomaton();
156+
}
157+
}
158+
}
159+
for (String privilege : checkForPrivileges) {
160+
IndexPrivilege indexPrivilege = IndexPrivilege.get(Collections.singleton(privilege));
161+
if (allowedIndexPrivilegesAutomaton != null
162+
&& Operations.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) {
163+
resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE);
151164
} else {
152-
allowedIndexPrivilegesAutomaton = group.privilege().getAutomaton();
165+
resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.FALSE);
153166
}
154167
}
155-
}
156-
for (String privilege : checkForPrivileges) {
157-
IndexPrivilege indexPrivilege = IndexPrivilege.get(Collections.singleton(privilege));
158-
if (allowedIndexPrivilegesAutomaton != null
159-
&& Operations.subsetOf(indexPrivilege.getAutomaton(), allowedIndexPrivilegesAutomaton)) {
160-
resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.TRUE);
161-
} else {
168+
} else {
169+
// the index pattern produced the empty automaton, presumably because the requested pattern expands exclusively inside the
170+
// restricted indices namespace - a namespace of indices that are normally hidden when granting/checking privileges - and
171+
// the pattern was not marked as `allowRestrictedIndices`. We try to anticipate this by considering _explicit_ restricted
172+
// indices even if `allowRestrictedIndices` is false.
173+
// TODO The `false` result is a _safe_ default but this is actually an error. Make it an error.
174+
for (String privilege : checkForPrivileges) {
162175
resourcePrivilegesMapBuilder.addResourcePrivilege(forIndexPattern, privilege, Boolean.FALSE);
163176
}
164177
}

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

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,6 @@ public class Role {
4949
this.runAs = Objects.requireNonNull(runAs);
5050
}
5151

52-
5352
public String[] names() {
5453
return names;
5554
}
@@ -120,7 +119,7 @@ public boolean checkIndicesAction(String action) {
120119
* @return an instance of {@link ResourcePrivilegesMap}
121120
*/
122121
public ResourcePrivilegesMap checkIndicesPrivileges(Set<String> checkForIndexPatterns, boolean allowRestrictedIndices,
123-
Set<String> checkForPrivileges) {
122+
Set<String> checkForPrivileges) {
124123
return indices.checkResourcePrivileges(checkForIndexPatterns, allowRestrictedIndices, checkForPrivileges);
125124
}
126125

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

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilegeDescriptor;
4949
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
5050
import org.elasticsearch.xpack.core.security.authz.privilege.ConditionalClusterPrivileges.ManageApplicationPrivileges;
51+
import org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames;
5152
import org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege;
5253
import org.elasticsearch.xpack.core.security.authz.privilege.Privilege;
5354
import org.elasticsearch.xpack.core.security.user.User;
@@ -493,6 +494,130 @@ public void testCheckingIndexPermissionsDefinedOnDifferentPatterns() throws Exce
493494
));
494495
}
495496

497+
public void testCheckExplicitRestrictedIndexPermissions() throws Exception {
498+
User user = new User(randomAlphaOfLengthBetween(4, 12));
499+
Authentication authentication = mock(Authentication.class);
500+
when(authentication.getUser()).thenReturn(user);
501+
final boolean restrictedIndexPermission = randomBoolean();
502+
final boolean restrictedMonitorPermission = randomBoolean();
503+
Role role = Role.builder("role")
504+
.add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, restrictedIndexPermission, ".sec*")
505+
.add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, restrictedMonitorPermission, ".security*")
506+
.build();
507+
RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null);
508+
509+
String explicitRestrictedIndex = randomFrom(RestrictedIndicesNames.RESTRICTED_NAMES);
510+
HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder()
511+
.indices(new String[] {".secret-non-restricted", explicitRestrictedIndex})
512+
.privileges("index", "monitor")
513+
.allowRestrictedIndices(false) // explicit false for test
514+
.build(), authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY);
515+
assertThat(response.isCompleteMatch(), is(false));
516+
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2));
517+
assertThat(response.getIndexPrivileges(), containsInAnyOrder(
518+
ResourcePrivileges.builder(".secret-non-restricted") // matches ".sec*" but not ".security*"
519+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
520+
.put("index", true).put("monitor", false).map()).build(),
521+
ResourcePrivileges.builder(explicitRestrictedIndex) // matches both ".sec*" and ".security*"
522+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
523+
.put("index", restrictedIndexPermission).put("monitor", restrictedMonitorPermission).map()).build()));
524+
525+
explicitRestrictedIndex = randomFrom(RestrictedIndicesNames.RESTRICTED_NAMES);
526+
response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder()
527+
.indices(new String[] {".secret-non-restricted", explicitRestrictedIndex})
528+
.privileges("index", "monitor")
529+
.allowRestrictedIndices(true) // explicit true for test
530+
.build(), authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY);
531+
assertThat(response.isCompleteMatch(), is(false));
532+
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2));
533+
assertThat(response.getIndexPrivileges(), containsInAnyOrder(
534+
ResourcePrivileges.builder(".secret-non-restricted") // matches ".sec*" but not ".security*"
535+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
536+
.put("index", true).put("monitor", false).map()).build(),
537+
ResourcePrivileges.builder(explicitRestrictedIndex) // matches both ".sec*" and ".security*"
538+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
539+
.put("index", restrictedIndexPermission).put("monitor", restrictedMonitorPermission).map()).build()));
540+
}
541+
542+
public void testCheckRestrictedIndexWildcardPermissions() throws Exception {
543+
User user = new User(randomAlphaOfLengthBetween(4, 12));
544+
Authentication authentication = mock(Authentication.class);
545+
when(authentication.getUser()).thenReturn(user);
546+
Role role = Role.builder("role")
547+
.add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, false, ".sec*")
548+
.add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, true, ".security*")
549+
.build();
550+
RBACAuthorizationInfo authzInfo = new RBACAuthorizationInfo(role, null);
551+
552+
HasPrivilegesResponse response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder()
553+
.indices(".sec*", ".security*")
554+
.privileges("index", "monitor")
555+
.build(), authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY);
556+
assertThat(response.isCompleteMatch(), is(false));
557+
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2));
558+
assertThat(response.getIndexPrivileges(), containsInAnyOrder(
559+
ResourcePrivileges.builder(".sec*")
560+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
561+
.put("index", true).put("monitor", false).map()).build(),
562+
ResourcePrivileges.builder(".security*")
563+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
564+
.put("index", true).put("monitor", true).map()).build()
565+
));
566+
567+
response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder()
568+
.indices(".sec*", ".security*")
569+
.privileges("index", "monitor")
570+
.allowRestrictedIndices(true)
571+
.build(), authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY);
572+
assertThat(response.isCompleteMatch(), is(false));
573+
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2));
574+
assertThat(response.getIndexPrivileges(), containsInAnyOrder(
575+
ResourcePrivileges.builder(".sec*")
576+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
577+
.put("index", false).put("monitor", false).map()).build(),
578+
ResourcePrivileges.builder(".security*")
579+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
580+
.put("index", false).put("monitor", true).map()).build()
581+
));
582+
583+
role = Role.builder("role")
584+
.add(FieldPermissions.DEFAULT, null, IndexPrivilege.INDEX, true, ".sec*")
585+
.add(FieldPermissions.DEFAULT, null, IndexPrivilege.MONITOR, false, ".security*")
586+
.build();
587+
authzInfo = new RBACAuthorizationInfo(role, null);
588+
589+
response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder()
590+
.indices(".sec*", ".security*")
591+
.privileges("index", "monitor")
592+
.build(), authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY);
593+
assertThat(response.isCompleteMatch(), is(false));
594+
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2));
595+
assertThat(response.getIndexPrivileges(), containsInAnyOrder(
596+
ResourcePrivileges.builder(".sec*")
597+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
598+
.put("index", true).put("monitor", false).map()).build(),
599+
ResourcePrivileges.builder(".security*")
600+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
601+
.put("index", true).put("monitor", true).map()).build()
602+
));
603+
604+
response = hasPrivileges(RoleDescriptor.IndicesPrivileges.builder()
605+
.indices(".sec*", ".security*")
606+
.privileges("index", "monitor")
607+
.allowRestrictedIndices(true)
608+
.build(), authentication, authzInfo, Collections.emptyList(), Strings.EMPTY_ARRAY);
609+
assertThat(response.isCompleteMatch(), is(false));
610+
assertThat(response.getIndexPrivileges(), Matchers.iterableWithSize(2));
611+
assertThat(response.getIndexPrivileges(), containsInAnyOrder(
612+
ResourcePrivileges.builder(".sec*")
613+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
614+
.put("index", true).put("monitor", false).map()).build(),
615+
ResourcePrivileges.builder(".security*")
616+
.addPrivileges(MapBuilder.newMapBuilder(new LinkedHashMap<String, Boolean>())
617+
.put("index", true).put("monitor", false).map()).build()
618+
));
619+
}
620+
496621
public void testCheckingApplicationPrivilegesOnDifferentApplicationsAndResources() throws Exception {
497622
List<ApplicationPrivilegeDescriptor> privs = new ArrayList<>();
498623
final ApplicationPrivilege app1Read = defineApplicationPrivilege(privs, "app1", "read", "data:read/*");

0 commit comments

Comments
 (0)