Skip to content
LdapUserDAO.java 50.3 KiB
Newer Older
/*
 ************************************************************************
 *******************  CANADIAN ASTRONOMY DATA CENTRE  *******************
 **************  CENTRE CANADIEN DE DONNÉES ASTRONOMIQUES  **************
 *
 *  (c) 2014.                            (c) 2014.
 *  Government of Canada                 Gouvernement du Canada
 *  National Research Council            Conseil national de recherches
 *  Ottawa, Canada, K1A 0R6              Ottawa, Canada, K1A 0R6
 *  All rights reserved                  Tous droits réservés
 *
 *  NRC disclaims any warranties,        Le CNRC dénie toute garantie
 *  expressed, implied, or               énoncée, implicite ou légale,
 *  statutory, of any kind with          de quelque nature que ce
 *  respect to the software,             soit, concernant le logiciel,
 *  including without limitation         y compris sans restriction
 *  any warranty of merchantability      toute garantie de valeur
 *  or fitness for a particular          marchande ou de pertinence
 *  purpose. NRC shall not be            pour un usage particulier.
 *  liable in any event for any          Le CNRC ne pourra en aucun cas
 *  damages, whether direct or           être tenu responsable de tout
 *  indirect, special or general,        dommage, direct ou indirect,
 *  consequential or incidental,         particulier ou général,
 *  arising from the use of the          accessoire ou fortuit, résultant
 *  software.  Neither the name          de l'utilisation du logiciel. Ni
 *  of the National Research             le nom du Conseil National de
 *  Council of Canada nor the            Recherches du Canada ni les noms
 *  names of its contributors may        de ses  participants ne peuvent
 *  be used to endorse or promote        être utilisés pour approuver ou
 *  products derived from this           promouvoir les produits dérivés
 *  software without specific prior      de ce logiciel sans autorisation
 *  written permission.                  préalable et particulière
 *                                       par écrit.
 *
 *  This file is part of the             Ce fichier fait partie du projet
 *  OpenCADC project.                    OpenCADC.
 *
 *  OpenCADC is free software:           OpenCADC est un logiciel libre ;
 *  you can redistribute it and/or       vous pouvez le redistribuer ou le
 *  modify it under the terms of         modifier suivant les termes de
 *  the GNU Affero General Public        la “GNU Affero General Public
 *  License as published by the          License” telle que publiée
 *  Free Software Foundation,            par la Free Software Foundation
 *  either version 3 of the              : soit la version 3 de cette
 *  License, or (at your option)         licence, soit (à votre gré)
 *  any later version.                   toute version ultérieure.
 *
 *  OpenCADC is distributed in the       OpenCADC est distribué
 *  hope that it will be useful,         dans l’espoir qu’il vous
 *  but WITHOUT ANY WARRANTY;            sera utile, mais SANS AUCUNE
 *  without even the implied             GARANTIE : sans même la garantie
 *  warranty of MERCHANTABILITY          implicite de COMMERCIALISABILITÉ
 *  or FITNESS FOR A PARTICULAR          ni d’ADÉQUATION À UN OBJECTIF
 *  PURPOSE.  See the GNU Affero         PARTICULIER. Consultez la Licence
 *  General Public License for           Générale Publique GNU Affero
 *  more details.                        pour plus de détails.
 *
 *  You should have received             Vous devriez avoir reçu une
 *  a copy of the GNU Affero             copie de la Licence Générale
 *  General Public License along         Publique GNU Affero avec
 *  with OpenCADC.  If not, see          OpenCADC ; si ce n’est
 *  <http://www.gnu.org/licenses/>.      pas le cas, consultez :
 *                                       <http://www.gnu.org/licenses/>.
 *
 *  $Revision: 4 $
 *
 ************************************************************************
 */
package ca.nrc.cadc.ac.server.ldap;

import java.security.AccessControlException;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Random;
import java.util.Set;

import javax.security.auth.x500.X500Principal;

import org.apache.log4j.Logger;

import ca.nrc.cadc.ac.Group;
Jeff Burke's avatar
Jeff Burke committed
import ca.nrc.cadc.ac.PersonalDetails;
import ca.nrc.cadc.ac.PosixDetails;
Patrick Dowler's avatar
Patrick Dowler committed
import ca.nrc.cadc.ac.Role;
Jeff Burke's avatar
Jeff Burke committed
import ca.nrc.cadc.ac.User;
import ca.nrc.cadc.ac.UserAlreadyExistsException;
import ca.nrc.cadc.ac.UserDetails;
import ca.nrc.cadc.ac.UserNotFoundException;
import ca.nrc.cadc.ac.UserRequest;
Patrick Dowler's avatar
Patrick Dowler committed
import ca.nrc.cadc.ac.client.GroupMemberships;
import ca.nrc.cadc.auth.AuthenticationUtil;
import ca.nrc.cadc.auth.DNPrincipal;
import ca.nrc.cadc.auth.HttpPrincipal;
import ca.nrc.cadc.auth.NumericPrincipal;
import ca.nrc.cadc.net.TransientException;
import ca.nrc.cadc.profiler.Profiler;
import ca.nrc.cadc.util.StringUtil;
import com.unboundid.ldap.sdk.AddRequest;
import com.unboundid.ldap.sdk.Attribute;
Jeff Burke's avatar
Jeff Burke committed
import com.unboundid.ldap.sdk.BindRequest;
import com.unboundid.ldap.sdk.BindResult;
import com.unboundid.ldap.sdk.DN;
import com.unboundid.ldap.sdk.DeleteRequest;
import com.unboundid.ldap.sdk.Filter;
Brian Major's avatar
Brian Major committed
import com.unboundid.ldap.sdk.LDAPConnection;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.LDAPSearchException;
Jeff Burke's avatar
Jeff Burke committed
import com.unboundid.ldap.sdk.Modification;
import com.unboundid.ldap.sdk.ModificationType;
import com.unboundid.ldap.sdk.ModifyDNRequest;
Jeff Burke's avatar
Jeff Burke committed
import com.unboundid.ldap.sdk.ModifyRequest;
import com.unboundid.ldap.sdk.ResultCode;
import com.unboundid.ldap.sdk.SearchRequest;
import com.unboundid.ldap.sdk.SearchResult;
import com.unboundid.ldap.sdk.SearchResultEntry;
import com.unboundid.ldap.sdk.SearchScope;
Jeff Burke's avatar
Jeff Burke committed
import com.unboundid.ldap.sdk.SimpleBindRequest;
import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedRequest;
import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedResult;

Brian Major's avatar
Brian Major committed
 * @param <T>
public class LdapUserDAO<T extends Principal> extends LdapDAO
{
    public static final String EMAIL_ADDRESS_CONFLICT_MESSAGE = 
    private static final Logger logger = Logger.getLogger(LdapUserDAO.class);

Patrick Dowler's avatar
Patrick Dowler committed
    private final Profiler profiler = new Profiler(LdapUserDAO.class);
Jeff Burke's avatar
Jeff Burke committed
    // Map of identity type to LDAP attribute
    private final Map<Class<?>, String> userLdapAttrib = new HashMap<Class<?>, String>();

    // Returned User attributes
    protected static final String LDAP_OBJECT_CLASS = "objectClass";
    protected static final String LDAP_INET_USER = "inetuser";
    protected static final String LDAP_INET_ORG_PERSON = "inetOrgPerson";
    protected static final String LDAP_CADC_ACCOUNT = "cadcaccount";
    protected static final String LDAP_NSACCOUNTLOCK = "nsaccountlock";
    protected static final String LDAP_MEMBEROF = "memberOf";
    protected static final String LDAP_ENTRYDN = "entrydn";
    protected static final String LDAP_COMMON_NAME = "cn";
    protected static final String LDAP_DISTINGUISHED_NAME = "distinguishedName";
    protected static final String LDAP_NUMERICID = "numericid";
    protected static final String LADP_USER_PASSWORD = "userPassword";
    protected static final String LDAP_FIRST_NAME = "givenName";
    protected static final String LDAP_LAST_NAME = "sn";
    protected static final String LDAP_ADDRESS = "address";
    protected static final String LDAP_CITY = "city";
    protected static final String LDAP_COUNTRY = "country";
    protected static final String LDAP_EMAIL = "email";
    protected static final String LDAP_INSTITUTE = "institute";
    protected static final String LDAP_UID = "uid";
    private String[] userAttribs = new String[]
    {
            LDAP_FIRST_NAME, LDAP_LAST_NAME, LDAP_ADDRESS, LDAP_CITY,
            LDAP_COUNTRY, LDAP_EMAIL, LDAP_INSTITUTE
    };
    private String[] firstLastAttribs = new String[]
    {
            LDAP_FIRST_NAME, LDAP_LAST_NAME
    };
    private String[] identityAttribs = new String[]
Patrick Dowler's avatar
Patrick Dowler committed
        LDAP_UID, LDAP_DISTINGUISHED_NAME, LDAP_NUMERICID, LDAP_ENTRYDN,
        LDAP_MEMBEROF // for group cache
Brian Major's avatar
Brian Major committed
    public LdapUserDAO(LdapConnections connections)
Brian Major's avatar
Brian Major committed
        super(connections);
        this.userLdapAttrib.put(HttpPrincipal.class, LDAP_UID);
        this.userLdapAttrib.put(X500Principal.class, LDAP_DISTINGUISHED_NAME);
        this.userLdapAttrib.put(NumericPrincipal.class, LDAP_NUMERICID);
        // add the id attributes to user and member attributes
Dustin Jenkins's avatar
Dustin Jenkins committed
        String[] princs = userLdapAttrib.values()
                .toArray(new String[userLdapAttrib.values().size()]);
        String[] tmp = new String[userAttribs.length + princs.length];
        System.arraycopy(princs, 0, tmp, 0, princs.length);
Dustin Jenkins's avatar
Dustin Jenkins committed
        System.arraycopy(userAttribs, 0, tmp, princs.length,
                         userAttribs.length);
        userAttribs = tmp;
        tmp = new String[firstLastAttribs.length + princs.length];
        System.arraycopy(princs, 0, tmp, 0, princs.length);
        System.arraycopy(firstLastAttribs, 0, tmp, princs.length,
                         firstLastAttribs.length);
        firstLastAttribs = tmp;
Jeff Burke's avatar
Jeff Burke committed
    /**
     * Verifies the username and password for an existing User.
     *
     * @param username username to verify.
     * @param password password to verify.
Jeff Burke's avatar
Jeff Burke committed
     * @throws TransientException
     * @throws UserNotFoundExceptionjoellama
    public Boolean doLogin(final String username, final String password)
Jeff Burke's avatar
Jeff Burke committed
        throws TransientException, UserNotFoundException
    {
        try
        {
            BindRequest bindRequest = new SimpleBindRequest(
                getUserDN(username, config.getUsersDN()), new String(password));

            LDAPConnection conn = this.getUnboundReadConnection();
Brian Major's avatar
Brian Major committed
            BindResult bindResult = conn.bind(bindRequest);
Jeff Burke's avatar
Jeff Burke committed

            if (bindResult != null && bindResult.getResultCode() == ResultCode.SUCCESS)
            {
Jeff Burke's avatar
Jeff Burke committed
            }
            else
            {
                throw new AccessControlException("Invalid username or password");
            }
        }
        catch (LDAPException e)
        {
            logger.debug("doLogin Exception: " + e, e);

            if (e.getResultCode() == ResultCode.INVALID_CREDENTIALS)
            {
                throw new AccessControlException("Invalid password");
            }
            else if (e.getResultCode() == ResultCode.NO_SUCH_OBJECT)
            {
                throw new AccessControlException("Invalid username");
            }
            else if (e.getResultCode() == ResultCode.UNWILLING_TO_PERFORM)
                throw new AccessControlException("Account inactivated");
            throw new RuntimeException("Unexpected LDAP exception", e);
     * Add the specified user to the active user tree.
     * @throws TransientException         If an temporary, unexpected problem occurred.
     * @throws UserAlreadyExistsException If the user already exists.
    public void addUser(final UserRequest<T> userRequest)
            throws TransientException, UserAlreadyExistsException
        try
        {
            getUser(userRequest.getUser().getUserID(), config.getUsersDN());
            final String error = userRequest.getUser().getUserID().getName() +
                " found in " + config.getUsersDN();
            throw new UserAlreadyExistsException(error);
        }
        catch (UserNotFoundException e1) {}

    private String getEmailAddress(final UserRequest<T> userRequest)
    {
        Set<PersonalDetails> personalDetails = userRequest.getUser().getDetails(PersonalDetails.class);
        if (personalDetails.isEmpty())
        {
            String error = userRequest.getUser().getUserID().getName() + " missing required PersonalDetails";
            throw new IllegalArgumentException(error);
        }
        
        PersonalDetails pd = personalDetails.iterator().next();
        if (!StringUtil.hasText(pd.email))
        {
            String error = userRequest.getUser().getUserID().getName() + " missing required email address";
            throw new IllegalArgumentException(error);
        }
        
        return pd.email;
    }
    
    private void checkUsers(final UserRequest<T> userRequest, 
            final String usersDN)
            throws TransientException, UserAlreadyExistsException
            getUser(userRequest.getUser().getUserID(), usersDN);
            final String error = "user " + userRequest.getUser().getUserID().getName() +
                                 " found in " + usersDN;
            throw new UserAlreadyExistsException(error);
        }
        catch (UserNotFoundException ok) { }
        // check if email address is already in use
            String emailAddress = getEmailAddress(userRequest); 
        catch (UserNotFoundException ok) { }
    }
    
    /**
     *Add the specified user to the pending user tree.
     *
     * @param userRequest                   The user to add.
     * @throws TransientException           If an temporary, unexpected problem occurred.
     * @throws UserAlreadyExistsException   If the user already exists.
     */
    public void addPendingUser(final UserRequest<T> userRequest)
            throws TransientException, UserAlreadyExistsException
    {
        // check current users
        checkUsers(userRequest, config.getUsersDN());
        
        // check pending users
        checkUsers(userRequest, config.getUserRequestsDN());
        addUser(userRequest, config.getUserRequestsDN());
    private void addUser(final UserRequest<T> userRequest, final String usersDN)
        throws TransientException, UserAlreadyExistsException
        final User<T> user = userRequest.getUser();
        final Class userType = user.getUserID().getClass();
        final String searchField = userLdapAttrib.get(userType);

        if (searchField == null)
        {
            throw new IllegalArgumentException("Unsupported principal type " + userType);
        try
        {
            List<Attribute> attributes = new ArrayList<Attribute>();
            addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_INET_ORG_PERSON);
            addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_INET_USER);
            addAttribute(attributes, LDAP_OBJECT_CLASS, LDAP_CADC_ACCOUNT);
            addAttribute(attributes, LDAP_COMMON_NAME, user.getUserID().getName());
            addAttribute(attributes, LADP_USER_PASSWORD, new String(userRequest.getPassword()));
            addAttribute(attributes, LDAP_NUMERICID, String.valueOf(genNextNumericId()));
            for (Principal princ : user.getIdentities())
            {
                if (princ instanceof X500Principal)
                {
                    addAttribute(attributes, LDAP_DISTINGUISHED_NAME, princ.getName());
            for (UserDetails details : user.details)
            {
                if (details.getClass() == PersonalDetails.class)
                {
                    PersonalDetails pd = (PersonalDetails) details;
                    addAttribute(attributes, LDAP_FIRST_NAME, pd.getFirstName());
                    addAttribute(attributes, LDAP_LAST_NAME, pd.getLastName());
                    addAttribute(attributes, LDAP_ADDRESS, pd.address);
                    addAttribute(attributes, LDAP_CITY, pd.city);
                    addAttribute(attributes, LDAP_COUNTRY, pd.country);
                    addAttribute(attributes, LDAP_EMAIL, pd.email);
                    addAttribute(attributes, LDAP_INSTITUTE, pd.institute);
                }
                else if (details.getClass() == PosixDetails.class)
                {
                    throw new UnsupportedOperationException(
                        "Support for users PosixDetails not available");
            DN userDN = getUserDN(user.getUserID().getName(), usersDN);
            AddRequest addRequest = new AddRequest(userDN, attributes);
Brian Major's avatar
Brian Major committed
            LDAPResult result = getReadWriteConnection().add(addRequest);
            LdapDAO.checkLdapResult(result.getResultCode());
        }
        catch (LDAPException e)
        {
            logger.error("addUser Exception: " + e, e);
            LdapUserDAO.checkUserLDAPResult(e.getResultCode());
            throw new RuntimeException("Unexpected LDAP exception", e);
        }
    }
Jeff Burke's avatar
Jeff Burke committed
    /**
     * Get the user specified by userID.
Jeff Burke's avatar
Jeff Burke committed
     *
     * @param userID The userID.
Jeff Burke's avatar
Jeff Burke committed
     * @return User instance.
     * @throws UserNotFoundException  when the user is not found in the main tree.
Dustin Jenkins's avatar
Dustin Jenkins committed
     * @throws TransientException     If an temporary, unexpected problem occurred.
Jeff Burke's avatar
Jeff Burke committed
     * @throws AccessControlException If the operation is not permitted.
Jeff Burke's avatar
Jeff Burke committed
     */
    public User<T> getUser(final T userID)
            throws UserNotFoundException, TransientException,
                   AccessControlException
    {
        return getUser(userID, config.getUsersDN());
    }

    /**
     * Get the user specified by the email address exists.
     *
     * @param emailAddress The user's email address.
     *
     * @return User instance.
     *
     * @throws UserNotFoundException  when the user is not found in the main tree.
     * @throws TransientException If an temporary, unexpected problem occurred.
     * @throws AccessControlException If the operation is not permitted.
     * @throws UserAlreadyExistsException A user with the same email address already exists
     */
    public User<Principal> getUserByEmailAddress(final String emailAddress)
            throws UserNotFoundException, TransientException,
            AccessControlException, UserAlreadyExistsException
        return getUserByEmailAddress(emailAddress, config.getUsersDN());
    /**
     * Obtain a user who is awaiting approval.
     *
     * @param userID        The user ID of the pending user.
     * @return              A User instance awaiting approval.
     *
     * @throws UserNotFoundException  when the user is not found in the user request tree.
     * @throws TransientException     If an temporary, unexpected problem occurred.
     * @throws AccessControlException If the operation is not permitted.
     */
    public User<T> getPendingUser(final T userID)
            throws UserNotFoundException, TransientException,
                   AccessControlException
    {
        return getUser(userID, config.getUserRequestsDN());
    }

    /**
     * Get the user specified by userID.
     *
     * @param userID  The userID.
     * @param usersDN The LDAP tree to search.
     * @return User instance.
     * @throws UserNotFoundException  when the user is not found.
     * @throws TransientException     If an temporary, unexpected problem occurred.
     * @throws AccessControlException If the operation is not permitted.
     */
    private User<T> getUser(final T userID, final String usersDN)
        throws UserNotFoundException, TransientException,
        AccessControlException
Dustin Jenkins's avatar
Dustin Jenkins committed
        String searchField = userLdapAttrib.get(userID.getClass());
        if (searchField == null)
        {
Jeff Burke's avatar
Jeff Burke committed
            throw new IllegalArgumentException(
                    "Unsupported principal type " + userID.getClass());
        }

        SearchResultEntry searchResult = null;
            Filter notFilter = Filter.createNOTFilter(Filter.createPresenceFilter(LDAP_NSACCOUNTLOCK));
            Filter equalsFilter = Filter.createEqualityFilter(searchField, userID.getName());
            filter = Filter.createANDFilter(notFilter, equalsFilter);
Brian Major's avatar
Brian Major committed
            SearchRequest searchRequest =
                    new SearchRequest(usersDN, SearchScope.ONE, filter, userAttribs);
Patrick Dowler's avatar
Patrick Dowler committed
            //if (proxy)
            //{
            //    String proxyDN = "dn:" + getSubjectDN().toNormalizedString();
            //    logger.debug("Proxying auth as: " + proxyDN);
            //    searchRequest.addControl(new ProxiedAuthorizationV2RequestControl(proxyDN));
            //}
Brian Major's avatar
Brian Major committed
            searchResult = getReadOnlyConnection().searchForEntry(searchRequest);
        }
        catch (LDAPException e)
        {
            LdapDAO.checkLdapResult(e.getResultCode());
        }

        if (searchResult == null)
        {
            // determine if the user is not there of if the calling user
            // doesn't have permission to see it
            SearchRequest searchRequest =
                    new SearchRequest(usersDN, SearchScope.ONE, filter, userAttribs);
            try
            {
                searchResult = getReadOnlyConnection().searchForEntry(searchRequest);
            }
            catch (LDAPException e)
            {
                LdapDAO.checkLdapResult(e.getResultCode());
            }

            if (searchResult == null)
            {
                String msg = "User not found " + userID.toString();
                logger.debug(msg);
                throw new UserNotFoundException(msg);
            }
            throw new AccessControlException("Permission denied");
        User<T> user = new User<T>(userID);
        String username = searchResult.getAttributeValue(userLdapAttrib.get(HttpPrincipal.class));
        logger.debug("username: " + username);
        user.getIdentities().add(new HttpPrincipal(username));
        Integer numericID = searchResult.getAttributeValueAsInteger(userLdapAttrib.get(NumericPrincipal.class));
        logger.debug("Numeric id: " + numericID);
        if (numericID == null)
        {
            // If the numeric ID does not return it means the user
            // does not have permission
            throw new AccessControlException("Permission denied");
        }
        user.getIdentities().add(new NumericPrincipal(numericID));

        String x500str = searchResult.getAttributeValue(userLdapAttrib.get(X500Principal.class));
        logger.debug("x500principal: " + x500str);

        if (x500str != null)
            user.getIdentities().add(new X500Principal(x500str));

        String fname = searchResult.getAttributeValue(LDAP_FIRST_NAME);
        String lname = searchResult.getAttributeValue(LDAP_LAST_NAME);
        PersonalDetails personaDetails = new PersonalDetails(fname, lname);
        personaDetails.address = searchResult.getAttributeValue(LDAP_ADDRESS);
        personaDetails.city = searchResult.getAttributeValue(LDAP_CITY);
        personaDetails.country = searchResult.getAttributeValue(LDAP_COUNTRY);
        personaDetails.email = searchResult.getAttributeValue(LDAP_EMAIL);
        personaDetails.institute = searchResult.getAttributeValue(LDAP_INSTITUTE);
        user.details.add(personaDetails);

        return user;
    }
    
    /**
     * Get the user specified by the email address exists.
     *
     * @param emailAddress  The user's email address.
     * @param usersDN The LDAP tree to search.
     * @return User ID
     * @throws UserNotFoundException  when the user is not found.
     * @throws TransientException     If an temporary, unexpected problem occurred.
     * @throws AccessControlException If the operation is not permitted.
     * @throws UserAlreadyExistsException A user with the same email address already exists
     */
    private User<Principal> getUserByEmailAddress(final String emailAddress, 
            throws UserNotFoundException, TransientException,
            AccessControlException, UserAlreadyExistsException
    {
        SearchResultEntry searchResult = null;
        Filter filter = null;
        try
        {
            Filter notFilter = Filter.createNOTFilter(Filter.createPresenceFilter(LDAP_NSACCOUNTLOCK));
            Filter equalsFilter = Filter.createEqualityFilter("email", emailAddress);
            filter = Filter.createANDFilter(notFilter, equalsFilter);
            logger.debug("search filter: " + filter);

            SearchRequest searchRequest =
                    new SearchRequest(usersDN, SearchScope.ONE, filter, userAttribs);

            searchResult = getReadOnlyConnection().searchForEntry(searchRequest);
        }
        catch (LDAPSearchException e)
        {
            if (e.getResultCode() == ResultCode.SIZE_LIMIT_EXCEEDED)
            {
                String msg = EMAIL_ADDRESS_CONFLICT_MESSAGE + emailAddress + " already in use";
                throw new UserAlreadyExistsException(msg);
        catch (LDAPException e)
        {
            LdapDAO.checkLdapResult(e.getResultCode());
        }

        if (searchResult == null)
        {
            // determine if the user is not there of if the calling user
            // doesn't have permission to see it
            SearchRequest searchRequest =
                    new SearchRequest(usersDN, SearchScope.ONE, filter, userAttribs);
            try
            {
                searchResult = getReadOnlyConnection().searchForEntry(searchRequest);
            }
            catch (LDAPSearchException e)
            {
                if (e.getResultCode() == ResultCode.SIZE_LIMIT_EXCEEDED)
                {
                    String msg = EMAIL_ADDRESS_CONFLICT_MESSAGE + emailAddress + " already in use";
                    throw new UserAlreadyExistsException(msg);
            catch (LDAPException e)
            {
                LdapDAO.checkLdapResult(e.getResultCode());
            }

            if (searchResult == null)
            {
                String msg = "User with email address " + emailAddress + " not found";
                logger.debug(msg);
                throw new UserNotFoundException(msg);
            }
            throw new AccessControlException("Permission denied");
        }

        String userIDString = searchResult.getAttributeValue(LDAP_UID);
        HttpPrincipal userID = new HttpPrincipal(userIDString);
        User<Principal> user = new User<Principal>(userID);
        String username = searchResult.getAttributeValue(userLdapAttrib.get(HttpPrincipal.class));
        logger.debug("username: " + username);
        user.getIdentities().add(new HttpPrincipal(username));

        Integer numericID = searchResult.getAttributeValueAsInteger(userLdapAttrib.get(NumericPrincipal.class));
        logger.debug("Numeric id: " + numericID);
        if (numericID == null)
        {
            // If the numeric ID does not return it means the user
            // does not have permission
            throw new AccessControlException("Permission denied");
        }
        user.getIdentities().add(new NumericPrincipal(numericID));

        String x500str = searchResult.getAttributeValue(userLdapAttrib.get(X500Principal.class));
        logger.debug("x500principal: " + x500str);

        if (x500str != null)
            user.getIdentities().add(new X500Principal(x500str));
        String fname = searchResult.getAttributeValue(LDAP_FIRST_NAME);
        String lname = searchResult.getAttributeValue(LDAP_LAST_NAME);
        PersonalDetails personaDetails = new PersonalDetails(fname, lname);
        personaDetails.address = searchResult.getAttributeValue(LDAP_ADDRESS);
        personaDetails.city = searchResult.getAttributeValue(LDAP_CITY);
        personaDetails.country = searchResult.getAttributeValue(LDAP_COUNTRY);
        personaDetails.email = searchResult.getAttributeValue(LDAP_EMAIL);
Patrick Dowler's avatar
Patrick Dowler committed
        personaDetails.institute = searchResult.getAttributeValue(LDAP_INSTITUTE);
        user.details.add(personaDetails);
        return user;
    public User<T> getAugmentedUser(final T userID)
        throws UserNotFoundException, TransientException
    {
        String searchField = userLdapAttrib.get(userID.getClass());
Brian Major's avatar
Brian Major committed
        profiler.checkpoint("getAugmentedUser.getSearchField");
        if (searchField == null)
        {
            throw new IllegalArgumentException(
                "Unsupported principal type " + userID.getClass());
        }

        try
        {
            Filter notFilter = Filter.createNOTFilter(Filter.createPresenceFilter(LDAP_NSACCOUNTLOCK));
            Filter equalsFilter = Filter.createEqualityFilter(searchField, userID.getName());
            Filter filter = Filter.createANDFilter(notFilter, equalsFilter);
            profiler.checkpoint("getAugmentedUser.createFilter");
            SearchRequest searchRequest = new SearchRequest(
                config.getUsersDN(), SearchScope.ONE, filter, identityAttribs);
            profiler.checkpoint("getAugmentedUser.createSearchRequest");
Brian Major's avatar
Brian Major committed
            SearchResultEntry searchResult = getReadOnlyConnection().searchForEntry(searchRequest);
            profiler.checkpoint("getAugmentedUser.searchForEntry");

            if (searchResult == null)
            {
                String msg = "User not found " + userID.toString();
                logger.debug(msg);
                throw new UserNotFoundException(msg);
            }

            User<T> user = new User<T>(userID);
            user.getIdentities().add(new HttpPrincipal(
                searchResult.getAttributeValue(LDAP_UID)));
Brian Major's avatar
Brian Major committed
            int numericID = searchResult.getAttributeValueAsInteger(LDAP_NUMERICID);
            logger.debug("numericID is " + numericID);
            user.getIdentities().add(new NumericPrincipal(numericID));
            String dn = searchResult.getAttributeValue(LDAP_DISTINGUISHED_NAME);
            if (dn != null)
            {
                user.getIdentities().add(new X500Principal(dn));
            }
            user.getIdentities().add(new DNPrincipal(searchResult.getAttributeValue(LDAP_ENTRYDN)));
Patrick Dowler's avatar
Patrick Dowler committed
            // cache memberOf values in the user
            GroupMemberships gms = new GroupMemberships(user);
            user.appData = gms; // add even if empty
            String[] mems = searchResult.getAttributeValues(LDAP_MEMBEROF);
            if (mems != null && mems.length > 0)
            {
                DN adminDN = new DN(config.getAdminGroupsDN());
                DN groupsDN = new DN(config.getGroupsDN());
                List<Group> memberOf = new ArrayList<Group>();
                List<Group> adminOf = new ArrayList<Group>();
                for (String m : mems)
                {
                    DN groupDN = new DN(m);
                    if (groupDN.isDescendantOf(groupsDN, false))
                        memberOf.add(createGroupFromDN(groupDN));
                    else if (groupDN.isDescendantOf(adminDN, false))
                        adminOf.add(createGroupFromDN(groupDN));
                }
                gms.add(adminOf, Role.ADMIN);
                gms.add(memberOf, Role.MEMBER);
            }
            profiler.checkpoint("getAugmentedUser.mapIdentities");
            return user;
        }
        catch (LDAPException e)
        {
            logger.debug("getGroup Exception: " + e, e);
            LdapDAO.checkLdapResult(e.getResultCode());
            throw new RuntimeException("BUG: checkLdapResult didn't throw an exception");
        }
Brian Major's avatar
Brian Major committed
        finally
        {
            profiler.checkpoint("Done getAugmentedUser");
        }
Patrick Dowler's avatar
Patrick Dowler committed
    // some pretty horrible hacks to avoid querying LDAP for group details...
    private Group createGroupFromDN(DN groupDN)
    {
        String cn = groupDN.getRDNString();
        String[] parts = cn.split("=");
        if (parts.length == 2 && parts[0].equals("cn"))
        {
            return new Group(parts[1]);
        }
        throw new RuntimeException("BUG: failed to extract group name from " + groupDN
                .toString());
    }
Dustin Jenkins's avatar
Dustin Jenkins committed
    /**
     * @return A Collection of User's.
     * @throws TransientException If an temporary, unexpected problem occurred.
Dustin Jenkins's avatar
Dustin Jenkins committed
     */
Jeff Burke's avatar
Jeff Burke committed
        throws AccessControlException, TransientException
     * @throws TransientException If an temporary, unexpected problem occurred.
     */
    public Collection<User<Principal>> getPendingUsers()
Jeff Burke's avatar
Jeff Burke committed
        throws AccessControlException, TransientException
    {
        return getUsers(config.getUserRequestsDN());
    }

    private Collection<User<Principal>> getUsers(final String usersDN)
Jeff Burke's avatar
Jeff Burke committed
        throws AccessControlException, TransientException
        final Collection<User<Principal>> users = new ArrayList<User<Principal>>();
        Filter notFilter = Filter.createNOTFilter(Filter.createPresenceFilter(LDAP_NSACCOUNTLOCK));
        Filter presenceFilter = Filter.createPresenceFilter(LDAP_UID);
        Filter filter = Filter.createANDFilter(notFilter, presenceFilter);
        logger.debug("search filter: " + filter);
        final String[] attributes = new String[]
            { LDAP_UID, LDAP_FIRST_NAME, LDAP_LAST_NAME };
        final SearchRequest searchRequest =
            new SearchRequest(usersDN, SearchScope.ONE, filter, attributes);
        try
        {
            final SearchResult searchResult =
                getReadOnlyConnection().search(searchRequest);
            LdapDAO.checkLdapResult(searchResult.getResultCode());
            for (SearchResultEntry next : searchResult.getSearchEntries())
                final String firstName =
                    next.getAttributeValue(LDAP_FIRST_NAME).trim();
                final String lastName =
                    next.getAttributeValue(LDAP_LAST_NAME).trim();
                final String uid = next.getAttributeValue(LDAP_UID).trim();
                User<Principal> user = new User<Principal>(new HttpPrincipal(uid));

                // Only add Personal Details if it is relevant.
                if (StringUtil.hasLength(firstName)
                    && StringUtil.hasLength(lastName))
                {
                    user.details.add(new PersonalDetails(firstName, lastName));
                }

                users.add(user);
        catch (LDAPSearchException e)
            if (e.getResultCode() == ResultCode.NO_SUCH_OBJECT)
            {
                final String message = "Could not find users root";
                logger.debug(message, e);
                throw new IllegalStateException(message);
            }
     * Move the pending user specified by userID from the
     * pending users tree to the active users tree.
     * @param userID
     * @return User instance.
     * @throws UserNotFoundException  when the user is not found.
     * @throws TransientException     If an temporary, unexpected problem occurred.
     * @throws AccessControlException If the operation is not permitted.
     */
    public User<T> approvePendingUser(final T userID)
        throws UserNotFoundException, TransientException, AccessControlException
    {
        User<T> pendingUser = getPendingUser(userID);

        Set<HttpPrincipal> httpPrincipals = pendingUser.getIdentities(HttpPrincipal.class);
        if (httpPrincipals.isEmpty())
        {
            throw new RuntimeException("BUG: missing HttpPrincipal for " + userID.getName());
        }
        HttpPrincipal httpPrincipal = httpPrincipals.iterator().next();
        String uid = "uid=" + httpPrincipal.getName();
        String dn = uid + "," + config.getUserRequestsDN();

        try
        {
            ModifyDNRequest modifyDNRequest =
                new ModifyDNRequest(dn, uid, true, config.getUsersDN());
Brian Major's avatar
Brian Major committed
            LdapDAO.checkLdapResult(getReadWriteConnection().modifyDN(modifyDNRequest).getResultCode());
        }
        catch (LDAPException e)
        {
            logger.debug("Modify Exception", e);
            LdapDAO.checkLdapResult(e.getResultCode());
        }
        try
        {
            return getUser(userID);
        }
        catch (UserNotFoundException e)
        {
            throw new RuntimeException(
                "BUG: approved user not found (" + userID.getName() + ")");
        }
    }

    /**
     * Update the user specified by User.
     *
     * @param userID
     * @return User instance.
     * @throws UserNotFoundException  when the user is not found.
     * @throws TransientException     If an temporary, unexpected problem occurred.
     * @throws AccessControlException If the operation is not permitted.
     */
    public User<T> modifyUser(final User<T> userID)
Jeff Burke's avatar
Jeff Burke committed
            throws UserNotFoundException, TransientException, AccessControlException
        User existingUser = getUser(userID.getUserID());
Jeff Burke's avatar
Jeff Burke committed

        List<Modification> mods = new ArrayList<Modification>();
        for (UserDetails details : userID.details)
Jeff Burke's avatar
Jeff Burke committed
        {
            if (details.getClass() == PersonalDetails.class)
            {
                PersonalDetails pd = (PersonalDetails) details;
                addModification(mods, LDAP_FIRST_NAME, pd.getFirstName());
                addModification(mods, LDAP_LAST_NAME, pd.getLastName());
                addModification(mods, LDAP_ADDRESS, pd.address);
                addModification(mods, LDAP_CITY, pd.city);
                addModification(mods, LDAP_COUNTRY, pd.country);
                addModification(mods, LDAP_EMAIL, pd.email);
                addModification(mods, LDAP_INSTITUTE, pd.institute);
Jeff Burke's avatar
Jeff Burke committed
            }
            else if (details.getClass() == PosixDetails.class)
            {
                throw new UnsupportedOperationException(
                    "Support for users PosixDetails not available");
        // set the x500 DNs if there
        Set<X500Principal> x500Principals = userID.getIdentities(X500Principal.class);
        if (x500Principals != null && !x500Principals.isEmpty())
        {
            Iterator<X500Principal> i = x500Principals.iterator();
            X500Principal next = null;
            while (i.hasNext())
            {
                next = i.next();
                addModification(mods, LDAP_DISTINGUISHED_NAME, next.getName());
            }
        }

            ModifyRequest modifyRequest = new ModifyRequest(getUserDN(userID), mods);
Patrick Dowler's avatar
Patrick Dowler committed
            //modifyRequest.addControl(
            //    new ProxiedAuthorizationV2RequestControl(
            //        "dn:" + getSubjectDN().toNormalizedString()));
Brian Major's avatar
Brian Major committed
            LdapDAO.checkLdapResult(getReadWriteConnection().modify(modifyRequest).getResultCode());
        catch (LDAPException e)
            logger.debug("Modify Exception", e);
            LdapDAO.checkLdapResult(e.getResultCode());
            return getUser(userID.getUserID());
Jeff Burke's avatar
Jeff Burke committed
        }
        catch (UserNotFoundException e)
        {
            throw new RuntimeException(
                "BUG: modified user not found (" + userID.getUserID() + ")");
    protected void updatePassword(HttpPrincipal userID, String oldPassword, String newPassword)
            throws UserNotFoundException, TransientException, AccessControlException
Jeff Burke's avatar
Jeff Burke committed
            DN userDN = getUserDN(user);
            //BindRequest bindRequest = new SimpleBindRequest(
            //        getUserDN(username, config.getUsersDN()), oldPassword);
            //LDAPConnection conn = this.getUnboundReadConnection();
            //conn.bind(bindRequest);
            LDAPConnection conn = this.getReadWriteConnection();
            PasswordModifyExtendedRequest passwordModifyRequest;
                        new PasswordModifyExtendedRequest(userDN.toNormalizedString(), 
                                null, new String(newPassword));              
                        new PasswordModifyExtendedRequest(userDN.toNormalizedString(), 
                                new String(oldPassword), new String(newPassword));                
            }
Jeff Burke's avatar
Jeff Burke committed
            PasswordModifyExtendedResult passwordModifyResult = (PasswordModifyExtendedResult)
Brian Major's avatar
Brian Major committed
                    conn.processExtendedOperation(passwordModifyRequest);
Jeff Burke's avatar
Jeff Burke committed
            LdapDAO.checkLdapResult(passwordModifyResult.getResultCode());
        }