Skip to content

Commit dc32b36

Browse files
Fix remote cluster credential secure settings reload (#111535) (#111738)
Due to the `cluster:admin/xpack/security` action name prefix, the internal action `cluster:admin/xpack/security/remote_cluster_credentials/reload` to reload remote cluster credentials fails for users that have the `manage` cluster privilege. This does not align with our documentation and the overall permission requirements for reloading secure settings. This PR renames the action to match the `manage` privilege. Since this is a local-only action there are no BWC concerns with this rename. Fixes: #111543 Co-authored-by: Elastic Machine <[email protected]>
1 parent 5d8a04b commit dc32b36

File tree

10 files changed

+118
-8
lines changed

10 files changed

+118
-8
lines changed

docs/changelog/111535.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
pr: 111535
2+
summary: Fix remote cluster credential secure settings reload
3+
area: Authorization
4+
type: bug
5+
issues: []

server/src/main/java/org/elasticsearch/action/admin/cluster/node/reload/TransportNodesReloadSecureSettingsAction.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ protected NodesReloadSecureSettingsResponse.NodeResponse nodeOperation(
123123
final List<Exception> exceptions = new ArrayList<>();
124124
// broadcast the new settings object (with the open embedded keystore) to all reloadable plugins
125125
pluginsService.filterPlugins(ReloadablePlugin.class).forEach(p -> {
126+
logger.debug("Reloading plugin [" + p.getClass().getSimpleName() + "]");
126127
try {
127128
p.reload(settingsWithKeystore);
128129
} catch (final Exception e) {

x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/security/action/ActionTypes.java

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,13 @@
2020
public final class ActionTypes {
2121
private ActionTypes() {};
2222

23+
// Note: this action is *not* prefixed with `cluster:admin/xpack/security` since it would otherwise be excluded from the `manage`
24+
// privilege -- instead it matches its prefix to `TransportNodesReloadSecureSettingsAction` which is the "parent" transport action
25+
// that invokes the overall reload flow.
26+
// This allows us to maintain the invariant that the parent reload secure settings action can be executed with the `manage` privilege
27+
// without trappy system-context switches.
2328
public static final ActionType<ActionResponse.Empty> RELOAD_REMOTE_CLUSTER_CREDENTIALS_ACTION = new ActionType<>(
24-
"cluster:admin/xpack/security/remote_cluster_credentials/reload"
29+
"cluster:admin/nodes/reload_secure_settings/security/remote_cluster_credentials"
2530
);
2631

2732
public static final ActionType<QueryUserResponse> QUERY_USER_ACTION = new ActionType<>("cluster:admin/xpack/security/user/query");

x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/AbstractRemoteClusterSecurityTestCase.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,12 @@ public abstract class AbstractRemoteClusterSecurityTestCase extends ESRestTestCa
4848
protected static final String USER = "test_user";
4949
protected static final SecureString PASS = new SecureString("x-pack-test-password".toCharArray());
5050
protected static final String REMOTE_SEARCH_USER = "remote_search_user";
51+
protected static final String MANAGE_USER = "manage_user";
5152
protected static final String REMOTE_METRIC_USER = "remote_metric_user";
5253
protected static final String REMOTE_TRANSFORM_USER = "remote_transform_user";
5354
protected static final String REMOTE_SEARCH_ROLE = "remote_search";
5455
protected static final String REMOTE_CLUSTER_ALIAS = "my_remote_cluster";
55-
private static final String KEYSTORE_PASSWORD = "keystore-password";
56+
static final String KEYSTORE_PASSWORD = "keystore-password";
5657

5758
protected static LocalClusterConfigProvider commonClusterConfig = cluster -> cluster.module("analysis-common")
5859
.keystorePassword(KEYSTORE_PASSWORD)
@@ -217,7 +218,7 @@ protected void removeRemoteClusterCredentials(String clusterAlias, MutableSettin
217218
}
218219

219220
@SuppressWarnings("unchecked")
220-
private void reloadSecureSettings() throws IOException {
221+
protected void reloadSecureSettings() throws IOException {
221222
final Request request = new Request("POST", "/_nodes/reload_secure_settings");
222223
request.setJsonEntity("{\"secure_settings_password\":\"" + KEYSTORE_PASSWORD + "\"}");
223224
final Response reloadResponse = adminClient().performRequest(request);

x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/java/org/elasticsearch/xpack/remotecluster/RemoteClusterSecurityReloadCredentialsRestIT.java

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,12 @@
3434
import java.util.Map;
3535
import java.util.stream.Collectors;
3636

37+
import static org.hamcrest.Matchers.anEmptyMap;
3738
import static org.hamcrest.Matchers.containsInAnyOrder;
39+
import static org.hamcrest.Matchers.instanceOf;
40+
import static org.hamcrest.Matchers.is;
41+
import static org.hamcrest.Matchers.not;
42+
import static org.hamcrest.Matchers.nullValue;
3843

3944
// account for slow stored secure settings updates (involves removing and re-creating the keystore)
4045
@TimeoutSuite(millis = 10 * TimeUnits.MINUTE)
@@ -78,6 +83,7 @@ public class RemoteClusterSecurityReloadCredentialsRestIT extends AbstractRemote
7883
})
7984
.rolesFile(Resource.fromClasspath("roles.yml"))
8085
.user(REMOTE_SEARCH_USER, PASS.toString(), "read_remote_shared_logs", false)
86+
.user(MANAGE_USER, PASS.toString(), "manage_role", false)
8187
.build();
8288
}
8389

@@ -237,4 +243,28 @@ private Response performRequestWithRemoteSearchUser(final Request request) throw
237243
return client().performRequest(request);
238244
}
239245

246+
@Override
247+
@SuppressWarnings("unchecked")
248+
protected void reloadSecureSettings() throws IOException {
249+
final Request request = new Request("POST", "/_nodes/reload_secure_settings");
250+
request.setJsonEntity("{\"secure_settings_password\":\"" + KEYSTORE_PASSWORD + "\"}");
251+
// execute as user with only `manage` cluster privilege
252+
final Response reloadResponse = performRequestWithManageUser(request);
253+
assertOK(reloadResponse);
254+
final Map<String, Object> map = entityAsMap(reloadResponse);
255+
assertThat(map.get("nodes"), instanceOf(Map.class));
256+
final Map<String, Object> nodes = (Map<String, Object>) map.get("nodes");
257+
assertThat(nodes, is(not(anEmptyMap())));
258+
for (Map.Entry<String, Object> entry : nodes.entrySet()) {
259+
assertThat(entry.getValue(), instanceOf(Map.class));
260+
final Map<String, Object> node = (Map<String, Object>) entry.getValue();
261+
assertThat(node.get("reload_exception"), nullValue());
262+
}
263+
}
264+
265+
private Response performRequestWithManageUser(final Request request) throws IOException {
266+
request.setOptions(RequestOptions.DEFAULT.toBuilder().addHeader("Authorization", headerFromRandomAuthMethod(MANAGE_USER, PASS)));
267+
return client().performRequest(request);
268+
}
269+
240270
}

x-pack/plugin/security/qa/multi-cluster/src/javaRestTest/resources/roles.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,6 @@ ccr_user_role:
3838
- names: [ 'leader-index', 'shared-*', 'metrics-*' ]
3939
privileges: [ 'cross_cluster_replication' ]
4040
clusters: [ "*" ]
41+
42+
manage_role:
43+
cluster: [ 'manage' ]

x-pack/plugin/security/qa/operator-privileges-tests/src/javaRestTest/java/org/elasticsearch/xpack/security/operator/Constants.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public class Constants {
5454
"cluster:admin/migration/get_system_feature",
5555
"cluster:admin/migration/post_system_feature",
5656
"cluster:admin/nodes/reload_secure_settings",
57+
"cluster:admin/nodes/reload_secure_settings/security/remote_cluster_credentials",
5758
"cluster:admin/persistent/completion",
5859
"cluster:admin/persistent/remove",
5960
"cluster:admin/persistent/start",
@@ -278,7 +279,6 @@ public class Constants {
278279
"cluster:admin/xpack/security/profile/suggest",
279280
"cluster:admin/xpack/security/profile/set_enabled",
280281
"cluster:admin/xpack/security/realm/cache/clear",
281-
"cluster:admin/xpack/security/remote_cluster_credentials/reload",
282282
"cluster:admin/xpack/security/role/delete",
283283
"cluster:admin/xpack/security/role/get",
284284
"cluster:admin/xpack/security/role/query",

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

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@
1414
import org.elasticsearch.action.ActionListener;
1515
import org.elasticsearch.action.ActionModule;
1616
import org.elasticsearch.action.ActionResponse;
17-
import org.elasticsearch.action.support.PlainActionFuture;
1817
import org.elasticsearch.client.internal.Client;
1918
import org.elasticsearch.cluster.ClusterName;
2019
import org.elasticsearch.cluster.ClusterState;
@@ -152,7 +151,6 @@
152151
import static org.mockito.Mockito.when;
153152

154153
public class SecurityTests extends ESTestCase {
155-
156154
private Security security = null;
157155
private ThreadContext threadContext = null;
158156
private SecurityContext securityContext = null;
@@ -960,8 +958,11 @@ public <T> List<T> loadExtensions(Class<T> extensionPointType) {
960958
public void testReload() throws Exception {
961959
final Settings settings = Settings.builder().put("xpack.security.enabled", true).put("path.home", createTempDir()).build();
962960

963-
final PlainActionFuture<ActionResponse.Empty> value = new PlainActionFuture<>();
961+
final ThreadPool threadPool = mock(ThreadPool.class);
962+
threadContext = new ThreadContext(Settings.EMPTY);
963+
when(threadPool.getThreadContext()).thenReturn(threadContext);
964964
final Client mockedClient = mock(Client.class);
965+
when(mockedClient.threadPool()).thenReturn(threadPool);
965966

966967
final JwtRealm mockedJwtRealm = mock(JwtRealm.class);
967968
final List<ReloadableSecurityComponent> reloadableComponents = List.of(mockedJwtRealm);
@@ -992,11 +993,17 @@ protected List<ReloadableSecurityComponent> getReloadableSecurityComponents() {
992993
verify(mockedJwtRealm).reload(same(inputSettings));
993994
}
994995

995-
public void testReloadWithFailures() throws Exception {
996+
public void testReloadWithFailures() {
996997
final Settings settings = Settings.builder().put("xpack.security.enabled", true).put("path.home", createTempDir()).build();
997998

998999
final boolean failRemoteClusterCredentialsReload = randomBoolean();
1000+
1001+
final ThreadPool threadPool = mock(ThreadPool.class);
1002+
threadContext = new ThreadContext(Settings.EMPTY);
1003+
when(threadPool.getThreadContext()).thenReturn(threadContext);
9991004
final Client mockedClient = mock(Client.class);
1005+
when(mockedClient.threadPool()).thenReturn(threadPool);
1006+
10001007
final JwtRealm mockedJwtRealm = mock(JwtRealm.class);
10011008
final List<ReloadableSecurityComponent> reloadableComponents = List.of(mockedJwtRealm);
10021009
if (failRemoteClusterCredentialsReload) {

x-pack/qa/password-protected-keystore/src/javaRestTest/java/org/elasticsearch/password_protected_keystore/ReloadSecureSettingsWithPasswordProtectedKeystoreRestIT.java

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
package org.elasticsearch.password_protected_keystore;
88

99
import org.elasticsearch.client.Request;
10+
import org.elasticsearch.client.RequestOptions;
1011
import org.elasticsearch.client.Response;
12+
import org.elasticsearch.client.ResponseException;
1113
import org.elasticsearch.common.settings.SecureString;
1214
import org.elasticsearch.common.settings.Settings;
1315
import org.elasticsearch.common.util.concurrent.ThreadContext;
@@ -18,6 +20,7 @@
1820
import org.elasticsearch.xcontent.ObjectPath;
1921
import org.junit.ClassRule;
2022

23+
import java.io.IOException;
2124
import java.util.Map;
2225

2326
import static org.hamcrest.Matchers.anyOf;
@@ -39,6 +42,7 @@ public class ReloadSecureSettingsWithPasswordProtectedKeystoreRestIT extends ESR
3942
.name("javaRestTest")
4043
.keystore(nodeSpec -> Map.of("xpack.security.transport.ssl.secure_key_passphrase", "transport-password"))
4144
.setting("xpack.security.enabled", "true")
45+
.setting("xpack.ml.enabled", "false")
4246
.setting("xpack.security.authc.anonymous.roles", "anonymous")
4347
.setting("xpack.security.transport.ssl.enabled", "true")
4448
.setting("xpack.security.transport.ssl.certificate", "transport.crt")
@@ -50,6 +54,9 @@ public class ReloadSecureSettingsWithPasswordProtectedKeystoreRestIT extends ESR
5054
.configFile("ca.crt", Resource.fromClasspath("ssl/ca.crt"))
5155
.user("admin_user", "admin-password")
5256
.user("test-user", "test-user-password", "user_role", false)
57+
.user("manage-user", "test-user-password", "manage_role", false)
58+
.user("manage-security-user", "test-user-password", "manage_security_role", false)
59+
.user("monitor-user", "test-user-password", "monitor_role", false)
5360
.build();
5461

5562
@Override
@@ -74,6 +81,33 @@ public void testReloadSecureSettingsWithCorrectPassword() throws Exception {
7481
}
7582
}
7683

84+
@SuppressWarnings("unchecked")
85+
public void testReloadSecureSettingsWithDifferentPrivileges() throws Exception {
86+
final Request request = new Request("POST", "/_nodes/reload_secure_settings");
87+
request.setJsonEntity("{\"secure_settings_password\":\"" + KEYSTORE_PASSWORD + "\"}");
88+
final Response response = performRequestWithUser("manage-user", request);
89+
final Map<String, Object> map = entityAsMap(response);
90+
assertThat(ObjectPath.eval("cluster_name", map), equalTo("javaRestTest"));
91+
assertThat(map.get("nodes"), instanceOf(Map.class));
92+
final Map<String, Object> nodes = (Map<String, Object>) map.get("nodes");
93+
assertThat(nodes.size(), equalTo(NUM_NODES));
94+
for (Map.Entry<String, Object> entry : nodes.entrySet()) {
95+
assertThat(entry.getValue(), instanceOf(Map.class));
96+
final Map<String, Object> node = (Map<String, Object>) entry.getValue();
97+
assertThat(node.get("reload_exception"), nullValue());
98+
}
99+
expectThrows403(() -> {
100+
final Request innerRequest = new Request("POST", "/_nodes/reload_secure_settings");
101+
innerRequest.setJsonEntity("{\"secure_settings_password\":\"" + KEYSTORE_PASSWORD + "\"}");
102+
performRequestWithUser("manage-security-user", innerRequest);
103+
});
104+
expectThrows403(() -> {
105+
final Request innerRequest = new Request("POST", "/_nodes/reload_secure_settings");
106+
innerRequest.setJsonEntity("{\"secure_settings_password\":\"" + KEYSTORE_PASSWORD + "\"}");
107+
performRequestWithUser("monitor-user", request);
108+
});
109+
}
110+
77111
@SuppressWarnings("unchecked")
78112
public void testReloadSecureSettingsWithIncorrectPassword() throws Exception {
79113
final Request request = new Request("POST", "_nodes/reload_secure_settings");
@@ -136,4 +170,16 @@ protected Settings restAdminSettings() {
136170
String token = basicAuthHeaderValue("admin_user", new SecureString("admin-password".toCharArray()));
137171
return Settings.builder().put(ThreadContext.PREFIX + ".Authorization", token).build();
138172
}
173+
174+
private static void expectThrows403(ThrowingRunnable runnable) {
175+
assertThat(expectThrows(ResponseException.class, runnable).getResponse().getStatusLine().getStatusCode(), equalTo(403));
176+
}
177+
178+
private Response performRequestWithUser(final String username, final Request request) throws IOException {
179+
request.setOptions(
180+
RequestOptions.DEFAULT.toBuilder()
181+
.addHeader("Authorization", basicAuthHeaderValue(username, new SecureString("test-user-password")))
182+
);
183+
return client().performRequest(request);
184+
}
139185
}

x-pack/qa/password-protected-keystore/src/javaRestTest/resources/roles.yml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,15 @@
22
user_role:
33
cluster: [ALL]
44
indices: []
5+
6+
manage_role:
7+
cluster: [MANAGE]
8+
indices: []
9+
10+
manage_security_role:
11+
cluster: [MANAGE_SECURITY]
12+
indices: []
13+
14+
monitor_role:
15+
cluster: [MONITOR]
16+
indices: []

0 commit comments

Comments
 (0)