/*
 * RHQ Management Platform
 * Copyright (C) 2005-2008 Red Hat, Inc.
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 2 of the License.
 *
 * This program 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 General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
 */
package org.rhq.enterprise.server.core.jaas;

import java.security.acl.Group;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.Set;
import java.util.Map.Entry;

import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import javax.naming.ldap.InitialLdapContext;
import javax.security.auth.login.LoginException;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.jboss.security.SimpleGroup;
import org.jboss.security.auth.spi.UsernamePasswordLoginModule;

import org.rhq.core.domain.auth.Subject;
import org.rhq.core.domain.resource.group.LdapGroup;
import org.rhq.core.domain.util.PageControl;
import org.rhq.core.domain.util.PageList;
import org.rhq.enterprise.server.auth.SubjectManagerLocal;
import org.rhq.enterprise.server.authz.RoleManagerLocal;
import org.rhq.enterprise.server.resource.group.LdapGroupManagerLocal;
import org.rhq.enterprise.server.util.LookupUtil;
import org.rhq.enterprise.server.util.security.UntrustedSSLSocketFactory;

/**
 * A login module for authenticating against an LDAP directory server using JNDI, based on configuration properites LDAP
 * module options:
 *
 * <pre>
 * java.naming.factory.initial
 *   This should be set to the fully qualified class name of the initial
 *   context factory.  Defaults to com.sun.jndi.ldap.LdapCtxFactory
 *
 * java.naming.provider.url
 *   The full url to the LDAP server.  Defaults to ldap://localhost.  Port
 *   389 is used unless java.naming.security.protocol is set to ssl.  In
 *   that case port 636 is used.
 *
 * java.naming.security.protocol
 *   Set this to 'ssl' to enable secure communications.  If the
 *   java.naming.provider.url is not set, it will be initialized with
 *   port 636.
 *
 * LoginProperty
 *   The LDAP property that contains the user name.  Defaults to cn.  If
 *   multiple matches are found, the first entry found is used.
 *
 * Filter
 *   Any additional filters to apply when doing the LDAP search.  Useful
 *   if you only want to authenticate against a group of users that have
 *   a given LDAP property set.  (CAMUser=true for example)
 *
 * BaseDN
 *   The base of the LDAP tree we are authenticating against.  For example:
 *   o=Covalent Technologies,c=US.  Multiple LDAP bases can be used by
 *   seperating each DN by ';'
 *
 * BindDN
 *   The BindDN to use if the LDAP server does not support anonymous searches.
 *
 * BindPW
 *   The password to use if the LDAP server does not support anonymous
 *   searches
 * </pre>
 */

public class LdapLoginModule extends UsernamePasswordLoginModule {
    private Log log = LogFactory.getLog(LdapLoginModule.class);

    LdapGroupManagerLocal ldapManager = LookupUtil.getLdapGroupManager();

    // The delimimiter to use when specifiying multiple BaseDN's.
    private static final String BASEDN_DELIMITER = ";";

    /**
     * Creates a new {@link LdapLoginModule} object.
     */
    public LdapLoginModule() {
    }

    /**
     * @see org.jboss.security.auth.spi.UsernamePasswordLoginModule#getUsersPassword()
     */
    protected String getUsersPassword() throws LoginException {
        return "";
    }

    /**
     * @see org.jboss.security.auth.spi.AbstractServerLoginModule#getRoleSets()
     */
    protected Group[] getRoleSets() throws LoginException {
        SimpleGroup roles = new SimpleGroup("Roles");

        //roles.addMember( new SimplePrincipal( "some user" ) );
        Group[] roleSets = { roles };
        return roleSets;
    }

    /**
     * @see org.jboss.security.auth.spi.UsernamePasswordLoginModule#validatePassword(java.lang.String,java.lang.String)
     */
    protected boolean validatePassword(String inputPassword, String expectedPassword) {
        // Load our LDAP specific properties
        Properties env = getProperties();

        // Load the BaseDN
        String baseDN = (String) options.get("BaseDN");
        if (baseDN == null) {
            // If the BaseDN is not specified, log an error and refuse the login attempt
            log.info("BaseDN is not set, refusing login");
            return false;
        }

        // Many LDAP servers allow bind's with an emtpy password. We will deny all requests with empty passwords
        if ((inputPassword == null) || inputPassword.equals("")) {
            log.debug("Empty password, refusing login");
            return false;
        }

        // Load the LoginProperty
        String loginProperty = (String) options.get("LoginProperty");
        if (loginProperty == null) {
            // Use the default
            loginProperty = "cn";
        }

        // Load any search filter
        String searchFilter = (String) options.get("Filter");

        // Find the user that is calling us
        String userName = getUsername();

        // Load any information we may need to bind
        String bindDN = (String) options.get("BindDN");
        String bindPW = (String) options.get("BindPW");
        if (bindDN != null) {
            env.setProperty(Context.SECURITY_PRINCIPAL, bindDN);
            env.setProperty(Context.SECURITY_CREDENTIALS, bindPW);
            env.setProperty(Context.SECURITY_AUTHENTICATION, "simple");
        }

        try {
            InitialLdapContext ctx = new InitialLdapContext(env, null);
            SearchControls searchControls = getSearchControls();

            // Add the search filter if specified.  This only allows for a single search filter.. i.e. foo=bar.
            String filter;
            if ((searchFilter != null) && (searchFilter.length() != 0)) {
                filter = "(&(" + loginProperty + "=" + userName + ")" + "(" + searchFilter + "))";
            } else {
                filter = "(" + loginProperty + "=" + userName + ")";
            }

            log.debug("Using LDAP filter=" + filter);

            // Loop through each configured base DN.  It may be useful
            // in the future to allow for a filter to be configured for
            // each BaseDN, but for now the filter will apply to all.
            String[] baseDNs = baseDN.split(BASEDN_DELIMITER);
            for (int x = 0; x < baseDNs.length; x++) {
                NamingEnumeration answer = ctx.search(baseDNs[x], filter, searchControls);
                boolean ldapApiNpeFound = false;
                if (!answer.hasMoreElements()) {//BZ:582471- ldap api bug
                    log.debug("User " + userName + " not found for BaseDN " + baseDNs[x]);

                    // Nothing found for this DN, move to the next one if we have one.
                    continue;
                }

                // We use the first match
                SearchResult si = (SearchResult) answer.next();

                // Construct the UserDN
                String userDN = si.getName() + "," + baseDNs[x];

                ctx.addToEnvironment(Context.SECURITY_PRINCIPAL, userDN);
                ctx.addToEnvironment(Context.SECURITY_CREDENTIALS, inputPassword);
                ctx.addToEnvironment(Context.SECURITY_AUTHENTICATION, "simple");

                //if successful then verified that user and pw are valid ldap credentials
                ctx.reconnect(null);

                //if group auth enabled and user acct already exists then insert authorization check
                String groupFilter = (String) options.get("GroupFilter");
                String groupMember = (String) options.get("GroupMemberFilter");
                SubjectManagerLocal sManager = LookupUtil.getSubjectManager();
                Subject ldapSubject = sManager.getSubjectByName(getUsername());
                if (ldapSubject != null && ((groupFilter != null) && !groupFilter.trim().equals(""))
                    && ((groupMember != null) && !groupMember.trim().equals(""))) {
                    //check authorized groups to see if this user is authorized via ldap
                    //BUT still must always return true as authz is handled by RHQ if roles/groups correct

                    //retrieve all ldap groups that this user is authorized for based on ldap group filter and group member settings
                    Set<String> authorizedLdapGroups = ldapManager.findAvailableGroupsFor(userName);
                    RoleManagerLocal roleManager = LookupUtil.getRoleManager();

                    //find all currently mapped ldap groups
                    PageList<LdapGroup> allCurrentLdapGroupsRegistered = ldapManager.findLdapGroups(PageControl
                        .getUnlimitedInstance());

                    //find all roles for currently mapped ldap groups.
                    //empty current user from all groups -synch
                    for (LdapGroup gp : allCurrentLdapGroupsRegistered) {
                        if (gp.getRole() != null) {
                            gp.getRole().removeSubject(ldapSubject);
                        }
                    }
                    if (authorizedLdapGroups.isEmpty()) {
                        return true; //bailing out as now correctly authorized correctly.
                    }

                    //else add this subject back to all AuthoriziedLdapGroups
                    //lookup all roles that map to the authorizedLdapGroup names
                    List authorizedList = new ArrayList(authorizedLdapGroups);
                    ldapManager.assignRolesToLdapSubject(ldapSubject.getId(), authorizedList);
                }
                return true;
            }

            // If we try all the BaseDN's and have not found a match, return false
            return false;
        } catch (Exception e) {
            log.info("Failed to validate password: " + e.getMessage());
            return false;
        }
    }

    /**
     * Load a default set of properties to use when connecting to the LDAP server. If basic authentication is needed,
     * the caller must set Context.SECURITY_PRINCIPAL, Context.SECURITY_CREDENTIALS and Context.SECURITY_AUTHENTICATION
     * appropriately.
     *
     * @return properties that are to be used when connecting to LDAP server
     */
    private Properties getProperties() {
        Properties env = new Properties();

        // Map all user options into into our environment
        Iterator iter = options.entrySet().iterator();
        while (iter.hasNext()) {
            Entry entry = (Entry) iter.next();
            if ((entry.getKey() != null) && (entry.getValue() != null)) {
                env.put(entry.getKey(), entry.getValue());
            }
        }

        // Set our default factory name if one is not given
        String factoryName = env.getProperty(Context.INITIAL_CONTEXT_FACTORY);
        if (factoryName == null) {
            env.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        }

        // Setup SSL if requested
        String protocol = env.getProperty(Context.SECURITY_PROTOCOL);
        if ((protocol != null) && protocol.equals("ssl")) {
            String ldapSocketFactory = env.getProperty("java.naming.ldap.factory.socket");
            if (ldapSocketFactory == null) {
                env.put("java.naming.ldap.factory.socket", UntrustedSSLSocketFactory.class.getName());
            }
            env.put(Context.SECURITY_PROTOCOL, "ssl");
        }

        // Set the LDAP url
        String providerUrl = env.getProperty(Context.PROVIDER_URL);
        if (providerUrl == null) {
            providerUrl = "ldap://localhost:" + (((protocol != null) && protocol.equals("ssl")) ? "636" : "389");
        }

        env.setProperty(Context.PROVIDER_URL, providerUrl);

        // Follow referrals automatically
        env.setProperty(Context.REFERRAL, "ignore");//BZ:582471- active directory query change

        return env;
    }

    /**
     * A simple method to construct a SearchControls object for use when doing LDAP searches. All of the defaults are
     * used, with the exception of the scope, which is set to SUBTREE rather than the default of ONE_LEVEL
     *
     * @return controls what is searched in LDAP
     */
    private SearchControls getSearchControls() {
        // Set the scope to subtree, default is one-level
        int scope = SearchControls.SUBTREE_SCOPE;

        // No limit on the time waiting for a response
        int timeLimit = 0;

        // No limit on the number of entries returned
        long countLimit = 0;

        // Attributes to return.
        String[] returnedAttributes = null;

        // Don't return the object
        boolean returnObject = false;

        // No dereferencing during the search
        boolean deference = false;

        SearchControls constraints = new SearchControls(scope, countLimit, timeLimit, returnedAttributes, returnObject,
            deference);
        return constraints;
    }
}