blob: f39453e41ca68814c7cbdfd33be9da09bc08b92d [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.solr.security;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.lang.invoke.MethodHandles;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Map;
import java.util.Random;
import java.util.Set;
import java.util.concurrent.TimeUnit;
import com.codahale.metrics.MetricRegistry;
import org.apache.commons.io.IOUtils;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.ByteArrayEntity;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
import org.apache.solr.client.solrj.impl.BaseHttpSolrClient;
import org.apache.solr.client.solrj.impl.HttpClientUtil;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.GenericSolrRequest;
import org.apache.solr.client.solrj.request.QueryRequest;
import org.apache.solr.client.solrj.request.RequestWriter.StringPayloadContentWriter;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.request.V2Request;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.cloud.SolrCloudAuthTestCase;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.DocCollection;
import org.apache.solr.common.cloud.Replica;
import org.apache.solr.common.cloud.Slice;
import org.apache.solr.common.params.CommonParams;
import org.apache.solr.common.params.MapSolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.util.NamedList;
import org.apache.solr.common.util.TimeSource;
import org.apache.solr.common.util.Utils;
import org.apache.solr.util.LogLevel;
import org.apache.solr.util.SolrCLI;
import org.apache.solr.util.TimeOut;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import static java.nio.charset.StandardCharsets.UTF_8;
import static java.util.Collections.singletonMap;
public class BasicAuthIntegrationTest extends SolrCloudAuthTestCase {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final String COLLECTION = "authCollection";
@Before
public void setupCluster() throws Exception {
configureCluster(3)
.addConfig("conf", configset("cloud-minimal"))
.configure();
CollectionAdminRequest.createCollection(COLLECTION, "conf", 3, 1).process(cluster.getSolrClient());
cluster.waitForActiveCollection(COLLECTION, 3, 3);
}
@After
public void tearDownCluster() throws Exception {
shutdownCluster();
}
@Test
//commented 9-Aug-2018 @BadApple(bugUrl="https://issues.apache.org/jira/browse/SOLR-12028") // 21-May-2018
// commented out on: 17-Feb-2019 @BadApple(bugUrl="https://issues.apache.org/jira/browse/SOLR-12028") // annotated on: 24-Dec-2018
@LogLevel("org.apache.solr.security=DEBUG")
public void testBasicAuth() throws Exception {
boolean isUseV2Api = random().nextBoolean();
String authcPrefix = "/admin/authentication";
String authzPrefix = "/admin/authorization";
if(isUseV2Api){
authcPrefix = "/____v2/cluster/security/authentication";
authzPrefix = "/____v2/cluster/security/authorization";
}
NamedList<Object> rsp;
HttpClient cl = null;
try {
cl = HttpClientUtil.createClient(null);
JettySolrRunner randomJetty = cluster.getRandomJetty(random());
String baseUrl = randomJetty.getBaseUrl().toString();
verifySecurityStatus(cl, baseUrl + authcPrefix, "/errorMessages", null, 20);
zkClient().setData("/security.json", STD_CONF.replaceAll("'", "\"").getBytes(UTF_8), true);
verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/class", "solr.BasicAuthPlugin", 20);
randomJetty.stop();
cluster.waitForJettyToStop(randomJetty);
randomJetty.start();
cluster.waitForAllNodes(30);
cluster.waitForActiveCollection(COLLECTION, 3, 3);
baseUrl = randomJetty.getBaseUrl().toString();
verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/class", "solr.BasicAuthPlugin", 20);
assertNumberOfMetrics(16); // Basic auth metrics available
assertAuthMetricsMinimums(1, 0, 1, 0, 0, 0);
assertPkiAuthMetricsMinimums(0, 0, 0, 0, 0, 0);
String command = "{\n" +
"'set-user': {'harry':'HarryIsCool'}\n" +
"}";
@SuppressWarnings({"rawtypes"})
final SolrRequest genericReq;
if (isUseV2Api) {
genericReq = new V2Request.Builder("/cluster/security/authentication").POST().build();
} else {
genericReq = new GenericSolrRequest(SolrRequest.METHOD.POST, authcPrefix, new ModifiableSolrParams());
((GenericSolrRequest)genericReq).setContentWriter(new StringPayloadContentWriter(command, CommonParams.JSON_MIME));
}
// avoid bad connection races due to shutdown
cluster.getSolrClient().getHttpClient().getConnectionManager().closeExpiredConnections();
cluster.getSolrClient().getHttpClient().getConnectionManager().closeIdleConnections(1, TimeUnit.MILLISECONDS);
BaseHttpSolrClient.RemoteSolrException exp = expectThrows(BaseHttpSolrClient.RemoteSolrException.class, () -> {
cluster.getSolrClient().request(genericReq);
});
assertEquals(401, exp.code());
assertAuthMetricsMinimums(2, 0, 2, 0, 0, 0);
assertPkiAuthMetricsMinimums(0, 0, 0, 0, 0, 0);
command = "{\n" +
"'set-user': {'harry':'HarryIsUberCool'}\n" +
"}";
HttpPost httpPost = new HttpPost(baseUrl + authcPrefix);
setAuthorizationHeader(httpPost, makeBasicAuthHeader("solr", "SolrRocks"));
httpPost.setEntity(new ByteArrayEntity(command.getBytes(UTF_8)));
httpPost.addHeader("Content-Type", "application/json; charset=UTF-8");
verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication.enabled", "true", 20);
HttpResponse r = cl.execute(httpPost);
int statusCode = r.getStatusLine().getStatusCode();
Utils.consumeFully(r.getEntity());
assertEquals("proper_cred sent, but access denied", 200, statusCode);
assertPkiAuthMetricsMinimums(0, 0, 0, 0, 0, 0);
assertAuthMetricsMinimums(4, 1, 3, 0, 0, 0);
baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString();
verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/credentials/harry", NOT_NULL_PREDICATE, 20);
command = "{\n" +
"'set-user-role': {'harry':'admin'}\n" +
"}";
executeCommand(baseUrl + authzPrefix, cl,command, "solr", "SolrRocks");
assertAuthMetricsMinimums(5, 2, 3, 0, 0, 0);
baseUrl = cluster.getRandomJetty(random()).getBaseUrl().toString();
verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/user-role/harry", NOT_NULL_PREDICATE, 20);
executeCommand(baseUrl + authzPrefix, cl, Utils.toJSONString(singletonMap("set-permission", Utils.makeMap
("collection", "x",
"path", "/update/*",
"role", "dev"))), "harry", "HarryIsUberCool" );
verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/permissions[1]/collection", "x", 20);
assertAuthMetricsMinimums(8, 3, 5, 0, 0, 0);
executeCommand(baseUrl + authzPrefix, cl,Utils.toJSONString(singletonMap("set-permission", Utils.makeMap
("name", "collection-admin-edit", "role", "admin"))), "harry", "HarryIsUberCool" );
verifySecurityStatus(cl, baseUrl + authzPrefix, "authorization/permissions[2]/name", "collection-admin-edit", 20);
assertAuthMetricsMinimums(10, 4, 6, 0, 0, 0);
CollectionAdminRequest.Reload reload = CollectionAdminRequest.reloadCollection(COLLECTION);
try (HttpSolrClient solrClient = getHttpSolrClient(baseUrl)) {
expectThrows(BaseHttpSolrClient.RemoteSolrException.class, () -> solrClient.request(reload));
reload.setMethod(SolrRequest.METHOD.POST);
expectThrows(BaseHttpSolrClient.RemoteSolrException.class, () -> solrClient.request(reload));
}
cluster.getSolrClient().request(CollectionAdminRequest.reloadCollection(COLLECTION)
.setBasicAuthCredentials("harry", "HarryIsUberCool"));
expectThrows(BaseHttpSolrClient.RemoteSolrException.class, () -> {
cluster.getSolrClient().request(CollectionAdminRequest.reloadCollection(COLLECTION)
.setBasicAuthCredentials("harry", "Cool12345"));
});
assertAuthMetricsMinimums(14, 5, 8, 1, 0, 0);
executeCommand(baseUrl + authzPrefix, cl,"{set-permission : { name : update , role : admin}}", "harry", "HarryIsUberCool");
UpdateRequest del = new UpdateRequest().deleteByQuery("*:*");
del.setBasicAuthCredentials("harry","HarryIsUberCool");
del.setCommitWithin(10);
del.process(cluster.getSolrClient(), COLLECTION);
//Test for SOLR-12514. Create a new jetty . This jetty does not have the collection.
//Make a request to that jetty and it should fail
JettySolrRunner aNewJetty = cluster.startJettySolrRunner();
SolrClient aNewClient = aNewJetty.newClient();
UpdateRequest delQuery = null;
delQuery = new UpdateRequest().deleteByQuery("*:*");
delQuery.setBasicAuthCredentials("harry","HarryIsUberCool");
delQuery.process(aNewClient, COLLECTION);//this should succeed
try {
BaseHttpSolrClient.RemoteSolrException e = expectThrows(BaseHttpSolrClient.RemoteSolrException.class, () -> {
new UpdateRequest().deleteByQuery("*:*").process(aNewClient, COLLECTION);
});
assertTrue(e.getMessage(), e.getMessage().contains("Authentication failed"));
} finally {
aNewClient.close();
cluster.stopJettySolrRunner(aNewJetty);
}
addDocument("harry","HarryIsUberCool","id", "4");
executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: true}}", "harry", "HarryIsUberCool");
verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/blockUnknown", "true", 20, "harry", "HarryIsUberCool");
verifySecurityStatus(cl, baseUrl + "/admin/info/key", "key", NOT_NULL_PREDICATE, 20);
assertAuthMetricsMinimums(17, 8, 8, 1, 0, 0);
String[] toolArgs = new String[]{
"status", "-solr", baseUrl};
ByteArrayOutputStream baos = new ByteArrayOutputStream();
PrintStream stdoutSim = new PrintStream(baos, true, StandardCharsets.UTF_8.name());
SolrCLI.StatusTool tool = new SolrCLI.StatusTool(stdoutSim);
try {
System.setProperty("basicauth", "harry:HarryIsUberCool");
tool.runTool(SolrCLI.processCommandLineArgs(SolrCLI.joinCommonAndToolOptions(tool.getOptions()), toolArgs));
@SuppressWarnings({"rawtypes"})
Map obj = (Map) Utils.fromJSON(new ByteArrayInputStream(baos.toByteArray()));
assertTrue(obj.containsKey("version"));
assertTrue(obj.containsKey("startTime"));
assertTrue(obj.containsKey("uptime"));
assertTrue(obj.containsKey("memory"));
} catch (Exception e) {
log.error("RunExampleTool failed due to: {}; stdout from tool prior to failure: {}"
, e, baos.toString(StandardCharsets.UTF_8.name())); // nowarn
}
SolrParams params = new MapSolrParams(Collections.singletonMap("q", "*:*"));
// Query that fails due to missing credentials
exp = expectThrows(BaseHttpSolrClient.RemoteSolrException.class, () -> {
cluster.getSolrClient().query(COLLECTION, params);
});
assertEquals(401, exp.code());
assertAuthMetricsMinimums(19, 8, 8, 1, 2, 0);
assertPkiAuthMetricsMinimums(3, 3, 0, 0, 0, 0);
// Query that succeeds
GenericSolrRequest req = new GenericSolrRequest(SolrRequest.METHOD.GET, "/select", params);
req.setBasicAuthCredentials("harry", "HarryIsUberCool");
cluster.getSolrClient().request(req, COLLECTION);
assertAuthMetricsMinimums(20, 8, 8, 1, 2, 0);
assertPkiAuthMetricsMinimums(10, 10, 0, 0, 0, 0);
addDocument("harry","HarryIsUberCool","id", "5");
assertAuthMetricsMinimums(23, 11, 9, 1, 2, 0);
assertPkiAuthMetricsMinimums(14, 14, 0, 0, 0, 0);
// Reindex collection depends on streaming request that needs to authenticate against new collection
CollectionAdminRequest.ReindexCollection reindexReq = CollectionAdminRequest.reindexCollection(COLLECTION);
reindexReq.setBasicAuthCredentials("harry", "HarryIsUberCool");
cluster.getSolrClient().request(reindexReq, COLLECTION);
assertAuthMetricsMinimums(24, 12, 9, 1, 2, 0);
assertPkiAuthMetricsMinimums(15, 15, 0, 0, 0, 0);
// Validate forwardCredentials
assertEquals(1, executeQuery(params("q", "id:5"), "harry", "HarryIsUberCool").getResults().getNumFound());
assertAuthMetricsMinimums(25, 13, 9, 1, 2, 0);
assertPkiAuthMetricsMinimums(19, 19, 0, 0, 0, 0);
executeCommand(baseUrl + authcPrefix, cl, "{set-property : { forwardCredentials: true}}", "harry", "HarryIsUberCool");
verifySecurityStatus(cl, baseUrl + authcPrefix, "authentication/forwardCredentials", "true", 20, "harry", "HarryIsUberCool");
assertEquals(1, executeQuery(params("q", "id:5"), "harry", "HarryIsUberCool").getResults().getNumFound());
assertAuthMetricsMinimums(32, 20, 9, 1, 2, 0);
assertPkiAuthMetricsMinimums(19, 19, 0, 0, 0, 0);
executeCommand(baseUrl + authcPrefix, cl, "{set-property : { blockUnknown: false}}", "harry", "HarryIsUberCool");
} finally {
if (cl != null) {
HttpClientUtil.close(cl);
}
}
}
private void assertNumberOfMetrics(int num) {
MetricRegistry registry0 = cluster.getJettySolrRunner(0).getCoreContainer().getMetricManager().registry("solr.node");
assertNotNull(registry0);
assertEquals(num, registry0.getMetrics().entrySet().stream().filter(e -> e.getKey().startsWith("SECURITY")).count());
}
private QueryResponse executeQuery(ModifiableSolrParams params, String user, String pass) throws IOException, SolrServerException {
@SuppressWarnings({"rawtypes"})
SolrRequest req = new QueryRequest(params);
req.setBasicAuthCredentials(user, pass);
QueryResponse resp = (QueryResponse) req.process(cluster.getSolrClient(), COLLECTION);
assertNull(resp.getException());
assertEquals(0, resp.getStatus());
return resp;
}
private void addDocument(String user, String pass, String... fields) throws IOException, SolrServerException {
SolrInputDocument doc = new SolrInputDocument();
boolean isKey = true;
String key = null;
for (String field : fields) {
if (isKey) {
key = field;
isKey = false;
} else {
doc.setField(key, field);
}
}
UpdateRequest update = new UpdateRequest();
update.setBasicAuthCredentials(user, pass);
update.add(doc);
cluster.getSolrClient().request(update, COLLECTION);
update.commit(cluster.getSolrClient(), COLLECTION);
}
public static void executeCommand(String url, HttpClient cl, String payload,
String user, String pwd) throws Exception {
// HACK: work around for SOLR-13464...
//
// note the authz/authn objects in use on each node before executing the command,
// then wait until we see new objects on every node *after* executing the command
// before returning...
final Set<Map.Entry<String,Object>> initialPlugins
= getAuthPluginsInUseForCluster(url).entrySet();
HttpPost httpPost;
HttpResponse r;
httpPost = new HttpPost(url);
setAuthorizationHeader(httpPost, makeBasicAuthHeader(user, pwd));
httpPost.setEntity(new ByteArrayEntity(payload.getBytes(UTF_8)));
httpPost.addHeader("Content-Type", "application/json; charset=UTF-8");
r = cl.execute(httpPost);
String response = IOUtils.toString(r.getEntity().getContent(), StandardCharsets.UTF_8);
assertEquals("Non-200 response code. Response was " + response, 200, r.getStatusLine().getStatusCode());
assertFalse("Response contained errors: " + response, response.contains("errorMessages"));
Utils.consumeFully(r.getEntity());
// HACK (continued)...
final TimeOut timeout = new TimeOut(30, TimeUnit.SECONDS, TimeSource.NANO_TIME);
timeout.waitFor("core containers never fully updated their auth plugins",
() -> {
final Set<Map.Entry<String,Object>> tmpSet
= getAuthPluginsInUseForCluster(url).entrySet();
tmpSet.retainAll(initialPlugins);
return tmpSet.isEmpty();
});
}
public static Replica getRandomReplica(DocCollection coll, Random random) {
ArrayList<Replica> l = new ArrayList<>();
for (Slice slice : coll.getSlices()) {
l.addAll(slice.getReplicas());
}
Collections.shuffle(l, random);
return l.isEmpty() ? null : l.get(0);
}
//the password is 'SolrRocks'
//this could be generated everytime. But , then we will not know if there is any regression
protected static final String STD_CONF = "{\n" +
" 'authentication':{\n" +
" 'blockUnknown':'false',\n" +
" 'class':'solr.BasicAuthPlugin',\n" +
" 'credentials':{'solr':'orwp2Ghgj39lmnrZOTm7Qtre1VqHFDfwAEzr0ApbN3Y= Ju5osoAqOX8iafhWpPP01E5P+sg8tK8tHON7rCYZRRw='}},\n" +
" 'authorization':{\n" +
" 'class':'solr.RuleBasedAuthorizationPlugin',\n" +
" 'user-role':{'solr':'admin'},\n" +
" 'permissions':[{'name':'security-edit','role':'admin'}]}}";
}