/*

   Derby - Class org.apache.derbyTesting.functionTests.tests.jdbcapi.LDAPAuthenticationTest

   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.derbyTesting.functionTests.tests.jdbcapi;

import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Properties;
import javax.sql.DataSource;
import junit.framework.Test;
import org.apache.derbyTesting.junit.BaseJDBCTestCase;
import org.apache.derbyTesting.junit.BaseTestSuite;
import org.apache.derbyTesting.junit.DatabasePropertyTestSetup;
import org.apache.derbyTesting.junit.JDBC;
import org.apache.derbyTesting.junit.JDBCDataSource;
import org.apache.derbyTesting.junit.SecurityManagerSetup;
import org.apache.derbyTesting.junit.TestConfiguration;

// tests that appropriate and invalid connections can be made to 
// an LDAPServer when property authenticationProvider is set to 'LDAP'.
// This test assumes at least one valid user (to be passed in) and one
// additional user (kathy / kathyS) to be setup in ou=People on the
// LDAPServer.
public class LDAPAuthenticationTest extends BaseJDBCTestCase {

    private static String ldapServer;
    private static String ldapPort;
    private static String dnString;
    private static String ldapUser; // existing valid user on ldap server
    private static String ldapPassword; // password for existing valid user on ldap server
    private static String ldapContextFactory; // optional initial context factory
        // if not passed in with -DderbyTesting.ldapContextFactory, uses sun's

    // create own policy file, so we can connect to the ldap server
    private static final String POLICY_FILE_NAME =
        "org/apache/derbyTesting/functionTests/tests/jdbcapi/LDAPTests.policy";
    
    /** Creates a new instance of the Test */
    public LDAPAuthenticationTest(String name) {
        super(name);
    }

    /**
     * Ensure all connections are not in auto commit mode.
     */
    protected void initializeConnection(Connection conn) throws SQLException {
        conn.setAutoCommit(false);
    }

    public static Test suite() {
        if (JDBC.vmSupportsJSR169())
            return new BaseTestSuite(
                "cannot run with JSR169 - missing functionality" +
                " for org.apache.derby.iapi.jdbc.AuthenticationService");

        ldapUser=getSystemProperty("derbyTesting.ldapUser");

        if (ldapUser == null || ldapUser.length() < 1)
            return new BaseTestSuite(
                "LDAPAuthenticationTest requires property " +
                "derbyTesting.ldapUser set to a valid user set up on the " +
                "ldapServer, eg: -DderbyTesting.ldapUser=CharliesPwd. In addition," +
                "test requires a user 'kathy', pwd 'kathyS' to be set up");

        ldapPassword=getSystemProperty("derbyTesting.ldapPassword");

        if (ldapPassword == null || ldapPassword.length() < 1)
            return new BaseTestSuite(
                "LDAPAuthenticationTest requires property " +
                "derbyTesting.ldapPassword set the password of a valid user set" +
                " up on the ldapServer, eg: -DderbyTesting.ldapPassword=Charlie");

        ldapServer=getSystemProperty("derbyTesting.ldapServer");

        if (ldapServer == null || ldapServer.length() < 1)
            return new BaseTestSuite(
                "LDAPAuthenticationTest requires property " +
                "derbyTesting.ldapServer set, eg: " +
                "-DderbyTesting.ldapServer=myldapserver.myorg.org");

        ldapPort=getSystemProperty("derbyTesting.ldapPort");

        if (ldapPort == null || ldapPort.length() < 1)
            return new BaseTestSuite(
                "LDAPAuthenticationTest requires property " +
                "derbyTesting.ldapPort set, eg: -DderbyTesting.ldapPort=333");

        dnString=getSystemProperty("derbyTesting.dnString");

        if (dnString == null || dnString.length() < 1)
            return new BaseTestSuite(
                "LDAPAuthenticationTest requires property " +
                "derbyTesting.dnString for setting o=, eg: " +
                "-DderbyTesting.dnString=myJNDIstring");

        ldapContextFactory=getSystemProperty("derbyTesting.ldapContextFactory");

        BaseTestSuite suite = new BaseTestSuite("LDAPAuthenticationTest");
        suite.addTest(baseSuite("LDAPAuthenticationTest:embedded",
            "testLDAPConnection"));
        suite.addTest(TestConfiguration.clientServerDecorator(
            baseSuite("LDAPAuthenticationTest:client", "testLDAPConnection")));

        // Grant ALL FILES execute, and getPolicy permissions, as well as
        // resolve/connect for the LDAP server identified with the property.
        return new SecurityManagerSetup(suite, POLICY_FILE_NAME);
    }

    public static Test baseSuite(String name, String fixture) {
        BaseTestSuite suite = new BaseTestSuite(name);
        Test test = new LDAPAuthenticationTest(fixture);
        setBaseProps(suite, test);

        // This test needs to run in a new single use database as we're setting
        // a number of properties
        return TestConfiguration.singleUseDatabaseDecorator(suite);
    }

    protected static void setBaseProps(BaseTestSuite suite, Test test)
    {
        // set some debugging at database level properties
        Properties props = new Properties();
        props.setProperty("derby.infolog.append", "true");
        props.setProperty("derby.debug.true", "AuthenticationTrace");
        // add some users. these should not be defined on the ldap server
        props.setProperty("derby.user.system", "manager");
        props.setProperty("derby.user.Jamie", "theHooligan");
        suite.addTest(new DatabasePropertyTestSetup (test, props, true));
    }

    protected void tearDown() throws Exception {
        removeSystemProperty("derby.connection.requireAuthentication");
        super.tearDown();
    }

    protected void setDatabaseProperty(
            String propertyName, String value, Connection conn) 
    throws SQLException {
        CallableStatement setDBP =  conn.prepareCall(
        "CALL SYSCS_UTIL.SYSCS_SET_DATABASE_PROPERTY(?, ?)");
        setDBP.setString(1, propertyName);
        setDBP.setString(2, value);
        setDBP.execute();
        setDBP.close();
    }

    protected String getDatabaseProperty(
            String propertyName, Connection conn) 
    throws SQLException {
        PreparedStatement getDBP =  conn.prepareStatement(
        "VALUES SYSCS_UTIL.SYSCS_GET_DATABASE_PROPERTY(?)");
        getDBP.setString(1, propertyName);
        ResultSet rs = getDBP.executeQuery();
        rs.next();
        String value = rs.getString(1);
        rs.close();
        return value;
    }    
     
    public void testLDAPConnection() throws SQLException {
        // setup 
        String dbName = TestConfiguration.getCurrent().getDefaultDatabaseName();
        DataSource ds = JDBCDataSource.getDataSource();
        Connection conn = ds.getConnection("system", "admin");
        // set the ldap properties
        setDatabaseProperty("derby.connection.requireAuthentication", "true", conn);
        setDatabaseProperty("derby.authentication.provider", "LDAP", conn);
        setDatabaseProperty("derby.authentication.server", ldapServer, conn);
        setDatabaseProperty("derby.authentication.ldap.searchBase", "o=" + dnString, conn);
        setDatabaseProperty("derby.authentication.ldap.searchFilter","(&(objectClass=inetOrgPerson)(uid=%USERNAME%))", conn);
        // java.naming.factory.initial is Context.INITIAL_CONTEXT_FACTORY
        // but using literal string here to avoid unnecessary import.
        // If the initial context factory is not provided it'll default to 
        // com.sun.jndi.ldap.LdapCtxFactory in LDAPAuthenticationSchemeImpl.
        if ((ldapContextFactory != null) && (ldapContextFactory.length() > 0))
            setDatabaseProperty("java.naming.factory.initial", ldapContextFactory, conn);
        commit();
        // shutdown the database as system, so the properties take effect
        TestConfiguration.getCurrent().shutdownDatabase();
        conn.close();

        // actual test. 
        // first attempt simple connection that should succeed
        ds = JDBCDataSource.getDataSource(dbName);
        assertLDAPDSConnectionOK(ds, ldapUser, ldapPassword);
        assertLDAPDrvMgrConnectionOK(dbName, ldapUser, ldapPassword);
        // try to get a connection for a user that has been added, but who
        // should *not* be on the ldap server; should fail
        assertInvalidLDAPDSConnectionFails(ds, "Jamie", "theHooligan");
        assertInvalidLDAPDrvMgrConnectionFails(dbName, "Jamie", "thHooligan");
        // try to get a connection using the valid ldapuser, but incorrect pwd
        assertInvalidLDAPDSConnectionFails(ds, ldapUser, ldapPassword + "ish");
        assertInvalidLDAPDrvMgrConnectionFails(dbName, ldapUser, ldapPassword + "ish");

        // set the users DN locally
        Connection conn3 = ds.getConnection(ldapUser, ldapPassword);
        String tmpString1 = "derby.user." + ldapUser;
        String tmpString2 = "uid=" + ldapUser + ",ou=People,o=" + dnString; 
        setDatabaseProperty(tmpString1, tmpString2, conn3);
        conn3.commit();
        // restart to let setting take effect
        JDBCDataSource.setBeanProperty(ds, "shutdownDatabase", "shutdown");
        // shutdown really only happens on next attempt to connect
        try {
            ds.getConnection(ldapUser, ldapPassword);
            fail("expected system shutdown resulting in 08006 error.");
        } catch (SQLException e) {
            assertSQLState("08006", e);
        }
        conn3.close();
        
        // It is now required to re-establish the name
        // of the database...
        dbName = TestConfiguration.getCurrent().getDefaultDatabaseName();
        ds = JDBCDataSource.getDataSource(dbName);
        // test again
        // connect using a valid ldap user/pwd
        assertLDAPDSConnectionOK(ds, ldapUser, ldapPassword);
        assertLDAPDrvMgrConnectionOK(dbName, ldapUser, ldapPassword);
        // try to use a not specified elsewhere, but hopefully valid ldapUser:
        // as no local DN is cached, look-up will be performed with the default
        // search filter.
        assertLDAPDSConnectionOK(ds, "kathy", "kathyS");
        assertLDAPDrvMgrConnectionOK(dbName, "kathy", "kathyS");
        // again try to get a connection for a user that has been added, but
        // should *not* be on the ldap server; should fail
        assertInvalidLDAPDSConnectionFails(ds, "Jamie", "theHooligan");
        assertInvalidLDAPDrvMgrConnectionFails(dbName, "Jamie", "theHooligan");
        // try to get a connection using the valid ldapuser, but incorrect pwd
        assertInvalidLDAPDSConnectionFails(ds, ldapUser, ldapPassword + "ish");
        assertInvalidLDAPDrvMgrConnectionFails(dbName, ldapUser, ldapPassword + "ish");
        
        assertLDAPDSConnectionOK(ds, ldapUser, ldapPassword);
        // as we don't have requireAuthentication, or the authentication provider,
        // set on system level, we can shutdown the system with any user.
        assertDSSystemShutdownOK("someuser", "somestring");
        
        // cleanup. Can't use default teardown mechanism yet as it uses default
        // user/password and we're set to LDAP Authentication at this point.
        cleanup(ds);
    }

    // cleanup gets a conn using the ldap user & pwd, so we can unset the
    // requireAuthentication so the default teardown & db cleanup can happen
    protected void cleanup(DataSource ds) throws SQLException {
        Connection conn2 = ds.getConnection(ldapUser, ldapPassword);
        setDatabaseProperty("derby.connection.requireAuthentication", "false", conn2);
        conn2.commit();
        JDBCDataSource.setBeanProperty(ds, "shutdownDatabase", "shutdown");
        // shutdown only really happens on attempting to get a connection;
        // this exercise is needed so the DatabasePropertySetup.teardown() can work.
        // Getting a 08006 is not really part of the test, but would be good
        // to be warned of if it happens.
        try {
            ds.getConnection(ldapUser, ldapPassword);
            fail("expected system shutdown resulting in 08006 error.");
        } catch (SQLException e) {
            assertSQLState("08006", e);
        }
        conn2.close();
    }

    public void assertLDAPDSConnectionOK(
            DataSource ds, String user, String password)
    throws SQLException {
        Connection conn = ds.getConnection(user, password);
        assertNotNull(conn);
        conn.close();
    }

    protected void assertLDAPDrvMgrConnectionOK(
            String dbName, String user, String password)
    throws SQLException
    {
        String url = TestConfiguration.getCurrent().getJDBCUrl(dbName);
        Connection conn = DriverManager.getConnection(url, user, password);
        assertNotNull(conn);
        conn.close();
    }

    public void assertInvalidLDAPDSConnectionFails(
            DataSource ds, String user, String password)
    throws SQLException {
        try {
            ds.getConnection(user, password);
            fail("expected invalid connection error (for DataSource)");
        } catch (SQLException se) {
            assertSQLState("08004", se);
        }
    }

    protected void assertInvalidLDAPDrvMgrConnectionFails(
            String dbName, String user, String password)
    throws SQLException
    {
        String url = TestConfiguration.getCurrent().getJDBCUrl(dbName);
        try {
            DriverManager.getConnection(url, user, password).close();
            fail("expected invalid connection error (for DataSource)");
        } catch (SQLException se) {
            assertSQLState("08004", se);
        }
    }

    protected void assertDSSystemShutdownOK(
            String user, String password)
    throws SQLException {
        DataSource ds;
        if (usingEmbedded())
        {
            // we cannot use JDBCDataSource.getDataSource() (which uses the
            // default database name), unless we specifically clear the 
            // databaseName. Otherwise, only the database will be shutdown.
            // The alternative is to use jDBCDataSource.getDatasource(dbName),
            // where dbName is an empty string - this will in the current code
            // be interpreted as a system shutdown.
            ds = JDBCDataSource.getDataSource();
            JDBCDataSource.clearStringBeanProperty(ds, "databaseName");
        }
        else 
        {
            // With client, we cannot user clearStringBeanProperty on the  
            // databaseName, that will result in error 08001 - 
            // Required DataSource property databaseName not set.
            // So, we pass an empty string as databaseName, which the current
            // code interprets as a system shutdown.
            ds = JDBCDataSource.getDataSource("");
        }

        JDBCDataSource.setBeanProperty(ds, "shutdownDatabase", "shutdown");
        try {
            ds.getConnection(user, password);
            fail("expected system shutdown resulting in XJ015 error");
        } catch (SQLException e) {
            // expect XJ015, system shutdown, on successful shutdown
            assertSQLState("XJ015", e);
        }
    }
}
