/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (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.apache.org/licenses/LICENSE-2.0
 * 
 * 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 org.apache.jetspeed.security.spi.impl;

import javax.naming.AuthenticationException;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.Attribute;
import javax.naming.directory.BasicAttribute;
import javax.naming.directory.DirContext;
import javax.naming.directory.ModificationItem;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;

import org.apache.commons.lang.StringUtils;
import org.apache.jetspeed.security.CredentialPasswordEncoder;
import org.apache.jetspeed.security.InvalidPasswordException;
import org.apache.jetspeed.security.JetspeedPrincipalType;
import org.apache.jetspeed.security.PasswordCredential;
import org.apache.jetspeed.security.SecurityException;
import org.apache.jetspeed.security.User;
import org.apache.jetspeed.security.UserManager;
import org.apache.jetspeed.security.spi.AlgorithmUpgradeCredentialPasswordEncoder;
import org.apache.jetspeed.security.spi.JetspeedSecuritySynchronizer;
import org.apache.jetspeed.security.spi.UserPasswordCredentialAccessManager;
import org.apache.jetspeed.security.spi.UserPasswordCredentialManager;
import org.apache.jetspeed.security.spi.UserPasswordCredentialPolicyManager;
import org.apache.jetspeed.security.spi.UserPasswordCredentialStorageManager;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ldap.core.DistinguishedName;
import org.springframework.ldap.filter.AndFilter;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.Filter;
import org.springframework.ldap.filter.HardcodedFilter;
import org.springframework.ldap.pool.factory.PoolingContextSource;
import org.springframework.ldap.support.LdapUtils;

/**
 * @version $Id: LdapUserPasswordCredentialManagerImpl.java 931491 2010-04-07 10:20:17Z ate $
 */
public class LdapUserPasswordCredentialManagerImpl implements UserPasswordCredentialManager
{
    private static final long serialVersionUID = 1131764631931510796L;
    
    static final Logger log = LoggerFactory.getLogger(UserPasswordCredentialManager.class);
    
    private UserPasswordCredentialStorageManager upcsm;
    private UserPasswordCredentialAccessManager upcam;
    private UserPasswordCredentialPolicyManager upcpm;
    private UserManager um;
    private JetspeedSecuritySynchronizer synchronizer;
    private PoolingContextSource poolingContextsource;
    private String userEntryPrefix;
    private DistinguishedName userSearchPath;
    private SearchControls searchControls;
    private Filter userFilter;
    private CredentialPasswordEncoder cpe;
    private boolean persistCredentials;
    private boolean changePasswordByUser;
    
    public LdapUserPasswordCredentialManagerImpl(UserPasswordCredentialStorageManager upcsm, UserPasswordCredentialAccessManager upcam,
                                                 UserPasswordCredentialPolicyManager upcpm, CredentialPasswordEncoder cpe,
                                                 PoolingContextSource poolingContextSource, 
                                                 String userSearchBase, String userFilter, String userEntryPrefix, String searchScope)
    {
        this.upcsm = upcsm;
        this.upcam = upcam;
        this.upcpm = upcpm;
        this.cpe =  cpe != null && (upcpm == null || upcpm.getCredentialPasswordEncoder() != cpe) ? cpe : null;
        this.poolingContextsource = poolingContextSource;
        this.userEntryPrefix = userEntryPrefix;        
        this.userSearchPath = new DistinguishedName(userSearchBase);
        if (!StringUtils.isEmpty(userFilter))
        {
            this.userFilter = new HardcodedFilter(userFilter);
        }        
        this.searchControls = new SearchControls();
        this.searchControls.setReturningAttributes(new String[]{});
        this.searchControls.setReturningObjFlag(false);
        this.searchControls.setSearchScope(Integer.parseInt(searchScope));
    }
    
    protected String getUserDn(String userName) throws SecurityException
    {
        DirContext ctx = null;
        try
        {
            Filter filter = new EqualsFilter(userEntryPrefix, userName);
            if (userFilter != null)
            {
                filter = new AndFilter().and(userFilter).and(filter);
            }
            ctx = poolingContextsource.getReadOnlyContext();
            NamingEnumeration<SearchResult> results = ctx.search(userSearchPath, filter.encode(), searchControls);
            
            String dn = null;         
            if (null != results && results.hasMore())
            {
                SearchResult result = results.next();
                dn = result.getNameInNamespace();
            }
            if (dn == null)
            {
                throw new SecurityException(SecurityException.PRINCIPAL_DOES_NOT_EXIST.createScoped(JetspeedPrincipalType.USER, userName));
            }
            return dn;
        }
        catch (NamingException nex)
        {
            throw new SecurityException(SecurityException.UNEXPECTED.create(getClass().getName(), "authenticateUser", nex.getMessage()), nex);
        }
        finally
        {
            LdapUtils.closeContext(ctx);
        }
    }

    protected void authenticateUser(String userName, String dn, String password) throws SecurityException
    {
        DirContext ctx = null;
        try
        {
            // Note: this "authenticating" context is (logically) not pooled
            ctx = poolingContextsource.getContextSource().getContext(dn, password);
            ctx.close();
            ctx = null;
        }
        catch (AuthenticationException aex)
        {
            if (aex.getMessage() != null && aex.getMessage().equalsIgnoreCase("[LDAP: error code 49 - Invalid Credentials]"))
            {
                throw new InvalidPasswordException();
            }
            else
            {
                throw new SecurityException(aex);
            }
        }
        catch (NamingException nex)
        {
            throw new SecurityException(SecurityException.UNEXPECTED.create(getClass().getName(), "authenticateUser", nex.getMessage()), nex);
        }
        finally
        {
            LdapUtils.closeContext(ctx);
        }
    }
    
    protected void setPassword(String userName, String dn, String oldPassword, String newPassword, boolean changePasswordByUserOnly) throws SecurityException
    {
        DirContext ctx = null;
        try
        {
            if (changePasswordByUserOnly)
            {
                // Note: this "authenticating" context is (logically) not pooled
                ctx = poolingContextsource.getContextSource().getContext(dn, oldPassword);
            }
            else
            {
                ctx = poolingContextsource.getReadWriteContext();
            }
            DistinguishedName name = new DistinguishedName(dn);
            name.removeFirst(new DistinguishedName(ctx.getNameInNamespace()));
            Attribute namingAttr = new BasicAttribute("userPassword", newPassword);
            ModificationItem[] items = new ModificationItem[1];
            items[0] = new ModificationItem(DirContext.REPLACE_ATTRIBUTE, namingAttr);
            ctx.modifyAttributes(name, items);
        }
        catch (NamingException nex)
        {
            throw new SecurityException(SecurityException.UNEXPECTED.create(getClass().getName(), "setPassword", nex.getMessage()), nex);
        }
        finally
        {
            LdapUtils.closeContext(ctx);
        }
    }
    
    public void setUserManager(UserManager um)
    {
        this.um = um;
    }
    
    public void setJetspeedSecuritySynchronizer(JetspeedSecuritySynchronizer synchronizer)
    {
        this.synchronizer = synchronizer;
    }
    
    public void setPersistCredentials(boolean persistCredentials)
    {
        this.persistCredentials = persistCredentials;
    }
    
    public boolean isPersistCredentials()
    {
        return persistCredentials;
    }
    
    public void setChangePasswordByUser(boolean changePasswordByUser)
    {
        this.changePasswordByUser = changePasswordByUser;
    }
    
    public boolean isChangePasswordByUser()
    {
        return changePasswordByUser;
    }
    
    public PasswordCredential getPasswordCredential(User user) throws SecurityException
    {
        if (isPersistCredentials())
        {
            PasswordCredential credential = upcsm.getPasswordCredential(user);
            if (!credential.isNew() && upcpm != null)
            {
                if (upcpm.onLoad(credential, user.getName()))
                {
                    upcsm.storePasswordCredential(credential);                
                }
            }
            return credential;
        }
        else
        {
            // create new transient credential
            PasswordCredentialImpl credential = new PasswordCredentialImpl();
            credential.setUser(user);
            return credential;
        }
    }

    public void storePasswordCredential(PasswordCredential credential) throws SecurityException
    {
        String userDn = null;
        boolean authenticated = false;
        boolean isNewPasswordSet = credential.isNewPasswordSet();
        String newPassword = credential.getNewPassword();
        String oldPassword = credential.getOldPassword();
        String password = credential.getPassword();
        boolean encoded = credential.isEncoded();
        
        if (SynchronizationStateAccess.isSynchronizing())
        {
            authenticated = true;
        }
        else if (isNewPasswordSet)
        {
            userDn = getUserDn(credential.getUserName());
            if (oldPassword != null)
            {
                authenticateUser(credential.getUserName(), userDn, oldPassword);
                authenticated = true;
            }
        }
        
        if (upcpm != null)
        {
            upcpm.onStore(credential, authenticated);
        }
        if (isPersistCredentials())
        {
            upcsm.storePasswordCredential(credential);
        }
        
        if (isNewPasswordSet && !SynchronizationStateAccess.isSynchronizing())
        {
            String ldapPassword = credential.getPassword();
            if (cpe != null && newPassword != null || !encoded)
            {
                // encode password for LDAP ourselves
                ldapPassword = cpe.encode(credential.getUserName(), newPassword != null ? newPassword : password);
            }
            setPassword(credential.getUserName(), userDn, oldPassword, ldapPassword, oldPassword != null ? changePasswordByUser : false);
        }
    }

    public PasswordCredential getAuthenticatedPasswordCredential(String userName, String password) throws SecurityException
    {
        if (!SynchronizationStateAccess.isSynchronizing())
        {
            authenticateUser(userName, getUserDn(userName), password);
            if (synchronizer != null)
            {
                synchronizer.synchronizeUserPrincipal(userName);
            }
        }
        PasswordCredential credential = isPersistCredentials() ? upcam.getPasswordCredential(userName) : new PasswordCredentialImpl();
        if (credential == null)
        {
            credential = new PasswordCredentialImpl();
            // persistCredentials but user credentials not yet synchronized/stored
            if (um == null)
            {
                log.error("New User PasswordCredential cannot be persisted: requires UserManager to be set!!!");
            }
            else
            {
                // to be able to store the new password credential it needs the User to be set
                ((PasswordCredentialImpl)credential).setUser(um.getUser(userName));
            }
        }
        boolean setPassword = false;
        if (isPersistCredentials() && (!credential.isNew() || credential.getUser() != null))
        {
            if (credential.isNew())
            {
                setPassword = true;
            }
            else
            {
                String encodedPassword = password;
                if (upcpm != null && upcpm.getCredentialPasswordEncoder() != null && credential.isEncoded())
                {
                    CredentialPasswordEncoder encoder = upcpm.getCredentialPasswordEncoder();
                    if (upcpm.getCredentialPasswordEncoder() instanceof AlgorithmUpgradeCredentialPasswordEncoder)
                    {
                        encodedPassword = ((AlgorithmUpgradeCredentialPasswordEncoder)encoder).encode(credential, password);
                    }
                    else
                    {
                        encodedPassword = encoder.encode(userName, password);
                    }
                }
                if (!credential.getPassword().equals(encodedPassword))
                {
                    setPassword = true;
                }
            }
            if (setPassword)
            {
                credential.setPassword(null, password);
                boolean synchronizing = SynchronizationStateAccess.isSynchronizing();
                try
                {
                    SynchronizationStateAccess.setSynchronizing(Boolean.TRUE);
                    storePasswordCredential(credential);
                }
                finally
                {
                    SynchronizationStateAccess.setSynchronizing(synchronizing ? Boolean.TRUE : Boolean.FALSE);
                }                
            }
            
            if (upcpm != null)
            {
                if (upcpm.onLoad(credential, userName))
                {
                    upcsm.storePasswordCredential(credential);
                }
                if (credential.isEnabled() && !credential.isExpired())
                {
                    if (upcpm.authenticate(credential, userName, password, true))
                    {
                        upcsm.storePasswordCredential(credential);
                    }
                    if (!credential.isEnabled() || credential.isExpired())
                    {
                        throw new SecurityException(SecurityException.PRINCIPAL_DOES_NOT_EXIST.createScoped(JetspeedPrincipalType.USER, userName));
                    }
                    else if (credential.getAuthenticationFailures() != 0)
                    {
                        throw new SecurityException(SecurityException.INVALID_PASSWORD);
                    }
                }
            }
        }
        if (credential.getUser() == null)
        {
            if (!credential.isNew())
            {            
                try
                {
                    upcam.loadPasswordCredentialUser(credential);
                }
                catch (Exception e)
                {
                    throw new SecurityException(SecurityException.PRINCIPAL_DOES_NOT_EXIST.createScoped(JetspeedPrincipalType.USER, userName), e);
                }            
            }
            else
            {
                ((PasswordCredentialImpl)credential).setUserName(userName);
            }
        }
        return credential;
    }
}
