Skip to content

Commit 22b59e6

Browse files
committed
Add more context to index access denied errors
Access denied messages for indices were overly brief and missed two pieces of useful information: 1. The names of the indices for which access was denied 2. The privileges that could be used to grant that access This change improves the access denied messages for index based actions by adding the index and privilege names. Privilege names are listed in order from least-privilege to most-privileged so that the first recommended path to resolution is also the lowest privilege change. Relates: elastic#42166
1 parent 56e5d32 commit 22b59e6

File tree

10 files changed

+426
-162
lines changed

10 files changed

+426
-162
lines changed

server/src/main/java/org/elasticsearch/common/util/iterable/Iterables.java

+12
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.util.Iterator;
2525
import java.util.List;
2626
import java.util.Objects;
27+
import java.util.function.Predicate;
2728
import java.util.stream.Stream;
2829
import java.util.stream.StreamSupport;
2930

@@ -103,6 +104,17 @@ public static <T> T get(Iterable<T> iterable, int position) {
103104
}
104105
}
105106

107+
public static <T> int indexOf(Iterable<T> iterable, Predicate<T> predicate) {
108+
int i = 0;
109+
for (T element : iterable) {
110+
if (predicate.test(element)) {
111+
return i;
112+
}
113+
i++;
114+
}
115+
return -1;
116+
}
117+
106118
public static long size(Iterable<?> iterable) {
107119
return StreamSupport.stream(iterable.spliterator(), true).count();
108120
}

server/src/test/java/org/elasticsearch/common/util/iterable/IterablesTests.java

+16
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
import java.util.Iterator;
2727
import java.util.List;
2828
import java.util.NoSuchElementException;
29+
import java.util.stream.Collectors;
30+
import java.util.stream.Stream;
2931

32+
import static org.hamcrest.Matchers.is;
3033
import static org.hamcrest.object.HasToString.hasToString;
3134

3235
public class IterablesTests extends ESTestCase {
@@ -86,6 +89,19 @@ public void testFlatten() {
8689
assertEquals(1, count);
8790
}
8891

92+
public void testIndexOf() {
93+
final List<String> list = Stream.generate(() -> randomAlphaOfLengthBetween(3, 9))
94+
.limit(randomIntBetween(10, 30))
95+
.distinct()
96+
.collect(Collectors.toUnmodifiableList());
97+
for (int i = 0; i < list.size(); i++) {
98+
final String val = list.get(i);
99+
assertThat(Iterables.indexOf(list, val::equals), is(i));
100+
}
101+
assertThat(Iterables.indexOf(list, s -> false), is(-1));
102+
assertThat(Iterables.indexOf(list, s -> true), is(0));
103+
}
104+
89105
private void test(Iterable<String> iterable) {
90106
try {
91107
Iterables.get(iterable, -1);

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

+23
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88

99
import org.elasticsearch.action.ActionListener;
1010
import org.elasticsearch.cluster.metadata.IndexAbstraction;
11+
import org.elasticsearch.common.Nullable;
12+
import org.elasticsearch.common.Strings;
1113
import org.elasticsearch.transport.TransportRequest;
1214
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesRequest;
1315
import org.elasticsearch.xpack.core.security.action.user.GetUserPrivilegesResponse;
@@ -292,6 +294,14 @@ public boolean isAuditable() {
292294
return auditable;
293295
}
294296

297+
/**
298+
* Returns additional context about an authorization failure, if {@link #isGranted()} is false.
299+
*/
300+
@Nullable
301+
public String getFailureContext() {
302+
return null;
303+
}
304+
295305
/**
296306
* Returns a new authorization result that is granted and auditable
297307
*/
@@ -321,6 +331,19 @@ public IndexAuthorizationResult(boolean auditable, IndicesAccessControl indicesA
321331
this.indicesAccessControl = indicesAccessControl;
322332
}
323333

334+
@Override
335+
public String getFailureContext() {
336+
if (isGranted()) {
337+
return null;
338+
} else {
339+
return getFailureDescription(indicesAccessControl.getDeniedIndices());
340+
}
341+
}
342+
343+
public static String getFailureDescription(Collection<?> deniedIndices) {
344+
return "on indices [" + Strings.collectionToCommaDelimitedString(deniedIndices) + "]";
345+
}
346+
324347
public IndicesAccessControl getIndicesAccessControl() {
325348
return indicesAccessControl;
326349
}

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

+9
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import org.elasticsearch.xpack.core.security.authz.permission.DocumentPermissions;
1212
import org.elasticsearch.xpack.core.security.authz.permission.FieldPermissions;
1313

14+
import java.util.Collection;
1415
import java.util.Collections;
1516
import java.util.HashMap;
1617
import java.util.Map;
1718
import java.util.Set;
19+
import java.util.stream.Collectors;
1820

1921
/**
2022
* Encapsulates the field and document permissions per concrete index based on the current request.
@@ -51,6 +53,13 @@ public boolean isGranted() {
5153
return granted;
5254
}
5355

56+
public Collection<?> getDeniedIndices() {
57+
return this.indexPermissions.entrySet().stream()
58+
.filter(e -> e.getValue().granted == false)
59+
.map(Map.Entry::getKey)
60+
.collect(Collectors.toUnmodifiableSet());
61+
}
62+
5463
/**
5564
* Encapsulates the field and document permissions for an index.
5665
*/

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

+21-7
Original file line numberDiff line numberDiff line change
@@ -13,32 +13,34 @@
1313
import org.elasticsearch.action.admin.indices.close.CloseIndexAction;
1414
import org.elasticsearch.action.admin.indices.create.AutoCreateAction;
1515
import org.elasticsearch.action.admin.indices.create.CreateIndexAction;
16-
import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction;
17-
import org.elasticsearch.xpack.core.action.CreateDataStreamAction;
18-
import org.elasticsearch.xpack.core.action.DeleteDataStreamAction;
19-
import org.elasticsearch.xpack.core.action.GetDataStreamAction;
2016
import org.elasticsearch.action.admin.indices.delete.DeleteIndexAction;
2117
import org.elasticsearch.action.admin.indices.get.GetIndexAction;
2218
import org.elasticsearch.action.admin.indices.mapping.get.GetFieldMappingsAction;
2319
import org.elasticsearch.action.admin.indices.mapping.get.GetMappingsAction;
2420
import org.elasticsearch.action.admin.indices.mapping.put.AutoPutMappingAction;
21+
import org.elasticsearch.action.admin.indices.resolve.ResolveIndexAction;
2522
import org.elasticsearch.action.admin.indices.settings.get.GetSettingsAction;
2623
import org.elasticsearch.action.admin.indices.validate.query.ValidateQueryAction;
2724
import org.elasticsearch.common.Strings;
25+
import org.elasticsearch.xpack.core.action.CreateDataStreamAction;
26+
import org.elasticsearch.xpack.core.action.DeleteDataStreamAction;
27+
import org.elasticsearch.xpack.core.action.GetDataStreamAction;
2828
import org.elasticsearch.xpack.core.ccr.action.ForgetFollowerAction;
2929
import org.elasticsearch.xpack.core.ccr.action.PutFollowAction;
3030
import org.elasticsearch.xpack.core.ccr.action.UnfollowAction;
3131
import org.elasticsearch.xpack.core.ilm.action.ExplainLifecycleAction;
3232
import org.elasticsearch.xpack.core.security.support.Automatons;
3333

3434
import java.util.Arrays;
35+
import java.util.Collection;
3536
import java.util.Collections;
3637
import java.util.HashSet;
3738
import java.util.Locale;
3839
import java.util.Map;
3940
import java.util.Set;
4041
import java.util.concurrent.ConcurrentHashMap;
4142
import java.util.function.Predicate;
43+
import java.util.stream.Collectors;
4244

4345
import static java.util.Map.entry;
4446
import static org.elasticsearch.xpack.core.security.support.Automatons.patterns;
@@ -95,7 +97,7 @@ public final class IndexPrivilege extends Privilege {
9597
public static final IndexPrivilege MAINTENANCE = new IndexPrivilege("maintenance", MAINTENANCE_AUTOMATON);
9698
public static final IndexPrivilege AUTO_CONFIGURE = new IndexPrivilege("auto_configure", AUTO_CONFIGURE_AUTOMATON);
9799

98-
private static final Map<String, IndexPrivilege> VALUES = Map.ofEntries(
100+
private static final Map<String, IndexPrivilege> VALUES = sortByAccessLevel(Map.ofEntries(
99101
entry("none", NONE),
100102
entry("all", ALL),
101103
entry("manage", MANAGE),
@@ -114,7 +116,7 @@ public final class IndexPrivilege extends Privilege {
114116
entry("manage_leader_index", MANAGE_LEADER_INDEX),
115117
entry("manage_ilm", MANAGE_ILM),
116118
entry("maintenance", MAINTENANCE),
117-
entry("auto_configure", AUTO_CONFIGURE));
119+
entry("auto_configure", AUTO_CONFIGURE)));
118120

119121
public static final Predicate<String> ACTION_MATCHER = ALL.predicate();
120122
public static final Predicate<String> CREATE_INDEX_MATCHER = CREATE_INDEX.predicate();
@@ -152,7 +154,7 @@ private static IndexPrivilege resolve(Set<String> name) {
152154
if (ACTION_MATCHER.test(part)) {
153155
actions.add(actionToPattern(part));
154156
} else {
155-
IndexPrivilege indexPrivilege = VALUES.get(part);
157+
IndexPrivilege indexPrivilege = part == null ? null : VALUES.get(part);
156158
if (indexPrivilege != null && size == 1) {
157159
return indexPrivilege;
158160
} else if (indexPrivilege != null) {
@@ -182,4 +184,16 @@ public static Set<String> names() {
182184
return Collections.unmodifiableSet(VALUES.keySet());
183185
}
184186

187+
/**
188+
* Returns the names of privileges that grant the specified action.
189+
* @return A collection of names, ordered (to the extent possible) from least privileged (e.g. {@link #CREATE_DOC})
190+
* to most privileged (e.g. {@link #ALL})
191+
* @see Privilege#sortByAccessLevel
192+
*/
193+
public static Collection<String> findPrivilegesThatGrant(String action) {
194+
return VALUES.entrySet().stream()
195+
.filter(e -> e.getValue().predicate.test(action))
196+
.map(e -> e.getKey())
197+
.collect(Collectors.toUnmodifiableList());
198+
}
185199
}

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

+23
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,16 @@
66
package org.elasticsearch.xpack.core.security.authz.privilege;
77

88
import org.apache.lucene.util.automaton.Automaton;
9+
import org.apache.lucene.util.automaton.Operations;
910
import org.elasticsearch.xpack.core.security.support.Automatons;
1011

1112
import java.util.Collections;
13+
import java.util.Comparator;
14+
import java.util.HashMap;
15+
import java.util.Map;
1216
import java.util.Set;
17+
import java.util.SortedMap;
18+
import java.util.TreeMap;
1319
import java.util.function.Predicate;
1420

1521
import static org.elasticsearch.xpack.core.security.support.Automatons.patterns;
@@ -74,4 +80,21 @@ public String toString() {
7480
public Automaton getAutomaton() {
7581
return automaton;
7682
}
83+
84+
/**
85+
* Sorts the map of privileges from least-privilege to most-privilege
86+
*/
87+
static <T extends Privilege> SortedMap<String, T> sortByAccessLevel(Map<String, T> privileges) {
88+
// How many other privileges is this privilege a subset of. Those with a higher count are considered to be a lower privilege
89+
final Map<String, Long> subsetCount = new HashMap<>(privileges.size());
90+
privileges.forEach((name, priv) -> subsetCount.put(name,
91+
privileges.values().stream().filter(p2 -> p2 != priv && Operations.subsetOf(priv.automaton, p2.automaton)).count())
92+
);
93+
94+
final Comparator<String> compare = Comparator.<String>comparingLong(key -> subsetCount.getOrDefault(key, 0L)).reversed()
95+
.thenComparing(Comparator.naturalOrder());
96+
final TreeMap<String, T> tree = new TreeMap<>(compare);
97+
tree.putAll(privileges);
98+
return Collections.unmodifiableSortedMap(tree);
99+
}
77100
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
7+
package org.elasticsearch.xpack.core.security.authz.privilege;
8+
9+
import org.elasticsearch.action.admin.indices.refresh.RefreshAction;
10+
import org.elasticsearch.action.admin.indices.shrink.ShrinkAction;
11+
import org.elasticsearch.action.admin.indices.stats.IndicesStatsAction;
12+
import org.elasticsearch.action.delete.DeleteAction;
13+
import org.elasticsearch.action.index.IndexAction;
14+
import org.elasticsearch.action.search.SearchAction;
15+
import org.elasticsearch.action.update.UpdateAction;
16+
import org.elasticsearch.common.util.iterable.Iterables;
17+
import org.elasticsearch.test.ESTestCase;
18+
19+
import java.util.List;
20+
import java.util.Set;
21+
22+
import static org.elasticsearch.xpack.core.security.authz.privilege.IndexPrivilege.findPrivilegesThatGrant;
23+
import static org.hamcrest.Matchers.equalTo;
24+
import static org.hamcrest.Matchers.lessThan;
25+
26+
public class IndexPrivilegeTests extends ESTestCase {
27+
28+
/**
29+
* The {@link IndexPrivilege#values()} map is sorted so that privilege names that offer the _least_ access come before those that
30+
* offer _more_ access. There is no guarantee of ordering between privileges that offer non-overlapping privileges.
31+
*/
32+
public void testOrderingOfPrivilegeNames() throws Exception {
33+
final Set<String> names = IndexPrivilege.values().keySet();
34+
final int all = Iterables.indexOf(names, "all"::equals);
35+
final int manage = Iterables.indexOf(names, "manage"::equals);
36+
final int monitor = Iterables.indexOf(names, "monitor"::equals);
37+
final int read = Iterables.indexOf(names, "read"::equals);
38+
final int write = Iterables.indexOf(names, "write"::equals);
39+
final int index = Iterables.indexOf(names, "index"::equals);
40+
final int create_doc = Iterables.indexOf(names, "create_doc"::equals);
41+
final int delete = Iterables.indexOf(names, "delete"::equals);
42+
43+
assertThat(read, lessThan(all));
44+
assertThat(manage, lessThan(all));
45+
assertThat(monitor, lessThan(manage));
46+
assertThat(write, lessThan(all));
47+
assertThat(index, lessThan(write));
48+
assertThat(create_doc, lessThan(index));
49+
assertThat(delete, lessThan(write));
50+
}
51+
52+
public void testFindPrivilegesThatGrant() {
53+
assertThat(findPrivilegesThatGrant(SearchAction.NAME), equalTo(List.of("read", "all")));
54+
assertThat(findPrivilegesThatGrant(IndexAction.NAME), equalTo(List.of("create_doc", "create", "index", "write", "all")));
55+
assertThat(findPrivilegesThatGrant(UpdateAction.NAME), equalTo(List.of("index", "write", "all")));
56+
assertThat(findPrivilegesThatGrant(DeleteAction.NAME), equalTo(List.of("delete", "write", "all")));
57+
assertThat(findPrivilegesThatGrant(IndicesStatsAction.NAME), equalTo(List.of("monitor", "manage", "all")));
58+
assertThat(findPrivilegesThatGrant(RefreshAction.NAME), equalTo(List.of("maintenance", "manage", "all")));
59+
assertThat(findPrivilegesThatGrant(ShrinkAction.NAME), equalTo(List.of("manage", "all")));
60+
}
61+
62+
}

0 commit comments

Comments
 (0)