From fd4343aeb0f1ec827d76e0e62128e969a9618fa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 10 Jun 2026 08:43:10 +0200 Subject: [PATCH 01/11] SOLR-18233 Strengthen Basic Authentication password policy and harden template users created by bin/solr auth enable (#4477) --- ...n-Basic-Authentication-password-policy.yml | 7 +++++ .../java/org/apache/solr/cli/AuthTool.java | 24 +++++++++++++-- .../Sha256AuthenticationProvider.java | 5 ++++ solr/core/src/resources/security.json | 10 +++---- .../TestSha256AuthenticationProvider.java | 27 +++++++++++++++++ .../pages/basic-authentication-plugin.adoc | 8 +++-- .../pages/solr-control-script-reference.adoc | 30 +++++++++++++++---- .../web/js/angular/controllers/security.js | 6 ++++ 8 files changed, 100 insertions(+), 17 deletions(-) create mode 100644 changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml diff --git a/changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml b/changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml new file mode 100644 index 000000000000..3c06a95dfe6a --- /dev/null +++ b/changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml @@ -0,0 +1,7 @@ +title: Strengthen Basic Authentication password policy and harden template users created by bin/solr auth enable +type: fixed +authors: + - name: Jan Høydahl +links: + - name: SOLR-18233 + url: https://issues.apache.org/jira/browse/SOLR-18233 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 5c19ac300d7f..3487787684d1 100644 --- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java @@ -227,8 +227,12 @@ private void handleBasicAuth(CommandLine cli) throws Exception { } while (password.isEmpty()); } - boolean blockUnknown = - Boolean.parseBoolean(cli.getOptionValue(BLOCK_UNKNOWN_OPTION, "true")); + if (username.equals(password)) { + 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); @@ -238,7 +242,11 @@ private void handleBasicAuth(CommandLine cli) throws Exception { ObjectMapper mapper = new ObjectMapper(); JsonNode securityJson1 = mapper.readTree(resource.openStream()); - ((ObjectNode) securityJson1).put("blockUnknown", blockUnknown); + // 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)); @@ -286,6 +294,16 @@ private void handleBasicAuth(CommandLine cli) throws Exception { String.format( Locale.ROOT, "Successfully enabled basic auth with username [%s].", username); echo(successMessage); + if (!updateIncludeFileOnly) { + CLIO.out( + "\nIMPORTANT: The following template users have been created with NO password set" + + " and cannot log in until passwords are assigned:"); + CLIO.out(" - admin (roles: admin, index, search)"); + CLIO.out(" - index (roles: index, search)"); + CLIO.out(" - search (roles: search)"); + CLIO.out( + "Set their passwords using the Admin UI Security page or the authentication API."); + } return; } case "disable": diff --git a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java index 31c38537f695..6e1646a5666f 100644 --- a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java +++ b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java @@ -93,6 +93,7 @@ public void init(Map pluginConfig) { @Override public boolean authenticate(String username, String password) { + if (username != null && username.equals(password)) return false; String cred = credentials.get(username); if (cred == null || cred.isEmpty()) return false; cred = cred.trim(); @@ -165,6 +166,10 @@ public Map edit(Map latestConf, List config = new HashMap<>(); + Map credentials = new HashMap<>(); + credentials.put(user, hashedValue); + config.put("credentials", credentials); + + Sha256AuthenticationProvider provider = new Sha256AuthenticationProvider(); + provider.init(config); + assertFalse( + "authenticate() must reject username==password even when hash matches", + provider.authenticate(user, user)); + } + + public void testSetUserRejectsUsernameEqualPassword() { + Sha256AuthenticationProvider provider = new Sha256AuthenticationProvider(); + provider.init(createConfigMap("ignore", "me")); + Map latestConf = createConfigMap("ignore", "me"); + String user = "bob"; + CommandOperation cmd = new CommandOperation("set-user", Map.of(user, user)); + provider.edit(latestConf, List.of(cmd)); + assertTrue("set-user should report an error when username==password", cmd.hasError()); + } + private Map createConfigMap(String user, String pw) { Map config = new HashMap<>(); Map credentials = new HashMap<>(); diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/basic-authentication-plugin.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/basic-authentication-plugin.adoc index 19536088939e..9f1a062feb6d 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/basic-authentication-plugin.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/basic-authentication-plugin.adoc @@ -24,10 +24,11 @@ To control user permissions, you may need to configure an authorization plugin a == Enable Basic Authentication -To use Basic authentication, you must first create a `security.json` file. -This file and where to put it is described in detail in the section xref:authentication-and-authorization-plugins.adoc#configuring-security-json[Configuring security.json]. +When running in cloud mode, Basic authentication can be enabled from the command line using the `bin/solr auth enable` command, which applies a best-practice security template with pre-configured roles and permissions. +See xref:solr-control-script-reference.adoc#enabling-basic-authentication[Enabling Basic Authentication] in the Solr Control Script Reference for details. -If running in cloud mode, you can use the `bin/solr auth` command-line utility to enable security for a new installation, see: `bin/solr auth --help` for more details. +Alternatively, you can create the `security.json` file manually. +This file and where to put it is described in detail in the section xref:authentication-and-authorization-plugins.adoc#configuring-security-json[Configuring security.json]. For Basic authentication, `security.json` must have an `authentication` block which defines the class being used for authentication. Usernames and passwords could be added when the file is created, or can be added later with the Authentication API, described below. @@ -185,6 +186,7 @@ If users need to be restricted to a specific collection, that can be done with t === Add a User or Edit a Password The `set-user` command allows you to add users and change their passwords. +Passwords must not be identical to the username. For example, the following defines two users and their passwords: [tabs#set-user] diff --git a/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc b/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc index f507216396be..aae46c7c6cc9 100644 --- a/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc +++ b/solr/solr-ref-guide/modules/deployment-guide/pages/solr-control-script-reference.adoc @@ -916,19 +916,23 @@ The `bin/solr` script allows enabling or disabling Authentication, allowing you Currently this command is only available when using SolrCloud mode and must be run on the machine hosting Solr. -For Basic Authentication the script provides https://github.com/apache/solr/blob/main/solr/core/src/resources/security.json[user roles and permission mappings], and maps the created user to the `superadmin` role. +For Basic Authentication the script provides https://github.com/apache/solr/blob/main/solr/core/src/resources/security.json[user roles and permission mappings], and maps the created user to all roles (`superadmin`, `admin`, `index`, `search`). === Enabling Basic Authentication The command `bin/solr auth enable` configures Solr to use Basic Authentication when accessing the User Interface, using `bin/solr` and any API requests. +NOTE: This command currently requires SolrCloud mode — it uploads the generated `security.json` to ZooKeeper so that all nodes pick it up automatically. +For user-managed (standalone) clusters, you must create the `security.json` file manually and place it in each node's Solr home directory. +See xref:basic-authentication-plugin.adoc[] for details. + TIP: For more information about Solr's authentication plugins, see the section xref:securing-solr.adoc[]. For more information on Basic Authentication support specifically, see the section xref:basic-authentication-plugin.adoc[]. The `bin/solr auth enable` command makes several changes to enable Basic Authentication: -* Take the base https://github.com/apache/solr/blob/main/solr/core/resources/security.json[security.json] file, evolves it using `auth` command parameters, and uploads the new file to ZooKeeper. +* Takes the base https://github.com/apache/solr/blob/main/solr/core/src/resources/security.json[security.json] template with best-practice roles and permissions, applies `auth` command parameters, and uploads the result to ZooKeeper. + * Adds two lines to `bin/solr.in.sh` or `bin\solr.in.cmd` to set the authentication type, and the path to `basicAuth.conf`: + @@ -940,6 +944,19 @@ SOLR_AUTHENTICATION_OPTS="-Dsolr.httpclient.config=/path/to/solr-{solr-full-vers ---- * Creates the file `server/solr/basicAuth.conf` to store the credential information that is used with `bin/solr` commands. +In addition to the operator-created user, the command also creates three template users with predefined role assignments. +These users have no password set and cannot log in until passwords are explicitly assigned: + +[cols="1,2",options="header"] +|=== +|Username |Roles +|`admin` |admin, index, search +|`index` |index, search +|`search` |search +|=== + +After enabling Basic Authentication, set passwords for these template users using the Admin UI Security page or the xref:basic-authentication-plugin.adoc#add-a-user-or-edit-a-password[authentication API]. + Here are some example usages: [source,plain] @@ -981,11 +998,14 @@ Either `--credentials` or `--prompt` *must* be specified. + [%autowidth,frame=none] |=== -|Optional |Default: `true` +|Optional |Default: use value from `security.json` template (`false`) |=== + -When `true`, this blocks out access to unauthenticated users from accessing Solr. -When `false`, unauthenticated users will still be able to access Solr, but only for operations not explicitly requiring a user role in the Authorization plugin configuration. +Controls whether unauthenticated requests are blocked. +The default `security.json` template sets `blockUnknown` to `false` because it includes a `RuleBasedAuthorizationPlugin` with fine-grained permissions — unauthenticated users can only access endpoints explicitly granted to the `null` role (by default *health* and *metrics-read*, left open so that load balancers and monitoring tools can operate without credentials). +All other operations require an authenticated user with the appropriate role. ++ +If you want to require authentication for _all_ requests (including health checks and metrics), pass `--block-unknown true` explicitly. `--solr-include-file `:: + diff --git a/solr/webapp/web/js/angular/controllers/security.js b/solr/webapp/web/js/angular/controllers/security.js index fd65a289988f..1ffdf85460a0 100644 --- a/solr/webapp/web/js/angular/controllers/security.js +++ b/solr/webapp/web/js/angular/controllers/security.js @@ -412,6 +412,12 @@ solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cooki return false; } + var username = $scope.upsertUser.username ? $scope.upsertUser.username.trim() : ""; + if (password === username) { + $scope.validationError = "Password must not be the same as the username"; + return false; + } + return true; }; From 067cd5e0c518d2354fcfe5b003f68f2f8d3bcaa7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 10 Jun 2026 09:38:38 +0200 Subject: [PATCH 02/11] SOLR-18233 Fix test failure in AuthToolTest --- solr/core/src/test/org/apache/solr/cli/AuthToolTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java b/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java index 28d62999ced9..bcdb8d805abe 100644 --- a/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java +++ b/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java @@ -67,7 +67,7 @@ public void testEnableAuth() throws Exception { "--solr-include-file", solrIncludeFile.toAbsolutePath().toString(), "--credentials", - "solr:solr", + "solr:solrRocks", "--block-unknown", "true" }; From 5e112736e1fea6460b09ed92fc968f5867789f61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 10 Jun 2026 12:00:05 +0200 Subject: [PATCH 03/11] Fix test CloudAuthStreamTest to use password != username --- .../solrj/io/stream/CloudAuthStreamTest.java | 66 +++++++++++-------- 1 file changed, 38 insertions(+), 28 deletions(-) diff --git a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/CloudAuthStreamTest.java b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/CloudAuthStreamTest.java index 225ca994ec34..7b70f29bcf77 100644 --- a/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/CloudAuthStreamTest.java +++ b/solr/solrj-streaming/src/test/org/apache/solr/client/solrj/io/stream/CloudAuthStreamTest.java @@ -65,16 +65,24 @@ public class CloudAuthStreamTest extends SolrCloudTestCase { private static String solrUrl = null; + /** + * Helper: every user's password is derived from their username via this method. The password must + * differ from the username (Solr's Basic Auth policy rejects passwords equal to the username). + */ + private static String passwordFor(final String user) { + return user + "_password"; + } + /** * Helper that returns the original {@link SolrRequest} with its original type so it can - * be chained. This method knows that for the purpose of this test, every username is its own - * password + * be chained. This method knows that for the purpose of this test, every user's password is + * derived from their username via {@link #passwordFor}. * * @see SolrRequest#setBasicAuthCredentials */ private static > T setBasicAuthCredentials(T req, String user) { assertNotNull(user); - req.setBasicAuthCredentials(user, user); + req.setBasicAuthCredentials(user, passwordFor(user)); return req; } @@ -82,9 +90,11 @@ private static > T setBasicAuthCredentials(T req, Strin public static void setupCluster() throws Exception { final List users = Arrays.asList(READ_ONLY_USER, WRITE_X_USER, WRITE_Y_USER, ADMIN_USER); - // For simplicity: every user uses a password the same as their name... + // For simplicity: every user's password is derived from their name via passwordFor()... final Map credentials = - users.stream().collect(Collectors.toMap(Function.identity(), s -> getSaltedHashedValue(s))); + users.stream() + .collect( + Collectors.toMap(Function.identity(), s -> getSaltedHashedValue(passwordFor(s)))); // For simplicity: Every user is their own role... final Map roles = @@ -123,7 +133,7 @@ public static void setupCluster() throws Exception { for (String collection : Arrays.asList(COLLECTION_X, COLLECTION_Y)) { CollectionAdminRequest.createCollection(collection, "_default", 2, 2) - .setBasicAuthCredentials(ADMIN_USER, ADMIN_USER) + .setBasicAuthCredentials(ADMIN_USER, passwordFor(ADMIN_USER)) .process(cluster.getSolrClient()); } @@ -246,7 +256,7 @@ public void testEchoStream() throws Exception { params( "qt", "/stream", "expr", "echo(hello world)")); - solrStream.setCredentials(READ_ONLY_USER, READ_ONLY_USER); + solrStream.setCredentials(READ_ONLY_USER, passwordFor(READ_ONLY_USER)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); assertEquals("hello world", tuples.get(0).get("echo")); @@ -295,7 +305,7 @@ public void testSimpleUpdateStream() throws Exception { "/stream", "expr", "update(" + COLLECTION_X + ",batchSize=1," + "tuple(id=42,a_i=1,b_i=5))")); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); assertEquals(1L, tuples.get(0).get("totalIndexed")); @@ -338,7 +348,7 @@ public void testSimpleUpdateStreamInsufficientCredentials() throws Exception { "expr", "update(" + COLLECTION_X + ",batchSize=1," + "tuple(id=42,a_i=1,b_i=5))")); - solrStream.setCredentials(user, user); + solrStream.setCredentials(user, passwordFor(user)); // NOTE: Can't make any assertions about Exception: SOLR-14226 expectThrows( @@ -361,7 +371,7 @@ public void testIndirectUpdateStream() throws Exception { "/stream", "expr", "update(" + COLLECTION_X + ",batchSize=1," + "tuple(id=42,a_i=1,b_i=5))")); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); assertEquals(1L, tuples.get(0).get("totalIndexed")); @@ -390,7 +400,7 @@ public void testIndirectUpdateStream() throws Exception { new SolrStream( solrUrl + "/" + COLLECTION_Y, // NOTE: Y route params("qt", "/stream", "expr", expr)); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); assertEquals(10L, tuples.get(0).get("batchIndexed")); @@ -410,7 +420,7 @@ public void testIndirectUpdateStream() throws Exception { new SolrStream( solrUrl + "/" + COLLECTION_X, // NOTE: X route params("qt", "/stream", "expr", expr)); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(3, tuples.size()); @@ -439,7 +449,7 @@ public void testIndirectUpdateStreamInsufficientCredentials() throws Exception { "/stream", "expr", "update(" + COLLECTION_X + ",batchSize=1," + "tuple(id=42,a_i=1,b_i=5))")); - solrStream.setCredentials(WRITE_Y_USER, WRITE_Y_USER); + solrStream.setCredentials(WRITE_Y_USER, passwordFor(WRITE_Y_USER)); // NOTE: Can't make any assertions about Exception: SOLR-14226 expectThrows( @@ -459,7 +469,7 @@ public void testExecutorUpdateStream() throws Exception { + ", batchSize=5, tuple(id=42,a_i=1,b_i=5))\"))"; final SolrStream solrStream = new SolrStream(solrUrl + "/" + COLLECTION_X, params("qt", "/stream", "expr", expr)); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(0, tuples.size()); @@ -486,7 +496,7 @@ public void testExecutorUpdateStreamInsufficientCredentials() throws Exception { new SolrStream( solrUrl + "/" + path, params("qt", "/stream", "_trace", "executor_via_" + trace, "expr", expr)); - solrStream.setCredentials(user, user); + solrStream.setCredentials(user, passwordFor(user)); // NOTE: Because of the background threads, no failures will to be returned to client... final List tuples = getTuples(solrStream); @@ -518,7 +528,7 @@ public void testDaemonUpdateStream() throws Exception { + ",tuple(id=42,a_i=1,b_i=5)))"; final SolrStream solrStream = new SolrStream(daemonUrl, params("qt", "/stream", "expr", expr)); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); // daemon starting status } @@ -533,7 +543,7 @@ public void testDaemonUpdateStream() throws Exception { params( "qt", "/stream", "action", "list")); - daemonCheck.setCredentials(WRITE_X_USER, WRITE_X_USER); + daemonCheck.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(daemonCheck); assertEquals(1, tuples.size()); // our daemon; iterations = tuples.get(0).getLong("iterations"); @@ -555,7 +565,7 @@ public void testDaemonUpdateStream() throws Exception { "qt", "/stream", "action", "kill", "id", "daemonId")); - daemonKiller.setCredentials(WRITE_X_USER, WRITE_X_USER); + daemonKiller.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(daemonKiller); assertEquals(1, tuples.size()); // daemon death status } @@ -582,7 +592,7 @@ public void testDaemonUpdateStreamInsufficientCredentials() throws Exception { final SolrStream solrStream = new SolrStream( daemonUrl, params("qt", "/stream", "_trace", "start_" + daemonId, "expr", expr)); - solrStream.setCredentials(user, user); + solrStream.setCredentials(user, passwordFor(user)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); // daemon starting status } @@ -598,7 +608,7 @@ public void testDaemonUpdateStreamInsufficientCredentials() throws Exception { "qt", "/stream", "_trace", "check_" + daemonId, "action", "list")); - daemonCheck.setCredentials(user, user); + daemonCheck.setCredentials(user, passwordFor(user)); final List tuples = getTuples(daemonCheck); assertEquals(1, tuples.size()); // our daemon; if (log.isInfoEnabled()) { @@ -631,7 +641,7 @@ public void testDaemonUpdateStreamInsufficientCredentials() throws Exception { "kill", "id", daemonId)); - daemonKiller.setCredentials(user, user); + daemonKiller.setCredentials(user, passwordFor(user)); final List tuples = getTuples(daemonKiller); assertEquals(1, tuples.size()); // daemon death status } @@ -663,7 +673,7 @@ public void testSimpleDeleteStream() throws Exception { "/stream", "expr", "delete(" + COLLECTION_X + ",batchSize=1," + "tuple(id=42))")); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); assertEquals(1L, tuples.get(0).get("totalIndexed")); @@ -697,7 +707,7 @@ public void testSimpleDeleteStreamByQuery() throws Exception { final SolrStream solrStream = new SolrStream(solrUrl + "/" + COLLECTION_X, params("qt", "/stream", "expr", expr)); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(2, tuples.size()); assertEquals(5L, tuples.get(0).get("totalIndexed")); @@ -758,7 +768,7 @@ public void testSimpleDeleteStreamInsufficientCredentials() throws Exception { "expr", "update(" + COLLECTION_X + ",batchSize=1," + "tuple(id=42))")); - solrStream.setCredentials(user, user); + solrStream.setCredentials(user, passwordFor(user)); // NOTE: Can't make any assertions about Exception: SOLR-14226 expectThrows( @@ -801,7 +811,7 @@ public void testIndirectDeleteStream() throws Exception { "/stream", "expr", "delete(" + COLLECTION_X + ",batchSize=1," + "tuple(id=42z))")); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); assertEquals(1L, tuples.get(0).get("totalIndexed")); @@ -825,7 +835,7 @@ public void testIndirectDeleteStream() throws Exception { new SolrStream( solrUrl + "/" + COLLECTION_Y, // NOTE: Y route params("qt", "/stream", "expr", expr)); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(1, tuples.size()); assertEquals(10L, tuples.get(0).get("batchIndexed")); @@ -849,7 +859,7 @@ public void testIndirectDeleteStream() throws Exception { new SolrStream( solrUrl + "/" + COLLECTION_X, // NOTE: X route params("qt", "/stream", "expr", expr)); - solrStream.setCredentials(WRITE_X_USER, WRITE_X_USER); + solrStream.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER)); final List tuples = getTuples(solrStream); assertEquals(3, tuples.size()); @@ -888,7 +898,7 @@ public void testIndirectDeleteStreamInsufficientCredentials() throws Exception { "/stream", "expr", "delete(" + COLLECTION_X + ",batchSize=1," + "tuple(id=42))")); - solrStream.setCredentials(WRITE_Y_USER, WRITE_Y_USER); + solrStream.setCredentials(WRITE_Y_USER, passwordFor(WRITE_Y_USER)); // NOTE: Can't make any assertions about Exception: SOLR-14226 expectThrows( From 0ce8f529b90212931c4b60ac106978b384fc9b6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 10 Jun 2026 12:17:57 +0200 Subject: [PATCH 04/11] Add sysprop solr.security.auth.basicauth.allowuseraspassword as an escape hatch Document this in upgrade notes --- ...n-Basic-Authentication-password-policy.yml | 2 +- .../Sha256AuthenticationProvider.java | 23 ++++++++++++-- .../TestSha256AuthenticationProvider.java | 31 +++++++++++++++++++ .../pages/major-changes-in-solr-10.adoc | 7 +++++ 4 files changed, 60 insertions(+), 3 deletions(-) diff --git a/changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml b/changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml index 3c06a95dfe6a..68d8a849616d 100644 --- a/changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml +++ b/changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml @@ -1,4 +1,4 @@ -title: Strengthen Basic Authentication password policy and harden template users created by bin/solr auth enable +title: Strengthen Basic Authentication password policy (password must differ from username) and harden template users created by bin/solr auth enable. The check can be temporarily disabled with -Dsolr.security.auth.basicauth.allowuseraspassword=true (env SOLR_SECURITY_AUTH_BASICAUTH_ALLOWUSERASPASSWORD) as an upgrade escape hatch. type: fixed authors: - name: Jan Høydahl diff --git a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java index 6e1646a5666f..9ab59fd72c4b 100644 --- a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java +++ b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java @@ -33,6 +33,7 @@ import org.apache.solr.api.AnnotatedApi; import org.apache.solr.api.Api; import org.apache.solr.common.util.CommandOperation; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.common.util.ValidatingJsonMap; import org.apache.solr.handler.admin.api.ModifyBasicAuthConfigAPI; import org.slf4j.Logger; @@ -43,6 +44,19 @@ public class Sha256AuthenticationProvider static String CANNOT_DELETE_LAST_USER_ERROR = "You cannot delete the last user. At least one user must be configured at all times."; + + /** + * System property (or {@code SOLR_SECURITY_AUTH_BASICAUTH_ALLOWUSERASPASSWORD} environment + * variable) that, when explicitly set to {@code true}, disables the check that rejects a password + * equal to its username, both at login time and when creating or editing a user via the {@code + * set-user} command (UI, API or CLI). This is an escape hatch for users upgrading to 9.11.0 or + * 10.1.0 who still have Basic Auth users with weak passwords equal to the username; it lets them + * keep logging in (and managing those accounts) while they migrate to stronger passwords. + * Defaults to {@code false}. + */ + public static final String ALLOW_USER_AS_PASSWORD_PROP = + "solr.security.auth.basicauth.allowuseraspassword"; + private Map credentials; private String realm; private Map promptHeader; @@ -93,7 +107,11 @@ public void init(Map pluginConfig) { @Override public boolean authenticate(String username, String password) { - if (username != null && username.equals(password)) return false; + if (username != null + && username.equals(password) + && !EnvUtils.getPropertyAsBool(ALLOW_USER_AS_PASSWORD_PROP, false)) { + return false; + } String cred = credentials.get(username); if (cred == null || cred.isEmpty()) return false; cred = cred.trim(); @@ -166,7 +184,8 @@ public Map edit(Map latestConf, List config = new HashMap<>(); + Map credentials = new HashMap<>(); + credentials.put(user, hashedValue); + config.put("credentials", credentials); + + Sha256AuthenticationProvider provider = new Sha256AuthenticationProvider(); + provider.init(config); + + System.setProperty(Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP, "true"); + try { + assertTrue( + "authenticate() must allow username==password when escape-hatch sysprop is true", + provider.authenticate(user, user)); + assertFalse( + "authenticate() must still reject a genuinely wrong password", + provider.authenticate(user, "WrongPassword")); + + // The escape hatch also relaxes set-user, so username==password is accepted there. + Map latestConf = createConfigMap("ignore", "me"); + CommandOperation cmd = new CommandOperation("set-user", Map.of(user, user)); + provider.edit(latestConf, List.of(cmd)); + assertFalse( + "set-user should allow username==password when escape hatch is enabled", cmd.hasError()); + } finally { + System.clearProperty(Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP); + } + } + public void testSetUserRejectsUsernameEqualPassword() { Sha256AuthenticationProvider provider = new Sha256AuthenticationProvider(); provider.init(createConfigMap("ignore", "me")); diff --git a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc index 627816636c50..4295e141c65b 100644 --- a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc +++ b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-10.adoc @@ -81,6 +81,13 @@ Solr 10.1 nodes only send and accept the `SolrAuthV2` (v2) header for inter-node Before performing a rolling upgrade to 10.1, ensure no node in the cluster has `solr.pki.sendVersion=v1` set, as those nodes would send the legacy `SolrAuth` header that 10.1 nodes will reject. The `solr.pki.sendVersion` and `solr.pki.acceptVersions` system properties are no longer recognized in 10.1 and can be removed from your configuration. +The Basic Authentication password policy has been strengthened: a password may no longer be equal to its username. +Login attempts where the password equals the username are now rejected, and creating or editing such a user (via the Admin UI, the Authentication API, or `bin/solr auth`) is no longer allowed. + +If you are upgrading and still have Basic Auth users whose password equals their username, you can temporarily re-enable the old behavior by setting the system property `-Dsolr.security.auth.basicauth.allowuseraspassword=true` (or the environment variable `SOLR_SECURITY_AUTH_BASICAUTH_ALLOWUSERASPASSWORD=true`). +When enabled, this escape hatch relaxes both the login-time check and the user creation/editing check, so existing accounts keep working and can still be managed. +It is intended as a temporary measure while you migrate the affected accounts to stronger passwords, and should be removed once that is done. + === SolrJ HttpSolrClient returns; this time as a base class for HttpJettySolrClient and HttpJdkSolrClient. From 47e6842d624548f90d94dbb66786d63ef5c72de4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 10 Jun 2026 12:27:47 +0200 Subject: [PATCH 05/11] Fix more tests --- .../solr/cloud/TestQueryingOnDownCollection.java | 5 +++-- .../AuthWithShardHandlerFactoryOverrideTest.java | 10 +++++----- .../solr/security/BasicAuthOnSingleNodeTest.java | 6 +++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/solr/core/src/test/org/apache/solr/cloud/TestQueryingOnDownCollection.java b/solr/core/src/test/org/apache/solr/cloud/TestQueryingOnDownCollection.java index 1c992140e073..e95bf3493fb4 100644 --- a/solr/core/src/test/org/apache/solr/cloud/TestQueryingOnDownCollection.java +++ b/solr/core/src/test/org/apache/solr/cloud/TestQueryingOnDownCollection.java @@ -37,7 +37,8 @@ public class TestQueryingOnDownCollection extends SolrCloudTestCase { private static final String COLLECTION_NAME = "infected"; private static final String USERNAME = "solr"; - private static final String PASSWORD = "solr"; + // Password must differ from the username (Basic Auth policy rejects username==password) + private static final String PASSWORD = "SolrRocks"; @BeforeClass public static void setupCluster() throws Exception { @@ -155,7 +156,7 @@ private void downAllReplicas() throws Exception { + " \"authentication\":{\n" + " \"blockUnknown\": true,\n" + " \"class\":\"solr.BasicAuthPlugin\",\n" - + " \"credentials\":{\"solr\":\"EEKn7ywYk5jY8vG9TyqlG2jvYuvh1Q7kCCor6Hqm320= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n" + + " \"credentials\":{\"solr\":\"JeRyxP8A3dVWhFgFbf/Eg2RXmuoU8BE5gbNQyxmGAJQ= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n" + " },\n" + " \"authorization\":{\n" + " \"class\":\"solr.RuleBasedAuthorizationPlugin\",\n" diff --git a/solr/core/src/test/org/apache/solr/security/AuthWithShardHandlerFactoryOverrideTest.java b/solr/core/src/test/org/apache/solr/security/AuthWithShardHandlerFactoryOverrideTest.java index b1dfedc3f787..1c7c2c171696 100644 --- a/solr/core/src/test/org/apache/solr/security/AuthWithShardHandlerFactoryOverrideTest.java +++ b/solr/core/src/test/org/apache/solr/security/AuthWithShardHandlerFactoryOverrideTest.java @@ -38,7 +38,7 @@ public class AuthWithShardHandlerFactoryOverrideTest extends SolrCloudAuthTestCa + " \"authentication\":{\n" + " \"blockUnknown\": true,\n" + " \"class\":\"solr.BasicAuthPlugin\",\n" - + " \"credentials\":{\"solr\":\"EEKn7ywYk5jY8vG9TyqlG2jvYuvh1Q7kCCor6Hqm320= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n" + + " \"credentials\":{\"solr\":\"JeRyxP8A3dVWhFgFbf/Eg2RXmuoU8BE5gbNQyxmGAJQ= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n" + " },\n" + " \"authorization\":{\n" + " \"class\":\"solr.RuleBasedAuthorizationPlugin\",\n" @@ -56,11 +56,11 @@ public void setupCluster() throws Exception { .withSecurityJson(SECURITY_CONF) .configure(); CollectionAdminRequest.createCollection(COLLECTION, "conf", 4, 1) - .setBasicAuthCredentials("solr", "solr") + .setBasicAuthCredentials("solr", "SolrRocks") .process(cluster.getSolrClient()); CollectionAdminRequest.createAlias(ALIAS, COLLECTION) - .setBasicAuthCredentials("solr", "solr") + .setBasicAuthCredentials("solr", "SolrRocks") .process(cluster.getSolrClient()); cluster.waitForActiveCollection(COLLECTION, 4, 4); @@ -89,7 +89,7 @@ public void collectionTest() throws Exception { for (int i = 0; i < 30; i++) { SolrResponse response = new QueryRequest(params("q", "*:*")) - .setBasicAuthCredentials("solr", "solr") + .setBasicAuthCredentials("solr", "SolrRocks") .process(client, COLLECTION); // likely to be non-null, even if an error occurred assertNotNull(response); @@ -109,7 +109,7 @@ public void aliasTest() throws Exception { for (int i = 0; i < 30; i++) { SolrResponse response = new QueryRequest(params("q", "*:*")) - .setBasicAuthCredentials("solr", "solr") + .setBasicAuthCredentials("solr", "SolrRocks") .process(client, ALIAS); // likely to be non-null, even if an error occurred assertNotNull(response); diff --git a/solr/core/src/test/org/apache/solr/security/BasicAuthOnSingleNodeTest.java b/solr/core/src/test/org/apache/solr/security/BasicAuthOnSingleNodeTest.java index f6f35c37d3bd..26ca70cdf13a 100644 --- a/solr/core/src/test/org/apache/solr/security/BasicAuthOnSingleNodeTest.java +++ b/solr/core/src/test/org/apache/solr/security/BasicAuthOnSingleNodeTest.java @@ -37,7 +37,7 @@ public void setupCluster() throws Exception { .withSecurityJson(STD_CONF) .configure(); CollectionAdminRequest.createCollection(COLLECTION, "conf", 4, 1) - .setBasicAuthCredentials("solr", "solr") + .setBasicAuthCredentials("solr", "SolrRocks") .process(cluster.getSolrClient()); cluster.waitForActiveCollection(COLLECTION, 4, 4); } @@ -60,7 +60,7 @@ public void basicTest() throws Exception { for (int i = 0; i < 30; i++) { assertNotNull( new QueryRequest(params("q", "*:*")) - .setBasicAuthCredentials("solr", "solr") + .setBasicAuthCredentials("solr", "SolrRocks") .process(client, COLLECTION)); } } @@ -106,7 +106,7 @@ public void testDeleteSecurityJsonZnode() throws Exception { + " \"authentication\":{\n" + " \"blockUnknown\": true,\n" + " \"class\":\"solr.BasicAuthPlugin\",\n" - + " \"credentials\":{\"solr\":\"EEKn7ywYk5jY8vG9TyqlG2jvYuvh1Q7kCCor6Hqm320= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n" + + " \"credentials\":{\"solr\":\"JeRyxP8A3dVWhFgFbf/Eg2RXmuoU8BE5gbNQyxmGAJQ= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n" + " },\n" + " \"authorization\":{\n" + " \"class\":\"solr.RuleBasedAuthorizationPlugin\",\n" From cfc990c674c8c912ccacdf988811cad8bbea22a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 17 Jun 2026 10:11:45 +0200 Subject: [PATCH 06/11] SOLR-18233 Let AuthTool respect the allowuseraspassword escape-hatch sysprop When the operator sets SOLR_SECURITY_AUTH_BASICAUTH_ALLOWUSERASPASSWORD=true in the shell running bin/solr, the CLI guard in AuthTool now passes through username==password credentials, matching the behaviour of the server-side Sha256AuthenticationProvider. Adds two new tests to AuthToolTest covering the rejection and escape-hatch paths; also resets ZK security.json before each test to prevent inter-test interference. --- .../java/org/apache/solr/cli/AuthTool.java | 5 +- .../org/apache/solr/cli/AuthToolTest.java | 47 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) 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 3487787684d1..4b46e99d6042 100644 --- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java @@ -36,6 +36,7 @@ import org.apache.commons.cli.Options; import org.apache.lucene.util.Constants; import org.apache.solr.common.cloud.SolrZkClient; +import org.apache.solr.common.util.EnvUtils; import org.apache.solr.core.SolrCore; import org.apache.solr.security.Sha256AuthenticationProvider; import org.apache.zookeeper.KeeperException; @@ -227,7 +228,9 @@ private void handleBasicAuth(CommandLine cli) throws Exception { } while (password.isEmpty()); } - if (username.equals(password)) { + 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."); diff --git a/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java b/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java index bcdb8d805abe..aa31392a2f4f 100644 --- a/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java +++ b/solr/core/src/test/org/apache/solr/cli/AuthToolTest.java @@ -17,10 +17,12 @@ package org.apache.solr.cli; +import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import org.apache.commons.io.file.PathUtils; import org.apache.solr.cloud.SolrCloudTestCase; +import org.apache.solr.security.Sha256AuthenticationProvider; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; @@ -42,6 +44,10 @@ public static void setupCluster() throws Exception { public void setUp() throws Exception { super.setUp(); dir = createTempDir("AuthToolTest").toAbsolutePath(); + // Reset ZK security state before each test to avoid interference between tests + if (cluster.getZkClient().exists("/security.json")) { + cluster.getZkClient().setData("/security.json", "{}".getBytes(StandardCharsets.UTF_8)); + } } @Override @@ -73,4 +79,45 @@ public void testEnableAuth() throws Exception { }; assertEquals(0, CLITestHelper.runTool(args, AuthTool.class)); } + + @Test + public void testEnableAuthRejectsUsernameEqualPassword() throws Exception { + Path solrIncludeFile = Files.createFile(dir.resolve("solrIncludeFile2.txt")); + String[] args = { + "auth", + "enable", + "-z", + cluster.getZkClient().getZkServerAddress(), + "--auth-conf-dir", + dir.toAbsolutePath().toString(), + "--solr-include-file", + solrIncludeFile.toAbsolutePath().toString(), + "--credentials", + "solr:solr" + }; + assertNotEquals(0, CLITestHelper.runTool(args, AuthTool.class)); + } + + @Test + public void testEnableAuthAllowsUsernameEqualPasswordWithEscapeHatch() throws Exception { + System.setProperty(Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP, "true"); + try { + Path solrIncludeFile = Files.createFile(dir.resolve("solrIncludeFile3.txt")); + String[] args = { + "auth", + "enable", + "-z", + cluster.getZkClient().getZkServerAddress(), + "--auth-conf-dir", + dir.toAbsolutePath().toString(), + "--solr-include-file", + solrIncludeFile.toAbsolutePath().toString(), + "--credentials", + "solr:solr" + }; + assertEquals(0, CLITestHelper.runTool(args, AuthTool.class)); + } finally { + System.clearProperty(Sha256AuthenticationProvider.ALLOW_USER_AS_PASSWORD_PROP); + } + } } From c2609ae3cd3cc7d5e0dd8e6184d7fa0350741385 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 17 Jun 2026 10:18:13 +0200 Subject: [PATCH 07/11] SOLR-18233 Print operator user's role assignment after enabling basic auth After "Successfully enabled basic auth with username [X]", now also prints that user X was assigned the highest access level (superadmin, admin, index, search), so operators see a complete picture of their security setup alongside the template user list. --- solr/core/src/java/org/apache/solr/cli/AuthTool.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) 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 4b46e99d6042..3a5da5c0e81c 100644 --- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java @@ -295,7 +295,9 @@ private void handleBasicAuth(CommandLine cli) throws Exception { updateIncludeFileEnableAuth(includeFile, basicAuthConfFile); final String successMessage = String.format( - Locale.ROOT, "Successfully enabled basic auth with username [%s].", username); + Locale.ROOT, + "Successfully enabled basic auth with username [%s] assigned to all roles (superadmin, admin, index, search).", + username); echo(successMessage); if (!updateIncludeFileOnly) { CLIO.out( From fbd3da2976243bfc94a908aa2431ff741681d48e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 17 Jun 2026 10:29:57 +0200 Subject: [PATCH 08/11] SOLR-18233 Return HTTP 400 when security API command operations fail SecurityConfHandler was adding errorMessages to the response body but returning HTTP 200 / status:0, matching neither the client's expectation nor the pattern established by SchemaHandler and SolrConfigHandler which both throw SolrErrorWrappingException(BAD_REQUEST) on command errors. Align SecurityConfHandler with those handlers so that a failed set-user (e.g. password == username) returns HTTP 400. Update SecurityConfHandlerTest to assert the exception rather than inspecting the response NamedList. --- .../handler/admin/SecurityConfHandler.java | 5 +++-- .../admin/SecurityConfHandlerTest.java | 22 ++++++++++--------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java b/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java index 315397f3e4e4..7e9476faf9eb 100644 --- a/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java +++ b/solr/core/src/java/org/apache/solr/handler/admin/SecurityConfHandler.java @@ -33,6 +33,7 @@ import org.apache.solr.api.ApiBag; import org.apache.solr.api.ApiBag.ReqHandlerToApi; import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.common.SolrErrorWrappingException; import org.apache.solr.common.SolrException; import org.apache.solr.common.SpecProvider; import org.apache.solr.common.params.CommonParams; @@ -135,8 +136,8 @@ private void doEdit( if (out == null) { List> errs = CommandOperation.captureErrors(commandsCopy); if (!errs.isEmpty()) { - rsp.add(CommandOperation.ERR_MSGS, errs); - return; + throw new SolrErrorWrappingException( + SolrException.ErrorCode.BAD_REQUEST, "error processing commands", errs); } log.debug("No edits made"); return; diff --git a/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java index 6177c9f1a3a6..5b5f4556c17e 100644 --- a/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java +++ b/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java @@ -24,6 +24,8 @@ import java.util.Map; import org.apache.solr.SolrTestCaseJ4; import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.common.SolrErrorWrappingException; +import org.apache.solr.common.SolrException; import org.apache.solr.common.params.ModifiableSolrParams; import org.apache.solr.common.util.CommandOperation; import org.apache.solr.common.util.ContentStreamBase; @@ -162,17 +164,17 @@ public void testEdit() throws Exception { + " 'method':'POST'," + " 'role': 'admin'\n" + " }}"; - req = new SolrQueryRequestBase(null, new ModifiableSolrParams()); - req.getContext().put("httpMethod", SolrRequest.METHOD.POST); - req.getContext().put("path", "/admin/authorization"); + final SolrQueryRequestBase badReq = new SolrQueryRequestBase(null, new ModifiableSolrParams()); + badReq.getContext().put("httpMethod", SolrRequest.METHOD.POST); + badReq.getContext().put("path", "/admin/authorization"); o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8), ""); - req.setContentStreams(List.of(o)); - rsp = new SolrQueryResponse(); - handler.handleRequestBody(req, rsp); - @SuppressWarnings({"rawtypes"}) - List l = - (List) ((Map) ((List) rsp.getValues().get("errorMessages")).get(0)).get("errorMessages"); - assertEquals(1, l.size()); + badReq.setContentStreams(List.of(o)); + final SolrQueryResponse badRsp = new SolrQueryResponse(); + SolrErrorWrappingException ex = + assertThrows( + SolrErrorWrappingException.class, () -> handler.handleRequestBody(badReq, badRsp)); + assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code()); + assertTrue(ex.getMessage().contains("method is not a valid key for the permission")); handler.close(); } From 99e07c664ccfe7c27aab9746c422ac162ccfe89c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 17 Jun 2026 11:06:07 +0200 Subject: [PATCH 09/11] Fix review feedback by Copilot --- solr/core/src/java/org/apache/solr/cli/AuthTool.java | 11 ++++++++--- solr/webapp/web/js/angular/controllers/security.js | 6 ------ 2 files changed, 8 insertions(+), 9 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 3a5da5c0e81c..1e6a29853305 100644 --- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java +++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java @@ -28,8 +28,10 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.Arrays; +import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; +import java.util.Map; import java.util.stream.Collectors; import org.apache.commons.cli.CommandLine; import org.apache.commons.cli.Option; @@ -300,12 +302,15 @@ private void handleBasicAuth(CommandLine cli) throws Exception { username); echo(successMessage); if (!updateIncludeFileOnly) { + Map 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:"); - CLIO.out(" - admin (roles: admin, index, search)"); - CLIO.out(" - index (roles: index, search)"); - CLIO.out(" - search (roles: search)"); + templateUsers.forEach((u, roles) -> CLIO.out(" - " + u + " (roles: " + roles + ")")); CLIO.out( "Set their passwords using the Admin UI Security page or the authentication API."); } diff --git a/solr/webapp/web/js/angular/controllers/security.js b/solr/webapp/web/js/angular/controllers/security.js index 1ffdf85460a0..fd65a289988f 100644 --- a/solr/webapp/web/js/angular/controllers/security.js +++ b/solr/webapp/web/js/angular/controllers/security.js @@ -412,12 +412,6 @@ solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cooki return false; } - var username = $scope.upsertUser.username ? $scope.upsertUser.username.trim() : ""; - if (password === username) { - $scope.validationError = "Password must not be the same as the username"; - return false; - } - return true; }; From 58f218cf3e4e093c4bde2a46953a43c6dccff67c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Wed, 17 Jun 2026 15:08:29 +0200 Subject: [PATCH 10/11] Display security pages' error dialog on http 400 error --- solr/webapp/web/js/angular/app.js | 14 +++++++++-- .../web/js/angular/controllers/security.js | 23 ++++++++++++++++--- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index abdd53f0c59c..f7061ff424ee 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -379,6 +379,10 @@ solrAdminApp.config([ if ($rootScope.exceptions[config.url]) { delete $rootScope.exceptions[config.url]; } + if (config.url && config.url.startsWith("/api/cluster/security/")) { + // Clear any stale security panel error when a new security request starts + $rootScope.$broadcast('securityApiClearError'); + } activeRequests++; if (sessionStorage.getItem("auth.header")) { config.headers['Authorization'] = sessionStorage.getItem("auth.header"); @@ -420,8 +424,9 @@ solrAdminApp.config([ return rejection; } - // Schema Designer handles errors internally to provide a better user experience than the global error handler + // Schema Designer and Security panels handle errors internally to provide a better user experience than the global error handler var isHandledBySchemaDesigner = rejection.config.url && rejection.config.url.startsWith("/api/schema-designer/"); + var isHandledBySecurity = rejection.config.url && rejection.config.url.startsWith("/api/cluster/security/"); if (rejection.status === 0) { $rootScope.$broadcast('connectionStatusActive'); if (!$rootScope.retryCount) $rootScope.retryCount=0; @@ -453,6 +458,9 @@ solrAdminApp.config([ } else if (rejection.status === 403 && !isHandledBySchemaDesigner) { // No permission $rootScope.showAuthzFailures = true; + } else if (isHandledBySecurity) { + // Let the security panel surface the detailed error in its own dialog + $rootScope.$broadcast('securityApiError', rejection); } else { // schema designer prefers to handle errors itself if (!isHandledBySchemaDesigner) { @@ -464,10 +472,12 @@ solrAdminApp.config([ return {request: started, response: ended, responseError: failed}; }) -.config(function($httpProvider) { +.config(function($httpProvider, $qProvider) { $httpProvider.interceptors.push("httpInterceptor"); // Force BasicAuth plugin to serve us a 'Authorization: xBasic xxxx' header so browser will not pop up login dialogue $httpProvider.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'; + // Suppress AngularJS 1.6+ "Possibly unhandled rejection" console noise; errors are handled via callbacks and the security/schema-designer error dialogs + $qProvider.errorOnUnhandledRejections(false); }) .directive('fileModel', function ($parse) { return { diff --git a/solr/webapp/web/js/angular/controllers/security.js b/solr/webapp/web/js/angular/controllers/security.js index fd65a289988f..776116eedd52 100644 --- a/solr/webapp/web/js/angular/controllers/security.js +++ b/solr/webapp/web/js/angular/controllers/security.js @@ -160,14 +160,31 @@ solrAdminApp.controller('SecurityController', function ($scope, $timeout, $cooki $scope.errorHandler = function (e) { var error = e.data && e.data.error ? e.data.error : null; if (error && error.msg) { - $scope.securityAPIError = error.msg; - $scope.securityAPIErrorDetails = e.data.errorDetails; + var allMessages = []; + if (error.details && Array.isArray(error.details)) { + error.details.forEach(function(detail) { + if (detail.errorMessages && Array.isArray(detail.errorMessages)) { + allMessages = allMessages.concat(detail.errorMessages); + } + }); + } + $scope.securityAPIError = allMessages.length > 0 ? allMessages[0] : error.msg; + $scope.securityAPIErrorDetails = allMessages.length > 1 ? allMessages.slice(1).join("\n") : null; } else if (e.data && e.data.message) { $scope.securityAPIError = e.data.message; - $scope.securityAPIErrorDetails = JSON.stringify(e.data); + $scope.securityAPIErrorDetails = null; } }; + // Security API command errors (HTTP 4xx/5xx) are broadcast by the global httpInterceptor; + // show them in this panel's error dialog rather than the generic page-header banner. + $scope.$on('securityApiError', function (event, rejection) { + $scope.errorHandler(rejection); + }); + $scope.$on('securityApiClearError', function () { + $scope.closeErrorDialog(); + }); + $scope.showHelp = function (id) { if ($scope.helpId && ($scope.helpId === id || id === '')) { delete $scope.helpId; From 6a986fac12f9b9c8df5ee006cf63b78f9c46f30f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20H=C3=B8ydahl?= Date: Thu, 18 Jun 2026 13:47:31 +0200 Subject: [PATCH 11/11] Fix copilot review comment from backport branch --- solr/webapp/web/js/angular/app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solr/webapp/web/js/angular/app.js b/solr/webapp/web/js/angular/app.js index f7061ff424ee..e4be0017384e 100644 --- a/solr/webapp/web/js/angular/app.js +++ b/solr/webapp/web/js/angular/app.js @@ -455,7 +455,7 @@ solrAdminApp.config([ sessionStorage.setItem("auth.location", $location.path()); $location.path('/login'); } - } else if (rejection.status === 403 && !isHandledBySchemaDesigner) { + } else if (rejection.status === 403 && !isHandledBySchemaDesigner && !isHandledBySecurity) { // No permission $rootScope.showAuthzFailures = true; } else if (isHandledBySecurity) {