/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2000-2010 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * https://glassfish.dev.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */

/*
 * @(#)LdapUserRepository.java	1.29 09/07/07
 */ 

package com.sun.messaging.jmq.jmsserver.auth.ldap;

import java.io.*;
import java.util.*;
import java.security.PrivilegedAction;
import javax.naming.Context;
import javax.naming.AuthenticationException;
import javax.naming.NamingException;
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.directory.Attribute;
import javax.naming.ldap.LdapName;
import javax.naming.ldap.Rdn;
import javax.naming.directory.Attributes;
import javax.security.auth.Subject;
import javax.security.auth.Refreshable;
import javax.security.auth.login.LoginException;
import com.sun.messaging.jmq.auth.api.FailedLoginException;
import com.sun.messaging.jmq.util.log.Logger;
import com.sun.messaging.jmq.jmsserver.Globals;
import com.sun.messaging.jmq.jmsserver.auth.AccessController;
import com.sun.messaging.jmq.auth.jaas.MQUser;
import com.sun.messaging.jmq.auth.jaas.MQGroup;
import com.sun.messaging.jmq.jmsserver.resources.BrokerResources;
import com.sun.messaging.jmq.jmsserver.net.tls.TLSProtocol;
import com.sun.messaging.jmq.auth.api.server.model.*;
import com.sun.messaging.jmq.util.Password;

/**
 *
 */
public class LdapUserRepository implements UserRepository
{
    public static final String TYPE = "ldap";

    private static boolean DEBUG = false;
	private transient static final Logger logger = Globals.getLogger();
	private transient static final BrokerResources br = Globals.getBrokerResources();

    private String authType;
    private Properties authProps = null;

    private static String INITIAL_CONTEXT_FACTORY = "com.sun.jndi.ldap.LdapCtxFactory";
    private static String DEFAULT_SSLFACTORY = "javax.net.ssl.SSLSocketFactory";
    private static String TRUST_SSLFACTORY = "com.sun.messaging.jmq.jmsserver.auth.ldap.TrustSSLSocketFactory"; 
    private static final int DEFAULT_TIMELIMIT = 180000; //milliseconds

	private static final String DN_USRFORMAT = "dn";

    private final static String PROP_SERVER_SUFFIX  =".server";
    public final static String PROP_BINDDN_SUFFIX  =".principal";
    public final static String PROP_BINDPW_SUFFIX  =".password";
    private final static String PROP_UIDATTR_SUFFIX =".uidattr";
	private final static String PROP_USRFORMAT_SUFFIX =".usrformat";
    private final static String PROP_USRFILTER_SUFFIX =".usrfilter";
    private final static String PROP_BASE_SUFFIX    =".base";
    private final static String PROP_GRPBASE_SUFFIX =".grpbase";
    private final static String PROP_GIDATTR_SUFFIX =".gidattr";
    private final static String PROP_MEMATTR_SUFFIX =".memattr";
    private final static String PROP_GRPFILTER_SUFFIX =".grpfilter";
    private final static String PROP_GRPSEARCH_SUFFIX =".grpsearch";
    private final static String PROP_TIMEOUT_SUFFIX   =".timeout";
    private final static String PROP_SSL_SUFFIX =".ssl.enabled";
    private final static String PROP_SSLFACTORY_SUFFIX =".ssl.socketfactory";
    private String server = null;
    private String bindDN = null;
    private String bindPW = null;
    private String base = null;
    private LdapName ldapbase = null;
    private String uidattr = null;
	private String usrformat = null;
    private String usrfilter = null;
    private int timelimit = DEFAULT_TIMELIMIT;
    private boolean grpsearch = true;
    private String grpbase = null;
    private String gidattr = null;
    private String memattr = null;
    private String grpfilter = null;
    private String repository = null;
    private boolean sslprotocol = false;
    private String sslfactory = null;

    public LdapUserRepository() { }

    public String getType() {
        return TYPE;
    }

    public void open(String authType, Properties authProperties,
                     Refreshable cacheData) throws LoginException {
        this.authType = authType;
        this.authProps = authProperties;
        String rep = authProps.getProperty(
              AccessController.PROP_AUTHENTICATION_PREFIX+authType+
                       AccessController.PROP_USER_REPOSITORY_SUFFIX);
        if (rep == null) {
        throw new LoginException(Globals.getBrokerResources().getKString(
             BrokerResources.X_USER_REPOSITORY_NOT_DEFINED, authType));
        }
        repository=rep;
        if (!rep.equals(TYPE)) {
        String[] args = {rep, TYPE, this.getClass().getName()};
        throw new LoginException(Globals.getBrokerResources().getKString(
             BrokerResources.X_REPOSITORY_TYPE_MISMATCH, args));
        }
        String prefix = AccessController.PROP_USER_REPOSITORY_PREFIX+rep;
        server = authProps.getProperty(prefix+PROP_SERVER_SUFFIX);
        if (server == null || server.trim().equals("")) {
        String[] args = {authType, rep, PROP_SERVER_SUFFIX};
        throw new LoginException(Globals.getBrokerResources().getKString(
             BrokerResources.X_LDAP_REPOSITORY_PROPERTY_NOT_DEFINED, args));
        }
        server = "ldap://"+server;
        bindDN = authProps.getProperty(prefix+PROP_BINDDN_SUFFIX);
        if (bindDN != null && !bindDN.trim().equals("")) {
            bindPW = authProps.getProperty(prefix+PROP_BINDPW_SUFFIX);
            int retry = 0;
            Password pw = null;
            boolean setProp = bindPW == null || bindPW.equals("");
            while (bindPW == null || bindPW.trim().equals("") && retry < 5) {
                pw = new Password();
                if (pw.echoPassword()) {
                    System.err.println(Globals.getBrokerResources().getString(BrokerResources.W_ECHO_PASSWORD));
                }
                System.err.print(Globals.getBrokerResources().getString(BrokerResources.M_ENTER_KEY_LDAP, bindDN));
                System.err.flush();

                bindPW = pw.getPassword();

                // Limit the number of times we try reading the passwd.
                // If the VM is run in the background the readLine()
                // will always return null and we'd get stuck in the loop
                retry++;
            }
            if (bindPW == null || bindPW.trim().equals("")) {
                logger.log(Logger.WARNING, BrokerResources.W_NO_LDAP_PASSWD, bindPW);
                bindDN = null;
            } else if (setProp) {
                authProps.put(prefix+PROP_BINDPW_SUFFIX, bindPW);
            }
        }
        else {
            bindDN = null;
        }
        usrformat = authProps.getProperty(prefix+PROP_USRFORMAT_SUFFIX);
        if (usrformat != null) {
            usrformat = usrformat.trim();
            if (usrformat.equals("")) {
                usrformat = null;
            } else if (!usrformat.trim().equals(DN_USRFORMAT)) {
                throw new LoginException(Globals.getBrokerResources().getKString(
                                         BrokerResources.X_UNSUPPORTED_PROPERTY_VALUE,
                                         ""+prefix+PROP_USRFORMAT_SUFFIX, usrformat));
            }
        }
        base = authProps.getProperty(prefix+PROP_BASE_SUFFIX);
        if (base != null && base.trim().equals("")) base = null;
        if (base == null && (usrformat == null || !usrformat.equals(DN_USRFORMAT))) {
        String[] args = {authType, rep, PROP_BASE_SUFFIX};
        throw new LoginException(Globals.getBrokerResources().getKString(
             BrokerResources.X_LDAP_REPOSITORY_PROPERTY_NOT_DEFINED, args));
        }
        ldapbase = null;
        if (base != null && usrformat != null && usrformat.equals(DN_USRFORMAT)) {
            try {
            ldapbase = new LdapName(base);
            } catch (Exception e) {
            throw new LoginException(e.toString());
            }
        }
        uidattr = authProps.getProperty(prefix+PROP_UIDATTR_SUFFIX);
        if (uidattr == null || uidattr.trim().equals("")) {
        String[] args = {authType, rep, PROP_UIDATTR_SUFFIX};
        throw new LoginException(Globals.getBrokerResources().getKString(
             BrokerResources.X_LDAP_REPOSITORY_PROPERTY_NOT_DEFINED, args));
        }
        usrfilter = authProps.getProperty(prefix+PROP_USRFILTER_SUFFIX);
        if (usrfilter != null && usrfilter.trim().equals("")) {
            usrfilter = null;
        }
        String tlimit = authProps.getProperty(prefix+PROP_TIMEOUT_SUFFIX);
        if (tlimit != null) {
            try {
            timelimit = Integer.parseInt(tlimit) * 1000;
            } catch (NumberFormatException e) {
            timelimit = -1;
            }
        }
        if (timelimit < 0) {
            timelimit = DEFAULT_TIMELIMIT;
        }

        String grpsrch = authProps.getProperty(prefix+PROP_GRPSEARCH_SUFFIX);
        if (grpsrch != null && grpsrch.equals("false")) {
            grpsearch = false;
        }
        if (grpsearch)  {

        grpbase = authProps.getProperty(prefix+PROP_GRPBASE_SUFFIX);
        if (grpbase == null || grpbase.trim().equals("")) {
        String[] args = {authType, rep, PROP_GRPBASE_SUFFIX};
        throw new LoginException(Globals.getBrokerResources().getKString(
             BrokerResources.X_LDAP_REPOSITORY_PROPERTY_NOT_DEFINED, args));
        }
        gidattr = authProps.getProperty(prefix+PROP_GIDATTR_SUFFIX);
        if (gidattr == null || gidattr.trim().equals("")) {
        String[] args = {authType, rep, PROP_GIDATTR_SUFFIX};
        throw new LoginException(Globals.getBrokerResources().getKString(
             BrokerResources.X_LDAP_REPOSITORY_PROPERTY_NOT_DEFINED, args));
        }
        memattr = authProps.getProperty(prefix+PROP_MEMATTR_SUFFIX);
        if (memattr == null || memattr.trim().equals("")) {
        String[] args = {authType, rep, PROP_MEMATTR_SUFFIX};
        throw new LoginException(Globals.getBrokerResources().getKString(
             BrokerResources.X_LDAP_REPOSITORY_PROPERTY_NOT_DEFINED, args));
        }
        grpfilter = authProps.getProperty(prefix+PROP_GRPFILTER_SUFFIX);
        if (grpfilter != null && grpfilter.trim().equals("")) {
            grpfilter = null;
        }

        } //if grpsearch

        String ssl = authProps.getProperty(prefix+PROP_SSL_SUFFIX);
        if (ssl != null && ssl.equals("true")) {
            sslprotocol = true;
            ssl = authProps.getProperty(prefix+PROP_SSLFACTORY_SUFFIX);
            if (ssl != null && !ssl.trim().equals("")) {
                sslfactory = ssl.trim();
            }
        }
    }

    /**
     * Find the user in the repository and compare the credential with
     * the user's  credential in database
     *
     * @param user the user name
     * @param credential password (String type) for "basic" is the password
     * @param extra null for basic, nonce if digest
     * @param matchType must be "basic"
     *        
     * @return the authenticated subject  <BR>
     *         null if no match found <BR>
     *
     * @exception LoginException
     */
    public Subject findMatch(String user, Object credential,
                             Object extra, String matchType) throws LoginException {
        if (matchType != null && matchType.equals(AccessController.AUTHTYPE_BASIC)) {
        return jmqbasicFindMatch(user, (String)credential);
        }
        String matchtyp = (matchType == null) ? "null": matchType;
        String[] args = {matchtyp, authType, getType(), AccessController.AUTHTYPE_BASIC};
        throw new LoginException(Globals.getBrokerResources().getKString(
              BrokerResources.X_UNSUPPORTED_USER_REPOSITORY_MATCHTYPE, args));
    }
      
    private Subject jmqbasicFindMatch(String user, String userpwd) throws LoginException {
        if (DEBUG) {
        logger.log(Logger.INFO, "Authenticate[basic] "+user+":"+userpwd+
                                  ((usrformat == null) ? ":":":usrformat="+usrformat));
        }
        /*
         * LDAP requires the password to be nonempty for simple authentication.
         * otherwise it automatically converts the authentication to "none"
         */
        if (userpwd == null || userpwd.trim().equals("")) {
        throw new LoginException(Globals.getBrokerResources().getKString(
                                 BrokerResources.X_PASSWORD_NOT_PROVIDED, user));
        }
        if (user == null || user.trim().equals("")) {
        throw new LoginException(Globals.getBrokerResources().getKString(
                                 BrokerResources.X_USERNAME_NOT_PROVIDED, user));
        }

        String url = server;
        if (DEBUG) {
        logger.log(Logger.INFO, "LDAP server: "+ url);
        }
        Hashtable env = new Hashtable(11);
        env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
        env.put(Context.PROVIDER_URL, url);
        env.put(Context.REFERRAL, "follow"); //see JNDI doc
        if (sslprotocol) {
        env.put(Context.SECURITY_PROTOCOL,"ssl");
        if (sslfactory != null)
            env.put("java.naming.ldap.factory.socket", sslfactory);
        }

        String dnName = null;
        boolean dnformat = false;
        if (usrformat != null && usrformat.equals(DN_USRFORMAT)) {
            dnformat = true;
            dnName = user;
            user = handleDNusrformat(user);
        } else {
            dnName = searchDN(user, env);
        }
        DirContext ctx = null;
        Exception ee = null;
        try {
            if (!dnformat) {
                logger.log(Logger.INFO, br.getKString(
                           BrokerResources.I_AUTHENTICATE_USER_AS, user, dnName));
            } else {
                logger.log(Logger.INFO, br.getKString(
                           BrokerResources.I_AUTHENTICATE_AS_USER, dnName, user));
            }
            env.put(Context.SECURITY_AUTHENTICATION, "simple");
            env.put(Context.SECURITY_PRINCIPAL, dnName);
            env.put(Context.SECURITY_CREDENTIALS, userpwd);
            try {
                ctx = new InitialDirContext(env);
                ctx.close();

                Subject subject = new Subject();
                subject.getPrincipals().add(new MQUser(user));
                try {
                    findGroups(dnName, subject);
                } catch (NamingException e) {
                    String emsg = Globals.getBrokerResources().getKString(
                    BrokerResources.X_LDAP_GROUP_SEARCH_ERROR, user+" ["+dnName+"]");
                    logger.logStack(Logger.ERROR, emsg, e);
                    throw new LoginException(emsg+":"+e.getMessage());
                }
                return subject;
            } catch (javax.naming.AuthenticationException e) {
                if (DEBUG) {
                    logger.log(Logger.INFO, e.getMessage(), e);
                }
                throw new FailedLoginException(e.getMessage());
            }

        } catch (Exception e) {
            if (e instanceof FailedLoginException) throw (FailedLoginException)e;
            if (e instanceof LoginException) throw (LoginException)e;
            String emsg = null;
            if (e instanceof NamingException) {
                emsg = ((NamingException)e).toString(true);
            } else {
                emsg = e.toString();
            }
            logger.logStack(Logger.ERROR, emsg, e);
            throw new LoginException(emsg);
        } finally {
            try {
                if (ctx != null) ctx.close();
            } catch (NamingException ne) {}
        }
    }

    private String searchDN(String user, Hashtable env) throws LoginException {

        if (bindDN != null && bindPW != null) {
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, bindDN);
        env.put(Context.SECURITY_CREDENTIALS, bindPW);
        }

        DirContext ctx = null;
        try {

            ctx = new InitialDirContext(env);
            SearchControls ctls = new SearchControls();
            ctls.setReturningAttributes(new String[0]);       // return no attrs
            ctls.setSearchScope(SearchControls.SUBTREE_SCOPE); //
            ctls.setTimeLimit(timelimit); //in milliseconds

            String filter = uidattr+"="+user;
            if (usrfilter != null) {
                filter = "(&("+usrfilter+")("+filter+"))";
            }
            if (DEBUG) {
            logger.log(Logger.INFO, "filter:"+filter+":");
            }
            NamingEnumeration enm = ctx.search(base, filter, ctls);
            int count = 0;
            String dnName = null;
            while (enm.hasMore()) {
                if (count != 0) {
                    enm.close();
                    throw new NamingException(Globals.getBrokerResources().getKString(
                                  BrokerResources.X_NOT_UNIQUE_USER, user, repository));
                }
                SearchResult sr = (SearchResult)enm.next();
                if (!sr.isRelative()) { //XXX ???
                    throw new NamingException(Globals.getBrokerResources().getKString(
                      BrokerResources.X_LDAP_SEARCH_RESULT_NOT_RELATIVE, sr.getName()));
                }
                dnName = sr.getName() + ", " + base;
                count++;
            }
            ctx.close();
            if (dnName == null) {
                throw new FailedLoginException(Globals.getBrokerResources().getKString(
                                    BrokerResources.X_DN_NOT_FOUND, user, repository));
            }

            if (DEBUG) {
                logger.log(Logger.INFO, "dn="+dnName);
            }
            return dnName;

        } catch (Exception e) {
            if (e instanceof FailedLoginException) throw (FailedLoginException)e;
            String emsg = null;
            if (e instanceof NamingException) {
                emsg = ((NamingException)e).toString(true);
            } else {
                emsg = e.toString();
            }
            logger.logStack(Logger.ERROR, emsg, e);
            throw new LoginException(emsg);
        } finally {
            try {
                if (ctx != null) ctx.close();
            } catch (NamingException ne) {}
        }
    }

   private String handleDNusrformat(String user) throws LoginException {
        String dnuser = user;
        try {
            LdapName ldapn = new LdapName(user);
            if (ldapbase != null && !ldapn.startsWith(ldapbase)) {
                throw new LoginException(
                br.getKString(br.X_DN_BASE_NOTMATCH, user, ldapbase.toString()));
            }
            Iterator itr = (ldapn.getRdns()).iterator();
            Attributes attrs = null;
            Attribute attr = null;
            Object attrv = null;
            while (itr.hasNext()) {
                attrs = (Attributes)((Rdn)itr.next()).toAttributes();
                attr = attrs.get(uidattr);
                if (attr == null) continue;
                attrv = attr.get(0);
                if (attrv == null) break;
                if (!(attrv instanceof String)) {
                    throw new LoginException(br.getKString(
                          BrokerResources.X_ATTRIBUTE_NOT_STRING_TYPE,
                          uidattr+"["+dnuser+"]", attrv.getClass().getName()));
                }
                if (((String)attrv).trim().equals("")) {
                    attrv = null;
                    break;
                }
                return (String)attrv;
            }
            throw new LoginException(br.getKString(
                  BrokerResources.X_ATTRIBUTE_NOT_FOUND_IN, uidattr, dnuser));
        } catch (Exception e) {
            if (e instanceof LoginException) throw (LoginException)e;
            String emsg = null;
            if (e instanceof NamingException) {
                emsg = ((NamingException)e).toString(true);
            } else {
                emsg = e.toString();
            }
            logger.logStack(Logger.ERROR, emsg, e);
            throw new LoginException(e.getMessage());
        }
    }

    /**
     * @return list of groups the dn is a member 
     */
    private void findGroups(String dn, Subject subject) throws NamingException {
        if (!grpsearch) return;

        Hashtable env = new Hashtable(11);
        env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
        env.put(Context.PROVIDER_URL, server);
        env.put(Context.REFERRAL, "follow");
        if (bindDN != null) {
        env.put(Context.SECURITY_AUTHENTICATION, "simple");
        env.put(Context.SECURITY_PRINCIPAL, bindDN);
        env.put(Context.SECURITY_CREDENTIALS, bindPW);
        }
        if (sslprotocol) {
        env.put(Context.SECURITY_PROTOCOL,"ssl");
        if (sslfactory != null)
            env.put("java.naming.ldap.factory.socket", sslfactory);
        }

        DirContext ctx = null;
        Exception ee = null;
        try {

        ctx = new InitialDirContext(env);
        SearchControls ctls = new SearchControls();
        String attr[] = new String[1];
        attr[0] = gidattr;
        ctls.setReturningAttributes(attr);
        ctls.setSearchScope(SearchControls.ONELEVEL_SCOPE); 
        ctls.setTimeLimit(timelimit); //in milliseconds

        String filter = memattr+"="+dn; 
        if (grpfilter != null) { 
            filter = "(&("+grpfilter+")("+filter+"))";
        }
        if (DEBUG) {
        logger.log(Logger.INFO, "filter:"+filter+":");
        }
        NamingEnumeration em = ctx.search(grpbase, filter, ctls);

        SearchResult sr = null;
        Attributes attrs = null;
        Attribute grp = null;
        String group = null;
        while (em.hasMore()) {
            sr = (SearchResult)em.next();
            if (!sr.isRelative()) {
            throw new NamingException(Globals.getBrokerResources().getKString(
                BrokerResources.X_LDAP_SEARCH_RESULT_NOT_RELATIVE, sr.getName()));
            }
            attrs = sr.getAttributes();
            if (attrs != null) {
                grp = attrs.get(gidattr);
                if (grp != null) {
                    group = (String)grp.get(0);
                    if (group != null && !group.equals("")) {
                        if (DEBUG) {
                        logger.log(Logger.INFO, "found group:"+ group+":");
                        }
                        final Subject tempSubject = subject;
                        final String tempGroup = group;
                        java.security.AccessController.doPrivileged(
                            new PrivilegedAction<Object>() {
                                public Object run(){
                                    tempSubject.getPrincipals().add(new MQGroup(tempGroup));
                                    return null;
                                }
                            }
                        );
/*
//                        subject.getPrincipals().add(new MQGroup(group));
*/                    }
                }
            }
        }
        ctx.close();
        ctx = null;
        return;

        } catch (Exception e) {
            if (e instanceof NamingException) throw (NamingException)e;
            NamingException ne = new NamingException(e.toString());
            ne.initCause(e);
            throw ne;
        } finally {
            if (ctx != null) ctx.close(); 
        }
    }

    public Refreshable getCacheData() {
       return null;  //not cache
    }

    public void close() throws LoginException { }
}
