/* Copyright (c) 2001-2019, The HSQL Development Group
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 * Redistributions of source code must retain the above copyright notice, this
 * list of conditions and the following disclaimer.
 *
 * Redistributions in binary form must reproduce the above copyright notice,
 * this list of conditions and the following disclaimer in the documentation
 * and/or other materials provided with the distribution.
 *
 * Neither the name of the HSQL Development Group nor the names of its
 * contributors may be used to endorse or promote products derived from this
 * software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
 * ARE DISCLAIMED. IN NO EVENT SHALL HSQL DEVELOPMENT GROUP, HSQLDB.ORG,
 * OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */


package org.hsqldb.auth;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
import java.util.Set;

import org.hsqldb.lib.FrameworkLogger;

/**
 * Delegates authentication decisions, and optionally determination of user
 * roles and schema, to a different HyperSQL catalog, which may be in the same
 * JVM or remote.
 *
 * For now, at least, this class uses DriverManager to make the ephemeral
 * database connections.
 *
 * @see AuthFunctionBean
 * @author Blaine Simpson (blaine dot simpson at admc dot com)
 * @since 2.0.1
 */
public class HsqldbSlaveAuthBean implements AuthFunctionBean {
    private static FrameworkLogger logger =
            FrameworkLogger.getLog(HsqldbSlaveAuthBean.class);

    private String masterJdbcUrl, validationUser, validationPassword;
    private boolean delegateRolesSchema = true;
    protected boolean initialized;

    /**
     * Use this method and setValidationPassword if you want access to the
     * master database to be verified upon instance initialization.
     */
    public void setValidationUser(String validationUser) {
        this.validationUser = validationUser;
    }

    /**
     * Use this method and setValidationUser if you want access to the
     * master database to be verified upon instance initialization.
     */
    public void setValidationPassword(String validationPassword) {
        this.validationPassword = validationPassword;
    }

    public void setMasterJdbcUrl(String masterJdbcUrl) {
        this.masterJdbcUrl = masterJdbcUrl;
    }

    /**
     * Defaults to true.
     *
     * Whether roles and initial schema for the new session will be determined
     * by what they are for this user in the master database.
     */
    public void setDelegateRolesSchema(boolean doDelegateRolesSchema) {
        delegateRolesSchema = doDelegateRolesSchema;
    }

    public HsqldbSlaveAuthBean() {
        // Intentionally empty
    }

    /**
     * @throws IllegalStateException if any required setting has not been set.
     * @throws SQLException if properties 'validationUser' and
     *    'validationPassword' have been set, but we fail to connect to the
     *    master database.
     */
    public void init() throws SQLException {
        if (masterJdbcUrl == null) {
            throw new IllegalStateException(
                    "Required property 'masterJdbcUrl' not set");
        }
        if (validationUser != null || validationPassword != null) {
            if (validationUser == null || validationPassword == null) {
                throw new IllegalStateException(
                        "If you set one property of 'validationUser' or "
                        + "'validationPassword', then you must set both.");
            }
            Connection c = null;
            SQLException problem = null;
            try {
                c = DriverManager.getConnection(
                        masterJdbcUrl, validationUser, validationPassword);
            } catch (SQLException se) {
                logger.error("Master/slave Connection validation failure", se);
                problem = se;  // Just indicates to let the original exception
                  // percolate through in the finally block, to prevent an
                  // exception in the finally block from obscuring the ultimate
                  // cause of the problem.
            } finally {
                if (c != null) try {
                    c.close();
                    c = null;  // Encourage GC
                } catch (SQLException nestedSe) {
                    logger.error(
                            "Failed to close test master/slave Connection",
                            nestedSe);
                    if (problem == null) {
                        throw nestedSe;
                    }
                }
            }
        }
        initialized = true;
    }

    /**
     * @see AuthFunctionBean#authenticate(String, String)
     */
    public String[] authenticate(String userName, String password)
            throws DenyException {
        if (!initialized) {
            throw new IllegalStateException(
                "You must invoke the 'init' method to initialize the "
                + HsqldbSlaveAuthBean.class.getName() + " instance.");
        }
        Connection c = null;
        try {
            c = DriverManager.getConnection(masterJdbcUrl, userName, password);
            if (delegateRolesSchema) {
                Set<String> schemaAndRoles = AuthUtils.getEnabledRoles(c);
                String schemaOnMaster = AuthUtils.getInitialSchema(c);
                if (schemaOnMaster != null) {
                    schemaAndRoles.add(schemaOnMaster);
                }
                logger.finer("Slave delegating schema+roles: "
                        + schemaAndRoles);
                return schemaAndRoles.toArray(new String[0]);
            }
            return null;
        } catch (SQLException se) {
            throw new DenyException();
        } finally {
            if (c != null) try {
                c.close();
                c = null;  // Encourage GC
            } catch (SQLException nestedSe) {
                logger.severe(
                        "Failed to close master/slave Connection", nestedSe);
            }
        }
    }
}
