/***********************************************************************
 * 
 *  Copyright (C) 2006 Novell, Inc. All Rights Reserved.
 *
 *  This library is free software; you can redistribute it and/or
 *  modify it under the terms of the GNU Lesser General Public
 *  License as published by the Free Software Foundation; version 2.1
 *  of the License.
 *
 *  This library is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *  Library Lesser General Public License for more details.
 *
 *  You should have received a copy of the GNU Lesser General Public
 *  License along with this library; if not, Novell, Inc.
 * 
 *  To contact Novell about this file by physical or electronic mail, 
 *  you may find current contact information at www.novell.com.
 * 
 *  Author: Juan Carlos Luciani <jluciani@novell.com>
 *  
 ***********************************************************************/

package com.novell.casa.authtoksvc;

import java.io.Serializable;
import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.ServiceUnavailableException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchResult;
import javax.naming.directory.SearchControls;

import org.ietf.jgss.GSSContext;
import org.ietf.jgss.GSSCredential;
import org.ietf.jgss.GSSException;
import org.ietf.jgss.GSSManager;
import org.ietf.jgss.GSSName;
import org.ietf.jgss.Oid;

import org.bandit.util.config.Realm;
import org.apache.log4j.Logger;

/**
 * Krb5Authenticate Class.
 * <p>
 * This class implementes an authentication mechanism for
 * the processing of authentication requests utilizing a
 * Kerberos5 token.
 * 
 */
public final class Krb5Authenticate implements AuthMechanism, Serializable
{
   private static Logger m_log = null;

   private SvcConfig       m_svcConfig;
   private AuthMechConfig  m_mechConfig;

   /*
    * GSS Long Lived variables
    */
   protected GSSManager       m_manager;
   protected Oid              m_krb5;
   protected GSSName          m_svcName;
   protected GSSCredential    m_credential;

   /**
    * Krb5 Token Class.
    */
   private final class Krb5Token
   {
      private String m_principalName = "";

      /**
       * Constructor.
       * <p>
       * The format of the Krb5 token is as follows:
       * <p>
       * Base64.encode(GSS-API Token data));
       *
       * @param encodedToken  Base64 encoded GSS-API token.
       * @param parent        Associated Krb5Authenticate object.
       * @throws Exception
       */
      public Krb5Token(char[] encodedToken, Krb5Authenticate parent) throws Exception
      {
         // Decode the token
         byte[] tokenBytes = Base64Coder.decode(encodedToken);

         try
         {
            // Create a context and validate the token
            GSSContext context = parent.m_manager.createContext(parent.m_credential);
            System.err.println("tokenLength = " + tokenBytes.length);
            context.acceptSecContext(tokenBytes, 0, tokenBytes.length);

            // Save the principal name of the authenticated entity
            GSSName principalName = context.getSrcName();
            m_principalName = principalName.toString();

            // Clean up
            context.dispose();
         }
         catch (GSSException e)
         {
            m_log.warn("Krb5Token Constructor()- GSS Exception caught: " + e.getLocalizedMessage());
            throw new Exception("Authentication Failure", e);
         }
      }

      /**
       * Returns the name of the authenticated principal.
       *
       * @return Principal name.
       */
      public String getPrincipalName()
      {
         return m_principalName;
      }
   }

   /**
    * Constructor.
    */
   public Krb5Authenticate()
   {
      // Nothing to do at this time
   }

   /**
    * Initialize the mechanism.
    *
    * @param svcConfig  Service configuration object.
    * @param mechConfig Authentication mechanism configuration object.
    * @throws Exception
    */
   public final void init(SvcConfig svcConfig, AuthMechConfig mechConfig) throws Exception
   {
      m_svcConfig = svcConfig;
      m_mechConfig = mechConfig;

      m_log = Logger.getLogger(Krb5Authenticate.class);

      String servicePrincipal = mechConfig.getSetting(AuthMechConfig.Krb5ServicePrincipalName);
      if (servicePrincipal != null)
      {
         try
         {
            // Make sure that the system property "javax.security.auth.useSubjectCredsOnly"
            // is set to "false" to avoid having to utilize JAAS (at least if using IBM's JVM)
            System.setProperty("javax.security.auth.useSubjectCredsOnly", "false");

            // Initalize our GSS variables
            //
            // Get an instance of the default GSSManager
            m_manager = GSSManager.getInstance();

            // Create an OID specifying the Krb5 mechanism
            m_krb5 = new Oid("1.2.840.113554.1.2.2");

            // Create our host based service name
            m_svcName = m_manager.createName(servicePrincipal,
                                             GSSName.NT_HOSTBASED_SERVICE,
                                             m_krb5);

            // Now acquire our credentials
            m_credential = m_manager.createCredential(m_svcName,
                                                      GSSCredential.INDEFINITE_LIFETIME,
                                                      m_krb5,
                                                      GSSCredential.ACCEPT_ONLY);
         }
         catch (GSSException e)
         {
            m_log.debug("Constructor()- GSS Exception caught: " + e.getLocalizedMessage());
            throw new Exception("Failed to instantiate needed GSS objects", e);
         }
      }
      else
      {
         m_log.warn("Constructor()- Service Principal Name not configured");
         throw new Exception("Service Principal Name not configured");
      }
   }

   /**
    * invoke() implementation.
    *
    * @param authReqMsg Authentication request message.
    * @return Id of the authenticated entity.
    * @throws Exception
    */
   public final String invoke(AuthReqMsg authReqMsg) throws Exception
   {
      String identId = null;

      try
      {
         m_log.debug("invoke()");

         // Now parse the Kerberos Token
         Krb5Token krb5Token = new Krb5Token(authReqMsg.getAuthMechToken(), this);

         // Open a directory context and use it to identify the users
         // associated with the specified surname.
         Hashtable env = new Hashtable();
         env.put(Context.INITIAL_CONTEXT_FACTORY, "org.bandit.ia.IAInitialCtxFactory");
         env.put(Realm.REALM_CONFIG_LOCATION, m_svcConfig.getSetting(SvcConfig.IdentityAbstractionConfigFile));
         env.put(Realm.REALM_SELECTOR, authReqMsg.getRealm());
         if (m_svcConfig.m_realmsInfo.proxyCredentialsInRealmFile(authReqMsg.getRealm()) == false)
         {
            env.put(Context.SECURITY_PRINCIPAL, m_svcConfig.m_realmsInfo.proxyUsernameCredential(authReqMsg.getRealm()));
            env.put(Context.SECURITY_CREDENTIALS, m_svcConfig.m_realmsInfo.proxyPasswordCredential(authReqMsg.getRealm()));
         }

         int retries = 3;
         while (retries != 0)
         {
            // Instantiate DirContext watching for an exception since it
            // would be an indication that we should not retry the
            // operation.
            DirContext ctx;
            try
            {
               ctx = new InitialDirContext(env);
            }
            catch (Exception e)
            {
               m_log.warn("invoke(): Exception caught instantiating DirContext, msg = " + e.getMessage());

               // Stop retrying
               break;
            }

            // Now search for a user with a matching  kerberos principal name
            // watching for a service unavailable exception as an indication that we
            // should retry the operation.
            //
            // Set up a search control so that the search is scoped to the sub-tree
            try
            {
               SearchControls controls = new SearchControls();
               controls.setSearchScope(SearchControls.SUBTREE_SCOPE);

               // Obtain the search roots
               String[] searchRoots = m_svcConfig.m_realmsInfo.getSearchRoots(authReqMsg.getRealm());
               if (searchRoots == null)
               {
                  // No configured search roots
                  searchRoots = new String[] {""};
               }

               // Determine the search string to be utilized based on the type of directory
               // associated with the realm.
               String searchString;
               String realmType = m_svcConfig.m_realmsInfo.getType(authReqMsg.getRealm());
               if (realmType != null)
               {
                  if (realmType.equalsIgnoreCase(RealmsInfo.eDirectoryRealm))
                     searchString = "(krbPrincipalName={0})";
                  else if (realmType.equalsIgnoreCase(RealmsInfo.ActiveDirectoryRealm))
                     searchString = "(userPrincipalName={0})";
                  else
                  {
                     m_log.warn("invoke()- Unsupported realm type " + realmType);
                     throw new Exception("Realm configuration error");
                  }
               }
               else
               {
                  m_log.warn("invoke()- Failed to obtain realm type for realm " + authReqMsg.getRealm());
                  throw new Exception("Realm configuration error");
               }

               // Go through the search roots stopping if the identity is resolved.
               for (int i = 0; i < searchRoots.length && identId == null; i++)
               {
                  // Perform search for this root
                  NamingEnumeration<SearchResult> answer = ctx.search(searchRoots[i],
                                                                      searchString,
                                                                      new String[] {krb5Token.getPrincipalName()},
                                                                      controls);

                  // Proceed based on the result of the search
                  if (answer.hasMore())
                  {
                     // The search succeeded, set the identity id.
                     SearchResult sr = answer.next();
                     if (searchRoots[i].equals(""))
                     {
                        identId = sr.getNameInNamespace();
                     }
                     else
                     {
                        identId = sr.getName() + "," + searchRoots[i];
                     }
                  }
                  else
                  {
                     m_log.info("invoke()- No matching identity entities found");
                  }
               }
            }
            catch (ServiceUnavailableException e)
            {
               m_log.warn("invoke()- ServiceUnavailable exception caught looking up attributes, msg = " + e.getMessage());

               // Retry the operation
               retries --;
               continue;
            }

            // No need to retry
            break;
         }
      }
      catch (NamingException e)
      {
         // Log the error
         m_log.warn("invoke()- NamingException: " + e.toString());
      }
      catch (Exception e)
      {
         m_log.warn("invoke()- Exception: " + e.toString());
      }

      // Return the authentication result
      return identId;
   }

   /**
    * Return the mechanism id.
    *
    * @return Mechanism id.
    */
   public final String getId()
   {
      return "Krb5Authenticate";
   }
}
