From a700e8ab33fa0dd568f2f3966e7f6ee45c738222 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 25 Nov 2021 01:47:47 +0200 Subject: [PATCH 1/6] URL option for BaseRunAsSuperuserCommand Add a --url option for elasticsearch-reset-password and elasticsearch-create-enrollment-token CLI Tools ( and any tools that would extend BaseRunAsSuperuserCommand ). The tools use CommandLineHttpClient internally, which tries its best to deduce the URL of the local node based on the configuration but there are certain cases where it either fails or returns an unwanted result. Concretely: - CommandLineHttpClient#getDefaultURL will always return a URL with the port set to 9200, unless otherwise explicitly set in the configuration. When running multiple nodes on the same host, subsequent nodes get sequential port numbers after 9200 by default and this means that the CLI tool will always connect the first of n nodes in a given host. Since these tools depend on a file realm local user, requests to other nodes would fail - When an ES node binds and listens to many addresses, there can be the case that not all of the IP addresses are added as SANs in the certificate that is used for TLS on the HTTP layer. CommandLineHttpClient#getDefaultURL will pick an address based on a preference order but that address might not be in the SANs and thus all requests to the node would fail due to failed hostname verification. Manually setting `--url` to an appropriate value allows users to overcome these edge cases. --- .../commands/create-enrollment-token.asciidoc | 11 +++-- .../commands/reset-password.asciidoc | 7 ++- .../esnative/tool/ResetPasswordTool.java | 3 +- .../ExternalEnrollmentTokenGenerator.java | 34 +++++++-------- .../tool/CreateEnrollmentTokenTool.java | 11 ++++- .../tool/BaseRunAsSuperuserCommand.java | 11 +++-- ...ExternalEnrollmentTokenGeneratorTests.java | 43 +++++++++++-------- .../esnative/tool/ResetPasswordToolTests.java | 33 ++++++++++++++ .../tool/CreateEnrollmentTokenToolTests.java | 42 ++++++++++++++++-- 9 files changed, 146 insertions(+), 49 deletions(-) diff --git a/docs/reference/commands/create-enrollment-token.asciidoc b/docs/reference/commands/create-enrollment-token.asciidoc index 4fd95f1b7bef4..37ec9dabb02e4 100644 --- a/docs/reference/commands/create-enrollment-token.asciidoc +++ b/docs/reference/commands/create-enrollment-token.asciidoc @@ -12,7 +12,7 @@ The `elasticsearch-create-enrollment-token` command creates enrollment tokens fo [source,shell] ---- bin/elasticsearch-create-enrollment-token -[-f, --force] [-h, --help] [-E ] [-s, --scope] +[-f, --force] [-h, --help] [-E ] [-s, --scope] [--url] ---- [discrete] @@ -23,7 +23,7 @@ Use this command to create enrollment tokens, which you can use to enroll new with an existing {es} cluster that has security features enabled. The command generates (and subsequently removes) a temporary user in the <> to run the request that creates enrollment tokens. -IMPORTANT: You cannot use this tool if the file realm is disabled in your +IMPORTANT: You cannot use this tool if the file realm is disabled in your `elasticsearch.yml` file. This command uses an HTTP connection to connect to the cluster and run the user @@ -42,12 +42,17 @@ option. For more information about debugging connection failures, see `-E `:: Configures a standard {es} or {xpack} setting. -`-f, --force`:: Forces the command to run against an unhealthy cluster. +`-f, --force`:: Forces the command to run against an unhealthy cluster. `-h, --help`:: Returns all of the command parameters. `-s, --scope`:: Specifies the scope of the generated token. Supported values are `node` and `kibana`. +`--url`:: Specifies the base URL (hostname and port) that the tool uses to submit API +requests to {es}. The default value is determined from the settings in your +`elasticsearch.yml` file. If `xpack.security.http.ssl.enabled` is set to `true`, +you must specify an HTTPS URL. + [discrete] === Examples diff --git a/docs/reference/commands/reset-password.asciidoc b/docs/reference/commands/reset-password.asciidoc index 012874fd61171..03927ed0d5a87 100644 --- a/docs/reference/commands/reset-password.asciidoc +++ b/docs/reference/commands/reset-password.asciidoc @@ -14,7 +14,7 @@ the native realm and built-in users. bin/elasticsearch-reset-password [-a, --auto] [-b, --batch] [-E , String> httpInfo = getNodeInfo(user, password); + final String apiKey = getApiKeyCredentials(user, password, action, baseUrl); + final Tuple, String> httpInfo = getNodeInfo(user, password, baseUrl); return new EnrollmentToken(apiKey, fingerprint, httpInfo.v2(), httpInfo.v1()); } @@ -89,12 +87,12 @@ private HttpResponse.HttpResponseBuilder responseBuilder(InputStream is) throws return httpResponseBuilder; } - protected URL createAPIKeyUrl() throws MalformedURLException, URISyntaxException { - return new URL(defaultUrl, (defaultUrl.toURI().getPath() + "/_security/api_key").replaceAll("/+", "/")); + protected URL createAPIKeyUrl(URL baseUrl) throws MalformedURLException, URISyntaxException { + return new URL(baseUrl, (baseUrl.toURI().getPath() + "/_security/api_key").replaceAll("/+", "/")); } - protected URL getHttpInfoUrl() throws MalformedURLException, URISyntaxException { - return new URL(defaultUrl, (defaultUrl.toURI().getPath() + "/_nodes/_local/http").replaceAll("/+", "/")); + protected URL getHttpInfoUrl(URL baseUrl) throws MalformedURLException, URISyntaxException { + return new URL(baseUrl, (baseUrl.toURI().getPath() + "/_nodes/_local/http").replaceAll("/+", "/")); } @SuppressWarnings("unchecked") @@ -114,7 +112,7 @@ static String getVersion(Map nodesInfo) { return nodeInfo.get("version").toString(); } - protected String getApiKeyCredentials(String user, SecureString password, String action) throws Exception { + protected String getApiKeyCredentials(String user, SecureString password, String action, URL baseUrl) throws Exception { final CheckedSupplier createApiKeyRequestBodySupplier = () -> { XContentBuilder xContentBuilder = JsonXContent.contentBuilder(); xContentBuilder.startObject() @@ -129,7 +127,7 @@ protected String getApiKeyCredentials(String user, SecureString password, String return Strings.toString(xContentBuilder); }; - final URL createApiKeyUrl = createAPIKeyUrl(); + final URL createApiKeyUrl = createAPIKeyUrl(baseUrl); final HttpResponse httpResponseApiKey = client.execute( "POST", createApiKeyUrl, @@ -155,8 +153,8 @@ protected String getApiKeyCredentials(String user, SecureString password, String return apiId + ":" + apiKey; } - protected Tuple, String> getNodeInfo(String user, SecureString password) throws Exception { - final URL httpInfoUrl = getHttpInfoUrl(); + protected Tuple, String> getNodeInfo(String user, SecureString password, URL baseUrl) throws Exception { + final URL httpInfoUrl = getHttpInfoUrl(baseUrl); final HttpResponse httpResponseHttp = client.execute("GET", httpInfoUrl, user, password, () -> null, is -> responseBuilder(is)); final int httpCode = httpResponseHttp.getHttpStatus(); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java index 84c6ccf4964ea..954badc86e47a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenTool.java @@ -22,16 +22,19 @@ import org.elasticsearch.xpack.security.enrollment.ExternalEnrollmentTokenGenerator; import org.elasticsearch.xpack.security.tool.BaseRunAsSuperuserCommand; +import java.net.URL; import java.util.List; import java.util.function.Function; public class CreateEnrollmentTokenTool extends BaseRunAsSuperuserCommand { private final OptionSpec scope; + private final Function clientFunction; private final CheckedFunction createEnrollmentTokenFunction; static final List ALLOWED_SCOPES = List.of("node", "kibana"); CreateEnrollmentTokenTool() { + this( environment -> new CommandLineHttpClient(environment), environment -> KeyStoreWrapper.load(environment.configFile()), @@ -46,6 +49,7 @@ public class CreateEnrollmentTokenTool extends BaseRunAsSuperuserCommand { ) { super(clientFunction, keyStoreFunction, "Creates enrollment tokens for elasticsearch nodes and kibana instances"); this.createEnrollmentTokenFunction = createEnrollmentTokenFunction; + this.clientFunction = clientFunction; scope = parser.acceptsAll(List.of("scope", "s"), "The scope of this enrollment token, can be either \"node\" or \"kibana\"") .withRequiredArg() .required(); @@ -74,12 +78,15 @@ protected void validate(Terminal terminal, OptionSet options, Environment env) t protected void executeCommand(Terminal terminal, OptionSet options, Environment env, String username, SecureString password) throws Exception { final String tokenScope = scope.value(options); + final URL baseUrl = options.has(urlOption) + ? new URL(options.valueOf(urlOption)) + : new URL(clientFunction.apply(env).getDefaultURL()); try { ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = createEnrollmentTokenFunction.apply(env); if (tokenScope.equals("node")) { - terminal.println(externalEnrollmentTokenGenerator.createNodeEnrollmentToken(username, password).getEncoded()); + terminal.println(externalEnrollmentTokenGenerator.createNodeEnrollmentToken(username, password, baseUrl).getEncoded()); } else { - terminal.println(externalEnrollmentTokenGenerator.createKibanaEnrollmentToken(username, password).getEncoded()); + terminal.println(externalEnrollmentTokenGenerator.createKibanaEnrollmentToken(username, password, baseUrl).getEncoded()); } } catch (Exception e) { terminal.errorPrintln("Unable to create enrollment token for scope [" + tokenScope + "]"); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java index 22f01d57ee91d..8cbf52a3cac1a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/tool/BaseRunAsSuperuserCommand.java @@ -8,6 +8,7 @@ package org.elasticsearch.xpack.security.tool; import joptsimple.OptionSet; +import joptsimple.OptionSpec; import joptsimple.OptionSpecBuilder; import org.elasticsearch.Version; @@ -57,6 +58,7 @@ public abstract class BaseRunAsSuperuserCommand extends KeyStoreAwareCommand { private static final int PASSWORD_LENGTH = 14; private final OptionSpecBuilder force; + protected final OptionSpec urlOption; private final Function clientFunction; private final CheckedFunction keyStoreFunction; @@ -72,6 +74,7 @@ public BaseRunAsSuperuserCommand( List.of("f", "force"), "Use this option to force execution of the command against a cluster that is currently unhealthy." ); + urlOption = parser.accepts("url", "the URL where the elasticsearch node listens for connections.").withRequiredArg(); } @Override @@ -120,7 +123,7 @@ protected final void execute(Terminal terminal, OptionSet options, Environment e attributesChecker.check(terminal); final boolean forceExecution = options.has(force); - checkClusterHealthWithRetries(newEnv, terminal, username, password, 5, forceExecution); + checkClusterHealthWithRetries(newEnv, options, terminal, username, password, 5, forceExecution); executeCommand(terminal, options, newEnv, username, password); } catch (Exception e) { int exitCode; @@ -195,6 +198,7 @@ private void ensureFileRealmEnabled(Settings settings) throws Exception { */ private void checkClusterHealthWithRetries( Environment env, + OptionSet options, Terminal terminal, String username, SecureString password, @@ -202,7 +206,8 @@ private void checkClusterHealthWithRetries( boolean force ) throws Exception { CommandLineHttpClient client = clientFunction.apply(env); - final URL clusterHealthUrl = CommandLineHttpClient.createURL(new URL(client.getDefaultURL()), "_cluster/health", "?pretty"); + final URL baseUrl = options.has(urlOption) ? new URL(options.valueOf(urlOption)) : new URL(client.getDefaultURL()); + final URL clusterHealthUrl = CommandLineHttpClient.createURL(baseUrl, "_cluster/health", "?pretty"); final HttpResponse response; try { response = client.execute("GET", clusterHealthUrl, username, password, () -> null, CommandLineHttpClient::responseBuilder); @@ -225,7 +230,7 @@ private void checkClusterHealthWithRetries( ); Thread.sleep(1000); retries -= 1; - checkClusterHealthWithRetries(env, terminal, username, password, retries, force); + checkClusterHealthWithRetries(env, options, terminal, username, password, retries, force); } else { throw new UserException( ExitCodes.DATA_ERROR, diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java index 63824c270e7c9..1256347fc7171 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java @@ -85,8 +85,9 @@ public void testCreateSuccess() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); - final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(); - final URL getHttpInfoURL = externalEnrollmentTokenGenerator.getHttpInfoUrl(); + final URL baseURL = new URL("http://localhost:9200"); + final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); + final URL getHttpInfoURL = externalEnrollmentTokenGenerator.getHttpInfoUrl(baseURL); final HttpResponse httpResponseOK = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>()); when(client.execute(anyString(), any(URL.class), anyString(), any(SecureString.class), anyCheckedSupplier(), anyCheckedFunction())) @@ -147,7 +148,8 @@ public void testCreateSuccess() throws Exception { final String tokenNode = externalEnrollmentTokenGenerator.createNodeEnrollmentToken( "elastic", - new SecureString("elastic".toCharArray()) + new SecureString("elastic".toCharArray()), + baseURL ).getEncoded(); Map infoNode = getDecoded(tokenNode); @@ -158,7 +160,8 @@ public void testCreateSuccess() throws Exception { final String tokenKibana = externalEnrollmentTokenGenerator.createKibanaEnrollmentToken( "elastic", - new SecureString("elastic".toCharArray()) + new SecureString("elastic".toCharArray()), + baseURL ).getEncoded(); Map infoKibana = getDecoded(tokenKibana); @@ -171,8 +174,9 @@ public void testCreateSuccess() throws Exception { public void testFailedCreateApiKey() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); + final URL baseURL = new URL("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); - final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(); + final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); final HttpResponse httpResponseNotOK = new HttpResponse(HttpURLConnection.HTTP_BAD_REQUEST, new HashMap<>()); when( @@ -188,7 +192,7 @@ public void testFailedCreateApiKey() throws Exception { IllegalStateException ex = expectThrows( IllegalStateException.class, - () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())) + () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray()), baseURL) .getEncoded() ); assertThat(ex.getMessage(), Matchers.containsString("Unexpected response code [400] from calling POST ")); @@ -197,9 +201,10 @@ public void testFailedCreateApiKey() throws Exception { public void testFailedRetrieveHttpInfo() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); + final URL baseURL = new URL("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); - final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(); - final URL getHttpInfoURL = externalEnrollmentTokenGenerator.getHttpInfoUrl(); + final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); + final URL getHttpInfoURL = externalEnrollmentTokenGenerator.getHttpInfoUrl(baseURL); final HttpResponse httpResponseOK = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>()); when( @@ -248,7 +253,7 @@ public void testFailedRetrieveHttpInfo() throws Exception { IllegalStateException ex = expectThrows( IllegalStateException.class, - () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())) + () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray()), baseURL) .getEncoded() ); assertThat(ex.getMessage(), Matchers.containsString("Unexpected response code [400] from calling GET ")); @@ -276,8 +281,9 @@ public void testFailedNoCaInKeystore() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); - final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(); - final URL getHttpInfoURL = externalEnrollmentTokenGenerator.getHttpInfoUrl(); + final URL baseURL = new URL("http://localhost:9200"); + final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); + final URL getHttpInfoURL = externalEnrollmentTokenGenerator.getHttpInfoUrl(baseURL); final HttpResponse httpResponseOK = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>()); when( @@ -326,7 +332,7 @@ public void testFailedNoCaInKeystore() throws Exception { IllegalStateException ex = expectThrows( IllegalStateException.class, - () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())) + () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray()), baseURL) .getEncoded() ); assertThat( @@ -360,8 +366,9 @@ public void testFailedManyCaInKeystore() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); - final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(); - final URL getHttpInfoURL = externalEnrollmentTokenGenerator.getHttpInfoUrl(); + final URL baseURL = new URL("http://localhost:9200"); + final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); + final URL getHttpInfoURL = externalEnrollmentTokenGenerator.getHttpInfoUrl(baseURL); final HttpResponse httpResponseOK = new HttpResponse(HttpURLConnection.HTTP_OK, new HashMap<>()); when( @@ -410,7 +417,7 @@ public void testFailedManyCaInKeystore() throws Exception { IllegalStateException ex = expectThrows( IllegalStateException.class, - () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())) + () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray()), baseURL) .getEncoded() ); assertThat( @@ -432,6 +439,7 @@ public void testNoKeyStore() throws Exception { final Environment environment_no_keystore = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); + final URL baseURL = new URL("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator( environment_no_keystore, client @@ -439,7 +447,7 @@ public void testNoKeyStore() throws Exception { IllegalStateException ex = expectThrows( IllegalStateException.class, - () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())) + () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray()), baseURL) .getEncoded() ); assertThat( @@ -468,6 +476,7 @@ public void testEnrollmentNotEnabled() throws Exception { final Environment environment_not_enabled = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); when(client.getDefaultURL()).thenReturn("http://localhost:9200"); + final URL baseURL = new URL("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator( environment_not_enabled, client @@ -475,7 +484,7 @@ public void testEnrollmentNotEnabled() throws Exception { IllegalStateException ex = expectThrows( IllegalStateException.class, - () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray())) + () -> externalEnrollmentTokenGenerator.createNodeEnrollmentToken("elastic", new SecureString("elastic".toCharArray()), baseURL) .getEncoded() ); assertThat( diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetPasswordToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetPasswordToolTests.java index 330df329b4ded..943b2770172a5 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetPasswordToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetPasswordToolTests.java @@ -152,6 +152,39 @@ public void testSuccessInteractiveMode() throws Exception { assertThat(output, containsString("Password for the [" + user + "] user successfully reset.")); } + public void testUserCanPassUrlParameter() throws Exception { + URL url = new URL("http://localhost:9204"); + HttpResponse healthResponse = new HttpResponse(HttpURLConnection.HTTP_OK, Map.of("status", randomFrom("yellow", "green"))); + when( + client.execute( + anyString(), + eq(clusterHealthUrl(url)), + anyString(), + any(SecureString.class), + any(CheckedSupplier.class), + any(CheckedFunction.class) + ) + ).thenReturn(healthResponse); + HttpResponse changePasswordResponse = new HttpResponse(HttpURLConnection.HTTP_OK, Map.of()); + when( + client.execute( + anyString(), + eq(changePasswordUrl(url, user)), + anyString(), + any(SecureString.class), + any(CheckedSupplier.class), + any(CheckedFunction.class) + ) + ).thenReturn(changePasswordResponse); + terminal.addTextInput("y"); + execute(randomFrom("-u", "--username"), user, "--url", "http://localhost:9204"); + String output = terminal.getOutput(); + assertThat(output, containsString("This tool will reset the password of the [" + user + "] user to an autogenerated value.")); + assertThat(output, containsString("The password will be printed in the console.")); + assertThat(output, containsString("Password for the [" + user + "] user successfully reset.")); + assertThat(output, containsString("New value:")); + } + public void testUserCancelledAutoMode() throws Exception { terminal.addTextInput("n"); UserException e = expectThrows(UserException.class, () -> execute(randomFrom("-u", "--username"), user)); diff --git a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java index 16c7120ab3a76..d322ae2cfdc5d 100644 --- a/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java +++ b/x-pack/qa/security-tools-tests/src/test/java/org/elasticsearch/xpack/security/enrollment/tool/CreateEnrollmentTokenToolTests.java @@ -134,8 +134,12 @@ public void setup() throws Exception { "8.0.0", Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202") ); - when(externalEnrollmentTokenGenerator.createKibanaEnrollmentToken(anyString(), any(SecureString.class))).thenReturn(kibanaToken); - when(externalEnrollmentTokenGenerator.createNodeEnrollmentToken(anyString(), any(SecureString.class))).thenReturn(nodeToken); + when(externalEnrollmentTokenGenerator.createKibanaEnrollmentToken(anyString(), any(SecureString.class), any(URL.class))).thenReturn( + kibanaToken + ); + when(externalEnrollmentTokenGenerator.createNodeEnrollmentToken(anyString(), any(SecureString.class), any(URL.class))).thenReturn( + nodeToken + ); } @AfterClass @@ -167,6 +171,36 @@ public void testInvalidScope() throws Exception { ); } + public void testUserCanPassUrl() throws Exception { + HttpResponse healthResponse = new HttpResponse(HttpURLConnection.HTTP_OK, Map.of("status", randomFrom("yellow", "green"))); + when( + client.execute( + anyString(), + eq(clusterHealthUrl(new URL("http://localhost:9204"))), + anyString(), + any(SecureString.class), + any(CheckedSupplier.class), + any(CheckedFunction.class) + ) + ).thenReturn(healthResponse); + EnrollmentToken kibanaToken = new EnrollmentToken( + "DR6CzXkBDf8amV_48yYX:x3YqU_rqQwm-ESrkExcnOg", + "ce480d53728605674fcfd8ffb51000d8a33bf32de7c7f1e26b4d428f8a91362d", + "8.0.0", + Arrays.asList("[192.168.0.1:9201, 172.16.254.1:9202") + ); + when( + externalEnrollmentTokenGenerator.createKibanaEnrollmentToken( + anyString(), + any(SecureString.class), + eq(new URL("http://localhost:9204")) + ) + ).thenReturn(kibanaToken); + String output = execute("--scope", "kibana", "--url", "http://localhost:9204"); + assertThat(output, containsString("1WXzQ4eVlYOngzWXFVX3JxUXdtLUVTcmtFeGNuT2cifQ==")); + + } + public void testUnhealthyCluster() throws Exception { String scope = randomBoolean() ? "node" : "kibana"; URL url = new URL(client.getDefaultURL()); @@ -207,10 +241,10 @@ public void testEnrollmentDisabled() { public void testUnableToCreateToken() throws Exception { this.externalEnrollmentTokenGenerator = mock(ExternalEnrollmentTokenGenerator.class); - when(externalEnrollmentTokenGenerator.createKibanaEnrollmentToken(anyString(), any(SecureString.class))).thenThrow( + when(externalEnrollmentTokenGenerator.createKibanaEnrollmentToken(anyString(), any(SecureString.class), any(URL.class))).thenThrow( new IllegalStateException("example exception message") ); - when(externalEnrollmentTokenGenerator.createNodeEnrollmentToken(anyString(), any(SecureString.class))).thenThrow( + when(externalEnrollmentTokenGenerator.createNodeEnrollmentToken(anyString(), any(SecureString.class), any(URL.class))).thenThrow( new IllegalStateException("example exception message") ); String scope = randomBoolean() ? "node" : "kibana"; From 84d1590178f73bcf0c9d7b13bed458b2e6b760e9 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Thu, 25 Nov 2021 02:05:31 +0200 Subject: [PATCH 2/6] spotless --- .../security/authc/esnative/tool/ResetPasswordTool.java | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetPasswordTool.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetPasswordTool.java index a8e93e8b36137..f52d8f8add031 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetPasswordTool.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/esnative/tool/ResetPasswordTool.java @@ -95,11 +95,7 @@ protected void executeCommand(Terminal terminal, OptionSet options, Environment try { final CommandLineHttpClient client = clientFunction.apply(env); final URL baseUrl = options.has(urlOption) ? new URL(options.valueOf(urlOption)) : new URL(client.getDefaultURL()); - final URL changePasswordUrl = createURL( - baseUrl, - "_security/user/" + providedUsername + "/_password", - "?pretty" - ); + final URL changePasswordUrl = createURL(baseUrl, "_security/user/" + providedUsername + "/_password", "?pretty"); final HttpResponse httpResponse = client.execute( "POST", changePasswordUrl, From 8985bf7e3e837db52f734557911cb4fcdf26108a Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Sun, 28 Nov 2021 15:25:19 +0200 Subject: [PATCH 3/6] remove unnecessary mocks --- .../enrollment/ExternalEnrollmentTokenGeneratorTests.java | 7 ------- 1 file changed, 7 deletions(-) diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java index 1256347fc7171..339b3de9ecb49 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/enrollment/ExternalEnrollmentTokenGeneratorTests.java @@ -83,7 +83,6 @@ public void setupMocks() throws Exception { public void testCreateSuccess() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); - when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); final URL baseURL = new URL("http://localhost:9200"); final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); @@ -173,7 +172,6 @@ public void testCreateSuccess() throws Exception { public void testFailedCreateApiKey() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); - when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final URL baseURL = new URL("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); @@ -200,7 +198,6 @@ public void testFailedCreateApiKey() throws Exception { public void testFailedRetrieveHttpInfo() throws Exception { final CommandLineHttpClient client = mock(CommandLineHttpClient.class); - when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final URL baseURL = new URL("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); @@ -279,7 +276,6 @@ public void testFailedNoCaInKeystore() throws Exception { .build(); environment = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); - when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); final URL baseURL = new URL("http://localhost:9200"); final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); @@ -364,7 +360,6 @@ public void testFailedManyCaInKeystore() throws Exception { .build(); environment = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); - when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator(environment, client); final URL baseURL = new URL("http://localhost:9200"); final URL createAPIKeyURL = externalEnrollmentTokenGenerator.createAPIKeyUrl(baseURL); @@ -438,7 +433,6 @@ public void testNoKeyStore() throws Exception { .build(); final Environment environment_no_keystore = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); - when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final URL baseURL = new URL("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator( environment_no_keystore, @@ -475,7 +469,6 @@ public void testEnrollmentNotEnabled() throws Exception { .build(); final Environment environment_not_enabled = new Environment(settings, tempDir); final CommandLineHttpClient client = mock(CommandLineHttpClient.class); - when(client.getDefaultURL()).thenReturn("http://localhost:9200"); final URL baseURL = new URL("http://localhost:9200"); final ExternalEnrollmentTokenGenerator externalEnrollmentTokenGenerator = new ExternalEnrollmentTokenGenerator( environment_not_enabled, From 0d01fd82e311c36688c43395bdc5986fae4b4013 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Sun, 28 Nov 2021 15:25:33 +0200 Subject: [PATCH 4/6] add example for setting --url --- docs/reference/commands/reset-password.asciidoc | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/reference/commands/reset-password.asciidoc b/docs/reference/commands/reset-password.asciidoc index 03927ed0d5a87..cb0cf59eae1d4 100644 --- a/docs/reference/commands/reset-password.asciidoc +++ b/docs/reference/commands/reset-password.asciidoc @@ -61,7 +61,7 @@ option. For more information about debugging connection failures, see `--url`:: Specifies the base URL (hostname and port) that the tool uses to submit API requests to {es}. The default value is determined from the settings in your -`elasticsearch.yml` file. If `xpack.security.http.ssl.enabled` is set to `true`, +`elasticsearch.yml` file. If `xpack.security.http.ssl.enabled` is set to `true`, you must specify an HTTPS URL. `-v --verbose`:: Shows verbose output in the console. @@ -69,7 +69,7 @@ you must specify an HTTPS URL. === Examples The following example resets the password of the `elastic` user to an auto-generated value and -prints the new password in the console. +prints the new password in the console: [source,shell] ---- @@ -83,3 +83,11 @@ in the terminal for the desired password: ---- bin/elasticsearch-reset-password --username user1 -i ---- + +The following example resets the password of a native user with username `user2` to an auto-generated value +prints the new password in the console, while overriding the url where the elasticsearch node is reachable: + +[source,shell] +---- +bin/elasticsearch-reset-password --url "https://172.0.0.3:9200" --username user2 -i +---- From 33824c547e540e9d59c7bae2cbc5db5c739dbc22 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 29 Nov 2021 15:09:38 +0200 Subject: [PATCH 5/6] update docs --- .../commands/create-enrollment-token.asciidoc | 10 +++++++++- docs/reference/commands/reset-password.asciidoc | 4 ++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/docs/reference/commands/create-enrollment-token.asciidoc b/docs/reference/commands/create-enrollment-token.asciidoc index 37ec9dabb02e4..086c487edb2d9 100644 --- a/docs/reference/commands/create-enrollment-token.asciidoc +++ b/docs/reference/commands/create-enrollment-token.asciidoc @@ -48,7 +48,7 @@ option. For more information about debugging connection failures, see `-s, --scope`:: Specifies the scope of the generated token. Supported values are `node` and `kibana`. -`--url`:: Specifies the base URL (hostname and port) that the tool uses to submit API +`--url`:: Specifies the base URL (hostname and port of the local node) that the tool uses to submit API requests to {es}. The default value is determined from the settings in your `elasticsearch.yml` file. If `xpack.security.http.ssl.enabled` is set to `true`, you must specify an HTTPS URL. @@ -62,3 +62,11 @@ The following command creates an enrollment token for enrolling an {es} node int ---- bin/elasticsearch-create-enrollment-token -s node ---- + +The following command creates an enrollment token for enrolling a kibana instance into a cluster, +while overriding the url where the local elasticsearch node is reachable: + +[source,shell] +---- +bin/elasticsearch-create-enrollment-token -s kibana --url "https://172.0.0.3:9200" +---- diff --git a/docs/reference/commands/reset-password.asciidoc b/docs/reference/commands/reset-password.asciidoc index cb0cf59eae1d4..79ea11d9eeb4d 100644 --- a/docs/reference/commands/reset-password.asciidoc +++ b/docs/reference/commands/reset-password.asciidoc @@ -59,7 +59,7 @@ option. For more information about debugging connection failures, see `-u, --username`:: The username of the native realm user or built-in user. -`--url`:: Specifies the base URL (hostname and port) that the tool uses to submit API +`--url`:: Specifies the base URL (hostname and port of the local node) that the tool uses to submit API requests to {es}. The default value is determined from the settings in your `elasticsearch.yml` file. If `xpack.security.http.ssl.enabled` is set to `true`, you must specify an HTTPS URL. @@ -85,7 +85,7 @@ bin/elasticsearch-reset-password --username user1 -i ---- The following example resets the password of a native user with username `user2` to an auto-generated value -prints the new password in the console, while overriding the url where the elasticsearch node is reachable: +prints the new password in the console, while overriding the url where the local elasticsearch node is reachable: [source,shell] ---- From 36735f52430e26b95730020fa68439e520bd7407 Mon Sep 17 00:00:00 2001 From: Ioannis Kakavas Date: Mon, 29 Nov 2021 22:48:14 +0200 Subject: [PATCH 6/6] apply suggestions from code review --- docs/reference/commands/create-enrollment-token.asciidoc | 5 +++-- docs/reference/commands/reset-password.asciidoc | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/reference/commands/create-enrollment-token.asciidoc b/docs/reference/commands/create-enrollment-token.asciidoc index 086c487edb2d9..ca95649324702 100644 --- a/docs/reference/commands/create-enrollment-token.asciidoc +++ b/docs/reference/commands/create-enrollment-token.asciidoc @@ -63,8 +63,9 @@ The following command creates an enrollment token for enrolling an {es} node int bin/elasticsearch-create-enrollment-token -s node ---- -The following command creates an enrollment token for enrolling a kibana instance into a cluster, -while overriding the url where the local elasticsearch node is reachable: +The following command creates an enrollment token for enrolling a {kib} instance into a cluster. +The specified URL indicates where the elasticsearch-create-enrollment-token tool attempts to reach the +local {es} node: [source,shell] ---- diff --git a/docs/reference/commands/reset-password.asciidoc b/docs/reference/commands/reset-password.asciidoc index 79ea11d9eeb4d..b8823158d0d0f 100644 --- a/docs/reference/commands/reset-password.asciidoc +++ b/docs/reference/commands/reset-password.asciidoc @@ -85,8 +85,8 @@ bin/elasticsearch-reset-password --username user1 -i ---- The following example resets the password of a native user with username `user2` to an auto-generated value -prints the new password in the console, while overriding the url where the local elasticsearch node is reachable: - +prints the new password in the console. The specified URL indicates where the elasticsearch-reset-password +tool attempts to reach the local {es} node: [source,shell] ---- bin/elasticsearch-reset-password --url "https://172.0.0.3:9200" --username user2 -i