|
@@ -0,0 +1,197 @@
|
|
|
+/*
|
|
|
+ * Licensed to Elasticsearch under one or more contributor
|
|
|
+ * license agreements. See the NOTICE file distributed with
|
|
|
+ * this work for additional information regarding copyright
|
|
|
+ * ownership. Elasticsearch 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.elasticsearch.index.reindex;
|
|
|
+
|
|
|
+import org.elasticsearch.ElasticsearchStatusException;
|
|
|
+import org.elasticsearch.ElasticsearchSecurityException;
|
|
|
+import org.elasticsearch.action.ActionListener;
|
|
|
+import org.elasticsearch.action.ActionRequest;
|
|
|
+import org.elasticsearch.action.ActionResponse;
|
|
|
+import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
|
|
|
+import org.elasticsearch.action.search.SearchAction;
|
|
|
+import org.elasticsearch.action.support.ActionFilter;
|
|
|
+import org.elasticsearch.action.support.ActionFilterChain;
|
|
|
+import org.elasticsearch.action.support.WriteRequest.RefreshPolicy;
|
|
|
+import org.elasticsearch.common.bytes.BytesArray;
|
|
|
+import org.elasticsearch.common.inject.Inject;
|
|
|
+import org.elasticsearch.common.network.NetworkModule;
|
|
|
+import org.elasticsearch.common.settings.Settings;
|
|
|
+import org.elasticsearch.common.transport.TransportAddress;
|
|
|
+import org.elasticsearch.common.util.concurrent.ThreadContext;
|
|
|
+import org.elasticsearch.index.reindex.remote.RemoteInfo;
|
|
|
+import org.elasticsearch.plugins.ActionPlugin;
|
|
|
+import org.elasticsearch.plugins.Plugin;
|
|
|
+import org.elasticsearch.rest.RestStatus;
|
|
|
+import org.elasticsearch.tasks.Task;
|
|
|
+import org.elasticsearch.test.ESSingleNodeTestCase;
|
|
|
+import org.elasticsearch.threadpool.ThreadPool;
|
|
|
+import org.elasticsearch.transport.Netty4Plugin;
|
|
|
+import org.junit.Before;
|
|
|
+
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collection;
|
|
|
+import java.util.List;
|
|
|
+
|
|
|
+import static java.util.Collections.emptyMap;
|
|
|
+import static java.util.Collections.singletonList;
|
|
|
+import static java.util.Collections.singletonMap;
|
|
|
+import static org.elasticsearch.index.reindex.ReindexTestCase.matcher;
|
|
|
+import static org.hamcrest.Matchers.containsString;
|
|
|
+
|
|
|
+public class ReindexFromRemoteWithAuthTests extends ESSingleNodeTestCase {
|
|
|
+ private TransportAddress address;
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected Collection<Class<? extends Plugin>> getPlugins() {
|
|
|
+ return Arrays.asList(RetryTests.BogusPlugin.class,
|
|
|
+ Netty4Plugin.class,
|
|
|
+ ReindexFromRemoteWithAuthTests.TestPlugin.class,
|
|
|
+ ReindexPlugin.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ protected Settings nodeSettings() {
|
|
|
+ Settings.Builder settings = Settings.builder().put(super.nodeSettings());
|
|
|
+ // Weird incantation required to test with netty
|
|
|
+ settings.put("netty.assert.buglevel", false);
|
|
|
+ settings.put(NetworkModule.HTTP_ENABLED.getKey(), true);
|
|
|
+ // Whitelist reindexing from the http host we're going to use
|
|
|
+ settings.put(TransportReindexAction.REMOTE_CLUSTER_WHITELIST.getKey(), "myself");
|
|
|
+ settings.put(NetworkModule.HTTP_TYPE_KEY, Netty4Plugin.NETTY_HTTP_TRANSPORT_NAME);
|
|
|
+ return settings.build();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Before
|
|
|
+ public void setupSourceIndex() {
|
|
|
+ client().prepareIndex("source", "test").setSource("test", "test").setRefreshPolicy(RefreshPolicy.IMMEDIATE).get();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Before
|
|
|
+ public void fetchTransportAddress() {
|
|
|
+ NodeInfo nodeInfo = client().admin().cluster().prepareNodesInfo().get().getNodes().get(0);
|
|
|
+ address = nodeInfo.getHttp().getAddress().publishAddress();
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testReindexFromRemoteWithAuthentication() throws Exception {
|
|
|
+ RemoteInfo remote = new RemoteInfo("http", address.getHost(), address.getPort(), new BytesArray("{\"match_all\":{}}"), "Aladdin",
|
|
|
+ "open sesame", emptyMap());
|
|
|
+ ReindexRequestBuilder request = ReindexAction.INSTANCE.newRequestBuilder(client()).source("source").destination("dest")
|
|
|
+ .setRemoteInfo(remote);
|
|
|
+ assertThat(request.get(), matcher().created(1));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testReindexSendsHeaders() throws Exception {
|
|
|
+ RemoteInfo remote = new RemoteInfo("http", address.getHost(), address.getPort(), new BytesArray("{\"match_all\":{}}"), null, null,
|
|
|
+ singletonMap(TestFilter.EXAMPLE_HEADER, "doesn't matter"));
|
|
|
+ ReindexRequestBuilder request = ReindexAction.INSTANCE.newRequestBuilder(client()).source("source").destination("dest")
|
|
|
+ .setRemoteInfo(remote);
|
|
|
+ ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> request.get());
|
|
|
+ assertEquals(RestStatus.BAD_REQUEST, e.status());
|
|
|
+ assertThat(e.getMessage(), containsString("Hurray! Sent the header!"));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testReindexWithoutAuthenticationWhenRequired() throws Exception {
|
|
|
+ RemoteInfo remote = new RemoteInfo("http", address.getHost(), address.getPort(), new BytesArray("{\"match_all\":{}}"), null, null,
|
|
|
+ emptyMap());
|
|
|
+ ReindexRequestBuilder request = ReindexAction.INSTANCE.newRequestBuilder(client()).source("source").destination("dest")
|
|
|
+ .setRemoteInfo(remote);
|
|
|
+ ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> request.get());
|
|
|
+ assertEquals(RestStatus.UNAUTHORIZED, e.status());
|
|
|
+ assertThat(e.getMessage(), containsString("\"reason\":\"Authentication required\""));
|
|
|
+ assertThat(e.getMessage(), containsString("\"WWW-Authenticate\":\"Basic realm=auth-realm\""));
|
|
|
+ }
|
|
|
+
|
|
|
+ public void testReindexWithBadAuthentication() throws Exception {
|
|
|
+ RemoteInfo remote = new RemoteInfo("http", address.getHost(), address.getPort(), new BytesArray("{\"match_all\":{}}"), "junk",
|
|
|
+ "auth", emptyMap());
|
|
|
+ ReindexRequestBuilder request = ReindexAction.INSTANCE.newRequestBuilder(client()).source("source").destination("dest")
|
|
|
+ .setRemoteInfo(remote);
|
|
|
+ ElasticsearchStatusException e = expectThrows(ElasticsearchStatusException.class, () -> request.get());
|
|
|
+ assertThat(e.getMessage(), containsString("\"reason\":\"Bad Authorization\""));
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Plugin that demands authentication.
|
|
|
+ */
|
|
|
+ public static class TestPlugin extends Plugin implements ActionPlugin {
|
|
|
+ @Override
|
|
|
+ public List<Class<? extends ActionFilter>> getActionFilters() {
|
|
|
+ return singletonList(ReindexFromRemoteWithAuthTests.TestFilter.class);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public Collection<String> getRestHeaders() {
|
|
|
+ return Arrays.asList(TestFilter.AUTHORIZATION_HEADER, TestFilter.EXAMPLE_HEADER);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Action filter that will reject the request if it isn't authenticated.
|
|
|
+ */
|
|
|
+ public static class TestFilter implements ActionFilter {
|
|
|
+ /**
|
|
|
+ * The authorization required. Corresponds to username="Aladdin" and password="open sesame". It is the example in
|
|
|
+ * <a href="https://tools.ietf.org/html/rfc1945#section-11.1">HTTP/1.0's RFC</a>.
|
|
|
+ */
|
|
|
+ private static final String REQUIRED_AUTH = "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==";
|
|
|
+ private static final String AUTHORIZATION_HEADER = "Authorization";
|
|
|
+ private static final String EXAMPLE_HEADER = "Example-Header";
|
|
|
+ private final ThreadContext context;
|
|
|
+
|
|
|
+ @Inject
|
|
|
+ public TestFilter(ThreadPool threadPool) {
|
|
|
+ context = threadPool.getThreadContext();
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public int order() {
|
|
|
+ return Integer.MIN_VALUE;
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public <Request extends ActionRequest<Request>, Response extends ActionResponse> void apply(Task task, String action,
|
|
|
+ Request request, ActionListener<Response> listener, ActionFilterChain<Request, Response> chain) {
|
|
|
+ if (false == action.equals(SearchAction.NAME)) {
|
|
|
+ chain.proceed(task, action, request, listener);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ if (context.getHeader(EXAMPLE_HEADER) != null) {
|
|
|
+ throw new IllegalArgumentException("Hurray! Sent the header!");
|
|
|
+ }
|
|
|
+ String auth = context.getHeader(AUTHORIZATION_HEADER);
|
|
|
+ if (auth == null) {
|
|
|
+ ElasticsearchSecurityException e = new ElasticsearchSecurityException("Authentication required",
|
|
|
+ RestStatus.UNAUTHORIZED);
|
|
|
+ e.addHeader("WWW-Authenticate", "Basic realm=auth-realm");
|
|
|
+ throw e;
|
|
|
+ }
|
|
|
+ if (false == REQUIRED_AUTH.equals(auth)) {
|
|
|
+ throw new ElasticsearchSecurityException("Bad Authorization", RestStatus.FORBIDDEN);
|
|
|
+ }
|
|
|
+ chain.proceed(task, action, request, listener);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Override
|
|
|
+ public <Response extends ActionResponse> void apply(String action, Response response, ActionListener<Response> listener,
|
|
|
+ ActionFilterChain<?, Response> chain) {
|
|
|
+ chain.proceed(action, response, listener);
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|