/*
 * Copyright 2006-2007 Queplix Corp.
 *
 * Licensed under the Queplix Public License, Version 1.1.1 (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.queplix.com/solutions/commercial-open-source/queplix-public-license/
 *
 * 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 com.queplix.core.modules.security;

import com.queplix.core.error.GenericSystemException;
import com.queplix.core.integrator.security.AccessRightsManager;
import com.queplix.core.integrator.security.BadNameOrPasswordException;
import com.queplix.core.integrator.security.LogonSession;
import com.queplix.core.modules.security.jxb.Property;
import com.queplix.core.modules.eql.error.EQLException;
import com.queplix.core.modules.jeo.JEObjectHandler;
import com.queplix.core.modules.jeo.ejb.JEOManagerLocal;
import com.queplix.core.modules.jeo.ejb.JEOManagerLocalHome;
import com.queplix.core.modules.jeo.gen.UserObject;
import com.queplix.core.modules.jeo.gen.UserObjectHandler;
import com.queplix.core.utils.JNDINames;
import com.queplix.core.utils.cache.CacheObjectManager;
import com.queplix.core.utils.log.AbstractLogger;
import com.queplix.core.utils.log.Log;

import java.util.Properties;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.naming.NamingException;
import javax.naming.NamingEnumeration;
import javax.naming.Context;


/**
 * An implementation of LoginModule interface that authenticates 
 * user against LDAP Store
 * 
 * Does this in two steps:
 * 1) LDAP store bind with the provided credentials
 * 2) Search for credentials according to supplied filter.
 * 
 * Takes the following properties:
 * 
 * <li>providerURL - URL for the LDAP store. E.g. ldap://10.5.14.48:389  (port 636 is default for SSL)</li>
 * <li>allowEmptyPasswords - can be true or false. If false system will not allow logins with empty password </li>
 * <li>baseDN - distinguished name that specifies the context from where to search for user records. 
 *  E.g. for Active Directory domain "pro.local" it can be "CN=Users,DC=pro,DC=local"</li>
 * <li>baseFilter - specifies filter that will be used for searching users. 
 *           It accepts one parameter {0} corresponding to username. 
 *           E.g. (&amp;(SAMAccountName={0})(objectCategory=person))</li>
 * <li>usernameSuffix - is an optional property that will be used in initial user bind.
 *           E.g. value of "@pro.local" will bind user as username@pro.loca   
 * @author [AZ] Andrei Zudin
 * @version $Revision:
 */

public class LdapLoginModule 
    implements LoginModule {
    
    //  Cache object manager
    private static final CacheObjectManager com = new CacheObjectManager();
    
    // supported initialization property names
    private static final String providerURLProperty="providerURL";
    private static final String allowEmptyPasswordsProperty="allowEmptyPasswords";
    private static final String baseDNProperty="baseDN";
    private static final String baseFilterProperty="baseFilter";    
    private static final String usernameSuffixProperty="usernameSuffix";    
    
    protected final static String factoryName = "com.sun.jndi.ldap.LdapCtxFactory";
    protected final static String authType = "simple";
    protected String providerURL = null;
    protected String allowEmptyPasswords = null;   
    protected String baseDN = null;
    protected String baseFilter = null;
    protected String usernameSuffix = null;
    
    protected int searchTimeLimit = 10000;
    protected int searchScope = SearchControls.SUBTREE_SCOPE;    
    
    // Logger.
    private static AbstractLogger logger = Log.getLog(LdapLoginModule.class);

    
    /*
     * No javadoc
     * @see LoginModule#init
     */

    public void init(Property[] properties) {

        // Read property and initialize params
        if(properties != null) {
            for(int i = 0; i < properties.length; i++) {
                Property p = properties[i];
                String name = p.getName();
                String value = p.getValue();

                if(name.equalsIgnoreCase(providerURLProperty)) {
                    this.providerURL = value;
                } else if (name.equalsIgnoreCase(allowEmptyPasswordsProperty)) {
                    this.allowEmptyPasswords = value;
                } else if (name.equalsIgnoreCase(baseDNProperty)) {
                    this.baseDN = value;
                } else if (name.equalsIgnoreCase(baseFilterProperty)) {
                    this.baseFilter = value;
                } else if (name.equalsIgnoreCase(usernameSuffixProperty)) {
                    this.usernameSuffix = value;
                }
            }
        }
        
        if (    this.providerURL == null ||
                this.allowEmptyPasswords == null ||
                this.baseDN == null ||
                this.baseFilter == null ||
                this.usernameSuffix == null ) {
            
            throw new GenericSystemException("Could not initialize LdapLoginModule instance. One of required properties is missing.");
        }
        
        logger.INFO("LdapLoginModule initialized with the following properties: \n" + 
                "\t providerURL = " + this.providerURL +
                "\n\t allowEmptyPasswords = " + this.allowEmptyPasswords +
                "\n\t baseDN = " + this.baseDN +
                "\n\t baseFilter = " + this.baseFilter +
                "\n\t usernameSuffix = " + this.usernameSuffix);
    }
    
    /**
    Bind to the ldap server for authentication. 
    
    @param username
    @param credential
    @return true if the bind for authentication succeeded
    @throws NamingException
    */
   private boolean createLdapInitContext(String username, Object credential)
      throws NamingException {
       
       logger.INFO( "Creating LDAP initial context for user:" + username);

       InitialLdapContext ctx = null;
       try { 
           // get the initial context 
           ctx = constructInitialLdapContext (username, credential);
           // validate the user by binding against the userDN
           findUser (ctx, username, credential, baseDN, baseFilter);
           logger.INFO( "Initial LDAP context (" + ctx + ") for user " + username + " created successfully.");
       } finally {
           // Close context to release the connection
           if (ctx != null ) {
               ctx.close();
           }
       }
     
       return true;
   }
   
   /**
   @param ctx - the context to search from
   @param user - the input username
   @param credential - the bind credential
   @param baseDN - base DN to search the ctx from
   @param filter - the search filter string
   @return the userDN string for the successful authentication 
   @throws NamingException
   */
  protected String findUser (InitialLdapContext ctx,
     String user, Object credential, String baseDN, String filter)
     throws NamingException
  {
     SearchControls constraints = new SearchControls();
     constraints.setSearchScope(SearchControls.SUBTREE_SCOPE);
     constraints.setReturningAttributes(new String[0]);
     constraints.setTimeLimit(searchTimeLimit);

     NamingEnumeration results = null;

     Object[] filterArgs = {user};
     
     try {
         results = ctx.search(baseDN, filter, filterArgs, constraints);
     } catch (NamingException ne) {
         logger.WARN ("Error searching for user DN.", ne );
     }
     if (results.hasMore() == false) {
         throw new NamingException("Search of baseDN( " + baseDN + ") found no matches");
     }

     SearchResult sr = (SearchResult) results.next();
     String name = sr.getName();
     String userDN = null;
     if (sr.isRelative() == true)
        userDN = name + "," + baseDN;
     else
        throw new NamingException("Can't follow referal for authentication: " + name);

     results = null;
     logger.INFO("DN for user " + user + " found: " + userDN);
     
     return userDN;
  }

   
   
   private InitialLdapContext constructInitialLdapContext( String username, Object credential) 
       throws NamingException {
      
      Properties env = new Properties();

      env.setProperty(Context.INITIAL_CONTEXT_FACTORY, factoryName);
      env.setProperty(Context.SECURITY_AUTHENTICATION, authType);
      env.setProperty(Context.PROVIDER_URL, providerURL);
      env.setProperty(Context.SECURITY_PRINCIPAL, username + usernameSuffix);
      env.put(Context.SECURITY_CREDENTIALS, credential);
      
      /* Logging */
      Properties tmp = new Properties();
      tmp.putAll(env);
      tmp.setProperty(Context.SECURITY_CREDENTIALS, "***");
      logger.INFO("Loging into LDAP server, env=" + tmp.toString()); 

      return new InitialLdapContext(env, null);
   }
   
    /**
     * Authenticates user by supplied name and password in the LDAP store
     * Validate the password by creating an ldap InitialContext with the
     * 
     * @param loginName user login
     * @param password  not crypted password
     * @return UserObject
     * @throws BadNameOrPasswordException thrown if no such user exists in DB.
     */
    public UserObject doLogin (String loginName, String password) 
        throws BadNameOrPasswordException {
        
        boolean isValid = false;
        JEOManagerLocal jeoManager = getJEOManager();
        LogonSession ls = AccessRightsManager.getSystemLogonSession();
        UserObject user = null;

        if (password != null) {
           // See if this is an empty password that should be disallowed
           if (password.length() == 0) {
              // Check for an allowEmptyPasswords option
              boolean allowEmpPasswords = true;
              if (allowEmptyPasswords != null)
                 allowEmpPasswords = Boolean.valueOf(allowEmptyPasswords).booleanValue();
              if (allowEmpPasswords == false) {
                 logger.WARN( "Rejecting empty password due to allowEmptyPasswords");
                 throw new BadNameOrPasswordException();
              }
           }
           try {
              // Validate the password by trying to create an initial context
              isValid = createLdapInitContext(loginName, password);
           }
           catch (NamingException e) {
               logger.ERROR ("User " + loginName + " can not login.\n Error is: " + e.getMessage(), e);
               throw new GenericSystemException("User " + loginName + " can not login. Please contact System Administrator.", e);
           }
        }
        if (isValid) {
            try {
                JEObjectHandler userObjectHandler = UserObjectHandler.selectByLogin (jeoManager, ls, loginName);
                if (userObjectHandler != null) {
                    user = (UserObject) userObjectHandler.getJEObject();
                }
            } catch (EQLException e) {
                logger.ERROR("EQLException: " + e.getMessage(), e);
                throw new GenericSystemException("EQLException: " + e.getMessage());
            }
            if(user == null) {
                logger.WARN ("User was authenticated by LDAP Server, but can not be found in QueWeb database");
                throw new BadNameOrPasswordException();
            }
        }
        return user;
    }
    
    private static JEOManagerLocal getJEOManager() {
        return (JEOManagerLocal) com.getLocalObject(JNDINames.JEOManager, JEOManagerLocalHome.class);
    }
        
    
}
