/*
 * 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.ignite.client;

import java.nio.file.Paths;
import java.util.AbstractMap.SimpleEntry;
import java.util.function.Function;
import org.apache.ignite.Ignite;
import org.apache.ignite.IgniteCheckedException;
import org.apache.ignite.Ignition;
import org.apache.ignite.cache.query.SqlFieldsQuery;
import org.apache.ignite.configuration.ClientConfiguration;
import org.apache.ignite.configuration.ClientConnectorConfiguration;
import org.apache.ignite.configuration.DataRegionConfiguration;
import org.apache.ignite.configuration.DataStorageConfiguration;
import org.apache.ignite.configuration.IgniteConfiguration;
import org.apache.ignite.internal.util.typedef.internal.U;
import org.apache.ignite.ssl.SslContextFactory;
import org.junit.Before;
import org.junit.Test;

import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertTrue;

/**
 * Thin client security test.
 */
public class SecurityTest {
    /** Ignite home. */
    private static final String IGNITE_HOME = U.getIgniteHome();

    /**
     * Setup before each test.
     */
    @Before
    public void beforeEach() throws IgniteCheckedException {
        U.resolveWorkDirectory(U.defaultWorkDirectory(), "db", true);
    }

    /** Test SSL/TLS encryption. */
    @Test
    public void testEncryption() throws Exception {
        // Server-side security configuration
        IgniteConfiguration srvCfg = Config.getServerConfiguration();

        SslContextFactory sslCfg = new SslContextFactory();

        Function<String, String> rsrcPath = rsrc -> Paths.get(
            IGNITE_HOME == null ? "." : IGNITE_HOME,
            "modules",
            "core",
            "src",
            "test",
            "resources",
            rsrc
        ).toString();

        sslCfg.setKeyStoreFilePath(rsrcPath.apply("/server.jks"));
        sslCfg.setKeyStorePassword("123456".toCharArray());
        sslCfg.setTrustStoreFilePath(rsrcPath.apply("/trust.jks"));
        sslCfg.setTrustStorePassword("123456".toCharArray());

        srvCfg.setClientConnectorConfiguration(new ClientConnectorConfiguration()
            .setSslEnabled(true)
            .setSslClientAuth(true)
        );

        srvCfg.setSslContextFactory(sslCfg);

        // Client-side security configuration
        ClientConfiguration clientCfg = new ClientConfiguration().setAddresses(Config.SERVER);

        try (Ignite ignored = Ignition.start(srvCfg)) {
            boolean failed;

            try (IgniteClient client = Ignition.startClient(clientCfg)) {
                client.<Integer, String>cache(Config.DEFAULT_CACHE_NAME).put(1, "1");

                failed = false;
            }
            catch (Exception ex) {
                failed = true;
            }

            assertTrue("Client connection without SSL must fail", failed);

            // Not using user-supplied SSL Context Factory:
            try (IgniteClient client = Ignition.startClient(clientCfg
                .setSslMode(SslMode.REQUIRED)
                .setSslClientCertificateKeyStorePath(rsrcPath.apply("/client.jks"))
                .setSslClientCertificateKeyStoreType("JKS")
                .setSslClientCertificateKeyStorePassword("123456")
                .setSslTrustCertificateKeyStorePath(rsrcPath.apply("/trust.jks"))
                .setSslTrustCertificateKeyStoreType("JKS")
                .setSslTrustCertificateKeyStorePassword("123456")
                .setSslKeyAlgorithm("SunX509")
                .setSslTrustAll(false)
                .setSslProtocol(SslProtocol.TLS)
            )) {
                client.<Integer, String>cache(Config.DEFAULT_CACHE_NAME).put(1, "1");
            }

            // Using user-supplied SSL Context Factory
            try (IgniteClient client = Ignition.startClient(clientCfg
                .setSslMode(SslMode.REQUIRED)
                .setSslContextFactory(sslCfg)
            )) {
                client.<Integer, String>cache(Config.DEFAULT_CACHE_NAME).put(1, "1");
            }
        }
    }

    /** Test valid user authentication. */
    @Test
    public void testInvalidUserAuthentication() {
        Exception authError = null;

        try (Ignite ignored = igniteWithAuthentication();
             IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(Config.SERVER)
                 .setUserName("JOE")
                 .setUserPassword("password")
             )
        ) {
            client.getOrCreateCache("testAuthentication");
        }
        catch (Exception e) {
            authError = e;
        }

        assertNotNull("Authentication with invalid credentials succeeded", authError);
        assertTrue("Invalid type of authentication error", authError instanceof ClientAuthenticationException);
    }

    /** Test valid user authentication. */
    @Test
    public void testValidUserAuthentication() throws Exception {
        final String USER = "joe";
        final String PWD = "password";

        try (Ignite ignored = igniteWithAuthentication(new SimpleEntry<>(USER, PWD));
             IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(Config.SERVER)
                 .setUserName(USER)
                 .setUserPassword(PWD)
             )
        ) {
            client.getOrCreateCache("testAuthentication");
        }
    }

    /** Test user cannot create user. */
    @Test
    public void testUserCannotCreateUser() throws Exception {
        final String USER = "joe";
        final String PWD = "password";

        try (Ignite ignored = igniteWithAuthentication(new SimpleEntry<>(USER, PWD));
             IgniteClient client = Ignition.startClient(new ClientConfiguration().setAddresses(Config.SERVER)
                 .setUserName(USER)
                 .setUserPassword(PWD)
             )
        ) {
            Exception authError = null;

            try {
                client.query(
                    new SqlFieldsQuery(String.format("CREATE USER \"%s\" WITH PASSWORD '%s'", "joe2", "password"))
                ).getAll();
            }
            catch (Exception e) {
                authError = e;
            }

            assertNotNull("User created another user", authError);
        }
    }

    /**
     * @return Ignite configuration with authentication enabled
     */
    @SafeVarargs
    private static Ignite igniteWithAuthentication(SimpleEntry<String, String>... users) throws Exception {
        Ignite ignite = Ignition.start(Config.getServerConfiguration()
            .setAuthenticationEnabled(true)
            .setDataStorageConfiguration(new DataStorageConfiguration()
                .setDefaultDataRegionConfiguration(new DataRegionConfiguration().setPersistenceEnabled(true))
            )
        );

        ignite.cluster().active(true);

        for (SimpleEntry<String, String> u : users)
            createUser(u.getKey(), u.getValue());

        return ignite;
    }

    /**
     * Create user.
     */
    private static void createUser(String user, String pwd) throws Exception {
        try (IgniteClient client = Ignition.startClient(new ClientConfiguration()
            .setAddresses(Config.SERVER)
            .setUserName("ignite")
            .setUserPassword("ignite")
        )) {
            client.query(
                new SqlFieldsQuery(String.format("CREATE USER \"%s\" WITH PASSWORD '%s'", user, pwd))
            ).getAll();
        }
    }
}
