/*
 * 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.example.realm;

import org.apache.http.HttpEntity;
import org.apache.http.message.BasicHeader;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseException;
import org.elasticsearch.client.transport.NoNodeAvailableException;
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.transport.TransportAddress;
import org.elasticsearch.common.util.concurrent.ThreadContext;
import org.elasticsearch.common.xcontent.NamedXContentRegistry;
import org.elasticsearch.common.xcontent.XContentBuilder;
import org.elasticsearch.common.xcontent.XContentParser;
import org.elasticsearch.common.xcontent.XContentType;
import org.elasticsearch.common.xcontent.json.JsonXContent;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.test.ESIntegTestCase;
import org.elasticsearch.xpack.client.PreBuiltXPackTransportClient;
import org.elasticsearch.xpack.core.XPackPlugin;

import java.util.Collection;
import java.util.Collections;
import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;

/**
 * Integration test to test authentication with the custom realm. This test is run against an external cluster that is launched
 * by maven and this test is not expected to run within an IDE.
 */
public class CustomRealmIT extends ESIntegTestCase {

    // these users are configured external to this test in the integration test setup
    private static final String[] KNOWN_USERS = new String[] { "user1", "user2", "user3" };
    private static final String PASSWORD = "changeme";

    /**
     * The client used to connect to the external cluster must have authentication credentials since the cluster is
     * protected by shield
     */
    @Override
    protected Settings externalClusterClientSettings() {
        return Settings.builder()
                .put("transport.type", "security4")
                .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, randomFrom(KNOWN_USERS))
                .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, PASSWORD)
                .build();
    }

    /**
     * The plugins to load for the transport client. Shield must be loaded for the client in order to communicate with
     * a cluster protected by Shield.
     */
    @Override
    protected Collection<Class<? extends Plugin>> transportClientPlugins() {
        return Collections.singleton(XPackPlugin.class);
    }

    public void testHttpConnectionWithNoAuthentication() throws Exception {
        try {
            Response bad = getRestClient().performRequest("GET", "/", Collections.emptyMap());
            fail("an exception should be thrown but got: " + bad.getEntity().toString());
        } catch (ResponseException e) {
            Response response = e.getResponse();
            assertThat(response.getStatusLine().getStatusCode(), is(401));
            String value = response.getHeader("WWW-Authenticate");
            assertThat(value, is("custom-challenge"));
        }
    }

    public void testHttpAuthentication() throws Exception {
        Response response = getRestClient().performRequest("GET", "/", Collections.emptyMap(), (HttpEntity) null,
                new BasicHeader(CustomRealm.USER_HEADER, randomFrom(KNOWN_USERS)),
                new BasicHeader(CustomRealm.PW_HEADER, PASSWORD));
        assertThat(response.getStatusLine().getStatusCode(), is(200));
    }

    public void testTransportClient() throws Exception {
        NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get();
        List<NodeInfo>  nodes = nodeInfos.getNodes();
        assertTrue(nodes.size() > 0);
        TransportAddress publishAddress = randomFrom(nodes).getTransport().address().publishAddress();
        String clusterName = nodeInfos.getClusterName().value();

        Settings settings = Settings.builder()
                .put("cluster.name", clusterName)
                .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, randomFrom(KNOWN_USERS))
                .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, PASSWORD)
                .build();
        try (TransportClient client = new PreBuiltXPackTransportClient(settings)) {
            client.addTransportAddress(publishAddress);
            ClusterHealthResponse response = client.admin().cluster().prepareHealth().execute().actionGet();
            assertThat(response.isTimedOut(), is(false));
        }
    }

    public void testTransportClientWrongAuthentication() throws Exception {
        NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().get();
        List<NodeInfo> nodes = nodeInfos.getNodes();
        assertTrue(nodes.size() > 0);
        TransportAddress publishAddress = randomFrom(nodes).getTransport().address().publishAddress();
        String clusterName = nodeInfos.getClusterName().value();

        Settings settings;
        if (randomBoolean()) {
            settings = Settings.builder()
                    .put("cluster.name", clusterName)
                    .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, randomFrom(KNOWN_USERS) + randomAlphaOfLength(1))
                    .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, PASSWORD)
                    .build();
        } else {
            settings = Settings.builder()
                    .put("cluster.name", clusterName)
                    .put(ThreadContext.PREFIX + "." + CustomRealm.USER_HEADER, randomFrom(KNOWN_USERS))
                    .put(ThreadContext.PREFIX + "." + CustomRealm.PW_HEADER, randomAlphaOfLengthBetween(16, 32))
                    .build();
        }

        try (TransportClient client = new PreBuiltXPackTransportClient(settings)) {
            client.addTransportAddress(publishAddress);
            client.admin().cluster().prepareHealth().execute().actionGet();
            fail("authentication failure should have resulted in a NoNodesAvailableException");
        } catch (NoNodeAvailableException e) {
            // expected
        }
    }

    public void testSettingsFiltering() throws Exception {
        Response response = getRestClient().performRequest("GET", "/_nodes/settings", Collections.emptyMap(), (HttpEntity) null,
            new BasicHeader(CustomRealm.USER_HEADER, randomFrom(KNOWN_USERS)),
            new BasicHeader(CustomRealm.PW_HEADER, PASSWORD));
        assertThat(response.getStatusLine().getStatusCode(), is(200));

        XContentParser parser = JsonXContent.jsonXContent.createParser(NamedXContentRegistry.EMPTY, response.getEntity().getContent());
        XContentParser.Token token;
        Settings settings = null;
        while ((token = parser.nextToken()) != null) {
            if (token == XContentParser.Token.FIELD_NAME && parser.currentName().equals("settings")) {
                parser.nextToken();
                XContentBuilder builder = XContentBuilder.builder(parser.contentType().xContent());
                settings = Settings.builder()
                        .loadFromSource(builder.copyCurrentStructure(parser).bytes().utf8ToString(), XContentType.JSON)
                        .build();
                break;
            }
        }
        assertThat(settings, notNullValue());

        logger.error("settings for shield.authc.realms.custom.users {}", settings.getGroups("shield.authc.realms.custom.users"));
        // custom is the name configured externally...
        assertTrue(settings.getGroups("shield.authc.realms.custom.users").isEmpty());
    }
}