This is an automated email from the ASF dual-hosted git repository.
janhoy pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/solr.git
The following commit(s) were added to refs/heads/main by this push:
new f67f4713401 SOLR-17697: Refactor AuthTool: extract AuthParams and
parser-independent command methods (#4688)
f67f4713401 is described below
commit f67f471340129e9ae4920e03998ecb383ff13b79
Author: Jan Høydahl <[email protected]>
AuthorDate: Mon Aug 3 11:24:53 2026 +0200
SOLR-17697: Refactor AuthTool: extract AuthParams and parser-independent
command methods (#4688)
---
.../src/java/org/apache/solr/cli/AuthTool.java | 406 +++++++++++----------
1 file changed, 211 insertions(+), 195 deletions(-)
diff --git a/solr/core/src/java/org/apache/solr/cli/AuthTool.java
b/solr/core/src/java/org/apache/solr/cli/AuthTool.java
index df0133cfead..d4d7282b32b 100644
--- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java
@@ -32,11 +32,13 @@ import java.util.LinkedHashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
+import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.lucene.util.Constants;
+import org.apache.solr.client.solrj.impl.SolrZkClientTimeout;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.util.EnvUtils;
import org.apache.solr.core.SolrCore;
@@ -103,6 +105,22 @@ public class AuthTool extends ToolBase {
"This is where any authentication related configuration files,
if any, would be placed. Defaults to $SOLR_HOME.")
.get();
+ /**
+ * Parameters for the auth sub-commands, independent of the command line
parser.
+ *
+ * @param zkHost resolved ZooKeeper connection string, or null if {@code
updateIncludeFileOnly} is
+ * set or the connection string could not be resolved
+ */
+ record AuthParams(
+ boolean prompt,
+ String credentials,
+ String blockUnknown,
+ boolean updateIncludeFileOnly,
+ String solrIncludeFile,
+ String authConfDir,
+ boolean zkHostSpecified,
+ String zkHost) {}
+
public AuthTool(ToolRuntime runtime) {
super(runtime);
}
@@ -147,201 +165,178 @@ public class AuthTool extends ToolBase {
.addOptionGroup(getConnectionOptions());
}
- private void ensureArgumentIsValidBooleanIfPresent(CommandLine cli, Option
option) {
- if (cli.hasOption(option)) {
- final String value = cli.getOptionValue(option);
- if (!"true".equalsIgnoreCase(value) && !"false".equalsIgnoreCase(value))
{
- echo(
- "Argument ["
- + option.getLongOpt()
- + "] must be either true or false, but was ["
- + value
- + "]");
- runtime.exit(1);
- }
+ private void ensureArgumentIsValidBooleanIfPresent(String optionName, String
value) {
+ if (value != null && !"true".equalsIgnoreCase(value) &&
!"false".equalsIgnoreCase(value)) {
+ echo("Argument [" + optionName + "] must be either true or false, but
was [" + value + "]");
+ runtime.exit(1);
}
}
- private void handleBasicAuth(CommandLine cli) throws Exception {
- String cmd = cli.getArgs()[0];
- boolean prompt = Boolean.parseBoolean(cli.getOptionValue(PROMPT_OPTION,
"false"));
- boolean updateIncludeFileOnly =
- Boolean.parseBoolean(cli.getOptionValue(UPDATE_INCLUDE_FILE_OPTION,
"false"));
+ private void handleCommand(String cmd, AuthParams params) throws Exception {
switch (cmd) {
- case "enable":
- {
- if (!prompt && !cli.hasOption(CommonCLIOptions.CREDENTIALS_OPTION)) {
- CLIO.out("Option --credentials or --prompt is required with
enable.");
- runtime.exit(1);
- } else if (!prompt
- && (cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION) ==
null
- ||
!cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION).contains(":"))) {
- CLIO.out("Option --credentials is not in correct format.");
- runtime.exit(1);
- }
-
- String zkHost = null;
-
- if (!updateIncludeFileOnly) {
- try {
- zkHost = CLIUtils.getZkHost(cli);
- } catch (Exception ex) {
- if (cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION)) {
- CLIO.out(
- "Couldn't get ZooKeeper host. Please make sure that
ZooKeeper is running and the correct zk-host has been passed in.");
- } else {
- CLIO.out(
- "Couldn't get ZooKeeper host. Please make sure Solr is
running in cloud mode, or a zk-host has been passed in.");
- }
- runtime.exit(1);
- }
- if (zkHost == null) {
- if (cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION)) {
- CLIO.out(
- "Couldn't get ZooKeeper host. Please make sure that
ZooKeeper is running and the correct zk-host has been passed in.");
- } else {
- CLIO.out(
- "Couldn't get ZooKeeper host. Please make sure Solr is
running in cloud mode, or a zk-host has been passed in.");
- }
- runtime.exit(1);
- }
-
- // check if security is already enabled or not
- try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli,
zkHost)) {
- checkSecurityJsonExists(zkClient);
- }
- }
-
- String username, password;
- if (cli.hasOption(CommonCLIOptions.CREDENTIALS_OPTION)) {
- String credentials =
cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION);
- username = credentials.split(":")[0];
- password = credentials.split(":")[1];
- } else {
- Console console = System.console();
- // keep prompting until they've entered a non-empty username &
password
- do {
- username = console.readLine("Enter username: ");
- } while (username == null || username.trim().isEmpty());
- username = username.trim();
-
- do {
- password = new String(console.readPassword("Enter password: "));
- } while (password.isEmpty());
- }
-
- if (username.equals(password)
- && !EnvUtils.getPropertyAsBool(
- Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP,
false)) {
- CLIO.err(
- "Error: username and password must not be identical."
- + " This credential would never authenticate.");
- runtime.exit(1);
- }
-
- String resourceName = "security.json";
- final URL resource =
SolrCore.class.getClassLoader().getResource(resourceName);
- if (null == resource) {
- throw new IllegalArgumentException("invalid resource name: " +
resourceName);
- }
-
- ObjectMapper mapper = new ObjectMapper();
- JsonNode securityJson1 = mapper.readTree(resource.openStream());
- // Only override blockUnknown if explicitly passed; otherwise let
the template decide
- if (cli.hasOption(BLOCK_UNKNOWN_OPTION)) {
- boolean blockUnknown =
Boolean.parseBoolean(cli.getOptionValue(BLOCK_UNKNOWN_OPTION));
- ((ObjectNode)
securityJson1.get("authentication")).put("blockUnknown", blockUnknown);
- }
- JsonNode credentialsNode =
securityJson1.get("authentication").get("credentials");
- ((ObjectNode) credentialsNode)
- .put(username,
Sha256AuthenticationProvider.getSaltedHashedValue(password));
- JsonNode userRoleNode =
securityJson1.get("authorization").get("user-role");
- String[] predefinedRoles = {"superadmin", "admin", "search",
"index"};
- ArrayNode rolesNode = mapper.createArrayNode();
- for (String role : predefinedRoles) {
- rolesNode.add(role);
- }
- ((ObjectNode) userRoleNode).set(username, rolesNode);
- String securityJson = securityJson1.toPrettyString();
-
- if (!updateIncludeFileOnly) {
- echoIfVerbose("Uploading following security.json: " +
securityJson);
- try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli,
zkHost)) {
- zkClient.makePath(
- "/security.json",
securityJson.getBytes(StandardCharsets.UTF_8), false);
- }
- }
-
- String solrIncludeFilename =
cli.getOptionValue(SOLR_INCLUDE_FILE_OPTION);
- Path includeFile = Path.of(solrIncludeFilename);
- if (Files.notExists(includeFile) || !Files.isWritable(includeFile)) {
- CLIO.out(
- "Solr include file " + solrIncludeFilename + " doesn't exist
or is not writeable.");
- printAuthEnablingInstructions(username, password);
- runtime.exit(0);
- }
- String authConfDir = cli.getOptionValue(AUTH_CONF_DIR_OPTION);
- Path basicAuthConfFile = Path.of(authConfDir, "basicAuth.conf");
-
- if (!Files.isWritable(basicAuthConfFile.getParent())) {
- CLIO.out("Cannot write to file: " +
basicAuthConfFile.toAbsolutePath());
- printAuthEnablingInstructions(username, password);
- runtime.exit(0);
- }
-
- Files.writeString(
- basicAuthConfFile,
- "httpBasicAuthUser=" + username + "\nhttpBasicAuthPassword=" +
password,
- StandardCharsets.UTF_8);
-
- // update the solr.in.sh file to contain the necessary
authentication lines
- updateIncludeFileEnableAuth(includeFile, basicAuthConfFile);
- final String successMessage =
- String.format(
- Locale.ROOT,
- "Successfully enabled basic auth with username [%s] assigned
to all roles (superadmin, admin, index, search).",
- username);
- echo(successMessage);
- if (!updateIncludeFileOnly) {
- Map<String, String> templateUsers = new LinkedHashMap<>();
- templateUsers.put("admin", "admin, index, search");
- templateUsers.put("index", "index, search");
- templateUsers.put("search", "search");
- templateUsers.remove(username);
- CLIO.out(
- "\nIMPORTANT: The following template users have been created
with NO password set"
- + " and cannot log in until passwords are assigned:");
- templateUsers.forEach((u, roles) -> CLIO.out(" - " + u + "
(roles: " + roles + ")"));
- CLIO.out(
- "Set their passwords using the Admin UI Security page or the
authentication API.");
- }
- return;
- }
- case "disable":
- {
- clearSecurityJson(cli, updateIncludeFileOnly);
-
- String solrIncludeFilename =
cli.getOptionValue(SOLR_INCLUDE_FILE_OPTION);
- Path includeFile = Path.of(solrIncludeFilename);
- if (Files.notExists(includeFile) || !Files.isWritable(includeFile)) {
- CLIO.out(
- "Solr include file " + solrIncludeFilename + " doesn't exist
or is not writeable.");
- CLIO.out(
- "Security has been disabled. Please remove any SOLR_AUTH_TYPE
or SOLR_AUTHENTICATION_OPTS configuration from solr.in.sh/solr.in.cmd.\n");
- runtime.exit(0);
- }
-
- // update the solr.in.sh file to comment out the necessary
authentication lines
- updateIncludeFileDisableAuth(includeFile);
- return;
- }
- default:
+ case "enable" -> enableBasicAuth(params);
+ case "disable" -> disableBasicAuth(params);
+ default -> {
CLIO.out("Valid auth commands are: enable, disable.");
runtime.exit(1);
+ }
+ }
+ }
+
+ private void enableBasicAuth(AuthParams params) throws Exception {
+ if (!params.prompt() && params.credentials() == null) {
+ CLIO.out("Option --credentials or --prompt is required with enable.");
+ runtime.exit(1);
+ } else if (!params.prompt() && !params.credentials().contains(":")) {
+ CLIO.out("Option --credentials is not in correct format.");
+ runtime.exit(1);
}
- CLIO.out("Options not understood.");
- runtime.exit(1);
+ String zkHost = params.zkHost();
+
+ if (!params.updateIncludeFileOnly()) {
+ if (zkHost == null) {
+ printZkHostError(params.zkHostSpecified());
+ runtime.exit(1);
+ }
+
+ // check if security is already enabled or not
+ try (SolrZkClient zkClient = zkClient(zkHost)) {
+ checkSecurityJsonExists(zkClient);
+ }
+ }
+
+ String username, password;
+ if (params.credentials() != null) {
+ username = params.credentials().split(":")[0];
+ password = params.credentials().split(":")[1];
+ } else {
+ Console console = System.console();
+ // keep prompting until they've entered a non-empty username & password
+ do {
+ username = console.readLine("Enter username: ");
+ } while (username == null || username.trim().isEmpty());
+ username = username.trim();
+
+ do {
+ password = new String(console.readPassword("Enter password: "));
+ } while (password.isEmpty());
+ }
+
+ if (username.equals(password)
+ && !EnvUtils.getPropertyAsBool(
+ Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP, false)) {
+ CLIO.err(
+ "Error: username and password must not be identical."
+ + " This credential would never authenticate.");
+ runtime.exit(1);
+ }
+
+ String resourceName = "security.json";
+ final URL resource =
SolrCore.class.getClassLoader().getResource(resourceName);
+ if (null == resource) {
+ throw new IllegalArgumentException("invalid resource name: " +
resourceName);
+ }
+
+ ObjectMapper mapper = new ObjectMapper();
+ JsonNode securityJson1 = mapper.readTree(resource.openStream());
+ // Only override blockUnknown if explicitly passed; otherwise let the
template decide
+ if (params.blockUnknown() != null) {
+ boolean blockUnknown = Boolean.parseBoolean(params.blockUnknown());
+ ((ObjectNode) securityJson1.get("authentication")).put("blockUnknown",
blockUnknown);
+ }
+ JsonNode credentialsNode =
securityJson1.get("authentication").get("credentials");
+ ((ObjectNode) credentialsNode)
+ .put(username,
Sha256AuthenticationProvider.getSaltedHashedValue(password));
+ JsonNode userRoleNode =
securityJson1.get("authorization").get("user-role");
+ String[] predefinedRoles = {"superadmin", "admin", "search", "index"};
+ ArrayNode rolesNode = mapper.createArrayNode();
+ for (String role : predefinedRoles) {
+ rolesNode.add(role);
+ }
+ ((ObjectNode) userRoleNode).set(username, rolesNode);
+ String securityJson = securityJson1.toPrettyString();
+
+ if (!params.updateIncludeFileOnly()) {
+ echoIfVerbose("Uploading following security.json: " + securityJson);
+ try (SolrZkClient zkClient = zkClient(zkHost)) {
+ zkClient.makePath("/security.json",
securityJson.getBytes(StandardCharsets.UTF_8), false);
+ }
+ }
+
+ Path includeFile = Path.of(params.solrIncludeFile());
+ if (Files.notExists(includeFile) || !Files.isWritable(includeFile)) {
+ CLIO.out(
+ "Solr include file " + params.solrIncludeFile() + " doesn't exist or
is not writeable.");
+ printAuthEnablingInstructions(username, password);
+ runtime.exit(0);
+ }
+ Path basicAuthConfFile = Path.of(params.authConfDir(), "basicAuth.conf");
+
+ if (!Files.isWritable(basicAuthConfFile.getParent())) {
+ CLIO.out("Cannot write to file: " + basicAuthConfFile.toAbsolutePath());
+ printAuthEnablingInstructions(username, password);
+ runtime.exit(0);
+ }
+
+ Files.writeString(
+ basicAuthConfFile,
+ "httpBasicAuthUser=" + username + "\nhttpBasicAuthPassword=" +
password,
+ StandardCharsets.UTF_8);
+
+ // update the solr.in.sh file to contain the necessary authentication lines
+ updateIncludeFileEnableAuth(includeFile, basicAuthConfFile);
+ final String successMessage =
+ String.format(
+ Locale.ROOT,
+ "Successfully enabled basic auth with username [%s] assigned to
all roles (superadmin, admin, index, search).",
+ username);
+ echo(successMessage);
+ if (!params.updateIncludeFileOnly()) {
+ Map<String, String> templateUsers = new LinkedHashMap<>();
+ templateUsers.put("admin", "admin, index, search");
+ templateUsers.put("index", "index, search");
+ templateUsers.put("search", "search");
+ templateUsers.remove(username);
+ CLIO.out(
+ "\nIMPORTANT: The following template users have been created with NO
password set"
+ + " and cannot log in until passwords are assigned:");
+ templateUsers.forEach((u, roles) -> CLIO.out(" - " + u + " (roles: " +
roles + ")"));
+ CLIO.out("Set their passwords using the Admin UI Security page or the
authentication API.");
+ }
+ }
+
+ private void disableBasicAuth(AuthParams params) throws Exception {
+ clearSecurityJson(params);
+
+ Path includeFile = Path.of(params.solrIncludeFile());
+ if (Files.notExists(includeFile) || !Files.isWritable(includeFile)) {
+ CLIO.out(
+ "Solr include file " + params.solrIncludeFile() + " doesn't exist or
is not writeable.");
+ CLIO.out(
+ "Security has been disabled. Please remove any SOLR_AUTH_TYPE or
SOLR_AUTHENTICATION_OPTS configuration from solr.in.sh/solr.in.cmd.\n");
+ runtime.exit(0);
+ }
+
+ // update the solr.in.sh file to comment out the necessary authentication
lines
+ updateIncludeFileDisableAuth(includeFile);
+ }
+
+ private void printZkHostError(boolean zkHostSpecified) {
+ if (zkHostSpecified) {
+ CLIO.out(
+ "Couldn't get ZooKeeper host. Please make sure that ZooKeeper is
running and the correct zk-host has been passed in.");
+ } else {
+ CLIO.out(
+ "Couldn't get ZooKeeper host. Please make sure Solr is running in
cloud mode, or a zk-host has been passed in.");
+ }
+ }
+
+ private static SolrZkClient zkClient(String zkHost) {
+ return new SolrZkClient.Builder()
+ .withUrl(zkHost)
+ .withTimeout(SolrZkClientTimeout.DEFAULT_ZK_CLIENT_TIMEOUT,
TimeUnit.MILLISECONDS)
+ .build();
}
private void checkSecurityJsonExists(SolrZkClient zkClient)
@@ -357,10 +352,9 @@ public class AuthTool extends ToolBase {
}
}
- private void clearSecurityJson(CommandLine cli, boolean
updateIncludeFileOnly) throws Exception {
- String zkHost;
- if (!updateIncludeFileOnly) {
- zkHost = CLIUtils.getZkHost(cli);
+ private void clearSecurityJson(AuthParams params) throws Exception {
+ if (!params.updateIncludeFileOnly()) {
+ String zkHost = params.zkHost();
if (zkHost == null) {
runtime.print("ZK Host not found. Solr should be running in cloud
mode.");
runtime.exit(1);
@@ -368,7 +362,7 @@ public class AuthTool extends ToolBase {
echoIfVerbose("Uploading following security.json: {}");
- try (SolrZkClient zkClient = CLIUtils.getSolrZkClient(cli, zkHost)) {
+ try (SolrZkClient zkClient = zkClient(zkHost)) {
zkClient.makePath("/security.json",
"{}".getBytes(StandardCharsets.UTF_8), false);
}
}
@@ -470,13 +464,35 @@ public class AuthTool extends ToolBase {
@Override
public void runImpl(CommandLine cli) throws Exception {
- ensureArgumentIsValidBooleanIfPresent(cli, BLOCK_UNKNOWN_OPTION);
- ensureArgumentIsValidBooleanIfPresent(cli, UPDATE_INCLUDE_FILE_OPTION);
+ ensureArgumentIsValidBooleanIfPresent(
+ BLOCK_UNKNOWN_OPTION.getLongOpt(),
cli.getOptionValue(BLOCK_UNKNOWN_OPTION));
+ ensureArgumentIsValidBooleanIfPresent(
+ UPDATE_INCLUDE_FILE_OPTION.getLongOpt(),
cli.getOptionValue(UPDATE_INCLUDE_FILE_OPTION));
String type = cli.getOptionValue(TYPE_OPTION, "basicAuth");
// switch structure is here to support future auth options like oAuth
if (type.equals("basicAuth")) {
- handleBasicAuth(cli);
+ boolean updateIncludeFileOnly =
+ Boolean.parseBoolean(cli.getOptionValue(UPDATE_INCLUDE_FILE_OPTION,
"false"));
+ String zkHost = null;
+ if (!updateIncludeFileOnly) {
+ try {
+ zkHost = CLIUtils.getZkHost(cli);
+ } catch (Exception e) {
+ echoIfVerbose("Could not resolve ZooKeeper host: " + e.getMessage());
+ }
+ }
+ AuthParams params =
+ new AuthParams(
+ Boolean.parseBoolean(cli.getOptionValue(PROMPT_OPTION, "false")),
+ cli.getOptionValue(CommonCLIOptions.CREDENTIALS_OPTION),
+ cli.getOptionValue(BLOCK_UNKNOWN_OPTION),
+ updateIncludeFileOnly,
+ cli.getOptionValue(SOLR_INCLUDE_FILE_OPTION),
+ cli.getOptionValue(AUTH_CONF_DIR_OPTION),
+ cli.hasOption(CommonCLIOptions.ZK_HOST_OPTION),
+ zkHost);
+ handleCommand(cli.getArgs()[0], params);
} else {
throw new IllegalStateException("Only type=basicAuth supported at the
moment.");
}