/***********************************************************************
 * 
 *  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.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.Hashtable;

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

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


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

   private SvcConfig       m_svcConfig;
   private AuthMechConfig  m_mechConfig;

   /**
    * Password Token Class.
    */
   private static final class PwToken
   {
      private String m_username = "";
      private String m_password = "";

      /**
       * Constructor.
       * <p>
       * The format of the Pw token is as follows:
       * <p>
       * Base64.encode(new String("username\r\n" + "password\r\n"));
       *
       * @param encodedToken  Base64 encoded password token.
       * @throws IOException
       */
      public PwToken(char[] encodedToken) throws IOException, Exception
      {
         BufferedReader tokenReader = null;

         try
         {
            // Decode the token
            //
            // Unfortunately the password has to be passed to JNDI as a String. Given this,
            // we are going to go ahead and convert the token to a String to facilitate
            // the parsing operation..
            String token = new String(Base64Coder.decode(encodedToken), "UTF-8");
            tokenReader = new BufferedReader(new StringReader(token));

            // The first line contains the "username"
            m_username = tokenReader.readLine();

            // The second line contains the "password"
            m_password = tokenReader.readLine();

            // Check against zero length passwords
            if (m_password.equals(""))
            {
               m_log.warn("PwToken Constructor()- Zero length password diss-allowed");
               throw new Exception("Zero length password");
            }
         }
         finally
         {
            if (tokenReader != null)
            {
               try
               {
                  tokenReader.close();
               }
               catch (Exception e)
               {
                  // Do nothing
               }
            }
         }
      }

      /**
       * Returns the username.
       *
       * @return Username.
       */
      public final String getUsername()
      {
         return m_username;
      }

      /**
       * Returns the password.
       *
       * @return password.
       */
      public final String getPassword()
      {
         return m_password;
      }
   }

   /**
    * Constructor.
    */
   public PwdAuthenticate()
   {
      // 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(PwdAuthenticate.class);
   }

   /**
    * 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 PW Token
         PwToken pwToken = new PwToken(authReqMsg.getAuthMechToken());

         // 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()));
         }
         env.put("java.naming.ldap.derefAliases", "never");

         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());
               m_log.debug("invoke(): Exception caught instantiating DirContext", e);

               // Stop retrying
               break;
            }

            // Now search for a user with a matching surname 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
            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[] {""};
            }

            // Check if a search string has been configured
            String searchString = m_svcConfig.m_realmsInfo.getCntxtlessSearchString(authReqMsg.getRealm());
            if (searchString == null)
            {
               // Determine the search string to be utilized based on the type of directory
               // associated with the realm.
               String realmType = m_svcConfig.m_realmsInfo.getType(authReqMsg.getRealm());
               if (realmType != null)
               {
                  if (realmType.equalsIgnoreCase(RealmsInfo.eDirectoryRealm))
                     searchString = "(&(cn={0})(!(objectClass=aliasObject)))";
                  else if (realmType.equalsIgnoreCase(RealmsInfo.ActiveDirectoryRealm))
                     searchString = "(&(sAMAccountName={0})(!(objectClass=aliasObject)))";
                  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");
               }
            }

            try
            {
               int usernameMatches = 0;

               // 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[] {pwToken.getUsername()},
                                                                      controls);

                  // Enumerate through the users returned checking the password
                  while (answer.hasMore())
                  {
                     usernameMatches ++;
                     SearchResult sr = answer.next();

                     // Open a directory context for the user as a way of verifying its password
                     try
                     {
                        Hashtable env2 = new Hashtable();
                        env2.put(Context.INITIAL_CONTEXT_FACTORY, "org.bandit.ia.IAInitialCtxFactory");
                        env2.put(Realm.REALM_CONFIG_LOCATION, m_svcConfig.getSetting(SvcConfig.IdentityAbstractionConfigFile));
                        env2.put(Realm.REALM_SELECTOR, authReqMsg.getRealm());
                        env2.put(Context.SECURITY_AUTHENTICATION, "simple");
                        env2.put(Context.SECURITY_PRINCIPAL, sr.getNameInNamespace());
                        env2.put(Context.SECURITY_CREDENTIALS, pwToken.getPassword());

                        if ((new InitialDirContext(env2)) != null)
                        {
                           // The password must be valid, set the identity Id.
                           if (searchRoots[i].equals(""))
                           {
                              identId = sr.getName();
                           }
                           else
                           {
                              identId = sr.getName() + "," + searchRoots[i];
                           }
                           break;
                        }
                     }
                     catch (NamingException e)
                     {
                        m_log.warn("invoke()- NamingException: " + e.getExplanation());
                     }
                  }

                  // Try to validate the name provided if we did not find any users with matching
                  // surnames in case that the name is an FDN.
                  if (usernameMatches == 0)
                  {
                     // Open a directory context for the user as a way of verifying its password
                     try
                     {
                        Hashtable env2 = new Hashtable();
                        env2.put(Context.INITIAL_CONTEXT_FACTORY, "org.bandit.ia.IAInitialCtxFactory");
                        env2.put(Realm.REALM_CONFIG_LOCATION, m_svcConfig.getSetting(SvcConfig.IdentityAbstractionConfigFile));
                        env2.put(Realm.REALM_SELECTOR, authReqMsg.getRealm());
                        env2.put(Context.SECURITY_AUTHENTICATION, "simple");
                        env2.put(Context.SECURITY_PRINCIPAL, pwToken.getUsername());
                        env2.put(Context.SECURITY_CREDENTIALS, pwToken.getPassword());

                        if ((new InitialDirContext(env2)) != null)
                        {
                           // The password must be valid, set the identity Id.
                           identId = pwToken.getUsername();
                        }
                     }
                     catch (NamingException e)
                     {
                        m_log.warn("invoke()- NamingException: " + e.getExplanation());
                     }
                  }
               }
            }
            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;
         }

         // Check if we did not resolve the identity
         if (identId == null)
         {
            m_log.warn("invoke()- Failed to resolve identity for entity " + pwToken.getUsername());
         }
      }
      catch (NamingException e)
      {
         // Log the error
         m_log.warn("invoke()- NamingException on Proxy User: " + 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 "PwdAuthenticate";
   }
}
