Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

URL option for BaseRunAsSuperuserCommand #81025

Merged
merged 9 commits into from
Nov 29, 2021
11 changes: 8 additions & 3 deletions docs/reference/commands/create-enrollment-token.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -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 <KeyValuePair>] [-s, --scope]
[-f, --force] [-h, --help] [-E <KeyValuePair>] [-s, --scope] [--url]
----

[discrete]
Expand All @@ -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
<<file-realm,file realm>> 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
Expand All @@ -42,12 +42,17 @@ option. For more information about debugging connection failures, see

`-E <KeyValuePair>`:: 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.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be preferable that we drop the "If xpack.security.http.ssl.enabled is set to true,
you must specify an HTTPS URL" scheme condition, by having the tool check for that itself (like it does for the "default URL")?

Moreover, a URL is more than the scheme and the host and port pair, and using DNS names to refer to the node might not be ideal, as it might not be included in the SAN of the cert.
Do you think it would be preferable that this option be more focused on the ip and port that the local node can be reached at?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you think it would be preferable that this option be more focused on the ip and port that the local node can be reached at?

I contemplated the same but in the end settled to a single option as a simpler thing, happy to discuss.

a URL is more than the scheme and the host and port pair

true but what we should be aiming for here is not naming strictness for the sake of it but a name for the option that’s understandable by (or at least explainable for ) the majority of users. I think that url fits the bill.

and using DNS names to refer to the node might not be ideal, as it might not be included in the SAN of the cert.

the idea is that if you ever need to use this parameter you do so because you know that you want to talk to the node at an ip or hostname that is in the SANs but CommandLineHttpClient picks another. So you know what should be in the url (either a hostname or an IP address )

Do you think it would be preferable that we drop the "If xpack.security.http.ssl.enabled is set to true,
you must specify an HTTPS URL" scheme condition, by having the tool check for that itself (like it does for the "default URL")?

If we change this to two parameters then yes it makes sense , we shouldn’t be asking the users for a third parameter (scheme) but I’m not 100% that ee should change.

Do we introduce an ip and a port parameter? Do we mandate both if one is passed or have default values for the port ?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we introduce an ip and a port parameter? Do we mandate both if one is passed or have default values for the port ?

I don't have a preference. Having to specify both the ip and the port simultaneously (as is the case with this url parameter), as a single option is OK.


[discrete]
=== Examples

Expand Down
7 changes: 6 additions & 1 deletion docs/reference/commands/reset-password.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ the native realm and built-in users.
bin/elasticsearch-reset-password
[-a, --auto] [-b, --batch] [-E <KeyValuePair]
[-f, --force] [-h, --help] [-i, --interactive]
[-s, --silent] [-u, --username] [-v, --verbose]
[-s, --silent] [-u, --username] [--url] [-v, --verbose]
----

[discrete]
Expand Down Expand Up @@ -59,6 +59,11 @@ 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
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.

`-v --verbose`:: Shows verbose output in the console.
[discrete]
=== Examples
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,11 +94,8 @@ protected void executeCommand(Terminal terminal, OptionSet options, Environment
}
try {
final CommandLineHttpClient client = clientFunction.apply(env);
final URL changePasswordUrl = createURL(
new URL(client.getDefaultURL()),
"_security/user/" + providedUsername + "/_password",
"?pretty"
);
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 HttpResponse httpResponse = client.execute(
"POST",
changePasswordUrl,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,35 +45,33 @@ public class ExternalEnrollmentTokenGenerator extends BaseEnrollmentTokenGenerat
private final Environment environment;
private final SSLService sslService;
private final CommandLineHttpClient client;
private final URL defaultUrl;

public ExternalEnrollmentTokenGenerator(Environment environment) throws MalformedURLException {
this(environment, new CommandLineHttpClient(environment));
}

// protected for testing
protected ExternalEnrollmentTokenGenerator(Environment environment, CommandLineHttpClient client) throws MalformedURLException {
protected ExternalEnrollmentTokenGenerator(Environment environment, CommandLineHttpClient client) {
this.environment = environment;
this.sslService = new SSLService(environment);
this.client = client;
this.defaultUrl = new URL(client.getDefaultURL());
}

public EnrollmentToken createNodeEnrollmentToken(String user, SecureString password) throws Exception {
return this.create(user, password, NodeEnrollmentAction.NAME);
public EnrollmentToken createNodeEnrollmentToken(String user, SecureString password, URL baseUrl) throws Exception {
return this.create(user, password, NodeEnrollmentAction.NAME, baseUrl);
}

public EnrollmentToken createKibanaEnrollmentToken(String user, SecureString password) throws Exception {
return this.create(user, password, KibanaEnrollmentAction.NAME);
public EnrollmentToken createKibanaEnrollmentToken(String user, SecureString password, URL baseUrl) throws Exception {
return this.create(user, password, KibanaEnrollmentAction.NAME, baseUrl);
}

protected EnrollmentToken create(String user, SecureString password, String action) throws Exception {
protected EnrollmentToken create(String user, SecureString password, String action, URL baseUrl) throws Exception {
if (XPackSettings.ENROLLMENT_ENABLED.get(environment.settings()) != true) {
throw new IllegalStateException("[xpack.security.enrollment.enabled] must be set to `true` to create an enrollment token");
}
final String fingerprint = getHttpsCaFingerprint(sslService);
final String apiKey = getApiKeyCredentials(user, password, action);
final Tuple<List<String>, String> httpInfo = getNodeInfo(user, password);
final String apiKey = getApiKeyCredentials(user, password, action, baseUrl);
final Tuple<List<String>, String> httpInfo = getNodeInfo(user, password, baseUrl);
return new EnrollmentToken(apiKey, fingerprint, httpInfo.v2(), httpInfo.v1());
}

Expand All @@ -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")
Expand All @@ -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<String, Exception> createApiKeyRequestBodySupplier = () -> {
XContentBuilder xContentBuilder = JsonXContent.contentBuilder();
xContentBuilder.startObject()
Expand All @@ -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,
Expand All @@ -155,8 +153,8 @@ protected String getApiKeyCredentials(String user, SecureString password, String
return apiId + ":" + apiKey;
}

protected Tuple<List<String>, String> getNodeInfo(String user, SecureString password) throws Exception {
final URL httpInfoUrl = getHttpInfoUrl();
protected Tuple<List<String>, 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();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> scope;
private final Function<Environment, CommandLineHttpClient> clientFunction;
private final CheckedFunction<Environment, ExternalEnrollmentTokenGenerator, Exception> createEnrollmentTokenFunction;
static final List<String> ALLOWED_SCOPES = List.of("node", "kibana");

CreateEnrollmentTokenTool() {

this(
environment -> new CommandLineHttpClient(environment),
environment -> KeyStoreWrapper.load(environment.configFile()),
Expand All @@ -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();
Expand Down Expand Up @@ -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 + "]");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
package org.elasticsearch.xpack.security.tool;

import joptsimple.OptionSet;
import joptsimple.OptionSpec;
import joptsimple.OptionSpecBuilder;

import org.elasticsearch.Version;
Expand Down Expand Up @@ -57,6 +58,7 @@ public abstract class BaseRunAsSuperuserCommand extends KeyStoreAwareCommand {
private static final int PASSWORD_LENGTH = 14;

private final OptionSpecBuilder force;
protected final OptionSpec<String> urlOption;
private final Function<Environment, CommandLineHttpClient> clientFunction;
private final CheckedFunction<Environment, KeyStoreWrapper, Exception> keyStoreFunction;

Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -195,14 +198,16 @@ private void ensureFileRealmEnabled(Settings settings) throws Exception {
*/
private void checkClusterHealthWithRetries(
Environment env,
OptionSet options,
Terminal terminal,
String username,
SecureString password,
int retries,
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);
Expand All @@ -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,
Expand Down
Loading