/*
 *  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.directory.server.core.authn;


import static org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum.INSUFFICIENT_PASSWORD_QUALITY;
import static org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum.PASSWORD_TOO_SHORT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_CHANGED_TIME_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_END_TIME_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_FAILURE_TIME_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_GRACE_USE_TIME_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_HISTORY_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_LAST_SUCCESS_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_POLICY_SUBENTRY_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_RESET_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_START_TIME_AT;
import static org.apache.directory.api.ldap.model.entry.ModificationOperation.ADD_ATTRIBUTE;
import static org.apache.directory.api.ldap.model.entry.ModificationOperation.REMOVE_ATTRIBUTE;
import static org.apache.directory.api.ldap.model.entry.ModificationOperation.REPLACE_ATTRIBUTE;

import java.io.IOException;
import java.security.MessageDigest;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;

import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyRequest;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyResponse;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyResponseImpl;
import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants;
import org.apache.directory.api.ldap.model.constants.SchemaConstants;
import org.apache.directory.api.ldap.model.entry.Attribute;
import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
import org.apache.directory.api.ldap.model.entry.DefaultModification;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.entry.Modification;
import org.apache.directory.api.ldap.model.entry.ModificationOperation;
import org.apache.directory.api.ldap.model.entry.Value;
import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
import org.apache.directory.api.ldap.model.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapNoPermissionException;
import org.apache.directory.api.ldap.model.exception.LdapOperationException;
import org.apache.directory.api.ldap.model.exception.LdapOtherException;
import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
import org.apache.directory.api.ldap.model.name.Dn;
import org.apache.directory.api.ldap.model.password.PasswordUtil;
import org.apache.directory.api.ldap.model.schema.AttributeType;
import org.apache.directory.api.util.DateUtils;
import org.apache.directory.api.util.Strings;
import org.apache.directory.server.constants.ServerDNConstants;
import org.apache.directory.server.core.api.CoreSession;
import org.apache.directory.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.InterceptorEnum;
import org.apache.directory.server.core.api.LdapPrincipal;
import org.apache.directory.server.core.api.authn.ppolicy.CheckQualityEnum;
import org.apache.directory.server.core.api.authn.ppolicy.DefaultPasswordValidator;
import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyConfiguration;
import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyException;
import org.apache.directory.server.core.api.authn.ppolicy.PasswordValidator;
import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
import org.apache.directory.server.core.api.interceptor.Interceptor;
import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
import org.apache.directory.server.core.api.interceptor.context.CompareOperationContext;
import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
import org.apache.directory.server.core.api.interceptor.context.GetRootDseOperationContext;
import org.apache.directory.server.core.api.interceptor.context.HasEntryOperationContext;
import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
import org.apache.directory.server.core.api.interceptor.context.OperationContext;
import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
import org.apache.directory.server.core.api.interceptor.context.UnbindOperationContext;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.core.api.partition.PartitionTxn;
import org.apache.directory.server.core.authn.ppolicy.PpolicyConfigContainer;
import org.apache.directory.server.core.shared.DefaultCoreSession;
import org.apache.directory.server.i18n.I18n;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * An {@link Interceptor} that authenticates users.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public class AuthenticationInterceptor extends BaseInterceptor
{
    private static final Logger LOG = LoggerFactory.getLogger( AuthenticationInterceptor.class );

    /**
     * Speedup for logs
     */
    private static final boolean IS_DEBUG = LOG.isDebugEnabled();

    /** A Set of all the existing Authenticator to be used by the bind operation */
    private Set<Authenticator> authenticators = new HashSet<>();

    /** A map of authenticators associated with the authentication level required */
    private final EnumMap<AuthenticationLevel, Collection<Authenticator>> authenticatorsMapByType = new EnumMap<>( AuthenticationLevel.class );

    private CoreSession adminSession;

    // pwdpolicy state attribute types
    private AttributeType pwdResetAT;

    private AttributeType pwdChangedTimeAT;

    private AttributeType pwdHistoryAT;

    private AttributeType pwdFailurTimeAT;

    private AttributeType pwdAccountLockedTimeAT;

    private AttributeType pwdLastSuccessAT;

    private AttributeType pwdGraceUseTimeAT;

    private AttributeType pwdPolicySubentryAT;

    private AttributeType pwdStartTimeAT;

    private AttributeType pwdEndTimeAT;

    /** a container to hold all the ppolicies */
    private PpolicyConfigContainer pwdPolicyContainer;


    /**
     * Creates an authentication service interceptor.
     */
    public AuthenticationInterceptor()
    {
        super( InterceptorEnum.AUTHENTICATION_INTERCEPTOR );
    }


    /**
     * Registers and initializes all {@link Authenticator}s to this service.
     */
    @Override
    public void init( DirectoryService directoryService ) throws LdapException
    {
        super.init( directoryService );

        adminSession = directoryService.getAdminSession();

        if ( ( authenticators == null ) || authenticators.isEmpty() )
        {
            setDefaultAuthenticators();
        }

        // Register all authenticators
        for ( Authenticator authenticator : authenticators )
        {
            register( authenticator, directoryService );
        }

        loadPwdPolicyStateAttributeTypes();
    }


    /**
     * Initialize the set of authenticators with some default values
     */
    private void setDefaultAuthenticators()
    {
        if ( authenticators == null )
        {
            authenticators = new HashSet<>();
        }

        authenticators.clear();
        authenticators.add( new AnonymousAuthenticator( Dn.ROOT_DSE ) );
        authenticators.add( new SimpleAuthenticator( Dn.ROOT_DSE ) );
        authenticators.add( new StrongAuthenticator( Dn.ROOT_DSE ) );
    }


    public Set<Authenticator> getAuthenticators()
    {
        return authenticators;
    }


    /**
     * @param authenticators authenticators to be used by this AuthenticationInterceptor
     */
    public void setAuthenticators( Set<Authenticator> authenticators )
    {
        if ( authenticators == null )
        {
            this.authenticators.clear();
        }
        else
        {
            this.authenticators = authenticators;
        }
    }


    /**
     * @param authenticators authenticators to be used by this AuthenticationInterceptor
     */
    public void setAuthenticators( Authenticator[] authenticators )
    {
        if ( authenticators == null )
        {
            throw new IllegalArgumentException( "The given authenticators set is null" );
        }

        this.authenticators.clear();
        this.authenticatorsMapByType.clear();

        for ( Authenticator authenticator : authenticators )
        {
            try
            {
                register( authenticator, directoryService );
            }
            catch ( LdapException le )
            {
                LOG.error( "Cannot register authenticator {}", authenticator );
            }
        }
    }


    /**
     * Deinitializes and deregisters all {@link Authenticator}s from this service.
     */
    @Override
    public void destroy()
    {
        authenticatorsMapByType.clear();
        Set<Authenticator> copy = new HashSet<>( authenticators );
        authenticators = new HashSet<>();

        for ( Authenticator authenticator : copy )
        {
            authenticator.destroy();
        }
    }


    /**
     * Initializes the specified {@link Authenticator} and registers it to
     * this service.
     *
     * @param authenticator Authenticator to initialize and register by type
     * @param directoryService configuration info to supply to the Authenticator during initialization
     * @throws javax.naming.Exception if initialization fails.
     */
    private void register( Authenticator authenticator, DirectoryService directoryService ) throws LdapException
    {
        authenticator.init( directoryService );
        authenticators.add( authenticator );

        Collection<Authenticator> authenticatorList = getAuthenticators( authenticator.getAuthenticatorType() );

        if ( authenticatorList == null )
        {
            authenticatorList = new ArrayList<>();
            authenticatorsMapByType.put( authenticator.getAuthenticatorType(), authenticatorList );
        }

        if ( !authenticatorList.contains( authenticator ) )
        {
            authenticatorList.add( authenticator );
        }
    }


    /**
     * Returns the list of {@link Authenticator}s with the specified type.
     *
     * @param type type of Authenticator sought
     * @return A list of Authenticators of the requested type or <tt>null</tt> if no authenticator is found.
     */
    private Collection<Authenticator> getAuthenticators( AuthenticationLevel type )
    {
        Collection<Authenticator> result = authenticatorsMapByType.get( type );

        if ( ( result != null ) && ( !result.isEmpty() ) )
        {
            return result;
        }
        else
        {
            return null;
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void add( AddOperationContext addContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", addContext );
        }

        checkAuthenticated( addContext );

        Entry entry = addContext.getEntry();

        if ( !directoryService.isPwdPolicyEnabled() || addContext.isReplEvent() )
        {
            next( addContext );
            return;
        }

        PasswordPolicyConfiguration policyConfig = getPwdPolicy( entry );

        boolean isPPolicyReqCtrlPresent = addContext.hasRequestControl( PasswordPolicyRequest.OID );

        checkPwdReset( addContext );

        // Get the password depending on the configuration
        String passwordAttribute = SchemaConstants.USER_PASSWORD_AT;

        if ( isPPolicyReqCtrlPresent )
        {
            passwordAttribute = policyConfig.getPwdAttribute();
        }

        Attribute userPasswordAttribute = entry.get( passwordAttribute );

        if ( userPasswordAttribute != null )
        {
            Value userPassword = userPasswordAttribute.get();

            try
            {
                check( addContext, entry, userPassword.getBytes(), policyConfig );
            }
            catch ( PasswordPolicyException e )
            {
                if ( isPPolicyReqCtrlPresent )
                {
                    PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
                    responseControl.setPasswordPolicyError(
                        PasswordPolicyErrorEnum.get( e.getErrorCode() ) );
                    addContext.addResponseControl( responseControl );
                }

                // throw exception if userPassword quality checks fail
                throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION, e.getMessage(), e );
            }

            String pwdChangedTime = DateUtils.getGeneralizedTime( directoryService.getTimeProvider() );

            if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
            {
                // https://issues.apache.org/jira/browse/DIRSERVER-1978
                if ( !addContext.getSession().isAnAdministrator()
                    || entry.get( pwdChangedTimeAT ) == null )
                {
                    Attribute pwdChangedTimeAt = new DefaultAttribute( pwdChangedTimeAT );
                    pwdChangedTimeAt.add( pwdChangedTime );
                    entry.add( pwdChangedTimeAt );
                }
            }

            if ( policyConfig.isPwdMustChange() && addContext.getSession().isAnAdministrator() )
            {
                Attribute pwdResetAt = new DefaultAttribute( pwdResetAT );
                pwdResetAt.add( "TRUE" );
                entry.add( pwdResetAt );
            }

            if ( policyConfig.getPwdInHistory() > 0 )
            {
                Attribute pwdHistoryAt = new DefaultAttribute( pwdHistoryAT );
                byte[] pwdHistoryVal = new PasswordHistory( pwdChangedTime, userPassword.getBytes() ).getHistoryValue();
                pwdHistoryAt.add( pwdHistoryVal );
                entry.add( pwdHistoryAt );
            }
        }

        next( addContext );
    }


    /**
     * Return the selected authenticator given the DN and the level required.
     */
    private Authenticator selectAuthenticator( Dn bindDn, AuthenticationLevel level )
        throws LdapUnwillingToPerformException, LdapAuthenticationException
    {
        Authenticator selectedAuthenticator = null;
        Collection<Authenticator> levelAuthenticators = authenticatorsMapByType.get( level );

        if ( ( levelAuthenticators == null ) || levelAuthenticators.isEmpty() )
        {
            // No authenticators associated with this level : get out
            throw new LdapAuthenticationException( "Cannot Bind for Dn "
                + bindDn.getName() + ", no authenticator for the requested level " + level );
        }

        if ( levelAuthenticators.size() == 1 )
        {
            // Just pick the existing one
            for ( Authenticator authenticator : levelAuthenticators )
            {
                // Check that the bindDN fits
                if ( authenticator.isValid( bindDn ) )
                {
                    return authenticator;
                }
                else
                {
                    throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
                        "Cannot Bind for Dn " + bindDn.getName() 
                        + ", its not a descendant of the authenticator base DN '" + authenticator.getBaseDn() + "'" );
                }
            }
        }

        // We have more than one authenticator. Let's loop on all of them and
        // select the one that fits the bindDN
        Dn innerDn = Dn.ROOT_DSE;

        for ( Authenticator authenticator : levelAuthenticators )
        {
            if ( authenticator.isValid( bindDn ) )
            {
                // We have found a valid authenticator, let's check if it's the inner one
                if ( innerDn.isAncestorOf( authenticator.getBaseDn() ) )
                {
                    innerDn = authenticator.getBaseDn();
                    selectedAuthenticator = authenticator;
                }
            }
        }

        if ( selectedAuthenticator == null )
        {
            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
                    "Cannot Bind for Dn " + bindDn.getName() + ", there is no authenticator for it" );
        }
        
        return selectedAuthenticator;
    }
    
    
    private void internalModify( OperationContext opContext, ModifyOperationContext bindModCtx ) throws LdapException
    {
        Partition partition = opContext.getPartition();
        bindModCtx.setPartition( partition );
        PartitionTxn partitionTxn = null;

        try
        {
            partitionTxn = partition.beginWriteTransaction();
            bindModCtx.setTransaction( partitionTxn );

            directoryService.getPartitionNexus().modify( bindModCtx );

            partitionTxn.commit();
        }
        catch ( LdapException le )
        {
            try 
            {
                if ( partitionTxn != null )
                {
                    partitionTxn.abort();
                }
                
                throw le;
            }
            catch ( IOException ioe )
            {
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
        }
        catch ( IOException ioe )
        {
            try 
            {
                partitionTxn.abort();
                
                throw new LdapOtherException( ioe.getMessage(), ioe );
            }
            catch ( IOException ioe2 )
            {
                throw new LdapOtherException( ioe2.getMessage(), ioe2 );
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void bind( BindOperationContext bindContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", bindContext );
        }

        CoreSession session = bindContext.getSession();
        Dn bindDn = bindContext.getDn();

        if ( ( session != null )
            && ( session.getEffectivePrincipal() != null )
            && ( !session.isAnonymous() )
            && ( !session.isAdministrator() ) )
        {
            // null out the credentials
            bindContext.setCredentials( null );
        }

        // pick the first matching authenticator type
        AuthenticationLevel level = bindContext.getAuthenticationLevel();

        if ( level == AuthenticationLevel.UNAUTHENT )
        {
            // This is a case where the Bind request contains a Dn, but no password.
            // We don't check the Dn, we just return a UnwillingToPerform error
            // Cf RFC 4513, chap. 5.1.2
            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, "Cannot Bind for Dn "
                + bindDn.getName() );
        }

        PasswordPolicyException ppe = null;
        boolean isPPolicyReqCtrlPresent = bindContext.hasRequestControl( PasswordPolicyRequest.OID );
        PasswordPolicyResponse pwdRespCtrl = new PasswordPolicyResponseImpl();
        boolean authenticated = false;

        Authenticator authenticator = selectAuthenticator( bindDn, level );

        try
        {
            // perform the authentication
            LdapPrincipal principal = authenticator.authenticate( bindContext );

            if ( principal != null )
            {
                LdapPrincipal clonedPrincipal = ( LdapPrincipal ) ( principal.clone() );

                // remove creds so there is no security risk
                bindContext.setCredentials( null );
                clonedPrincipal.setUserPassword( Strings.EMPTY_BYTES );

                // authentication was successful
                CoreSession newSession = new DefaultCoreSession( clonedPrincipal, directoryService );
                bindContext.setSession( newSession );

                authenticated = true;
            }
        }
        catch ( PasswordPolicyException e )
        {
            ppe = e;
        }
        catch ( LdapAuthenticationException e )
        {
            // authentication failed, try the next authenticator
            LOG.info( "Authenticator {} failed to authenticate: {}", authenticator, bindContext.getDn() );
        }
        catch ( Exception e )
        {
            // Log other exceptions than LdapAuthenticationException
            LOG.info( "Unexpected failure for Authenticator {} : {}", authenticator, bindContext.getDn() );
        }

        if ( ppe != null )
        {
            if ( isPPolicyReqCtrlPresent )
            {
                pwdRespCtrl.setPasswordPolicyError( PasswordPolicyErrorEnum.get( ppe.getErrorCode() ) );
                bindContext.addResponseControl( pwdRespCtrl );
            }

            throw ppe;
        }

        Entry userEntry = bindContext.getEntry();

        PasswordPolicyConfiguration policyConfig = getPwdPolicy( userEntry );

        // load the user entry again if ppolicy is enabled, cause the authenticator might have modified the entry
        if ( policyConfig != null )
        {
            LookupOperationContext lookupContext = new LookupOperationContext( adminSession, bindDn,
                SchemaConstants.ALL_ATTRIBUTES_ARRAY );
            lookupContext.setPartition( bindContext.getPartition() );
            lookupContext.setTransaction( bindContext.getTransaction() );
            
            userEntry = directoryService.getPartitionNexus().lookup( lookupContext );
        }

        // check if the user entry is null, it will be null
        // in cases of anonymous bind
        if ( authenticated && ( userEntry == null ) && directoryService.isAllowAnonymousAccess() )
        {
            return;
        }

        if ( !authenticated )
        {
            if ( LOG.isInfoEnabled() )
            {
                LOG.info( "Cannot bind to the server " );
            }

            if ( ( policyConfig != null ) && ( userEntry != null ) )
            {
                Attribute pwdFailTimeAt = userEntry.get( pwdFailurTimeAT );

                if ( pwdFailTimeAt == null )
                {
                    pwdFailTimeAt = new DefaultAttribute( pwdFailurTimeAT );
                }
                else
                {
                    purgeFailureTimes( policyConfig, pwdFailTimeAt );
                }

                String failureTime = DateUtils.getGeneralizedTime( directoryService.getTimeProvider() );
                pwdFailTimeAt.add( failureTime );
                Modification pwdFailTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdFailTimeAt );

                List<Modification> mods = new ArrayList<>();
                mods.add( pwdFailTimeMod );

                int numFailures = pwdFailTimeAt.size();

                if ( policyConfig.isPwdLockout() && ( numFailures >= policyConfig.getPwdMaxFailure() ) )
                {
                    // Checking that we're not locking the admin user of the system partition
                    // See DIRSERVER-1812 (The default admin account should never get locked forever)
                    if ( !userEntry.getDn().equals( new Dn( schemaManager, ServerDNConstants.ADMIN_SYSTEM_DN ) ) )
                    {
                        Attribute pwdAccountLockedTimeAt = new DefaultAttribute( pwdAccountLockedTimeAT );

                        // if zero, lockout permanently, only admin can unlock it
                        if ( policyConfig.getPwdLockoutDuration() == 0 )
                        {
                            pwdAccountLockedTimeAt.add( "000001010000Z" );
                        }
                        else
                        {
                            pwdAccountLockedTimeAt.add( failureTime );
                        }

                        Modification pwdAccountLockedMod = new DefaultModification( REPLACE_ATTRIBUTE,
                            pwdAccountLockedTimeAt );
                        mods.add( pwdAccountLockedMod );

                        pwdRespCtrl.setPasswordPolicyError( PasswordPolicyErrorEnum.ACCOUNT_LOCKED );
                    }
                }
                else if ( policyConfig.getPwdMinDelay() > 0 )
                {
                    int numDelay = numFailures * policyConfig.getPwdMinDelay();
                    int maxDelay = policyConfig.getPwdMaxDelay();

                    if ( numDelay > maxDelay )
                    {
                        numDelay = maxDelay;
                    }

                    try
                    {
                        Thread.sleep( numDelay * 1000L );
                    }
                    catch ( InterruptedException e )
                    {
                        LOG.warn(
                            "Interrupted while delaying to send the failed authentication response for the user {}",
                            bindDn, e );
                    }
                }

                if ( !mods.isEmpty() )
                {
                    String csnVal = directoryService.getCSN().toString();
                    Modification csnMod = new DefaultModification( REPLACE_ATTRIBUTE, directoryService.getAtProvider()
                        .getEntryCSN(), csnVal );
                    mods.add( csnMod );
                    ModifyOperationContext bindModCtx = new ModifyOperationContext( adminSession );
                    bindModCtx.setDn( bindDn );
                    bindModCtx.setEntry( userEntry );
                    bindModCtx.setModItems( mods );
                    bindModCtx.setPushToEvtInterceptor( true );

                    internalModify( bindContext, bindModCtx );
                }
            }

            String upDn = bindDn == null ? "" : bindDn.getName();
            throw new LdapAuthenticationException( I18n.err( I18n.ERR_229, upDn ) );
        }
        else if ( policyConfig != null )
        {
            List<Modification> mods = new ArrayList<>();

            if ( policyConfig.getPwdMaxIdle() > 0 )
            {
                Attribute pwdLastSuccesTimeAt = new DefaultAttribute( pwdLastSuccessAT );
                pwdLastSuccesTimeAt.add( DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
                Modification pwdLastSuccesTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdLastSuccesTimeAt );
                mods.add( pwdLastSuccesTimeMod );
            }

            Attribute pwdFailTimeAt = userEntry.get( pwdFailurTimeAT );

            if ( pwdFailTimeAt != null )
            {
                Modification pwdFailTimeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdFailTimeAt );
                mods.add( pwdFailTimeMod );
            }

            Attribute pwdAccLockedTimeAt = userEntry.get( pwdAccountLockedTimeAT );

            if ( pwdAccLockedTimeAt != null )
            {
                Modification pwdAccLockedTimeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdAccLockedTimeAt );
                mods.add( pwdAccLockedTimeMod );
            }

            // checking the expiration time *after* performing authentication, do we need to care about millisecond precision?
            if ( ( policyConfig.getPwdMaxAge() > 0 ) && ( policyConfig.getPwdGraceAuthNLimit() > 0 ) )
            {
                Attribute pwdChangeTimeAttr = userEntry.get( pwdChangedTimeAT );

                if ( pwdChangeTimeAttr != null )
                {
                    boolean expired = PasswordUtil.isPwdExpired( pwdChangeTimeAttr.getString(),
                        policyConfig.getPwdMaxAge(), directoryService.getTimeProvider() );

                    if ( expired )
                    {
                        Attribute pwdGraceUseAttr = userEntry.get( pwdGraceUseTimeAT );
                        int numGraceAuth;

                        if ( pwdGraceUseAttr != null )
                        {
                            numGraceAuth = policyConfig.getPwdGraceAuthNLimit() - ( pwdGraceUseAttr.size() + 1 );
                        }
                        else
                        {
                            pwdGraceUseAttr = new DefaultAttribute( pwdGraceUseTimeAT );
                            numGraceAuth = policyConfig.getPwdGraceAuthNLimit() - 1;
                        }

                        pwdRespCtrl.setGraceAuthNRemaining( numGraceAuth );

                        pwdGraceUseAttr.add( DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
                        Modification pwdGraceUseMod = new DefaultModification( ADD_ATTRIBUTE, pwdGraceUseAttr );
                        mods.add( pwdGraceUseMod );
                    }
                }
            }

            if ( !mods.isEmpty() )
            {
                String csnVal = directoryService.getCSN().toString();
                Modification csnMod = new DefaultModification( REPLACE_ATTRIBUTE, directoryService.getAtProvider()
                    .getEntryCSN(), csnVal );
                mods.add( csnMod );

                ModifyOperationContext bindModCtx = new ModifyOperationContext( adminSession );
                bindModCtx.setDn( bindDn );
                bindModCtx.setEntry( userEntry );
                bindModCtx.setModItems( mods );
                bindModCtx.setPushToEvtInterceptor( true );
                
                internalModify( bindContext, bindModCtx );
            }

            if ( isPPolicyReqCtrlPresent )
            {
                int expiryWarnTime = getPwdTimeBeforeExpiry( userEntry, policyConfig );

                if ( expiryWarnTime > 0 )
                {
                    pwdRespCtrl.setTimeBeforeExpiration( expiryWarnTime );
                }

                if ( isPwdMustReset( userEntry ) )
                {
                    pwdRespCtrl.setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
                    bindContext.getSession().setPwdMustChange( true );
                }

                bindContext.addResponseControl( pwdRespCtrl );
            }
        }
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean compare( CompareOperationContext compareContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", compareContext );
        }

        checkAuthenticated( compareContext );
        checkPwdReset( compareContext );
        return next( compareContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void delete( DeleteOperationContext deleteContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", deleteContext );
        }

        // Check that we can execute this operation
        checkAuthenticated( deleteContext );

        // propagate the call to the next interceptor
        next( deleteContext );
        
        // if the deleted entry contains a password, then invalidate the associated caches
        invalidateAuthenticatorCaches( deleteContext.getDn() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry getRootDse( GetRootDseOperationContext getRootDseContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", getRootDseContext );
        }

        checkAuthenticated( getRootDseContext );
        checkPwdReset( getRootDseContext );

        return next( getRootDseContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean hasEntry( HasEntryOperationContext hasEntryContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", hasEntryContext );
        }

        checkAuthenticated( hasEntryContext );
        checkPwdReset( hasEntryContext );

        return next( hasEntryContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Entry lookup( LookupOperationContext lookupContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", lookupContext );
        }

        checkAuthenticated( lookupContext );
        checkPwdReset( lookupContext );

        return next( lookupContext );
    }


    private void invalidateAuthenticatorCaches( Dn principalDn )
    {
        for ( AuthenticationLevel authMech : authenticatorsMapByType.keySet() )
        {
            // try each authenticator
            for ( Authenticator authenticator : getAuthenticators( authMech ) )
            {
                authenticator.invalidateCache( principalDn );
            }
        }
    }

    
    /**
     * {@inheritDoc}
     */
    @Override
    public void modify( ModifyOperationContext modifyContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", modifyContext );
        }

        checkAuthenticated( modifyContext );

        if ( !directoryService.isPwdPolicyEnabled() || modifyContext.isReplEvent() )
        {
            processStandardModify( modifyContext );
        }
        else
        {
            processPasswordPolicydModify( modifyContext );
        }
    }

    
    /**
     * Proceed with the Modification operation when the PasswordPolicy is not activated.
     */
    private void processStandardModify( ModifyOperationContext modifyContext ) throws LdapException
    {
        next( modifyContext );

        List<Modification> modifications = modifyContext.getModItems();

        for ( Modification modification : modifications )
        {
            if ( directoryService.getAtProvider().getUserPassword()
                .equals( modification.getAttribute().getAttributeType() ) )
            {
                invalidateAuthenticatorCaches( modifyContext.getDn() );
                break;
            }
        }
    }

    
    /**
     * Proceed with the Modification operation when the PasswordPolicy is activated.
     */
    private void processPasswordPolicydModify( ModifyOperationContext modifyContext ) throws LdapException
    {
        // handle the case where pwdPolicySubentry AT is about to be deleted in this modify()
        PasswordPolicyConfiguration policyConfig = getPwdPolicy( modifyContext.getEntry() );

        PwdModDetailsHolder pwdModDetails = getPwdModDetails( modifyContext, policyConfig );

        if ( !pwdModDetails.isPwdModPresent() )
        {
            // We can going on, the password attribute is not present in the Modifications.
            next( modifyContext );
        }
        else
        {
            // The password is present in the modifications. Deal with the various use cases.
            CoreSession userSession = modifyContext.getSession();
            boolean isPPolicyReqCtrlPresent = modifyContext.hasRequestControl( PasswordPolicyRequest.OID );
            
            // First, check if the password must be changed, and if the operation allows it
            checkPwdMustChange( modifyContext, userSession, pwdModDetails, isPPolicyReqCtrlPresent );

            // Check the the old password is present if it's required by the PP config
            checkOldPwdRequired( modifyContext, policyConfig, pwdModDetails, isPPolicyReqCtrlPresent );

            // Check that we can't update the password if it's not allowed
            checkChangePwdAllowed( modifyContext, policyConfig, isPPolicyReqCtrlPresent );

            Entry entry = modifyContext.getEntry();

            boolean removePwdReset = false;

            List<Modification> mods = new ArrayList<>();

            if ( pwdModDetails.isAddOrReplace() )
            {
                if ( isPwdTooYoung( modifyContext, entry, policyConfig ) )
                {
                    if ( isPPolicyReqCtrlPresent )
                    {
                        PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
                        responseControl.setPasswordPolicyError(
                            PasswordPolicyErrorEnum.PASSWORD_TOO_YOUNG );
                        modifyContext.addResponseControl( responseControl );
                    }

                    throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION,
                        "password is too young to update" );
                }

                byte[] newPassword = pwdModDetails.getNewPwd();

                try
                {
                    check( modifyContext, entry, newPassword, policyConfig );
                }
                catch ( PasswordPolicyException e )
                {
                    if ( isPPolicyReqCtrlPresent )
                    {
                        PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
                        responseControl.setPasswordPolicyError(
                            PasswordPolicyErrorEnum.get( e.getErrorCode() ) );
                        modifyContext.addResponseControl( responseControl );
                    }

                    // throw exception if userPassword quality checks fail
                    throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION, e.getMessage(), e );
                }

                int histSize = policyConfig.getPwdInHistory();
                Modification pwdRemHistMod = null;
                Modification pwdAddHistMod = null;
                String pwdChangedTime = DateUtils.getGeneralizedTime( directoryService.getTimeProvider() );

                if ( histSize > 0 )
                {
                    Attribute pwdHistoryAt = entry.get( pwdHistoryAT );

                    if ( pwdHistoryAt == null )
                    {
                        pwdHistoryAt = new DefaultAttribute( pwdHistoryAT );
                    }

                    // Build the Modification containing the password history
                    pwdRemHistMod = buildPwdHistory( modifyContext, pwdHistoryAt, histSize, 
                        newPassword, isPPolicyReqCtrlPresent );

                    PasswordHistory newPwdHist = new PasswordHistory( pwdChangedTime, newPassword );
                    pwdHistoryAt.add( newPwdHist.getHistoryValue() );
                    pwdAddHistMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdHistoryAt );
                }

                next( modifyContext );

                invalidateAuthenticatorCaches( modifyContext.getDn() );

                LookupOperationContext lookupContext = new LookupOperationContext( adminSession, modifyContext.getDn(),
                    SchemaConstants.ALL_ATTRIBUTES_ARRAY );
                lookupContext.setPartition( modifyContext.getPartition() );
                lookupContext.setTransaction( modifyContext.getTransaction() );
                
                entry = directoryService.getPartitionNexus().lookup( lookupContext );

                if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
                {
                    Attribute pwdChangedTimeAt = new DefaultAttribute( pwdChangedTimeAT );
                    pwdChangedTimeAt.add( pwdChangedTime );
                    Modification pwdChangedTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdChangedTimeAt );
                    mods.add( pwdChangedTimeMod );
                }

                if ( pwdAddHistMod != null )
                {
                    mods.add( pwdAddHistMod );
                }

                if ( pwdRemHistMod != null )
                {
                    mods.add( pwdRemHistMod );
                }

                if ( policyConfig.isPwdMustChange() )
                {
                    Attribute pwdMustChangeAt = new DefaultAttribute( pwdResetAT );
                    Modification pwdMustChangeMod;

                    if ( modifyContext.getSession().isAnAdministrator() )
                    {
                        pwdMustChangeAt.add( "TRUE" );
                        pwdMustChangeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdMustChangeAt );
                    }
                    else
                    {
                        pwdMustChangeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdMustChangeAt );
                        removePwdReset = true;
                    }

                    mods.add( pwdMustChangeMod );
                }
            }

            // Add the attributes that have been modified following a Add/Replace password
            processModifyAddPwdAttributes( entry, mods, pwdModDetails );

            String csnVal = directoryService.getCSN().toString();
            Modification csnMod = new DefaultModification( REPLACE_ATTRIBUTE, directoryService.getAtProvider()
                .getEntryCSN(), csnVal );
            mods.add( csnMod );

            ModifyOperationContext internalModifyCtx = new ModifyOperationContext( adminSession );
            internalModifyCtx.setPushToEvtInterceptor( true );
            internalModifyCtx.setDn( modifyContext.getDn() );
            internalModifyCtx.setEntry( entry );
            internalModifyCtx.setModItems( mods );

            internalModify( modifyContext, internalModifyCtx );

            if ( removePwdReset || pwdModDetails.isDelete() )
            {
                userSession.setPwdMustChange( false );
            }
        }
    }
    
    
    /**
     * Build the list of passwordHistory
     */
    Modification buildPwdHistory( ModifyOperationContext modifyContext, Attribute pwdHistoryAt, 
        int histSize, byte[] newPassword, boolean isPPolicyReqCtrlPresent ) throws LdapOperationException
    {
        List<PasswordHistory> pwdHistLst = new ArrayList<>();

        for ( Value value : pwdHistoryAt )
        {
            PasswordHistory pwdh = new PasswordHistory( Strings.utf8ToString( value.getBytes() ) );

            // Admin user is exempt from history check
            // https://issues.apache.org/jira/browse/DIRSERVER-2084 
            if ( !modifyContext.getSession().isAnAdministrator() )
            {
                boolean matched = MessageDigest.isEqual( newPassword, pwdh.getPassword() );

                if ( matched )
                {
                    if ( isPPolicyReqCtrlPresent )
                    {
                        PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
                        responseControl.setPasswordPolicyError(
                            PasswordPolicyErrorEnum.PASSWORD_IN_HISTORY );
                        modifyContext.addResponseControl( responseControl );
                    }

                    throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION,
                        "invalid reuse of password present in password history" );
                }
            }

            pwdHistLst.add( pwdh );
        }
 
        Modification pwdRemHistMod = null;
        
        if ( pwdHistLst.size() >= histSize )
        {
            // see the javadoc of PasswordHistory
            Collections.sort( pwdHistLst );

            // remove the oldest value
            PasswordHistory remPwdHist = ( PasswordHistory ) pwdHistLst.toArray()[histSize - 1];
            Attribute tempAt = new DefaultAttribute( pwdHistoryAT );
            tempAt.add( remPwdHist.getHistoryValue() );
            pwdRemHistMod = new DefaultModification( REMOVE_ATTRIBUTE, tempAt );
        }

        return pwdRemHistMod;
    }
    
    
    /**
     * Add the passwordPolicy related Attributes from the modified entry
     */
    private void processModifyAddPwdAttributes( Entry entry, List<Modification> mods, PwdModDetailsHolder pwdModDetails )
    {
        Attribute pwdFailureTimeAt = entry.get( pwdFailurTimeAT );
    
        if ( pwdFailureTimeAt != null )
        {
            mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdFailureTimeAt ) );
        }
    
        Attribute pwdGraceUseTimeAt = entry.get( pwdGraceUseTimeAT );
    
        if ( pwdGraceUseTimeAt != null )
        {
            mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdGraceUseTimeAt ) );
        }
    
        if ( pwdModDetails.isDelete() )
        {
            Attribute pwdHistory = entry.get( pwdHistoryAT );
            
            if ( pwdHistory != null )
            {
                mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdHistory ) );
            }
    
            Attribute pwdChangedTimeAt = entry.get( pwdChangedTimeAT );
            
            if ( pwdChangedTimeAt != null )
            {
                mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdChangedTimeAt ) );
            }
    
            Attribute pwdMustChangeAt = entry.get( pwdResetAT );
            
            if ( pwdMustChangeAt != null )
            {
                mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdMustChangeAt ) );
            }
    
            Attribute pwdAccountLockedTimeAt = entry.get( pwdAccountLockedTimeAT );
            
            if ( pwdAccountLockedTimeAt != null )
            {
                mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdAccountLockedTimeAt ) );
            }
        }
    }

    
    /**
     * Check if the password has to be changed, but can't.
     */
    private void checkPwdMustChange( ModifyOperationContext modifyContext, CoreSession userSession, 
        PwdModDetailsHolder pwdModDetails, boolean isPPolicyReqCtrlPresent ) throws LdapNoPermissionException
    {
        if ( userSession.isPwdMustChange() && !pwdModDetails.isDelete() && pwdModDetails.isOtherModExists() )
       {
           if ( isPPolicyReqCtrlPresent )
           {
               PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
               responseControl.setPasswordPolicyError(
                   PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
               modifyContext.addResponseControl( responseControl );
           }

           throw new LdapNoPermissionException(
               "Password should be reset before making any changes to this entry" );
       }
    }
    
    
    /**
     * If the PP config request it, the old password must be supplied in the modifications. Check that it 
     * is present.
     */
    private void checkOldPwdRequired( ModifyOperationContext modifyContext, PasswordPolicyConfiguration policyConfig,
        PwdModDetailsHolder pwdModDetails, boolean isPPolicyReqCtrlPresent ) throws LdapNoPermissionException
    {
        if ( policyConfig.isPwdSafeModify() && !pwdModDetails.isDelete() && pwdModDetails.isAddOrReplace() )
        {
            String msg = "trying to update password attribute without the supplying the old password";
            LOG.debug( msg );

            if ( isPPolicyReqCtrlPresent )
            {
                PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
                responseControl.setPasswordPolicyError(
                    PasswordPolicyErrorEnum.MUST_SUPPLY_OLD_PASSWORD );
                modifyContext.addResponseControl( responseControl );
            }

            throw new LdapNoPermissionException( msg );
        }
    }
    
    
    /**
     * check that if the password modification is allowed by the PP config, or if the session is 
     * the admin. 
     */
    private void checkChangePwdAllowed( ModifyOperationContext modifyContext, PasswordPolicyConfiguration policyConfig,
        boolean isPPolicyReqCtrlPresent ) throws LdapNoPermissionException
    {
        if ( !policyConfig.isPwdAllowUserChange() && !modifyContext.getSession().isAnAdministrator() )
             
        {
            if ( isPPolicyReqCtrlPresent )
            {
                PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
                responseControl.setPasswordPolicyError(
                    PasswordPolicyErrorEnum.PASSWORD_MOD_NOT_ALLOWED );
                modifyContext.addResponseControl( responseControl );
            }

            throw new LdapNoPermissionException();
        }
    }

    
    /**
     * {@inheritDoc}
     */
    @Override
    public void move( MoveOperationContext moveContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", moveContext );
        }

        checkAuthenticated( moveContext );
        checkPwdReset( moveContext );
        next( moveContext );
        invalidateAuthenticatorCaches( moveContext.getDn() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", moveAndRenameContext );
        }

        checkAuthenticated( moveAndRenameContext );
        checkPwdReset( moveAndRenameContext );
        next( moveAndRenameContext );
        invalidateAuthenticatorCaches( moveAndRenameContext.getDn() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void rename( RenameOperationContext renameContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", renameContext );
        }

        checkAuthenticated( renameContext );
        checkPwdReset( renameContext );
        next( renameContext );
        invalidateAuthenticatorCaches( renameContext.getDn() );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public EntryFilteringCursor search( SearchOperationContext searchContext ) throws LdapException
    {
        if ( IS_DEBUG )
        {
            LOG.debug( "Operation Context: {}", searchContext );
        }

        checkAuthenticated( searchContext );
        checkPwdReset( searchContext );

        return next( searchContext );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void unbind( UnbindOperationContext unbindContext ) throws LdapException
    {
        next( unbindContext );
    }


    /**
     * Check if the current operation has a valid PrincipalDN or not.
     *
     * @param operation the operation type
     * @throws Exception
     */
    private void checkAuthenticated( OperationContext operation ) throws LdapException
    {
        if ( operation.getSession().isAnonymous() && !directoryService.isAllowAnonymousAccess()
            && !operation.getDn().isEmpty() )
        {
            String msg = I18n.err( I18n.ERR_5, operation.getName() );
            LOG.error( msg );
            throw new LdapNoPermissionException( msg );
        }
    }


    /**
     * Initialize the PasswordPolicy attributeTypes
     * 
     * @throws LdapException If the initialization failed
     */
    public void loadPwdPolicyStateAttributeTypes() throws LdapException
    {
        pwdResetAT = schemaManager.lookupAttributeTypeRegistry( PWD_RESET_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdResetAT );

        pwdChangedTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_CHANGED_TIME_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdChangedTimeAT );

        pwdHistoryAT = schemaManager.lookupAttributeTypeRegistry( PWD_HISTORY_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdHistoryAT );

        pwdFailurTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_FAILURE_TIME_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdFailurTimeAT );

        pwdAccountLockedTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_ACCOUNT_LOCKED_TIME_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdAccountLockedTimeAT );

        pwdLastSuccessAT = schemaManager.lookupAttributeTypeRegistry( PWD_LAST_SUCCESS_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdLastSuccessAT );

        pwdGraceUseTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_GRACE_USE_TIME_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdGraceUseTimeAT );

        pwdPolicySubentryAT = schemaManager.lookupAttributeTypeRegistry( PWD_POLICY_SUBENTRY_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdPolicySubentryAT );

        pwdStartTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_START_TIME_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdStartTimeAT );

        pwdEndTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_END_TIME_AT );
        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdEndTimeAT );
    }


    // ---------- private methods ----------------
    private void check( OperationContext operationContext, Entry entry,
        byte[] password, PasswordPolicyConfiguration policyConfig )
        throws LdapException
    {
        // https://issues.apache.org/jira/browse/DIRSERVER-1928
        if ( operationContext.getSession().isAnAdministrator() )
        {
            return;
        }
        
        CheckQualityEnum qualityVal = policyConfig.getPwdCheckQuality();

        if ( qualityVal == CheckQualityEnum.NO_CHECK )
        {
            return;
        }

        LdapSecurityConstants secConst = PasswordUtil.findAlgorithm( password );

        // do not perform quality check if the password is not plain text and
        // pwdCheckQuality value is set to 1
        if ( secConst != null )
        {
            if ( qualityVal == CheckQualityEnum.CHECK_ACCEPT )
            {
                return;
            }
            else
            {
                throw new PasswordPolicyException( "cannot verify the quality of the non-cleartext passwords",
                    INSUFFICIENT_PASSWORD_QUALITY.getValue() );
            }
        }

        String strPassword = Strings.utf8ToString( password );

        // perform the length validation
        validatePasswordLength( strPassword, policyConfig );

        PasswordValidator passwordValidator = policyConfig.getPwdValidator();
        
        if ( passwordValidator == null )
        {
            // Use the default one
            passwordValidator = new DefaultPasswordValidator();
        }
        
        passwordValidator.validate( strPassword, entry );
    }


    /**
     * validates the length of the password
     */
    private void validatePasswordLength( String password, PasswordPolicyConfiguration policyConfig )
        throws PasswordPolicyException
    {
        int maxLen = policyConfig.getPwdMaxLength();
        int minLen = policyConfig.getPwdMinLength();

        int pwdLen = password.length();

        if ( ( maxLen > 0 ) && ( pwdLen > maxLen ) )
        {
            throw new PasswordPolicyException( "Password should not have more than " + maxLen + " characters",
                INSUFFICIENT_PASSWORD_QUALITY.getValue() );
        }

        if ( ( minLen > 0 ) && ( pwdLen < minLen ) )
        {
            throw new PasswordPolicyException( "Password should have a minimum of " + minLen + " characters",
                PASSWORD_TOO_SHORT.getValue() );
        }
    }


    private int getPwdTimeBeforeExpiry( Entry userEntry, PasswordPolicyConfiguration policyConfig )
        throws LdapException
    {
        if ( policyConfig.getPwdMaxAge() == 0 )
        {
            return 0;
        }

        int warningAge = policyConfig.getPwdExpireWarning();

        if ( warningAge <= 0 )
        {
            return 0;
        }

        Attribute pwdChangedTimeAt = userEntry.get( pwdChangedTimeAT );
        if ( pwdChangedTimeAt == null )
        {
            pwdChangedTimeAt = userEntry.get( directoryService.getAtProvider().getCreateTimestamp() );
        }
        long changedTime = DateUtils.getDate( pwdChangedTimeAt.getString() ).getTime();

        long currentTime = directoryService.getTimeProvider().currentIimeMillis();
        long pwdAge = ( currentTime - changedTime ) / 1000;

        if ( pwdAge > policyConfig.getPwdMaxAge() )
        {
            return 0;
        }

        warningAge = policyConfig.getPwdMaxAge() - warningAge;

        if ( pwdAge >= warningAge )
        {
            long timeBeforeExpiration = ( ( long ) policyConfig.getPwdMaxAge() ) - pwdAge;

            if ( timeBeforeExpiration > Integer.MAX_VALUE )
            {
                timeBeforeExpiration = Integer.MAX_VALUE;
            }

            return ( int ) timeBeforeExpiration;
        }

        return 0;
    }


    /**
     * checks if the password is too young
     *
     * @param userEntry the user's entry
     * @return true if the password is young, false otherwise
     * @throws LdapException
     */
    private boolean isPwdTooYoung( OperationContext operationContext,
        Entry userEntry, PasswordPolicyConfiguration policyConfig ) throws LdapException
    {
        // https://issues.apache.org/jira/browse/DIRSERVER-1928
        if ( operationContext.getSession().isAnAdministrator() )
        {
            return false;
        }
        if ( policyConfig.getPwdMinAge() == 0 )
        {
            return false;
        }

        CoreSession userSession = operationContext.getSession();
        
        // see sections 7.8 and 7.2 of the ppolicy draft
        if ( policyConfig.isPwdMustChange() && userSession.isPwdMustChange() )
        {
            return false;
        }

        Attribute pwdChangedTimeAt = userEntry.get( pwdChangedTimeAT );

        if ( pwdChangedTimeAt != null )
        {
            long changedTime = DateUtils.getDate( pwdChangedTimeAt.getString() ).getTime();
            changedTime += policyConfig.getPwdMinAge() * 1000L;

            long currentTime = directoryService.getTimeProvider().currentIimeMillis();

            if ( changedTime > currentTime )
            {
                return true;
            }
        }

        return false;
    }


    /**
     * checks if the password must be changed after the initial bind
     *
     * @param userEntry the user's entry
     * @return true if must be changed, false otherwise
     * @throws LdapException
     */
    private boolean isPwdMustReset( Entry userEntry ) throws LdapException
    {
        boolean mustChange = false;

        Attribute pwdResetAt = userEntry.get( pwdResetAT );

        if ( pwdResetAt != null )
        {
            mustChange = Boolean.parseBoolean( pwdResetAt.getString() );
        }

        return mustChange;
    }


    private PwdModDetailsHolder getPwdModDetails( ModifyOperationContext modifyContext,
        PasswordPolicyConfiguration policyConfig ) throws LdapException
    {
        PwdModDetailsHolder pwdModDetails = new PwdModDetailsHolder();

        List<Modification> mods = modifyContext.getModItems();

        for ( Modification mod : mods )
        {
            Attribute at = mod.getAttribute();
            AttributeType passwordAttribute = schemaManager.lookupAttributeTypeRegistry( policyConfig.getPwdAttribute() );

            if ( at.getAttributeType().equals( passwordAttribute ) )
            {
                pwdModDetails.setPwdModPresent( true );
                ModificationOperation op = mod.getOperation();

                if ( op == REMOVE_ATTRIBUTE )
                {
                    pwdModDetails.setDelete( true );
                }
                else if ( op == REPLACE_ATTRIBUTE || op == ADD_ATTRIBUTE )
                {
                    pwdModDetails.setAddOrReplace( true );
                    pwdModDetails.setNewPwd( at.getBytes() );
                }

                switch ( op )
                {
                    case REMOVE_ATTRIBUTE:
                        pwdModDetails.setDelete( true );
                        break;
                        
                    case REPLACE_ATTRIBUTE:
                    case ADD_ATTRIBUTE:
                        pwdModDetails.setAddOrReplace( true );
                        pwdModDetails.setNewPwd( at.getBytes() );
                        break;
                        
                    default:
                        // nothing to do
                }
            }
            else
            {
                pwdModDetails.setOtherModExists( true );
            }
        }

        return pwdModDetails;
    }


    /**
     * checks to see if the user's password should be changed before performing any operations
     * other than bind, password update, unbind, abandon or StartTLS
     *
     * @param opContext the operation's context
     * @throws LdapException
     */
    private void checkPwdReset( OperationContext opContext ) throws LdapException
    {
        if ( directoryService.isPwdPolicyEnabled() )
        {
            CoreSession session = opContext.getSession();

            if ( session.isPwdMustChange() )
            {
                boolean isPPolicyReqCtrlPresent = opContext
                    .hasRequestControl( PasswordPolicyRequest.OID );

                if ( isPPolicyReqCtrlPresent )
                {
                    PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
                    responseControl.setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
                    opContext.addResponseControl( responseControl );
                }

                throw new LdapNoPermissionException( "password needs to be reset before performing this operation" );
            }
        }
    }

    private static class PwdModDetailsHolder
    {
        private boolean pwdModPresent = false;

        private boolean isDelete = false;

        private boolean isAddOrReplace = false;

        private boolean otherModExists = false;

        private byte[] newPwd;


        public boolean isPwdModPresent()
        {
            return pwdModPresent;
        }


        public void setPwdModPresent( boolean pwdModPresent )
        {
            this.pwdModPresent = pwdModPresent;
        }


        public boolean isDelete()
        {
            return isDelete;
        }


        public void setDelete( boolean isDelete )
        {
            this.isDelete = isDelete;
        }


        public boolean isAddOrReplace()
        {
            return isAddOrReplace;
        }


        public void setAddOrReplace( boolean isAddOrReplace )
        {
            this.isAddOrReplace = isAddOrReplace;
        }


        public boolean isOtherModExists()
        {
            return otherModExists;
        }


        public void setOtherModExists( boolean otherModExists )
        {
            this.otherModExists = otherModExists;
        }


        public byte[] getNewPwd()
        {
            return newPwd;
        }


        public void setNewPwd( byte[] newPwd )
        {
            this.newPwd = newPwd;
        }
    }


    /**
     * Gets the effective password policy of the given entry.
     * If the entry has defined a custom password policy by setting "pwdPolicySubentry" attribute
     * then the password policy associated with the Dn specified at the above attribute's value will be returned.
     * Otherwise the default password policy will be returned (if present)
     * 
     * @param userEntry the user's entry
     * @return the associated password policy
     * @throws LdapException If we weren't able to ftech the password policy
     */
    public PasswordPolicyConfiguration getPwdPolicy( Entry userEntry ) throws LdapException
    {
        if ( pwdPolicyContainer == null )
        {
            return null;
        }

        if ( userEntry == null )
        {
            return pwdPolicyContainer.getDefaultPolicy();
        }

        if ( pwdPolicyContainer.hasCustomConfigs() )
        {
            Attribute pwdPolicySubentry = userEntry.get( pwdPolicySubentryAT );

            if ( pwdPolicySubentry != null )
            {
                Dn configDn = dnFactory.create( pwdPolicySubentry.getString() );

                PasswordPolicyConfiguration custom = pwdPolicyContainer.getPolicyConfig( configDn );
                
                if ( custom != null )
                {
                    return custom;
                }
                else
                {
                    LOG.warn(
                        "The custom password policy for the user entry {} is not found, returning default policy configuration",
                        userEntry.getDn() );
                }
            }
        }

        return pwdPolicyContainer.getDefaultPolicy();
    }


    /**
     * set all the password policies to be used by the server.
     * This includes a default(i.e applicable to all entries) and custom(a.k.a per user) password policies
     * 
     * @param policyContainer the container holding all the password policies
     */
    public void setPwdPolicies( PpolicyConfigContainer policyContainer )
    {
        this.pwdPolicyContainer = policyContainer;
    }


    /**
     * {@inheritDoc}
     */
    public boolean isPwdPolicyEnabled()
    {
        return ( pwdPolicyContainer != null )
        && ( ( pwdPolicyContainer.getDefaultPolicy() != null )
        || ( pwdPolicyContainer.hasCustomConfigs() ) );
    }


    /**
     * @return the pwdPolicyContainer
     */
    public PpolicyConfigContainer getPwdPolicyContainer()
    {
        return pwdPolicyContainer;
    }


    /**
     * purges failure timestamps which are older than the configured interval
     * (section 7.6 in the draft)
     */
    private void purgeFailureTimes( PasswordPolicyConfiguration config, Attribute pwdFailTimeAt )
    {
        long interval = config.getPwdFailureCountInterval();

        if ( interval == 0 )
        {
            return;
        }

        interval *= 1000;

        long currentTime = directoryService.getTimeProvider().currentIimeMillis();

        Iterator<Value> itr = pwdFailTimeAt.iterator();

        while ( itr.hasNext() )
        {
            Value value = itr.next();
            String failureTime = value.getString();
            long time = DateUtils.getDate( failureTime ).getTime();
            time += interval;

            if ( currentTime >= time )
            {
                itr.remove();
            }
        }
    }
}
