Skip to content

Commit 926eb91

Browse files
authored
[Backport] Add more context to cluster access denied messages (#68263)
In #60357 we improved the error message when access to perform an action on an index was denied by including the index name and the privileges that would grant the action. This commit extends the second part of that change (the list of privileges that would resolve the problem) to situations when a cluster action is denied. This implementation for cluster privileges is slightly more complex than that of index privileges because cluster privileges can be dependent on parameters in the request, not just the action name. For example, "manage_own_api_key" should be suggested as a matching privilege when a user attempts to create an API key, or delete their own API key, but should not be suggested when that same user attempts to delete another user's API key. Backport of: #66900
1 parent 2d1e8b3 commit 926eb91

File tree

12 files changed

+356
-160
lines changed

12 files changed

+356
-160
lines changed

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -207,9 +207,14 @@ protected boolean extendedCheck(String action, TransportRequest request, Authent
207207

208208
@Override
209209
protected boolean doImplies(ActionBasedPermissionCheck permissionCheck) {
210-
return permissionCheck instanceof AutomatonPermissionCheck;
210+
/*
211+
* We know that "permissionCheck" has an automaton which is a subset of ours.
212+
* Which means "permissionCheck" _cannot_ grant an action that we don't (see ActionBasedPermissionCheck#check)
213+
* Since we grant _all_ requests on actions within our automaton, we must therefore grant _all_ actions+requests that the other
214+
* permission check grants.
215+
*/
216+
return true;
211217
}
212-
213218
}
214219

215220
// action, request based permission check

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

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ public class ActionClusterPrivilege implements NamedClusterPrivilege {
1919
private final String name;
2020
private final Set<String> allowedActionPatterns;
2121
private final Set<String> excludedActionPatterns;
22+
private final ClusterPermission permission;
2223

2324
/**
2425
* Constructor for {@link ActionClusterPrivilege} defining what cluster actions are accessible for the user with this privilege.
@@ -43,6 +44,7 @@ public ActionClusterPrivilege(final String name, final Set<String> allowedAction
4344
this.name = name;
4445
this.allowedActionPatterns = allowedActionPatterns;
4546
this.excludedActionPatterns = excludedActionPatterns;
47+
this.permission = buildPermission(ClusterPermission.builder()).build();
4648
}
4749

4850
@Override
@@ -62,4 +64,9 @@ public Set<String> getExcludedActionPatterns() {
6264
public ClusterPermission.Builder buildPermission(final ClusterPermission.Builder builder) {
6365
return builder.add(this, allowedActionPatterns, excludedActionPatterns);
6466
}
67+
68+
@Override
69+
public ClusterPermission permission() {
70+
return permission;
71+
}
6572
}

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

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
import org.elasticsearch.action.ingest.SimulatePipelineAction;
1919
import org.elasticsearch.common.Strings;
2020
import org.elasticsearch.common.util.set.Sets;
21+
import org.elasticsearch.transport.TransportRequest;
2122
import org.elasticsearch.xpack.core.ilm.action.GetLifecycleAction;
2223
import org.elasticsearch.xpack.core.ilm.action.GetStatusAction;
2324
import org.elasticsearch.xpack.core.ilm.action.StartILMAction;
@@ -28,15 +29,21 @@
2829
import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction;
2930
import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction;
3031
import org.elasticsearch.xpack.core.security.action.user.HasPrivilegesAction;
32+
import org.elasticsearch.xpack.core.security.authc.Authentication;
3133
import org.elasticsearch.xpack.core.slm.action.GetSnapshotLifecycleAction;
3234

35+
import java.util.Arrays;
36+
import java.util.Collection;
3337
import java.util.Collections;
38+
import java.util.Comparator;
39+
import java.util.HashMap;
3440
import java.util.Locale;
3541
import java.util.Map;
3642
import java.util.Objects;
3743
import java.util.Set;
44+
import java.util.SortedMap;
45+
import java.util.TreeMap;
3846
import java.util.stream.Collectors;
39-
import java.util.stream.Stream;
4047

4148
/**
4249
* Translates cluster privilege names into concrete implementations
@@ -153,8 +160,7 @@ public class ClusterPrivilegeResolver {
153160
public static final NamedClusterPrivilege MANAGE_LOGSTASH_PIPELINES = new ActionClusterPrivilege("manage_logstash_pipelines",
154161
Collections.unmodifiableSet(Sets.newHashSet("cluster:admin/logstash/pipeline/*")));
155162

156-
private static final Map<String, NamedClusterPrivilege> VALUES = Collections.unmodifiableMap(
157-
Stream.of(
163+
private static final Map<String, NamedClusterPrivilege> VALUES = sortByAccessLevel(Arrays.asList(
158164
NONE,
159165
ALL,
160166
MONITOR,
@@ -193,7 +199,7 @@ public class ClusterPrivilegeResolver {
193199
DELEGATE_PKI,
194200
MANAGE_OWN_API_KEY,
195201
MANAGE_ENRICH,
196-
MANAGE_LOGSTASH_PIPELINES).collect(Collectors.toMap(cp -> cp.name(), cp -> cp)));
202+
MANAGE_LOGSTASH_PIPELINES));
197203

198204
/**
199205
* Resolves a {@link NamedClusterPrivilege} from a given name if it exists.
@@ -234,4 +240,36 @@ private static String actionToPattern(String text) {
234240
return text + "*";
235241
}
236242

243+
/**
244+
* Returns the names of privileges that grant the specified action and request, for the given authentication context.
245+
* @return A collection of names, ordered (to the extent possible) from least privileged (e.g. {@link #MONITOR})
246+
* to most privileged (e.g. {@link #ALL})
247+
* @see #sortByAccessLevel(Collection)
248+
* @see org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission#check(String, TransportRequest, Authentication)
249+
*/
250+
public static Collection<String> findPrivilegesThatGrant(String action, TransportRequest request, Authentication authentication) {
251+
return Collections.unmodifiableList(VALUES.entrySet().stream()
252+
.filter(e -> e.getValue().permission().check(action, request, authentication))
253+
.map(Map.Entry::getKey)
254+
.collect(Collectors.toList()));
255+
}
256+
257+
/**
258+
* Sorts the collection of privileges from least-privilege to most-privilege (to the extent possible),
259+
* returning them in a sorted map keyed by name.
260+
*/
261+
static SortedMap<String, NamedClusterPrivilege> sortByAccessLevel(Collection<NamedClusterPrivilege> privileges) {
262+
// How many other privileges does this privilege imply. Those with a higher count are considered to be a higher privilege
263+
final Map<String, Long> impliesCount = new HashMap<>(privileges.size());
264+
privileges.forEach(priv -> impliesCount.put(priv.name(),
265+
privileges.stream().filter(p2 -> p2 != priv && priv.permission().implies(p2.permission())).count())
266+
);
267+
268+
final Comparator<String> compare = Comparator.<String>comparingLong(key -> impliesCount.getOrDefault(key, 0L))
269+
.thenComparing(Comparator.naturalOrder());
270+
final TreeMap<String, NamedClusterPrivilege> tree = new TreeMap<>(compare);
271+
privileges.forEach(p -> tree.put(p.name(), p));
272+
return Collections.unmodifiableSortedMap(tree);
273+
}
274+
237275
}

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ public class ManageOwnApiKeyClusterPrivilege implements NamedClusterPrivilege {
2727
private static final String PRIVILEGE_NAME = "manage_own_api_key";
2828
private static final String API_KEY_ID_KEY = "_security_api_key_id";
2929

30+
private final ClusterPermission permission;
31+
3032
private ManageOwnApiKeyClusterPrivilege() {
33+
permission = this.buildPermission(ClusterPermission.builder()).build();
3134
}
3235

3336
@Override
@@ -40,6 +43,11 @@ public ClusterPermission.Builder buildPermission(ClusterPermission.Builder build
4043
return builder.add(this, ManageOwnClusterPermissionCheck.INSTANCE);
4144
}
4245

46+
@Override
47+
public ClusterPermission permission() {
48+
return permission;
49+
}
50+
4351
private static final class ManageOwnClusterPermissionCheck extends ClusterPermission.ActionBasedPermissionCheck {
4452
public static final ManageOwnClusterPermissionCheck INSTANCE = new ManageOwnClusterPermissionCheck();
4553

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,24 @@
88
package org.elasticsearch.xpack.core.security.authz.privilege;
99

1010
import org.elasticsearch.xpack.core.security.authz.RoleDescriptor;
11+
import org.elasticsearch.xpack.core.security.authz.permission.ClusterPermission;
1112

1213
/**
1314
* A {@link ClusterPrivilege} that has a name. The named cluster privileges can be referred simply by name within a
1415
* {@link RoleDescriptor#getClusterPrivileges()}.
1516
*/
1617
public interface NamedClusterPrivilege extends ClusterPrivilege {
1718
String name();
19+
20+
/**
21+
* Returns a permission that represents this privilege only.
22+
* When building a role (or role-like object) that has many privileges, it is more efficient to build a shared permission using the
23+
* {@link #buildPermission(ClusterPermission.Builder)} method instead. This method is intended to allow callers to interrogate the
24+
* runtime permissions specifically granted by this privilege.
25+
* It is acceptable (and encouraged) for implementations of this method to cache (or precompute) the {@link ClusterPermission}
26+
* and return the same object on each call.
27+
* @see #buildPermission(ClusterPermission.Builder)
28+
*/
29+
ClusterPermission permission();
30+
1831
}

x-pack/plugin/core/src/test/java/org/elasticsearch/xpack/core/security/authz/permission/ClusterPermissionTests.java

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,24 @@
88
package org.elasticsearch.xpack.core.security.authz.permission;
99

1010
import org.elasticsearch.common.io.stream.StreamOutput;
11+
import org.elasticsearch.common.util.CollectionUtils;
1112
import org.elasticsearch.common.xcontent.XContentBuilder;
1213
import org.elasticsearch.test.ESTestCase;
1314
import org.elasticsearch.transport.TransportRequest;
1415
import org.elasticsearch.xpack.core.security.authc.Authentication;
1516
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilege;
1617
import org.elasticsearch.xpack.core.security.authz.privilege.ClusterPrivilegeResolver;
1718
import org.elasticsearch.xpack.core.security.authz.privilege.ConfigurableClusterPrivilege;
19+
import org.elasticsearch.xpack.core.security.authz.privilege.NamedClusterPrivilege;
1820
import org.junit.Before;
1921

2022
import java.io.IOException;
2123
import java.util.Collections;
24+
import java.util.List;
2225
import java.util.Objects;
2326
import java.util.Set;
2427
import java.util.function.Predicate;
28+
import java.util.stream.Collectors;
2529

2630
import static org.hamcrest.Matchers.containsInAnyOrder;
2731
import static org.hamcrest.Matchers.is;
@@ -230,6 +234,33 @@ public void testClusterPermissionSubsetIsImpliedByAllClusterPermission() {
230234
assertThat(allClusterPermission.implies(otherClusterPermission), is(true));
231235
}
232236

237+
public void testImpliesOnSecurityPrivilegeHierarchy() {
238+
final List<ClusterPermission> highToLow = CollectionUtils.arrayAsArrayList(
239+
ClusterPrivilegeResolver.ALL.permission(),
240+
ClusterPrivilegeResolver.MANAGE_SECURITY.permission(),
241+
ClusterPrivilegeResolver.MANAGE_API_KEY.permission(),
242+
ClusterPrivilegeResolver.MANAGE_OWN_API_KEY.permission()
243+
);
244+
245+
for (int i = 0; i < highToLow.size(); i++) {
246+
ClusterPermission high = highToLow.get(i);
247+
for (int j = i; j < highToLow.size(); j++) {
248+
ClusterPermission low = highToLow.get(j);
249+
assertThat("Permission " + name(high) + " should imply " + name(low), high.implies(low), is(true));
250+
}
251+
}
252+
}
253+
254+
private String name(ClusterPermission permission) {
255+
return permission.privileges().stream().map(priv -> {
256+
if (priv instanceof NamedClusterPrivilege) {
257+
return ((NamedClusterPrivilege) priv).name();
258+
} else {
259+
return priv.toString();
260+
}
261+
}).collect(Collectors.joining(","));
262+
}
263+
233264
private static class MockConfigurableClusterPrivilege implements ConfigurableClusterPrivilege {
234265
private Predicate<TransportRequest> requestPredicate;
235266

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
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+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
package org.elasticsearch.xpack.core.security.authz.privilege;
9+
10+
import org.elasticsearch.common.util.CollectionUtils;
11+
import org.elasticsearch.test.ESTestCase;
12+
13+
import java.util.Collections;
14+
import java.util.List;
15+
import java.util.SortedMap;
16+
17+
import static org.hamcrest.Matchers.contains;
18+
19+
public class ClusterPrivilegeResolverTests extends ESTestCase {
20+
21+
public void testSortByAccessLevel() throws Exception {
22+
final List<NamedClusterPrivilege> privileges = CollectionUtils.arrayAsArrayList(
23+
ClusterPrivilegeResolver.ALL,
24+
ClusterPrivilegeResolver.MONITOR,
25+
ClusterPrivilegeResolver.MANAGE,
26+
ClusterPrivilegeResolver.MANAGE_OWN_API_KEY,
27+
ClusterPrivilegeResolver.MANAGE_API_KEY,
28+
ClusterPrivilegeResolver.MANAGE_SECURITY
29+
);
30+
Collections.shuffle(privileges, random());
31+
final SortedMap<String, NamedClusterPrivilege> sorted = ClusterPrivilegeResolver.sortByAccessLevel(privileges);
32+
// This is:
33+
// "manage_own_api_key", "monitor" (neither of which grant anything else in the list), sorted by name
34+
// "manage" and "manage_api_key",(which each grant 1 other privilege in the list), sorted by name
35+
// "manage_security" and "all", sorted by access level ("all" implies "manage_security")
36+
assertThat(sorted.keySet(), contains("manage_own_api_key", "monitor", "manage", "manage_api_key", "manage_security", "all"));
37+
}
38+
39+
}

0 commit comments

Comments
 (0)