blob: e4af79fcf14c054b821e295520fa7a17aa0f6120 [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.cloud;
import org.apache.hadoop.util.Time;
import org.apache.lucene.util.LuceneTestCase;
import org.apache.solr.SolrTestCaseJ4;
import org.apache.solr.client.solrj.impl.CloudSolrClient;
import org.apache.solr.client.solrj.impl.HttpSolrClient;
import org.apache.solr.client.solrj.impl.LBHttpSolrClient;
import org.apache.solr.client.solrj.SolrClient;
import org.apache.solr.client.solrj.SolrRequest;
import org.apache.solr.client.solrj.embedded.JettySolrRunner;
import org.apache.solr.client.solrj.request.AbstractUpdateRequest.ACTION;
import org.apache.solr.client.solrj.request.CollectionAdminRequest;
import org.apache.solr.client.solrj.request.DelegationTokenRequest;
import org.apache.solr.client.solrj.request.UpdateRequest;
import org.apache.solr.client.solrj.response.DelegationTokenResponse;
import org.apache.solr.common.SolrException.ErrorCode;
import org.apache.solr.common.SolrInputDocument;
import org.apache.solr.common.cloud.SolrZkClient;
import org.apache.solr.common.params.SolrParams;
import org.apache.solr.common.params.ModifiableSolrParams;
import static org.apache.solr.security.HttpParamDelegationTokenPlugin.USER_PARAM;
import org.apache.http.HttpStatus;
import org.apache.solr.security.HttpParamDelegationTokenPlugin;
import org.apache.solr.security.KerberosPlugin;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import java.lang.invoke.MethodHandles;
import java.util.Collections;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Test the delegation token support in the {@link org.apache.solr.security.KerberosPlugin}.
*/
@LuceneTestCase.Slow
public class TestSolrCloudWithDelegationTokens extends SolrTestCaseJ4 {
private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass());
private static final int NUM_SERVERS = 2;
private static MiniSolrCloudCluster miniCluster;
private static HttpSolrClient solrClientPrimary;
private static HttpSolrClient solrClientSecondary;
@BeforeClass
public static void startup() throws Exception {
System.setProperty("authenticationPlugin", HttpParamDelegationTokenPlugin.class.getName());
System.setProperty(KerberosPlugin.DELEGATION_TOKEN_ENABLED, "true");
System.setProperty("solr.kerberos.cookie.domain", "127.0.0.1");
miniCluster = new MiniSolrCloudCluster(NUM_SERVERS, createTempDir(), buildJettyConfig("/solr"));
JettySolrRunner runnerPrimary = miniCluster.getJettySolrRunners().get(0);
solrClientPrimary =
new HttpSolrClient.Builder(runnerPrimary.getBaseUrl().toString())
.build();
JettySolrRunner runnerSecondary = miniCluster.getJettySolrRunners().get(1);
solrClientSecondary =
new HttpSolrClient.Builder(runnerSecondary.getBaseUrl().toString())
.build();
}
@AfterClass
public static void shutdown() throws Exception {
if (miniCluster != null) {
miniCluster.shutdown();
miniCluster = null;
}
if (null != solrClientPrimary) {
solrClientPrimary.close();
solrClientPrimary = null;
}
if (null != solrClientSecondary) {
solrClientSecondary.close();
solrClientSecondary = null;
}
System.clearProperty("authenticationPlugin");
System.clearProperty(KerberosPlugin.DELEGATION_TOKEN_ENABLED);
System.clearProperty("solr.kerberos.cookie.domain");
}
private String getDelegationToken(final String renewer, final String user, HttpSolrClient solrClient) throws Exception {
DelegationTokenRequest.Get get = new DelegationTokenRequest.Get(renewer) {
@Override
public SolrParams getParams() {
ModifiableSolrParams params = new ModifiableSolrParams(super.getParams());
params.set(USER_PARAM, user);
return params;
}
};
DelegationTokenResponse.Get getResponse = get.process(solrClient);
return getResponse.getDelegationToken();
}
private long renewDelegationToken(final String token, final int expectedStatusCode,
final String user, HttpSolrClient client) throws Exception {
DelegationTokenRequest.Renew renew = new DelegationTokenRequest.Renew(token) {
@Override
public SolrParams getParams() {
ModifiableSolrParams params = new ModifiableSolrParams(super.getParams());
params.set(USER_PARAM, user);
return params;
}
@Override
public Set<String> getQueryParams() {
Set<String> queryParams = super.getQueryParams();
queryParams.add(USER_PARAM);
return queryParams;
}
};
try {
DelegationTokenResponse.Renew renewResponse = renew.process(client);
assertEquals(HttpStatus.SC_OK, expectedStatusCode);
return renewResponse.getExpirationTime();
} catch (HttpSolrClient.RemoteSolrException ex) {
assertEquals(expectedStatusCode, ex.code());
return -1;
}
}
private void cancelDelegationToken(String token, int expectedStatusCode, HttpSolrClient client)
throws Exception {
DelegationTokenRequest.Cancel cancel = new DelegationTokenRequest.Cancel(token);
try {
cancel.process(client);
assertEquals(HttpStatus.SC_OK, expectedStatusCode);
} catch (HttpSolrClient.RemoteSolrException ex) {
assertEquals(expectedStatusCode, ex.code());
}
}
private void doSolrRequest(String token, int expectedStatusCode, HttpSolrClient client)
throws Exception {
doSolrRequest(token, expectedStatusCode, client, 1);
}
private void doSolrRequest(String token, int expectedStatusCode, HttpSolrClient client, int trials)
throws Exception {
int lastStatusCode = 0;
for (int i = 0; i < trials; ++i) {
lastStatusCode = getStatusCode(token, null, null, client);
if (lastStatusCode == expectedStatusCode) {
return;
}
Thread.sleep(1000);
}
assertEquals("Did not receive expected status code", expectedStatusCode, lastStatusCode);
}
@SuppressWarnings({"rawtypes"})
private SolrRequest getAdminRequest(final SolrParams params) {
return new CollectionAdminRequest.List() {
@Override
public SolrParams getParams() {
ModifiableSolrParams p = new ModifiableSolrParams(super.getParams());
p.add(params);
return p;
}
};
}
@SuppressWarnings({"rawtypes"})
private SolrRequest getUpdateRequest(boolean commit) {
UpdateRequest request = new UpdateRequest();
if (commit) {
request.setAction(ACTION.COMMIT, false, false);
}
SolrInputDocument doc = new SolrInputDocument();
doc.addField("id", "dummy_id");
request.add(doc);
return request;
}
@SuppressWarnings({"unchecked"})
private int getStatusCode(String token, final String user, final String op, HttpSolrClient client)
throws Exception {
SolrClient delegationTokenClient;
if (random().nextBoolean()) delegationTokenClient = new HttpSolrClient.Builder(client.getBaseURL().toString())
.withKerberosDelegationToken(token)
.withResponseParser(client.getParser())
.build();
else delegationTokenClient = new CloudSolrClient.Builder(Collections.singletonList(miniCluster.getZkServer().getZkAddress()), Optional.empty())
.withLBHttpSolrClientBuilder(new LBHttpSolrClient.Builder()
.withSocketTimeout(30000).withConnectionTimeout(15000)
.withResponseParser(client.getParser())
.withHttpSolrClientBuilder(
new HttpSolrClient.Builder()
.withKerberosDelegationToken(token)
))
.build();
try {
ModifiableSolrParams p = new ModifiableSolrParams();
if (user != null) p.set(USER_PARAM, user);
if (op != null) p.set("op", op);
@SuppressWarnings({"rawtypes"})
SolrRequest req = getAdminRequest(p);
if (user != null || op != null) {
Set<String> queryParams = new HashSet<>();
if (user != null) queryParams.add(USER_PARAM);
if (op != null) queryParams.add("op");
req.setQueryParams(queryParams);
}
try {
delegationTokenClient.request(req, null);
return HttpStatus.SC_OK;
} catch (HttpSolrClient.RemoteSolrException re) {
return re.code();
}
} finally {
delegationTokenClient.close();
}
}
private void doSolrRequest(HttpSolrClient client,
@SuppressWarnings({"rawtypes"})SolrRequest request,
int expectedStatusCode) throws Exception {
try {
client.request(request);
assertEquals(HttpStatus.SC_OK, expectedStatusCode);
} catch (HttpSolrClient.RemoteSolrException ex) {
assertEquals(expectedStatusCode, ex.code());
}
}
private void doSolrRequest(HttpSolrClient client,
@SuppressWarnings({"rawtypes"})SolrRequest request, String collectionName,
int expectedStatusCode) throws Exception {
try {
client.request(request, collectionName);
assertEquals(HttpStatus.SC_OK, expectedStatusCode);
} catch (HttpSolrClient.RemoteSolrException ex) {
assertEquals(expectedStatusCode, ex.code());
}
}
private void verifyTokenValid(String token) throws Exception {
// pass with token
doSolrRequest(token, HttpStatus.SC_OK, solrClientPrimary);
// fail without token
doSolrRequest(null, ErrorCode.UNAUTHORIZED.code, solrClientPrimary);
// pass with token on other server
doSolrRequest(token, HttpStatus.SC_OK, solrClientSecondary);
// fail without token on other server
doSolrRequest(null, ErrorCode.UNAUTHORIZED.code, solrClientSecondary);
}
/**
* Test basic Delegation Token get/verify
*/
@Test
public void testDelegationTokenVerify() throws Exception {
final String user = "bar";
// Get token
String token = getDelegationToken(null, user, solrClientPrimary);
assertNotNull(token);
verifyTokenValid(token);
}
private void verifyTokenCancelled(String token, HttpSolrClient client) throws Exception {
// fail with token on both servers. If cancelToOtherURL is true,
// the request went to other url, so FORBIDDEN should be returned immediately.
// The cancelled token may take awhile to propogate to the standard url (via ZK).
// This is of course the opposite if cancelToOtherURL is false.
doSolrRequest(token, ErrorCode.FORBIDDEN.code, client, 10);
// fail without token on both servers
doSolrRequest(null, ErrorCode.UNAUTHORIZED.code, solrClientPrimary);
doSolrRequest(null, ErrorCode.UNAUTHORIZED.code, solrClientSecondary);
}
@Test
public void testDelegationTokenCancel() throws Exception {
{
// Get token
String token = getDelegationToken(null, "user", solrClientPrimary);
assertNotNull(token);
// cancel token, note don't need to be authenticated to cancel (no user specified)
cancelDelegationToken(token, HttpStatus.SC_OK, solrClientPrimary);
verifyTokenCancelled(token, solrClientPrimary);
}
{
// cancel token on different server from where we got it
String token = getDelegationToken(null, "user", solrClientPrimary);
assertNotNull(token);
cancelDelegationToken(token, HttpStatus.SC_OK, solrClientSecondary);
verifyTokenCancelled(token, solrClientSecondary);
}
}
@Test
public void testDelegationTokenCancelFail() throws Exception {
// cancel a bogus token
cancelDelegationToken("BOGUS", ErrorCode.NOT_FOUND.code, solrClientPrimary);
{
// cancel twice, first on same server
String token = getDelegationToken(null, "bar", solrClientPrimary);
assertNotNull(token);
cancelDelegationToken(token, HttpStatus.SC_OK, solrClientPrimary);
cancelDelegationToken(token, ErrorCode.NOT_FOUND.code, solrClientSecondary);
cancelDelegationToken(token, ErrorCode.NOT_FOUND.code, solrClientPrimary);
}
{
// cancel twice, first on other server
String token = getDelegationToken(null, "bar", solrClientPrimary);
assertNotNull(token);
cancelDelegationToken(token, HttpStatus.SC_OK, solrClientSecondary);
cancelDelegationToken(token, ErrorCode.NOT_FOUND.code, solrClientSecondary);
cancelDelegationToken(token, ErrorCode.NOT_FOUND.code, solrClientPrimary);
}
}
private void verifyDelegationTokenRenew(String renewer, String user)
throws Exception {
{
// renew on same server
String token = getDelegationToken(renewer, user, solrClientPrimary);
assertNotNull(token);
long now = Time.now();
assertTrue(renewDelegationToken(token, HttpStatus.SC_OK, user, solrClientPrimary) > now);
verifyTokenValid(token);
}
{
// renew on different server
String token = getDelegationToken(renewer, user, solrClientPrimary);
assertNotNull(token);
long now = Time.now();
assertTrue(renewDelegationToken(token, HttpStatus.SC_OK, user, solrClientSecondary) > now);
verifyTokenValid(token);
}
}
@Test
//commented 20-Sep-2018 @BadApple(bugUrl="https://issues.apache.org/jira/browse/SOLR-12028") // added 23-Aug-2018
public void testDelegationTokenRenew() throws Exception {
// test with specifying renewer
verifyDelegationTokenRenew("bar", "bar");
// test without specifying renewer
verifyDelegationTokenRenew(null, "bar");
}
@Test
public void testDelegationTokenRenewFail() throws Exception {
// don't set renewer and try to renew as an a different user
String token = getDelegationToken(null, "bar", solrClientPrimary);
assertNotNull(token);
renewDelegationToken(token, ErrorCode.FORBIDDEN.code, "foo", solrClientPrimary);
renewDelegationToken(token, ErrorCode.FORBIDDEN.code, "foo", solrClientSecondary);
// set renewer and try to renew as different user
token = getDelegationToken("renewUser", "bar", solrClientPrimary);
assertNotNull(token);
renewDelegationToken(token, ErrorCode.FORBIDDEN.code, "notRenewUser", solrClientPrimary);
renewDelegationToken(token, ErrorCode.FORBIDDEN.code, "notRenewUser", solrClientSecondary);
}
/**
* Test that a non-delegation-token "op" http param is handled correctly
*/
@Test
public void testDelegationOtherOp() throws Exception {
assertEquals(HttpStatus.SC_OK, getStatusCode(null, "bar", "someSolrOperation", solrClientPrimary));
}
@Test
public void testZNodePaths() throws Exception {
getDelegationToken(null, "bar", solrClientPrimary);
SolrZkClient zkClient = new SolrZkClient(miniCluster.getZkServer().getZkAddress(), 1000);
try {
assertTrue(zkClient.exists("/security/zkdtsm", true));
assertTrue(zkClient.exists("/security/token", true));
} finally {
zkClient.close();
}
}
/**
* Test HttpSolrServer's delegation token support
*/
@Test
public void testDelegationTokenSolrClient() throws Exception {
// Get token
String token = getDelegationToken(null, "bar", solrClientPrimary);
assertNotNull(token);
@SuppressWarnings({"rawtypes"})
SolrRequest request = getAdminRequest(new ModifiableSolrParams());
// test without token
final HttpSolrClient ssWoToken =
new HttpSolrClient.Builder(solrClientPrimary.getBaseURL().toString())
.withResponseParser(solrClientPrimary.getParser())
.build();
try {
doSolrRequest(ssWoToken, request, ErrorCode.UNAUTHORIZED.code);
} finally {
ssWoToken.close();
}
final HttpSolrClient ssWToken = new HttpSolrClient.Builder(solrClientPrimary.getBaseURL().toString())
.withKerberosDelegationToken(token)
.withResponseParser(solrClientPrimary.getParser())
.build();
try {
// test with token via property
doSolrRequest(ssWToken, request, HttpStatus.SC_OK);
// test with param -- should throw an exception
ModifiableSolrParams tokenParam = new ModifiableSolrParams();
tokenParam.set("delegation", "invalidToken");
expectThrows(IllegalArgumentException.class,
() -> doSolrRequest(ssWToken, getAdminRequest(tokenParam), ErrorCode.FORBIDDEN.code));
} finally {
ssWToken.close();
}
}
/**
* Test HttpSolrServer's delegation token support for Update Requests
*/
@Test
public void testDelegationTokenSolrClientWithUpdateRequests() throws Exception {
String collectionName = "testDelegationTokensWithUpdate";
// Get token
String token = getDelegationToken(null, "bar", solrClientPrimary);
assertNotNull(token);
// Tests with update request.
// Before SOLR-13921, the request without commit will fail with a NullpointerException in DelegationTokenHttpSolrClient.createMethod
// due to a missing null check in the createMethod. (When requesting a commit, the setAction method will call setParams on the
// request so there is no NPE in the createMethod.)
final HttpSolrClient scUpdateWToken = new HttpSolrClient.Builder(solrClientPrimary.getBaseURL().toString())
.withKerberosDelegationToken(token)
.withResponseParser(solrClientPrimary.getParser())
.build();
// Create collection
CollectionAdminRequest.Create create = CollectionAdminRequest.createCollection(collectionName, 1, 1);
create.process(scUpdateWToken);
try {
// test update request with token via property and commit=true
@SuppressWarnings({"rawtypes"})
SolrRequest request = getUpdateRequest(true);
doSolrRequest(scUpdateWToken, request, collectionName, HttpStatus.SC_OK);
// test update request with token via property and commit=false
request = getUpdateRequest(false);
doSolrRequest(scUpdateWToken, request, collectionName, HttpStatus.SC_OK);
} finally {
scUpdateWToken.close();
}
}
}