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

import static com.google.common.truth.Truth.assertAbout;
import static org.apache.axiom.truth.xml.XMLTruth.xml;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

import javax.xml.namespace.QName;

import org.apache.axiom.om.OMAbstractFactory;
import org.apache.axiom.om.OMElement;
import org.apache.axiom.om.OMFactory;
import org.apache.axiom.om.OMNamespace;
import org.apache.axis2.client.ServiceClient;
import org.apache.axis2.testutils.ClientHelper;
import org.apache.axis2.testutils.JettyServer;
import org.apache.axis2.kernel.http.HTTPConstants;
import org.apache.commons.io.IOUtils;
import org.apache.neethi.Policy;
import org.apache.rampart.policy.model.KerberosConfig;
import org.apache.rampart.policy.model.RampartConfig;
import org.apache.rampart.util.KerberosServer;
import org.junit.After;
import org.junit.Before;
import org.junit.ClassRule;
import org.junit.Test;

/**
 * Tests Kerberos authentication over transport binding using a Kerberos token as supporting endorsing token.
 * The test will use Apache DS Kerberos server, see {@link KerberosServer}.
 * 
 * The test is tailored for Oracle Java execution since it uses <code>com.sun.security.auth.module.Krb5LoginModule</code> JAAS login module for Kerberos authentication.
 */
public class RampartKerberosTest {

    /**
     * Java system property for setting JAAS configuration file: {@value}
     */
    public static final String JAAS_CONF_SYS_PROP = "java.security.auth.login.config";
    
    /**
     * Java system property for setting Kerberos 5 configuration file: {@value}
     */
    public static final String KRB5_CONF_SYS_PROP = "java.security.krb5.conf";
    
    /**
     * JAAS configuration file to use: {@value}
     * <p>
     * Contains Kerberos login module entries for authenticating client and server principals:
     * </p>
     */
    public static final String KERBEROS_JAAS_CONF = "src/test/resources/kerberos/jaas.conf";
    
    /**
     * Kerberos configuration file <b>template</b> to use: {@value}
     * <p>
     * Specifies the Kerberos realm and KDC server to use, the configuration must contain a <code>KDC_PORT</code> literal
     * which will be replaced with actual KDC server port.
     * </p>
     */
    public static final String KERBEROS_CONF_TEMPLATE = "src/test/resources/kerberos/krb5.conf.template";
    
    /**
     * A token literal in kerberos5 configuration file template that must be replaced with actual KDC port value: {@value}
     */
    public static final String KERBEROS_CONF_KDC_PORT_TOKEN = "KDC_PORT";
    
    @ClassRule
    public static final JettyServer server = new JettyServer("target/test-resources/rampart_service_repo", true);
    
    @ClassRule
    public static final ClientHelper clientHelper = new ClientHelper(server, "target/test-resources/rampart_client_repo") {
        @Override
        protected void configureServiceClient(ServiceClient serviceClient) throws Exception {
            int timeout = 200000;
            serviceClient.getOptions().setTimeOutInMilliSeconds(timeout);
            serviceClient.getOptions().setProperty(HTTPConstants.SO_TIMEOUT, timeout);
            serviceClient.getOptions().setProperty(HTTPConstants.CONNECTION_TIMEOUT, timeout);

            serviceClient.engageModule("addressing");
            serviceClient.engageModule("rampart");
        }
    };
    
    @ClassRule
    public static final KerberosServer kerberosServer = new KerberosServer();
    
    /**
     * Stores any original JAAS configuration set via {@link #JAAS_CONF_SYS_PROP} property to restore it after test execution.
     */
    protected String jaasConf;
    
    /**
     * Stores any original Kerberos 5 configuration set via {@link #KRB5_CONF_SYS_PROP} property to restore it after test execution.
     */
    protected String krb5Conf;
    
    private static OMElement getEchoElement() {
        OMFactory fac = OMAbstractFactory.getOMFactory();
        OMNamespace omNs = fac.createOMNamespace(
                "http://example1.org/example1", "example1");
        OMElement method = fac.createOMElement("echo", omNs);
        OMElement value = fac.createOMElement("Text", omNs);
        value.addChild(fac.createOMText(value, "Testing Rampart with WS-SecPolicy"));
        method.addChild(value);

        return method;
    }

    @Test
    public void testKerberosOverTransportKeytab() throws Exception {
        final String serviceName = "KerberosOverTransportKeytab";
        
        ServiceClient serviceClient = clientHelper.createServiceClient(serviceName, null, null);

        System.out.println("Testing WS-Sec: Kerberos scenario: " + serviceName);
                
        
        RampartConfig rampartConfig = new RampartConfig();
        KerberosConfig kerberosConfig = new KerberosConfig();
        rampartConfig.setKerberosConfig(kerberosConfig);

        kerberosConfig.setJaasContext(serviceName + "Client");        

        Policy policy = new Policy();
        policy.addAssertion(rampartConfig);                
        serviceClient.getAxisService().getPolicySubject().attachPolicyComponent(policy);
        
        //Blocking invocation
        QName operation = new QName("http://rampart.apache.org", "echo");
        OMElement echoElement = getEchoElement();
        OMElement result = serviceClient.sendReceive(operation, echoElement);
        assertAbout(xml()).that(result).ignoringNamespaceDeclarations().hasSameContentAs(echoElement);
    }
    
    @Test
    public void testKerberosOverTransportPWCB() throws Exception {
        final String serviceName = "KerberosOverTransportPWCB";
        
        ServiceClient serviceClient = clientHelper.createServiceClient(serviceName, null, null);

        System.out.println("Testing WS-Sec: Kerberos scenario: " + serviceName);

        RampartConfig rampartConfig = new RampartConfig();
        rampartConfig.setUser("alice");        
        rampartConfig.setPwCbClass(org.apache.rahas.PWCallback.class.getName());
        
        KerberosConfig kerberosConfig = new KerberosConfig();
        rampartConfig.setKerberosConfig(kerberosConfig);

        kerberosConfig.setJaasContext(serviceName + "Client");

        Policy policy = new Policy();
        policy.addAssertion(rampartConfig);        
        serviceClient.getAxisService().getPolicySubject().attachPolicyComponent(policy);
        
        //Blocking invocation
        QName operation = new QName("http://rampart.apache.org", "echo");
        OMElement echoElement = getEchoElement();
        OMElement result = serviceClient.sendReceive(operation, echoElement);
        assertAbout(xml()).that(result).ignoringNamespaceDeclarations().hasSameContentAs(echoElement);
    }
    
    @Test
    public void testKerberosDelegation() throws Exception {
        final String serviceName = "KerberosDelegation";

        ServiceClient serviceClient = clientHelper.createServiceClient(serviceName, null, null);

        System.out.println("Testing WS-Sec: Kerberos scenario: " + serviceName);
                
        
        RampartConfig rampartConfig = new RampartConfig();
        KerberosConfig kerberosConfig = new KerberosConfig();
        rampartConfig.setKerberosConfig(kerberosConfig);

        kerberosConfig.setJaasContext(serviceName + "Client");  
        kerberosConfig.setRequstCredentialDelegation(true);

        Policy policy = new Policy();
        policy.addAssertion(rampartConfig);                
        serviceClient.getAxisService().getPolicySubject().attachPolicyComponent(policy);
        
        //Blocking invocation
        QName operation = new QName("http://rampart.apache.org", "echo");
        OMElement echoElement = getEchoElement();
        OMElement result = serviceClient.sendReceive(operation, echoElement);
        assertAbout(xml()).that(result).ignoringNamespaceDeclarations().hasSameContentAs(echoElement);
    }
    
    @Before
    public void setUp() throws Exception {
        System.setProperty("sun.security.krb5.debug", "true");
        System.setProperty("sun.security.jgss.debug", "true");
        
        //configure JGSS
        krb5Conf = System.getProperty(KRB5_CONF_SYS_PROP);
        
        File krb5ConfFile = generateKerberosConf();
        System.out.println("Using Kerberos configuration file: " + krb5ConfFile.getAbsolutePath());
        System.setProperty(KRB5_CONF_SYS_PROP, krb5ConfFile.getAbsolutePath());
        
        //configure JAAS
        jaasConf = System.getProperty(JAAS_CONF_SYS_PROP);
        System.out.println("Using Kerberos JAAS configuration file: " + new File(KERBEROS_JAAS_CONF).getAbsolutePath());
        System.setProperty(JAAS_CONF_SYS_PROP, KERBEROS_JAAS_CONF);
    }

    @After
    public void tearDown() throws Exception {
        if (jaasConf != null) {
            System.setProperty(JAAS_CONF_SYS_PROP, jaasConf);
        }
        else {
            System.clearProperty(JAAS_CONF_SYS_PROP);
        }
        
        if (krb5Conf != null) {
            System.setProperty(KRB5_CONF_SYS_PROP, krb5Conf);
        }
        else {
            System.clearProperty(KRB5_CONF_SYS_PROP);
        }
    }
    
    /**
     * Generates a Kerberos configuration file (krb5.conf) out of the {@link #KERBEROS_CONF_TEMPLATE} file,
     * replacing the {@link #KERBEROS_CONF_KDC_PORT_TOKEN} with actual KDC port.
     * 
     * @return The generated Kerberos configuration file. It will be generated under the following path:
     * <code>target/tmp/{thisClassSimpleName}_krb5.conf</code>
     * 
     * @throws IOException 
     */
    protected File generateKerberosConf() throws IOException {
        File tmpDir = new File("target" + File.separator + "tmp");
        if (!tmpDir.exists() && !tmpDir.mkdirs()) {
            throw new RuntimeException("Failed to create temp directory: " + tmpDir.getAbsolutePath());
        }
        
        File krb5ConfTemplate = new File(KERBEROS_CONF_TEMPLATE);
        if (!krb5ConfTemplate.exists()) {
            throw new IllegalArgumentException("Cannot find kerberos configuration file template: " + krb5ConfTemplate.getAbsolutePath());
        }
        
        FileInputStream krb5ConfTemplateIn = null;
        String krb5ConfContent;
        try {
            krb5ConfTemplateIn = new FileInputStream(krb5ConfTemplate);
            krb5ConfContent = IOUtils.toString(krb5ConfTemplateIn);
        }
        finally {
            IOUtils.closeQuietly(krb5ConfTemplateIn);
        }
        
        if (krb5ConfContent.indexOf(KERBEROS_CONF_KDC_PORT_TOKEN) == -1) {
            throw new IllegalArgumentException(String.format("Cannot find any %s token in kerberos configuration file template: %s",
                    KERBEROS_CONF_KDC_PORT_TOKEN, krb5ConfTemplate.getAbsolutePath()));
        }
            
        krb5ConfContent = krb5ConfContent.replace(KERBEROS_CONF_KDC_PORT_TOKEN, String.valueOf(kerberosServer.getPort()));
        
        File krb5Conf = new File(tmpDir, this.getClass().getSimpleName() + "_krb5.conf");
        FileOutputStream krb5ConfOut = null;
        try {
            krb5ConfOut = new FileOutputStream(krb5Conf);
            IOUtils.write(krb5ConfContent, krb5ConfOut);
        }
        finally {
            IOUtils.closeQuietly(krb5ConfOut);
        }
        
        return krb5Conf;
    }
}
