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;