| /* |
| * 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(); |
| } |
| } |
| |
| } |