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

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


The following commit(s) were added to refs/heads/main by this push:
     new c80c822c2ff SOLR-18233 Strengthen Basic Authentication password policy 
and harden template users created by bin/solr auth enable (take2) (#4534)
c80c822c2ff is described below

commit c80c822c2ff571f846bd8208c1a7d10e496e1ee8
Author: Jan Høydahl <[email protected]>
AuthorDate: Thu Jun 25 01:18:01 2026 +0200

    SOLR-18233 Strengthen Basic Authentication password policy and harden 
template users created by bin/solr auth enable (take2) (#4534)
---
 ...ngthen-Basic-Authentication-password-policy.yml |  7 +++
 .../src/java/org/apache/solr/cli/AuthTool.java     | 36 ++++++++++--
 .../solr/handler/admin/SecurityConfHandler.java    |  5 +-
 .../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     | 22 ++++----
 .../AuthWithShardHandlerFactoryOverrideTest.java   | 10 ++--
 .../solr/security/BasicAuthOnSingleNodeTest.java   |  6 +-
 .../security/TestSha256AuthenticationProvider.java | 58 +++++++++++++++++++
 .../pages/basic-authentication-plugin.adoc         |  8 ++-
 .../pages/solr-control-script-reference.adoc       | 30 ++++++++--
 .../pages/major-changes-in-solr-10.adoc            |  7 +++
 .../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, 307 insertions(+), 75 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 5c19ac300d7..1e6a2985330 100644
--- a/solr/core/src/java/org/apache/solr/cli/AuthTool.java
+++ b/solr/core/src/java/org/apache/solr/cli/AuthTool.java
@@ -28,14 +28,17 @@ import java.nio.charset.StandardCharsets;
 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;
 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,8 +230,14 @@ public class AuthTool extends ToolBase {
             } while (password.isEmpty());
           }
 
-          boolean blockUnknown =
-              Boolean.parseBoolean(cli.getOptionValue(BLOCK_UNKNOWN_OPTION, 
"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);
@@ -238,7 +247,11 @@ 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_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));
@@ -284,8 +297,23 @@ public class AuthTool extends ToolBase {
           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) {
+            Map<String, String> templateUsers = new LinkedHashMap<>();
+            templateUsers.put("admin", "admin, index, search");
+            templateUsers.put("index", "index, search");
+            templateUsers.put("search", "search");
+            templateUsers.remove(username);
+            CLIO.out(
+                "\nIMPORTANT: The following template users have been created 
with NO password set"
+                    + " and cannot log in until passwords are assigned:");
+            templateUsers.forEach((u, roles) -> CLIO.out("  - " + u + "  
(roles: " + roles + ")"));
+            CLIO.out(
+                "Set their passwords using the Admin UI Security page or the 
authentication API.");
+          }
           return;
         }
       case "disable":
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 315397f3e4e..7e9476faf9e 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.Api;
 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 @@ 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 SolrErrorWrappingException(
+              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 31c38537f69..9ab59fd72c4 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;
@@ -93,6 +107,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();
@@ -165,6 +184,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..aa31392a2f4 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")) {
+      cluster.getZkClient().setData("/security.json", 
"{}".getBytes(StandardCharsets.UTF_8));
+    }
   }
 
   @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 1c992140e07..e95bf3493fb 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 @@ 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 6177c9f1a3a..5b5f4556c17 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.List;
 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 class SecurityConfHandlerTest extends 
SolrTestCaseJ4 {
             + "                  '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();
   }
 
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 b1dfedc3f78..1c7c2c17169 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 f6f35c37d3b..26ca70cdf13 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 632fd16b35a..eec04231466 100644
--- 
a/solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java
+++ 
b/solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java
@@ -104,6 +104,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 19536088939..9f1a062feb6 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 f507216396b..aae46c7c6cc 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 <includeFilePath>`::
 +
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 cf30c381573..b374913e64b 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.
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 225ca994ec3..7b70f29bcf7 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} <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;
   }
 
@@ -82,9 +90,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 =
@@ -123,7 +133,7 @@ public class CloudAuthStreamTest extends SolrCloudTestCase {
 
     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 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"));
@@ -295,7 +305,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"));
@@ -338,7 +348,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(
@@ -361,7 +371,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"));
@@ -390,7 +400,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"));
@@ -410,7 +420,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());
 
@@ -439,7 +449,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(
@@ -459,7 +469,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());
 
@@ -486,7 +496,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);
@@ -518,7 +528,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
     }
@@ -533,7 +543,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");
@@ -555,7 +565,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
     }
@@ -582,7 +592,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
       }
@@ -598,7 +608,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()) {
@@ -631,7 +641,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
       }
@@ -663,7 +673,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"));
@@ -697,7 +707,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"));
@@ -758,7 +768,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(
@@ -801,7 +811,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"));
@@ -825,7 +835,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"));
@@ -849,7 +859,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());
 
@@ -888,7 +898,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 abdd53f0c59..e4be0017384 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;
@@ -450,9 +455,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) {
@@ -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 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