package org.apache.catalina.realm;

import javax.naming.*;
import javax.naming.directory.*;
import javax.naming.ldap.*;

import java.util.*;

import org.apache.catalina.LifecycleException;
import org.apache.catalina.Logger;
import org.apache.catalina.Realm;
import java.security.*;


/**
 * Implementation of <b>Realm</b> using LDAP (via the JNDI interface.)
 * @author Ellen Lockhart (eBuilt Inc.)
 */
public class LDAPRealm
    extends RealmBase {
    private String     mLdapContextFactory = "com.sun.jndi.ldap.LdapCtxFactory";
    private String     mLdapServer = null;
    private String     mLdapPort = null;
    private String     mLdapDN = null;
    private String     mLdapGroupContext = null;
    private String     mLdapGroupFilter = null;
    private String     mLdapRoleAttribute = null;
    private DirContext mDirContext = null;

    /**
     * Authenticate a user based on provided credentials 
     * @return on success, a Principal object representing the user and all his/her roles.  If
     * authentication is unsuccessful, <code>null</code> is returned.
     */
    public Principal authenticate(String username, String credentials) {
        DirContext   aDirContext = null;
        boolean      aIsAuthenticated = false;
        StringBuffer aLdapDNBuf = new StringBuffer(mLdapDN);
        String       aLdapDN = null;
      
  	     if( username==null || username.equals("") 
  	         || credentials == null || credentials.equals("")) {

	         return null;
        }   
        int aVarLocation = mLdapDN.indexOf("%u");
        if (aVarLocation != -1) {
            aLdapDNBuf.replace(aVarLocation, aVarLocation + 2, username);
            aLdapDN = aLdapDNBuf.toString();
        }   
        //log("distinguished name: " + aLdapDN);
        try {
            // Create a directory context with authentication criteria
            aDirContext = initDirectoryContext(aLdapDN, credentials);
            // if no exception thrown, authentication succeeded
            aIsAuthenticated = true;
        }  
        catch (Exception e) {
            log(e.getMessage());
        }
        finally{                                        
            try {
                if (aDirContext != null)
                    aDirContext.close();
            }
            catch (NamingException e) {
                log(e.getMessage() + " closing DirContext");
            }   
        }
      
        if (aIsAuthenticated) {
   	      log( "Authenticated " + username );
            return new GenericPrincipal(this, username, credentials, getRoles(username, aLdapDN));
        }
        else
            return null;
    }

    /**
     *  Check to see if a Principal has the specified role.  If the Principal object was not
     *  created in this realm, <code>false</code> will be returned. 
     */ 
    public boolean hasRole(Principal principal, String role) {
        try {
            GenericPrincipal genericPrincipal = (GenericPrincipal) principal;

            if(genericPrincipal.getRealm() != this)
                return false;
            return genericPrincipal.hasRole(role);
        }
        catch(ClassCastException e) {
            return false;
        }
    }

    /**
     * Not implemented (always returns <code>null</null>.)
     */
    protected Principal getPrincipal(String name) {
        return null;
    }
   
    /**
     * Not implemented (always returns <code>null</null>.)
     */
    protected String getPassword(String name) {
        return null;
    }

   
    public void start() throws LifecycleException {
        try {
            if (mDirContext == null) 
                mDirContext = initDirectoryContext(null,null);
        }
        catch (NamingException e) {
            log(e.getMessage());
            throw new LifecycleException(e.getMessage());
        }
    }

    public void stop() throws LifecycleException {
        if(mDirContext != null) {
            try {
                mDirContext.close();
                mDirContext = null;
            }
            catch (NamingException e) {
                log(e.getMessage());
                throw new LifecycleException(e.getMessage());
            }
        }
    }

    // implementation-specific methods

    /**
     * Set the name of the ldap server (called by the server on startup with supplied parms.)
     */
    public void setLdapServer(String ldapServer) {
        mLdapServer = ldapServer;
        log("ldap server set to " + mLdapServer);
    }

    /**
     * Set the ldap server port (called by the server on startup with supplied parms.)
     */
    public void setLdapPort(String ldapPort) {
        mLdapPort = ldapPort;
        log("ldap port set to " + mLdapPort);
    }

    /**
     * Set the distinguished name used to locate users (called by the 
     * server on startup with supplied parms.)
     * The ldapDN string in conf/server.xml allows a substitution variable 
     * <code>%u</code> which will be replaced at runtime with the username entered for login.
     */
    public void setLdapDN(String ldapDN) {
        mLdapDN = ldapDN;
        log("ldap user distinguished name set to " + mLdapDN);
    }
   
    /**
     * Set the ldap group context under which groups (for role assignments) 
     * will be located (called by the server on startup with supplied parms.)
     */
    public void setLdapGroupContext(String groupContext) {
        mLdapGroupContext = groupContext;
        log("ldap group context set to " + mLdapGroupContext);
    }
   
    /**
     * Set the the ldap group filter (called by the server on startup with supplied parms.)
     * This gives the search parameters under the group context for locating groups a
     * user belongs to.
     * The group filter allows two substitution variables: %u (the username as entered for login)
     * and %dn (the distinguished name, as configured for ldapDN.)  
     */
    public void setLdapGroupFilter(String groupFilter) {
        mLdapGroupFilter = groupFilter;
        log("ldap group filter set to " + mLdapGroupFilter);
    }

    /**
     * Set the ldap role attribute (called by the server on startup with supplied parm.)
     * This sets the name of the attribute in an object that describes a users role.
     */
    public void setLdapRoleAttribute(String roleAttribute) {
        mLdapRoleAttribute = roleAttribute;
        log("ldap role attribute set to " + mLdapRoleAttribute);
    }
   
    public void setLdapContextFactory(String ldapContextFactory) {
        mLdapContextFactory = ldapContextFactory;
        log("ldap (jndi) context factory set to " + mLdapContextFactory);
    }
   
    /**
     * Initialize the directory context used for user role lookups.
     */
    protected DirContext initDirectoryContext(String user, String password) 
        throws NamingException {
        //log("initDirectoryContext() authenticating: " + user);   
        Hashtable env = new Hashtable();
        env.put(javax.naming.Context.INITIAL_CONTEXT_FACTORY, mLdapContextFactory);
        env.put(javax.naming.Context.PROVIDER_URL, "ldap://" + mLdapServer + ":" + mLdapPort);
        if (user != null) {
            env.put(javax.naming.Context.SECURITY_AUTHENTICATION, "simple");
            env.put(javax.naming.Context.SECURITY_PRINCIPAL, user);
            env.put(javax.naming.Context.SECURITY_CREDENTIALS, password);
        }   
        env.put(javax.naming.Context.REFERRAL, "follow");
        return new InitialDirContext(env);
    }

    /**
     * Get a list of roles for the user. Group filter may use two variables: %u (the username)
     * and %dn (the distinguished name.)  These two values are passed in as parameters to this
     * method for possible use in variable substitution on the group filter before the role
     * search is done.
     * @param username - the username as entered for login
     * @param ldapDN - the distinguished name, as configured in conf/server.xml (after variable 
     * substitution is done)
     * @return list of user roles based on group context, filter, and role attribute.
     */
    protected List getRoles(String username, String ldapDN) {
	     ArrayList aUserRoles = new ArrayList();
        SearchControls aSearchControls = new SearchControls();
      
        int aVarLocation = mLdapGroupFilter.indexOf("%u");
        StringBuffer aLdapGroupFilterBuf = new StringBuffer(mLdapGroupFilter);
        if (aVarLocation != -1) {
            aLdapGroupFilterBuf.replace(aVarLocation, aVarLocation + 2, username);
        }
      
        aVarLocation = aLdapGroupFilterBuf.toString().indexOf("%dn");
        if (aVarLocation != -1)   
            aLdapGroupFilterBuf.replace(aVarLocation, aVarLocation + 3, ldapDN);

        //log("getting roles for " + username + " with group filter " + aLdapGroupFilterBuf.toString());

        try {
            NamingEnumeration aLdapSearchResults = 
                mDirContext.search(mLdapGroupContext, aLdapGroupFilterBuf.toString(), aSearchControls);
            for (;aLdapSearchResults.hasMore(); ) {
                SearchResult aSearchResult = (SearchResult)aLdapSearchResults.next();
                Attributes aAttributes = aSearchResult.getAttributes();
                for(NamingEnumeration aIds = aAttributes.getIDs(); aIds.hasMore();) {
                    String aId = (String)aIds.next();
                    Attribute aAttribute = aAttributes.get(aId);
                    //log("Attribute ID=" + aId + " Value(s)=");
                    for(NamingEnumeration aAttribValues = aAttribute.getAll(); aAttribValues.hasMore(); ) {
                        Object aAttribValue = aAttribValues.next();
                        //log(aAttribValue.toString());
                        if(mLdapRoleAttribute.equals(aId))
                            aUserRoles.add(aAttribValue.toString());
                    }   
                }
            }
        }
        catch(NamingException e) {
            log("Error occurred getting roles for " + username + ": " + e.getMessage());
        }

        //log("Found roles " + aUserRoles + " for " + username);
        return aUserRoles;
    }

}