This is an automated email from the ASF dual-hosted git repository.

janhoy pushed a commit to branch branch_9x
in repository https://gitbox.apache.org/repos/asf/solr.git


The following commit(s) were added to refs/heads/branch_9x by this push:
     new 6c8074c26a1 SOLR-18233 Strengthen Basic Authentication password policy 
(backport branch_9x) (#4519)
6c8074c26a1 is described below

commit 6c8074c26a11f5a6f1234c7468a99cc101103888
Author: Jan Høydahl <[email protected]>
AuthorDate: Thu Jun 25 01:30:50 2026 +0200

    SOLR-18233 Strengthen Basic Authentication password policy (backport 
branch_9x) (#4519)
---
 ...ngthen-Basic-Authentication-password-policy.yml |  7 +++
 .../src/java/org/apache/solr/cli/AuthTool.java     | 41 ++++++++++++--
 .../solr/handler/admin/SecurityConfHandler.java    |  4 +-
 .../security/Sha256AuthenticationProvider.java     | 24 ++++++++
 solr/core/src/resources/security.json              | 10 ++--
 .../src/test/org/apache/solr/cli/AuthToolTest.java | 49 +++++++++++++++-
 .../solr/cloud/TestQueryingOnDownCollection.java   |  5 +-
 .../handler/admin/SecurityConfHandlerTest.java     | 23 ++++----
 .../AuthWithShardHandlerFactoryOverrideTest.java   | 10 ++--
 .../solr/security/BasicAuthOnSingleNodeTest.java   |  6 +-
 .../security/TestSha256AuthenticationProvider.java | 59 +++++++++++++++++++
 .../pages/basic-authentication-plugin.adoc         |  8 ++-
 .../pages/solr-control-script-reference.adoc       | 30 ++++++++--
 .../pages/major-changes-in-solr-9.adoc             |  9 +++
 .../solrj/io/stream/CloudAuthStreamTest.java       | 66 +++++++++++++---------
 solr/webapp/web/js/angular/app.js                  | 16 +++++-
 solr/webapp/web/js/angular/controllers/security.js | 23 +++++++-
 17 files changed, 313 insertions(+), 77 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
new file mode 100644
index 00000000000..68d8a849616
--- /dev/null
+++ 
b/changelog/unreleased/SOLR-18233-Strengthen-Basic-Authentication-password-policy.yml
@@ -0,0 +1,7 @@
+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
+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 9d6779d1586..355e0a7ce7b 100644
--- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java
@@ -30,8 +30,10 @@ import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Arrays;
 import java.util.Base64;
+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.DeprecatedAttributes;
@@ -39,6 +41,7 @@ import org.apache.commons.cli.HelpFormatter;
 import org.apache.commons.cli.Option;
 import org.apache.lucene.util.Constants;
 import org.apache.solr.common.cloud.SolrZkClient;
+import org.apache.solr.common.util.EnvUtils;
 import org.apache.solr.common.util.StrUtils;
 import org.apache.solr.core.SolrCore;
 import org.apache.solr.security.Sha256AuthenticationProvider;
@@ -467,10 +470,14 @@ public class AuthTool extends ToolBase {
           } while (password.length() == 0);
         }
 
-        boolean blockUnknown =
-            Boolean.parseBoolean(
-                SolrCLI.getOptionWithDeprecatedAndDefault(
-                    cli, "block-unknown", "blockUnknown", "true"));
+        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);
@@ -480,7 +487,14 @@ public class AuthTool extends ToolBase {
 
         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") || cli.hasOption("blockUnknown")) {
+          boolean blockUnknown =
+              Boolean.parseBoolean(
+                  SolrCLI.getOptionWithDeprecatedAndDefault(
+                      cli, "block-unknown", "blockUnknown", "true"));
+          ((ObjectNode) 
securityJson1.get("authentication")).put("blockUnknown", blockUnknown);
+        }
         JsonNode credentialsNode = 
securityJson1.get("authentication").get("credentials");
         ((ObjectNode) credentialsNode)
             .put(username, 
Sha256AuthenticationProvider.getSaltedHashedValue(password));
@@ -534,8 +548,23 @@ public class AuthTool extends ToolBase {
             includeFile.toPath(), basicAuthConfFile.getAbsolutePath(), null, 
cli);
         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) {
+          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 0;
 
       case "disable":
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 0f3cc35d062..3f5823da131 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
@@ -136,8 +136,8 @@ public abstract class SecurityConfHandler extends 
RequestHandlerBase
       if (out == null) {
         List<Map<String, Object>> errs = 
CommandOperation.captureErrors(commandsCopy);
         if (!errs.isEmpty()) {
-          rsp.add(CommandOperation.ERR_MSGS, errs);
-          return;
+          throw new ApiBag.ExceptionWithErrObject(
+              SolrException.ErrorCode.BAD_REQUEST, "error processing 
commands", errs);
         }
         log.debug("No edits made");
         return;
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 ea00f1c4607..567a48bc36d 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 java.util.Set;
 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<String, String> credentials;
   private String realm;
   private Map<String, String> promptHeader;
@@ -94,6 +108,11 @@ public class Sha256AuthenticationProvider
 
   @Override
   public boolean authenticate(String username, String password) {
+    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,6 +185,11 @@ public class Sha256AuthenticationProvider
             cmd.addError("name and password must be non-null");
             return null;
           }
+          if (e.getKey().equals(String.valueOf(e.getValue()))
+              && !EnvUtils.getPropertyAsBool(ALLOW_USER_AS_PASSWORD_PROP, 
false)) {
+            cmd.addError("Password must not be the same as the username");
+            return null;
+          }
           putUser(e.getKey(), String.valueOf(e.getValue()), map);
         }
       }
diff --git a/solr/core/src/resources/security.json 
b/solr/core/src/resources/security.json
index f503f7c4e30..4406cf52a1b 100644
--- a/solr/core/src/resources/security.json
+++ b/solr/core/src/resources/security.json
@@ -3,10 +3,9 @@
     "blockUnknown": false,
     "class": "solr.BasicAuthPlugin",
     "credentials": {
-      "search": "9ch2qWOmNSeGpfcgLRXafhm5z3KeRti5qCNLn7SmK1I= 
aXNjZWd4YW9mMzZ0cjE1Nw==",
-      "index": "of9xlSadImtR0MH4obzJvKSZkuE5DIJh5NOui2hWDeA= 
dTRuYzU4Y3F4N2hxd2sxeA==",
-      "admin": "6clS8rTEj1x1LP/uRCxOZsLdps7Sovokru09WdJX+7A= 
NGMyZGFhN2lrNHFsdXZybA==",
-      "superadmin": "9wzPajmLBIIi8BmToy8lxveDxfL6Vl/BX/Ss3xrs3XQ= 
OWZna2hwendocXFnODU5ZQ=="
+      "search": "",
+      "index": "",
+      "admin": ""
     }
   },
   "authorization": {
@@ -67,8 +66,7 @@
     "user-role": {
       "search": ["search"],
       "index": ["index", "search"],
-      "admin": ["admin", "index", "search"],
-      "superadmin": ["superadmin", "admin", "index", "search"]
+      "admin": ["admin", "index", "search"]
     }
   }
 }
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 28d62999ced..1bb3d5b914c 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 class AuthToolTest extends SolrCloudTestCase {
   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", true)) {
+      cluster.getZkClient().setData("/security.json", 
"{}".getBytes(StandardCharsets.UTF_8), true);
+    }
   }
 
   @Override
@@ -67,10 +73,51 @@ public class AuthToolTest extends SolrCloudTestCase {
       "--solr-include-file",
       solrIncludeFile.toAbsolutePath().toString(),
       "--credentials",
-      "solr:solr",
+      "solr:solrRocks",
       "--block-unknown",
       "true"
     };
     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);
+    }
+  }
 }
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 6254d3d2917..9e2fc091cff 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 {
@@ -156,7 +157,7 @@ public class TestQueryingOnDownCollection extends 
SolrCloudTestCase {
           + "  \"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/handler/admin/SecurityConfHandlerTest.java 
b/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java
index 5a971eb7dd6..74fce7906ff 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,7 +24,9 @@ import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
 import org.apache.solr.SolrTestCaseJ4;
+import org.apache.solr.api.ApiBag;
 import org.apache.solr.client.solrj.SolrRequest;
+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;
@@ -163,17 +165,18 @@ public class SecurityConfHandlerTest extends 
SolrTestCaseJ4 {
             + "                  'method':'POST',"
             + "                  'role': 'admin'\n"
             + "                  }}";
-    req = new LocalSolrQueryRequest(null, new ModifiableSolrParams());
-    req.getContext().put("httpMethod", SolrRequest.METHOD.POST);
-    req.getContext().put("path", "/admin/authorization");
+    final LocalSolrQueryRequest badReq =
+        new LocalSolrQueryRequest(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(Collections.singletonList(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(Collections.singletonList(o));
+    final SolrQueryResponse badRsp = new SolrQueryResponse();
+    ApiBag.ExceptionWithErrObject ex =
+        assertThrows(
+            ApiBag.ExceptionWithErrObject.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();
   }
 
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 eff43b40d22..754a1ffdb7a 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 class AuthWithShardHandlerFactoryOverrideTest 
extends SolrCloudAuthTestCa
         .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 class AuthWithShardHandlerFactoryOverrideTest extends 
SolrCloudAuthTestCa
       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 class AuthWithShardHandlerFactoryOverrideTest 
extends SolrCloudAuthTestCa
       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 8b6708fe825..d867c5ed96b 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 class BasicAuthOnSingleNodeTest extends 
SolrCloudAuthTestCase {
         .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 class BasicAuthOnSingleNodeTest extends 
SolrCloudAuthTestCase {
       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 class BasicAuthOnSingleNodeTest extends 
SolrCloudAuthTestCase {
           + "  \"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/TestSha256AuthenticationProvider.java
 
b/solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java
index 1646f2a772e..c1bdfdab131 100644
--- 
a/solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java
+++ 
b/solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java
@@ -22,6 +22,7 @@ import java.io.IOException;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 import org.apache.solr.SolrTestCaseJ4;
 import org.apache.solr.common.util.CommandOperation;
@@ -106,6 +107,64 @@ public class TestSha256AuthenticationProvider extends 
SolrTestCaseJ4 {
     }
   }
 
+  public void testAuthenticateRejectsUsernameEqualPassword() {
+    // Simulate a credential store that has the username's own hash as the 
password
+    // (e.g. set up before this policy was in effect) and verify 
authenticate() still rejects it.
+    String user = "alice";
+    String hashedValue = 
Sha256AuthenticationProvider.getSaltedHashedValue(user);
+    Map<String, Object> config = new HashMap<>();
+    Map<String, String> 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 testAllowUserAsPasswordSyspropReenablesLogin() {
+    String user = "alice";
+    String hashedValue = 
Sha256AuthenticationProvider.getSaltedHashedValue(user);
+    Map<String, Object> config = new HashMap<>();
+    Map<String, String> 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<String, Object> 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"));
+    Map<String, Object> 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<String, Object> createConfigMap(String user, String pw) {
     Map<String, Object> config = new HashMap<>();
     Map<String, String> 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 4687fe5f6c0..f54c58093fb 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 (Format: `base64(sha256(sha256(salt+password))) 
base64(salt)`) could be added when the file is created, or can be added later 
with the Authentication API, described below.
@@ -150,6 +151,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 c6e66f7a880..053b6c06f11 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
@@ -1022,7 +1022,7 @@ 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/branch_9x/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/branch_9x/solr/core/src/resources/security.json[user
 roles and permission mappings], and maps the created user to all roles 
(`superadmin`, `admin`, `index`, `search`).
 For Kerberos it only enables the security.json, it doesn't set up any users or 
role mappings.
 
 
@@ -1030,12 +1030,16 @@ For Kerberos it only enables the security.json, it 
doesn't set up any users or r
 
 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/branch_9x/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`:
 +
@@ -1047,6 +1051,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]
@@ -1089,11 +1106,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 <includeFilePath>`::
 +
diff --git 
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc 
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
index 697d4a7745b..0a18962d1f1 100644
--- 
a/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
+++ 
b/solr/solr-ref-guide/modules/upgrade-notes/pages/major-changes-in-solr-9.adoc
@@ -121,6 +121,15 @@ The `blockUnknown` setting in the JWT Authentication 
plugin now defaults to `tru
 In Solr 9.10 and earlier, the code default was `false` (pass-through), which 
contradicted the reference guide documentation that described `true` as the 
default.
 Users upgrading from 9.10 who relied on the pass-through behavior must 
explicitly set `"blockUnknown": false` in their `security.json`.
 
+=== Basic Authentication
+
+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.
+
 == Solr 9.10
 
 === SolrJ
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 bcda78870c2..7567314dff3 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
@@ -66,16 +66,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} <em>with its 
original type</em> 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 extends SolrRequest<?>> T setBasicAuthCredentials(T req, 
String user) {
     assertNotNull(user);
-    req.setBasicAuthCredentials(user, user);
+    req.setBasicAuthCredentials(user, passwordFor(user));
     return req;
   }
 
@@ -83,9 +91,11 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
   public static void setupCluster() throws Exception {
     final List<String> 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<String, String> 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<String, String> roles =
@@ -125,7 +135,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
     for (String collection : Arrays.asList(COLLECTION_X, COLLECTION_Y)) {
       CollectionAdminRequest.createCollection(collection, "_default", 2, 2)
           .setPerReplicaState(SolrCloudTestCase.USE_PER_REPLICA_STATE)
-          .setBasicAuthCredentials(ADMIN_USER, ADMIN_USER)
+          .setBasicAuthCredentials(ADMIN_USER, passwordFor(ADMIN_USER))
           .process(cluster.getSolrClient());
     }
 
@@ -248,7 +258,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
             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<Tuple> tuples = getTuples(solrStream);
     assertEquals(1, tuples.size());
     assertEquals("hello world", tuples.get(0).get("echo"));
@@ -297,7 +307,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                 "/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<Tuple> tuples = getTuples(solrStream);
     assertEquals(1, tuples.size());
     assertEquals(1L, tuples.get(0).get("totalIndexed"));
@@ -340,7 +350,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                   "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(
@@ -363,7 +373,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                   "/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<Tuple> tuples = getTuples(solrStream);
       assertEquals(1, tuples.size());
       assertEquals(1L, tuples.get(0).get("totalIndexed"));
@@ -392,7 +402,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
           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<Tuple> tuples = getTuples(solrStream);
       assertEquals(1, tuples.size());
       assertEquals(10L, tuples.get(0).get("batchIndexed"));
@@ -412,7 +422,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
           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<Tuple> tuples = getTuples(solrStream);
       assertEquals(3, tuples.size());
 
@@ -441,7 +451,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                   "/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(
@@ -461,7 +471,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
             + ", 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<Tuple> tuples = getTuples(solrStream);
     assertEquals(0, tuples.size());
 
@@ -488,7 +498,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
             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<Tuple> tuples = getTuples(solrStream);
@@ -520,7 +530,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
               + ",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<Tuple> tuples = getTuples(solrStream);
       assertEquals(1, tuples.size()); // daemon starting status
     }
@@ -535,7 +545,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                 params(
                     "qt", "/stream",
                     "action", "list"));
-        daemonCheck.setCredentials(WRITE_X_USER, WRITE_X_USER);
+        daemonCheck.setCredentials(WRITE_X_USER, passwordFor(WRITE_X_USER));
         final List<Tuple> tuples = getTuples(daemonCheck);
         assertEquals(1, tuples.size()); // our daemon;
         iterations = tuples.get(0).getLong("iterations");
@@ -557,7 +567,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                   "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<Tuple> tuples = getTuples(daemonKiller);
       assertEquals(1, tuples.size()); // daemon death status
     }
@@ -584,7 +594,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
         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<Tuple> tuples = getTuples(solrStream);
         assertEquals(1, tuples.size()); // daemon starting status
       }
@@ -600,7 +610,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                       "qt", "/stream",
                       "_trace", "check_" + daemonId,
                       "action", "list"));
-          daemonCheck.setCredentials(user, user);
+          daemonCheck.setCredentials(user, passwordFor(user));
           final List<Tuple> tuples = getTuples(daemonCheck);
           assertEquals(1, tuples.size()); // our daemon;
           if (log.isInfoEnabled()) {
@@ -633,7 +643,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                     "kill",
                     "id",
                     daemonId));
-        daemonKiller.setCredentials(user, user);
+        daemonKiller.setCredentials(user, passwordFor(user));
         final List<Tuple> tuples = getTuples(daemonKiller);
         assertEquals(1, tuples.size()); // daemon death status
       }
@@ -665,7 +675,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                 "/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<Tuple> tuples = getTuples(solrStream);
     assertEquals(1, tuples.size());
     assertEquals(1L, tuples.get(0).get("totalIndexed"));
@@ -699,7 +709,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
 
       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<Tuple> tuples = getTuples(solrStream);
       assertEquals(2, tuples.size());
       assertEquals(5L, tuples.get(0).get("totalIndexed"));
@@ -760,7 +770,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                   "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(
@@ -803,7 +813,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                   "/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<Tuple> tuples = getTuples(solrStream);
       assertEquals(1, tuples.size());
       assertEquals(1L, tuples.get(0).get("totalIndexed"));
@@ -827,7 +837,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
           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<Tuple> tuples = getTuples(solrStream);
       assertEquals(1, tuples.size());
       assertEquals(10L, tuples.get(0).get("batchIndexed"));
@@ -851,7 +861,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
           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<Tuple> tuples = getTuples(solrStream);
       assertEquals(3, tuples.size());
 
@@ -890,7 +900,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
                   "/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(
diff --git a/solr/webapp/web/js/angular/app.js 
b/solr/webapp/web/js/angular/app.js
index 0cb3bf529b9..c8176396dd3 100644
--- a/solr/webapp/web/js/angular/app.js
+++ b/solr/webapp/web/js/angular/app.js
@@ -376,6 +376,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");
@@ -417,8 +421,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;
@@ -447,9 +452,12 @@ 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) {
+      // 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) {
@@ -461,10 +469,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 fd65a289988..776116eedd5 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;

Reply via email to