Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
36 changes: 32 additions & 4 deletions solr/core/src/java/org/apache/solr/cli/AuthTool.java
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,17 @@
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;
Expand Down Expand Up @@ -227,8 +230,14 @@ private void handleBasicAuth(CommandLine cli) throws Exception {
} 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);
Expand All @@ -238,7 +247,11 @@ private void handleBasicAuth(CommandLine cli) throws Exception {

ObjectMapper mapper = new ObjectMapper();
JsonNode securityJson1 = mapper.readTree(resource.openStream());
((ObjectNode) securityJson1).put("blockUnknown", blockUnknown);
// Only override blockUnknown if explicitly passed; otherwise let the template decide
if (cli.hasOption(BLOCK_UNKNOWN_OPTION)) {
boolean blockUnknown = Boolean.parseBoolean(cli.getOptionValue(BLOCK_UNKNOWN_OPTION));
((ObjectNode) securityJson1.get("authentication")).put("blockUnknown", blockUnknown);
}
JsonNode credentialsNode = securityJson1.get("authentication").get("credentials");
((ObjectNode) credentialsNode)
.put(username, Sha256AuthenticationProvider.getSaltedHashedValue(password));
Expand Down Expand Up @@ -284,8 +297,23 @@ private void handleBasicAuth(CommandLine cli) throws Exception {
updateIncludeFileEnableAuth(includeFile, basicAuthConfFile);
final String successMessage =
String.format(
Locale.ROOT, "Successfully enabled basic auth with username [%s].", username);
Locale.ROOT,
"Successfully enabled basic auth with username [%s] assigned to all roles (superadmin, admin, index, search).",
username);
echo(successMessage);
if (!updateIncludeFileOnly) {
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":
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.apache.solr.api.ApiBag;
import org.apache.solr.api.ApiBag.ReqHandlerToApi;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.SolrErrorWrappingException;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.SpecProvider;
import org.apache.solr.common.params.CommonParams;
Expand Down Expand Up @@ -135,8 +136,8 @@ private void doEdit(
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import org.apache.solr.api.AnnotatedApi;
import org.apache.solr.api.Api;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.EnvUtils;
import org.apache.solr.common.util.ValidatingJsonMap;
import org.apache.solr.handler.admin.api.ModifyBasicAuthConfigAPI;
import org.slf4j.Logger;
Expand All @@ -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;
Expand Down Expand Up @@ -93,6 +107,11 @@ public void init(Map<String, Object> pluginConfig) {

@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();
Expand Down Expand Up @@ -165,6 +184,11 @@ public Map<String, Object> edit(Map<String, Object> latestConf, List<CommandOper
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);
}
}
Expand Down
10 changes: 4 additions & 6 deletions solr/core/src/resources/security.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down Expand Up @@ -67,8 +66,7 @@
"user-role": {
"search": ["search"],
"index": ["index", "search"],
"admin": ["admin", "index", "search"],
"superadmin": ["superadmin", "admin", "index", "search"]
"admin": ["admin", "index", "search"]
}
}
}
49 changes: 48 additions & 1 deletion solr/core/src/test/org/apache/solr/cli/AuthToolTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -42,6 +44,10 @@ public static void setupCluster() throws Exception {
public void setUp() throws Exception {
super.setUp();
dir = createTempDir("AuthToolTest").toAbsolutePath();
// Reset ZK security state before each test to avoid interference between tests
if (cluster.getZkClient().exists("/security.json")) {
cluster.getZkClient().setData("/security.json", "{}".getBytes(StandardCharsets.UTF_8));
}
}

@Override
Expand All @@ -67,10 +73,51 @@ public void testEnableAuth() throws Exception {
"--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);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -155,7 +156,7 @@ private void downAllReplicas() throws Exception {
+ " \"authentication\":{\n"
+ " \"blockUnknown\": true,\n"
+ " \"class\":\"solr.BasicAuthPlugin\",\n"
+ " \"credentials\":{\"solr\":\"EEKn7ywYk5jY8vG9TyqlG2jvYuvh1Q7kCCor6Hqm320= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n"
+ " \"credentials\":{\"solr\":\"JeRyxP8A3dVWhFgFbf/Eg2RXmuoU8BE5gbNQyxmGAJQ= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n"
+ " },\n"
+ " \"authorization\":{\n"
+ " \"class\":\"solr.RuleBasedAuthorizationPlugin\",\n"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@
import java.util.Map;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.common.SolrErrorWrappingException;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.util.CommandOperation;
import org.apache.solr.common.util.ContentStreamBase;
Expand Down Expand Up @@ -162,17 +164,17 @@ public void testEdit() throws Exception {
+ " 'method':'POST',"
+ " 'role': 'admin'\n"
+ " }}";
req = new SolrQueryRequestBase(null, new ModifiableSolrParams());
req.getContext().put("httpMethod", SolrRequest.METHOD.POST);
req.getContext().put("path", "/admin/authorization");
final SolrQueryRequestBase badReq = new SolrQueryRequestBase(null, new ModifiableSolrParams());
badReq.getContext().put("httpMethod", SolrRequest.METHOD.POST);
badReq.getContext().put("path", "/admin/authorization");
o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8), "");
req.setContentStreams(List.of(o));
rsp = new SolrQueryResponse();
handler.handleRequestBody(req, rsp);
@SuppressWarnings({"rawtypes"})
List l =
(List) ((Map) ((List) rsp.getValues().get("errorMessages")).get(0)).get("errorMessages");
assertEquals(1, l.size());
badReq.setContentStreams(List.of(o));
final SolrQueryResponse badRsp = new SolrQueryResponse();
SolrErrorWrappingException ex =
assertThrows(
SolrErrorWrappingException.class, () -> handler.handleRequestBody(badReq, badRsp));
assertEquals(SolrException.ErrorCode.BAD_REQUEST.code, ex.code());
assertTrue(ex.getMessage().contains("method is not a valid key for the permission"));
handler.close();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -56,11 +56,11 @@ public void setupCluster() throws Exception {
.withSecurityJson(SECURITY_CONF)
.configure();
CollectionAdminRequest.createCollection(COLLECTION, "conf", 4, 1)
.setBasicAuthCredentials("solr", "solr")
.setBasicAuthCredentials("solr", "SolrRocks")
.process(cluster.getSolrClient());

CollectionAdminRequest.createAlias(ALIAS, COLLECTION)
.setBasicAuthCredentials("solr", "solr")
.setBasicAuthCredentials("solr", "SolrRocks")
.process(cluster.getSolrClient());

cluster.waitForActiveCollection(COLLECTION, 4, 4);
Expand Down Expand Up @@ -89,7 +89,7 @@ public void collectionTest() throws Exception {
for (int i = 0; i < 30; i++) {
SolrResponse response =
new QueryRequest(params("q", "*:*"))
.setBasicAuthCredentials("solr", "solr")
.setBasicAuthCredentials("solr", "SolrRocks")
.process(client, COLLECTION);
// likely to be non-null, even if an error occurred
assertNotNull(response);
Expand All @@ -109,7 +109,7 @@ public void aliasTest() throws Exception {
for (int i = 0; i < 30; i++) {
SolrResponse response =
new QueryRequest(params("q", "*:*"))
.setBasicAuthCredentials("solr", "solr")
.setBasicAuthCredentials("solr", "SolrRocks")
.process(client, ALIAS);
// likely to be non-null, even if an error occurred
assertNotNull(response);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public void setupCluster() throws Exception {
.withSecurityJson(STD_CONF)
.configure();
CollectionAdminRequest.createCollection(COLLECTION, "conf", 4, 1)
.setBasicAuthCredentials("solr", "solr")
.setBasicAuthCredentials("solr", "SolrRocks")
.process(cluster.getSolrClient());
cluster.waitForActiveCollection(COLLECTION, 4, 4);
}
Expand All @@ -60,7 +60,7 @@ public void basicTest() throws Exception {
for (int i = 0; i < 30; i++) {
assertNotNull(
new QueryRequest(params("q", "*:*"))
.setBasicAuthCredentials("solr", "solr")
.setBasicAuthCredentials("solr", "SolrRocks")
.process(client, COLLECTION));
}
}
Expand Down Expand Up @@ -106,7 +106,7 @@ public void testDeleteSecurityJsonZnode() throws Exception {
+ " \"authentication\":{\n"
+ " \"blockUnknown\": true,\n"
+ " \"class\":\"solr.BasicAuthPlugin\",\n"
+ " \"credentials\":{\"solr\":\"EEKn7ywYk5jY8vG9TyqlG2jvYuvh1Q7kCCor6Hqm320= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n"
+ " \"credentials\":{\"solr\":\"JeRyxP8A3dVWhFgFbf/Eg2RXmuoU8BE5gbNQyxmGAJQ= 6zkmjMjkMKyJX6/f0VarEWQujju5BzxZXub6WOrEKCw=\"}\n"
+ " },\n"
+ " \"authorization\":{\n"
+ " \"class\":\"solr.RuleBasedAuthorizationPlugin\",\n"
Expand Down
Loading
Loading