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;