/*
x@x@@ *   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.ppolicy;


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_EXPIRED;
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_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.server.core.integ.IntegrationUtils.getAdminNetworkConnection;
import static org.apache.directory.server.core.integ.IntegrationUtils.getNetworkConnectionAs;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;

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.PasswordPolicyRequestImpl;
import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyResponse;
import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants;
import org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants;
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.DefaultEntry;
import org.apache.directory.api.ldap.model.entry.Entry;
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.message.AddRequest;
import org.apache.directory.api.ldap.model.message.AddRequestImpl;
import org.apache.directory.api.ldap.model.message.AddResponse;
import org.apache.directory.api.ldap.model.message.BindRequest;
import org.apache.directory.api.ldap.model.message.BindRequestImpl;
import org.apache.directory.api.ldap.model.message.BindResponse;
import org.apache.directory.api.ldap.model.message.Control;
import org.apache.directory.api.ldap.model.message.ModifyRequest;
import org.apache.directory.api.ldap.model.message.ModifyRequestImpl;
import org.apache.directory.api.ldap.model.message.ModifyResponse;
import org.apache.directory.api.ldap.model.message.Response;
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.util.DateUtils;
import org.apache.directory.api.util.MockTimeProvider;
import org.apache.directory.api.util.Network;
import org.apache.directory.api.util.Strings;
import org.apache.directory.api.util.TimeProvider;
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.server.annotations.CreateLdapServer;
import org.apache.directory.server.annotations.CreateTransport;
import org.apache.directory.server.core.annotations.CreateDS;
import org.apache.directory.server.core.api.InterceptorEnum;
import org.apache.directory.server.core.api.authn.ppolicy.CheckQualityEnum;
import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyConfiguration;
import org.apache.directory.server.core.api.interceptor.Interceptor;
import org.apache.directory.server.core.authn.AuthenticationInterceptor;
import org.apache.directory.server.core.authn.ppolicy.PpolicyConfigContainer;
import org.apache.directory.server.core.hash.SshaPasswordHashingInterceptor;
import org.apache.directory.server.core.integ.AbstractLdapTestUnit;
import org.apache.directory.server.core.integ.ApacheDSTestExtension;
import org.apache.directory.server.core.integ.IntegrationUtils;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;


/**
 * Test cases for testing PasswordPolicy implementation.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
@ExtendWith( { ApacheDSTestExtension.class } )
@CreateLdapServer(transports =
    {
        @CreateTransport(protocol = "LDAP"),
        @CreateTransport(protocol = "LDAPS") })
// disable changelog, for more info see DIRSERVER-1528
@CreateDS(enableChangeLog = false, name = "PasswordPolicyTest")
public class PasswordPolicyIT extends AbstractLdapTestUnit
{
    private static final PasswordPolicyRequest PP_REQ_CTRL =
        new PasswordPolicyRequestImpl();

    private MockTimeProvider mockTimeProvider;
    
    private PasswordPolicyConfiguration policyConfig;

    private Dn customPolicyDn;

    /**
     * Set a default PaswordPolicy configuration
     */
    @BeforeEach
    public void setPwdPolicy() throws LdapException
    {
        policyConfig = new PasswordPolicyConfiguration();

        policyConfig.setPwdMaxAge( 110 );
        policyConfig.setPwdFailureCountInterval( 30 );
        policyConfig.setPwdMaxFailure( 2 );
        policyConfig.setPwdLockout( true );
        policyConfig.setPwdLockoutDuration( 0 );
        policyConfig.setPwdMinLength( 5 );
        policyConfig.setPwdInHistory( 5 );
        policyConfig.setPwdExpireWarning( 600 );
        policyConfig.setPwdGraceAuthNLimit( 5 );
        policyConfig.setPwdCheckQuality( CheckQualityEnum.CHECK_REJECT ); // DO NOT allow the password if its quality can't be checked

        PpolicyConfigContainer policyContainer = new PpolicyConfigContainer();
        Dn defaultPolicyDn = new Dn( ldapServer.getDirectoryService().getSchemaManager(), "cn=default" );
        policyContainer.addPolicy( defaultPolicyDn, policyConfig );
        policyContainer.setDefaultPolicyDn( defaultPolicyDn );

        customPolicyDn = new Dn( ldapServer.getDirectoryService().getSchemaManager(), "cn=custom" );
        policyContainer.addPolicy( customPolicyDn, policyConfig );

        AuthenticationInterceptor authenticationInterceptor = ( AuthenticationInterceptor ) getService()
            .getInterceptor( InterceptorEnum.AUTHENTICATION_INTERCEPTOR.getName() );

        authenticationInterceptor.setPwdPolicies( policyContainer );

        mockTimeProvider = new MockTimeProvider();
        getService().setTimeProvider( mockTimeProvider );
    }


    @AfterEach
    public void closeConnections()
    {
        IntegrationUtils.closeConnections();
        getService().setTimeProvider( TimeProvider.DEFAULT );
    }


    /**
     * Get the PasswordPolicy control from a response
     */
    private PasswordPolicyResponse getPwdRespCtrl( Response resp ) throws Exception
    {
        Control control = resp.getControls().get( PP_REQ_CTRL.getOid() );

        if ( control == null )
        {
            return null;
        }

        return ( PasswordPolicyResponse) control;
    }


    /**
     * Add a user with a password
     */
    private Dn addUser( LdapConnection adminConnection, String user, Object password ) throws Exception
    {
        Entry userEntry = new DefaultEntry(
            "cn=" + user + ",ou=system",
            "ObjectClass: top",
            "ObjectClass: person",
            "cn", user,
            "sn", user + "_sn",
            "userPassword", password );

        AddRequest addRequest = new AddRequestImpl();
        addRequest.setEntry( userEntry );
        addRequest.addControl( PP_REQ_CTRL );

        AddResponse addResp = adminConnection.add( addRequest );
        assertEquals( ResultCodeEnum.SUCCESS, addResp.getLdapResult().getResultCode() );
        PasswordPolicyResponse respCtrl = getPwdRespCtrl( addResp );
        assertNull( respCtrl );
        
        return userEntry.getDn();
    }


    /**
     * Changes the password of the user in the users own context from the 
     * old value to the new value.
     * 
     * @param userDn
     * @param oldPassword
     * @param newPassword
     * @return The ModifyResponse from the attempt
     * @throws Exception If unable to obtain the connection with the 
     * provided user an oldPassword.
     */
    private ModifyResponse changePassword( Dn userDn, String oldPassword, byte[] newPassword ) throws Exception 
    {
        LdapConnection userConnection = null;
        try {
            userConnection = getNetworkConnectionAs( ldapServer, userDn.toString(), oldPassword );
            ModifyRequest modifyRequest = new ModifyRequestImpl();
            modifyRequest.setName( userDn );
            modifyRequest.replace( "userPassword", newPassword );
            modifyRequest.addControl( PP_REQ_CTRL );
            return userConnection.modify( modifyRequest );
        }
        finally {
            userConnection.close();
        }
    }
    
    private ModifyResponse changePassword( Dn userDn, String oldPassword, String newPassword ) throws Exception 
    {
        return changePassword( userDn, oldPassword, newPassword.getBytes( Charset.forName( "UTF-8" ) ) );
    }


    /**
     * Check we can bind a user with a given password
     */
    private void checkBindSuccess( LdapConnection userConnection, Dn userDn, String password ) throws Exception
    {
        userConnection.bind( userDn, password );
        assertNotNull( userConnection );
        assertTrue( userConnection.isAuthenticated() );
    }


    /**
     * Check we cannot bind a user with a given password
     */
    private void checkBindFailure( Dn userDn, String password ) throws Exception
    {
        LdapConnection userConnection = null;
        try
        {
            userConnection = getNetworkConnectionAs( getLdapServer(), userDn.getName(), password );
            assertNull( userConnection );
        }
        catch ( LdapException le )
        {
            // Expected
        }
        finally 
        {
            if ( userConnection != null ) 
            {
                userConnection.close();
            }
        }
    }


    /**
     * Check that we can bind N times with a user/password
     */
    private void checkBind( LdapConnection connection, Dn userDn, String password, int nbIterations,
        String expectedMessage ) throws Exception
    {
        for ( int i = 0; i < nbIterations; i++ )
        {
            mockTimeProvider.addMillis( 1 ); // make sure pwdFailureTime uses a different timestamp
            
            try
            {
                connection.bind( userDn, password );
            }
            catch ( LdapAuthenticationException le )
            {
                assertEquals( expectedMessage, le.getMessage() );
            }
        }
    }


    /**
     * Verifies that the admin user is exempt from pwdHistory checks.
     * 
     * <a href="https://issues.apache.org/jira/browse/DIRSERVER-2084">DIRSERVER-
     * 2084</>
     */
    @Test
    public void testAdminExemptPwdHistory() throws Exception
    {
        String user = "adminexemptpwdhistory";
        String password = "12345";

        try (LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() ))
        {
            policyConfig.setPwdCheckQuality( CheckQualityEnum.CHECK_ACCEPT );

            Entry userEntry = adminConnection.lookup( addUser( adminConnection, user, password ), "*", "+" );
            Dn userDn = userEntry.getDn();

            ModifyResponse response = changePassword( userDn, password, password );
            assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION, response.getLdapResult().getResultCode() );
            PasswordPolicyResponse respCtrl = getPwdRespCtrl( response );
            assertNotNull( respCtrl );
            assertEquals( PasswordPolicyErrorEnum.PASSWORD_IN_HISTORY, respCtrl.getPasswordPolicyError() );

            ModifyRequest modifyRequest = new ModifyRequestImpl();
            modifyRequest.setName( userDn );
            modifyRequest.replace( "userPassword", password );
            response = adminConnection.modify( modifyRequest );
            assertEquals( ResultCodeEnum.SUCCESS, response.getLdapResult().getResultCode() );
            respCtrl = getPwdRespCtrl( response );
            assertNull( respCtrl );

            LdapConnection userConnection = getNetworkConnectionAs( getLdapServer(),
                    userDn.getName(), password );
            assertNotNull( userConnection );
            assertTrue( userConnection.isAuthenticated() );
        }
    }


    /**
     * Verifies that the admin user is exempt from pwdHistory checks.
     * 
     * <a href="https://issues.apache.org/jira/browse/DIRSERVER-2084">DIRSERVER-
     * 2084</>
     */
    @Test
    public void testAdminExemptPwdHistoryWithHashingInterceptor() throws Exception
    {
        String user = "adminexempthashedpwdhistory";
        String password = "12345";

        List<Interceptor> interceptors = getService().getInterceptors();
        List<Interceptor> newInterceptors = new ArrayList<Interceptor>();
        for ( Interceptor interceptor : interceptors ) 
        {
            newInterceptors.add( interceptor );
            if ( InterceptorEnum.EXCEPTION_INTERCEPTOR.getName().equals( interceptor.getName() ) ) 
            {
                interceptor = new SshaPasswordHashingInterceptor();
                interceptor.init( getService() );
                newInterceptors.add( interceptor );
            }
        }

        try 
        {
            getService().setInterceptors( newInterceptors );

            try (LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() ))
            {
                policyConfig.setPwdCheckQuality( CheckQualityEnum.CHECK_ACCEPT );

                Entry userEntry = adminConnection.lookup( addUser( adminConnection, user, password ), "*", "+" );
                Dn userDn = userEntry.getDn();

                ModifyResponse response = changePassword( userDn, password, password );
                assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION, response.getLdapResult().getResultCode() );
                PasswordPolicyResponse respCtrl = getPwdRespCtrl( response );
                assertNotNull( respCtrl );
                assertEquals( PasswordPolicyErrorEnum.PASSWORD_IN_HISTORY, respCtrl.getPasswordPolicyError() );

                ModifyRequest modifyRequest = new ModifyRequestImpl();
                modifyRequest.setName( userDn );
                modifyRequest.replace( "userPassword", password );
                response = adminConnection.modify( modifyRequest );
                assertEquals( ResultCodeEnum.SUCCESS, response.getLdapResult().getResultCode() );
                respCtrl = getPwdRespCtrl( response );
                assertNull( respCtrl );

                LdapConnection userConnection = getNetworkConnectionAs( getLdapServer(),
                        userDn.getName(), password );
                assertNotNull( userConnection );
                assertTrue( userConnection.isAuthenticated() );
            }
        }
        finally 
        {
            getService().setInterceptors( interceptors );
        }
    }
    
    
    /**
     * Test that we can't inject a hashed password when the ChekcQuality is CHECK_REJECT,
     * and that we can when the CheckQuality is CHECK_ACCEPT
     * 
     * Ignored because if an admin can avoid password policy anyway, and add's are 
     * performed by an admin, then this case would not get caught.  If there is a case
     * where a user adds another user then we should put that case here.
     */
    @Disabled
    @Test
    public void testAddUserWithHashedPwd() throws Exception
    {
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        byte[] password = PasswordUtil.createStoragePassword( "12345", LdapSecurityConstants.HASH_METHOD_CRYPT );

        Entry userEntry = new DefaultEntry(
            "cn=hashedpwd,ou=system",
            "ObjectClass: top",
            "ObjectClass: person",
            "cn: hashedpwd",
            "sn: hashedpwd_sn",
            "userPassword", password );

        AddRequest addRequest = new AddRequestImpl();
        addRequest.setEntry( userEntry );
        addRequest.addControl( PP_REQ_CTRL );

        // We should get a failure
        AddResponse addResp = adminConnection.add( addRequest );
        assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION, addResp.getLdapResult().getResultCode() );

        PasswordPolicyResponse respCtrl = getPwdRespCtrl( addResp );
        assertNotNull( respCtrl );
        assertEquals( INSUFFICIENT_PASSWORD_QUALITY, respCtrl.getPasswordPolicyError() );

        // Relax the Check Quality to CHECK_ACCEPT
        policyConfig.setPwdCheckQuality( CheckQualityEnum.CHECK_ACCEPT ); // allow the password if its quality can't be checked
        Attribute pwdAt = userEntry.get( SchemaConstants.USER_PASSWORD_AT );
        pwdAt.clear();
        pwdAt.add( password );

        addResp = adminConnection.add( addRequest );
        assertEquals( ResultCodeEnum.SUCCESS, addResp.getLdapResult().getResultCode() );
        respCtrl = getPwdRespCtrl( addResp );
        assertNull( respCtrl );

        LdapConnection userConnection = getNetworkConnectionAs( getLdapServer(), "cn=hashedpwd,ou=system", "12345" );
        assertNotNull( userConnection );
        assertTrue( userConnection.isAuthenticated() );
        adminConnection.close();
    }


    /**
     * Test that we can add a user with a pwdChangedTime as would be the case 
     * when importing from an ldif that already contained ppolicy information.
     * 
     * Tests the resolution to 
     * <a href="https://issues.apache.org/jira/browse/DIRSERVER-1978">DIRSERVER-1978</a>
     */
    @Test
    public void testAddUserWithPwdChangedTime() throws Exception
    {
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        String userDn = "cn=hashedpwd,ou=system";
        String pwdChangedTime = "20130913012307.296Z";
        Entry userEntry = new DefaultEntry(
            "cn=hashedpwd,ou=system",
            "ObjectClass: top",
            "ObjectClass: person",
            "cn: hashedpwd",
            "sn: hashedpwd_sn",
            "userPassword: set4now",
            PasswordPolicySchemaConstants.PWD_CHANGED_TIME_AT, pwdChangedTime );

        AddRequest addRequest = new AddRequestImpl();
        addRequest.setEntry( userEntry );
        addRequest.addControl( PP_REQ_CTRL );

        AddResponse addResp = adminConnection.add( addRequest );
        assertEquals( ResultCodeEnum.SUCCESS, addResp.getLdapResult().getResultCode() );

        userEntry = adminConnection.lookup( userDn, SchemaConstants.ALL_ATTRIBUTES_ARRAY );
        Attribute pwdChangedTimeAt = userEntry.get( PasswordPolicySchemaConstants.PWD_CHANGED_TIME_AT );
        assertNotNull( pwdChangedTimeAt );
        assertEquals( pwdChangedTime, pwdChangedTimeAt.getString() );

        adminConnection.close();
    }

    
    @Test
    public void testModifyUserWithHashedPwd() throws Exception
    {
        Dn userDn = new Dn( "cn=hashedpwdm,ou=system" );
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );
        Entry userEntry = new DefaultEntry(
            userDn.toString(),
            "ObjectClass: top",
            "ObjectClass: person",
            "cn: hashedpwdm",
            "sn: hashedpwdm_sn",
            "userPassword", "set4now" );
        adminConnection.add( userEntry );
    
        byte[] password = PasswordUtil.createStoragePassword( "12345", LdapSecurityConstants.HASH_METHOD_CRYPT );
        assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION,
            changePassword( userDn, "set4now", password ).getLdapResult().getResultCode() );
        
        checkBindFailure( userDn, "12345" );
    
        adminConnection.close();
    }


    @Test
    public void testPwdLockoutForever() throws Exception
    {
        policyConfig.setPwdMaxFailure( 2 );
        policyConfig.setPwdLockout( true );
        policyConfig.setPwdLockoutDuration( 0 );
        policyConfig.setPwdGraceAuthNLimit( 2 );
        policyConfig.setPwdFailureCountInterval( 30 );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=user2,ou=system" );
        Entry userEntry = new DefaultEntry(
            userDn.toString(),
            "ObjectClass: top",
            "ObjectClass: person",
            "cn: user2",
            "sn: user_sn",
            "userPassword: 12345" );

        AddRequest addRequest = new AddRequestImpl();
        addRequest.setEntry( userEntry );
        addRequest.addControl( PP_REQ_CTRL );

        AddResponse addResp = adminConnection.add( addRequest );
        assertEquals( ResultCodeEnum.SUCCESS, addResp.getLdapResult().getResultCode() );
        PasswordPolicyResponse respCtrl = getPwdRespCtrl( addResp );
        assertNull( respCtrl );

        BindRequest bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( "1234" ); // wrong password
        bindReq.addControl( PP_REQ_CTRL );

        LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );

        for ( int i = 0; i < 3; i++ )
        {
            mockTimeProvider.addMillis( 1000 );
            userConnection.bind( bindReq );
            assertFalse( userConnection.isAuthenticated() );
        }

        userEntry = adminConnection.lookup( userDn, SchemaConstants.ALL_ATTRIBUTES_ARRAY );
        Attribute pwdAccountLockedTime = userEntry.get( PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT );
        assertNotNull( pwdAccountLockedTime );
        assertEquals( "000001010000Z", pwdAccountLockedTime.getString() );

        bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( "12345" ); // correct password
        bindReq.addControl( PP_REQ_CTRL );
        userConnection.bind( bindReq );
        assertFalse( userConnection.isAuthenticated() ); // but still fails cause account is locked

        userConnection.close();

        // test deleting the password, it should clear all the ppolicy related attributes except the ppolicy subentry

        ModifyRequest modReq = new ModifyRequestImpl();
        modReq.setName( userDn );
        modReq.addControl( PP_REQ_CTRL );
        modReq.remove( userEntry.get( SchemaConstants.USER_PASSWORD_AT ) );

        ModifyResponse modResp = adminConnection.modify( modReq );
        assertEquals( ResultCodeEnum.SUCCESS, modResp.getLdapResult().getResultCode() );

        userEntry = adminConnection.lookup( userDn, "+" );
        assertNull( userEntry.get( PWD_FAILURE_TIME_AT ) );
        assertNull( userEntry.get( PWD_GRACE_USE_TIME_AT ) );
        assertNull( userEntry.get( PWD_HISTORY_AT ) );
        assertNull( userEntry.get( PWD_CHANGED_TIME_AT ) );
        assertNull( userEntry.get( PWD_ACCOUNT_LOCKED_TIME_AT ) );
        adminConnection.close();
    }


    /**
     * Test that we can't change the password before a given delay
     */
    @Test
    public void testPwdMinAge() throws Exception
    {
        policyConfig.setPwdMinAge( 5 );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=userMinAge,ou=system" );
        Entry userEntry = new DefaultEntry(
            userDn.toString(),
            "ObjectClass: top",
            "ObjectClass: person",
            "cn: userMinAge",
            "sn: userMinAge_sn",
            "userPassword: 12345" );
        adminConnection.add( userEntry );

        // We should not be able to modify the password : it's too recent
        mockTimeProvider.addMillis( 1000 );
        
        assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION,
            changePassword( userDn, "12345", "123456" ).getLdapResult().getResultCode() );

        // Wait for the pwdMinAge delay to be over
        mockTimeProvider.addMillis( 4000 );

        // Now, we should be able to modify the password
        assertEquals( ResultCodeEnum.SUCCESS,
            changePassword( userDn, "12345", "123456" ).getLdapResult().getResultCode() );

        try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            checkBindSuccess( userConnection, userDn, "123456" );
        }

        adminConnection.close();
    }


    /**
     * Test the number of password we keep in history
     */
    @Test
    public void testPwdInHistory() throws Exception
    {
        policyConfig.setPwdInHistory( 2 );

        try (LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() ))
        {
            Dn userDn = addUser( adminConnection, "userPwdHist", "12345" );

            try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
            {
                checkBindSuccess( userConnection, userDn, "12345" );
            }
            
            Entry entry = adminConnection.lookup( userDn, "*", "+" );
    
            Attribute pwdHistAt = entry.get( PasswordPolicySchemaConstants.PWD_HISTORY_AT );
            assertNotNull( pwdHistAt );
            assertEquals( 1, pwdHistAt.size() );
    
            mockTimeProvider.addMillis( 1000 ); // to avoid creating a history value with the same timestamp
    
            ModifyResponse response = changePassword( userDn, "12345", "67891" );
            assertEquals( ResultCodeEnum.SUCCESS, response.getLdapResult().getResultCode() );

            try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
            {
                checkBindSuccess( userConnection, userDn, "67891" );
            }
    
            entry = adminConnection.lookup( userDn, "*", "+" );

            pwdHistAt = entry.get( PasswordPolicySchemaConstants.PWD_HISTORY_AT );
            assertNotNull( pwdHistAt );
            assertEquals( 2, pwdHistAt.size() );
    
            mockTimeProvider.addMillis( 1000 ); // to avoid creating a history value with the same timestamp
    
            response = changePassword( userDn, "67891", "abcde" );
            assertEquals( ResultCodeEnum.SUCCESS, response.getLdapResult().getResultCode() );

            try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
            {
                checkBindSuccess( userConnection, userDn, "abcde" );
            }
    
            entry = adminConnection.lookup( userDn, "*", "+" );

            pwdHistAt = entry.get( PasswordPolicySchemaConstants.PWD_HISTORY_AT );
            assertNotNull( pwdHistAt );
            // it should still hold only 2 values
            assertEquals( 2, pwdHistAt.size() );
    
            response = changePassword( userDn, "abcde", "67891" );
            assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION, response.getLdapResult().getResultCode() );
            PasswordPolicyResponse respCtrl = getPwdRespCtrl( response );
            assertNotNull( respCtrl );
            assertEquals( PasswordPolicyErrorEnum.PASSWORD_IN_HISTORY, respCtrl.getPasswordPolicyError() );

            try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
            {
                checkBindSuccess( userConnection, userDn, "abcde" );
            }
    
            // Try to reuse the very first password, should succeed
            response = changePassword( userDn, "abcde", "12345" );
            assertEquals( ResultCodeEnum.SUCCESS, response.getLdapResult().getResultCode() );
        }
    }


    @Test
    public void testPwdLength() throws Exception
    {
        policyConfig.setPwdMinLength( 5 );
        policyConfig.setPwdMaxLength( 7 );
        policyConfig.setPwdCheckQuality( CheckQualityEnum.CHECK_REJECT );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=userLen,ou=system" );
        Entry userEntry = new DefaultEntry(
            userDn.toString(),
            "ObjectClass: top",
            "ObjectClass: person",
            "cn: userLen",
            "sn: userLen_sn",
            "userPassword: set4now" );
        adminConnection.add( userEntry );

        // Try with a password below the minLength
        assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION,
            changePassword( userDn, "set4now", "1234" ).getLdapResult().getResultCode() );

        checkBindFailure( userDn, "1234" );

        // Try with a password above the maxLength
        assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION,
            changePassword( userDn, "set4now", "12345678" ).getLdapResult().getResultCode() );

        checkBindFailure( userDn, "12345678" );

        // And try with a correct password
        assertEquals( ResultCodeEnum.SUCCESS,
            changePassword( userDn, "set4now", "123456" ).getLdapResult().getResultCode() );

        try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            checkBindSuccess( userConnection, userDn, "123456" );
        }

        adminConnection.close();
    }


    /**
     * Test the pwdMaxAge configuration. The password should expire when the pwdMaxAge is reached, and
     * the password should be locked as we don't have a grace limit. 
     */
    @Test
    public void testPwdMaxAgeNoGraceAuthNLimit() throws Exception
    {
        policyConfig.setPwdMaxAge( 5 );
        policyConfig.setPwdGraceAuthNLimit( 0 );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=userMaxAgeNoGraceAuthNLimit,ou=system" );
        String password = "12345";

        addUser( adminConnection, "userMaxAgeNoGraceAuthNLimit", password );

        BindRequest bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( Strings.getBytesUtf8( password ) );
        bindReq.addControl( PP_REQ_CTRL );

        try (LdapConnection userCon = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            mockTimeProvider.addMillis( 1000 ); // sleep for one second so that the password expire warning will be sent
    
            BindResponse bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
    
            PasswordPolicyResponse respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertTrue( respCtrl.getTimeBeforeExpiration() > 0 );
    
            mockTimeProvider.addMillis( 4500 ); // sleep for four seconds and a half so that the password expires
    
            // this time it should fail
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, bindResp.getLdapResult().getResultCode() );

            respCtrl = getPwdRespCtrl( bindResp );
            assertEquals( PASSWORD_EXPIRED, respCtrl.getPasswordPolicyError() );
        }

        adminConnection.close();
    }


    /**
     * Test the pwdMaxAge configuration with a grace limit. We should be able to bind two
     * times when the password has expired. 
     */
    @Test
    public void testPwdMaxAgeWithGraceAuthNLimit() throws Exception
    {
        policyConfig.setPwdMaxAge( 5 );
        policyConfig.setPwdGraceAuthNLimit( 2 );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=userMaxAgeWithGraceAuthNLimit,ou=system" );
        String password = "12345";

        addUser( adminConnection, "userMaxAgeWithGraceAuthNLimit", password );

        BindRequest bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( Strings.getBytesUtf8( password ) );
        bindReq.addControl( PP_REQ_CTRL );

        try (LdapConnection userCon = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            mockTimeProvider.addMillis( 1000 ); // sleep for one second so that the password expire warning will be sent
    
            BindResponse bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
    
            PasswordPolicyResponse respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertTrue( respCtrl.getTimeBeforeExpiration() > 0 );
    
            mockTimeProvider.addMillis( 4500 ); // sleep for four seconds and a half so that the password expires
    
            // bind for one more time, should succeed
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
            respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertEquals( 1, respCtrl.getGraceAuthNRemaining() );
            
            mockTimeProvider.addMillis( 1000 ); // make sure pwdGraceUseTime uses a different timestamp
            
            // bind for one last time, should succeed
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
            respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertEquals( 0, respCtrl.getGraceAuthNRemaining() );
            
            mockTimeProvider.addMillis( 1000 ); // make sure pwdGraceUseTime uses a different timestamp
            
            // this time it should fail
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, bindResp.getLdapResult().getResultCode() );
    
            respCtrl = getPwdRespCtrl( bindResp );
            assertEquals( PASSWORD_EXPIRED, respCtrl.getPasswordPolicyError() );
        }
        adminConnection.close();
    }


    /**
     * Test the pwdMaxAge configuration with a grace period. We should be able to bind for a 
     * period of time when the password has expired. 
     */
    @Test
    public void testPwdMaxAgeWithGraceExpire() throws Exception
    {
        policyConfig.setPwdMaxAge( 5 );
        policyConfig.setPwdGraceExpire( 2 );
        policyConfig.setPwdGraceAuthNLimit( 2 );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=userMaxAgeWithGraceExpire,ou=system" );
        String password = "12345";

        addUser( adminConnection, "userMaxAgeWithGraceExpire", password );

        BindRequest bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( Strings.getBytesUtf8( password ) );
        bindReq.addControl( PP_REQ_CTRL );

        try (LdapConnection userCon = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            mockTimeProvider.addMillis( 1000 ); // sleep for one second so that the password expire warning will be sent
    
            BindResponse bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
    
            PasswordPolicyResponse respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertTrue( respCtrl.getTimeBeforeExpiration() > 0 );
    
            mockTimeProvider.addMillis( 4500 ); // sleep for four seconds and a half so that the password expires
    
            // bind for one more time, should succeed
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
            respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertEquals( 1, respCtrl.getGraceAuthNRemaining() );
    
            // Wait 1 second, we should still be able to bind
            mockTimeProvider.addMillis( 1000 ); // make sure pwdGraceUseTime uses a different timestamp
            
            // bind for one last time, should succeed
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
            respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertEquals( 0, respCtrl.getGraceAuthNRemaining() );
    
            // this time it should fail
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, bindResp.getLdapResult().getResultCode() );
    
            respCtrl = getPwdRespCtrl( bindResp );
            assertEquals( PASSWORD_EXPIRED, respCtrl.getPasswordPolicyError() );
        }
        adminConnection.close();
    }


    /**
     * Test the pwdMaxAge configuration. The password should expire when the pwdMaxAge is reached.
     * @throws Exception
     */
    @Test
    public void testPwdMaxAgeAndGraceAuth() throws Exception
    {
        policyConfig.setPwdMaxAge( 5 );
        policyConfig.setPwdExpireWarning( 4 );
        policyConfig.setPwdGraceAuthNLimit( 2 );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=userMaxAgeAndGraceAuth,ou=system" );
        String password = "12345";

        addUser( adminConnection, "userMaxAgeAndGraceAuth", password );

        BindRequest bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( Strings.getBytesUtf8( password ) );
        bindReq.addControl( PP_REQ_CTRL );

        try (LdapConnection userCon = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            mockTimeProvider.addMillis( 1000 ); // sleep for one second so that the password expire warning will be sent
    
            BindResponse bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
    
            PasswordPolicyResponse respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertTrue( respCtrl.getTimeBeforeExpiration() > 0 );
    
            mockTimeProvider.addMillis( 4000 ); // sleep for four seconds so that the password expires
    
            // bind for two more times, should succeed
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
            respCtrl = getPwdRespCtrl( bindResp );
            assertNotNull( respCtrl );
            assertEquals( 1, respCtrl.getGraceAuthNRemaining() );
    
            // this extra second sleep is necessary to modify pwdGraceUseTime attribute with a different timestamp
            mockTimeProvider.addMillis( 1000 );
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.SUCCESS, bindResp.getLdapResult().getResultCode() );
            respCtrl = getPwdRespCtrl( bindResp );
            assertEquals( 0, respCtrl.getGraceAuthNRemaining() );
    
            // this time it should fail
            bindResp = userCon.bind( bindReq );
            assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, bindResp.getLdapResult().getResultCode() );
    
            respCtrl = getPwdRespCtrl( bindResp );
            assertEquals( PASSWORD_EXPIRED, respCtrl.getPasswordPolicyError() );
        }
        adminConnection.close();
    }


    @Test
    public void testModifyPwdSubentry() throws Exception
    {
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=ppolicySubentry,ou=system" );
        String password = "12345";
        Entry userEntry = new DefaultEntry(
            userDn.toString(),
            "ObjectClass: top",
            "ObjectClass: person",
            "cn: ppolicySubentry",
            "sn: ppolicySubentry_sn",
            "userPassword: " + password,
            "pwdPolicySubEntry:" + customPolicyDn.getName() );

        AddRequest addRequest = new AddRequestImpl();
        addRequest.setEntry( userEntry );
        addRequest.addControl( PP_REQ_CTRL );

        AddResponse addResp = adminConnection.add( addRequest );
        assertEquals( ResultCodeEnum.SUCCESS, addResp.getLdapResult().getResultCode() );

        userEntry = adminConnection.lookup( userDn, "*", "+" );
        assertEquals( customPolicyDn.getName(), userEntry.get( "pwdPolicySubEntry" ).getString() );

        ModifyRequest modReq = new ModifyRequestImpl();
        modReq.setName( userDn );
        String modSubEntryDn = "cn=policy,ou=system";
        modReq.replace( "pwdPolicySubEntry", modSubEntryDn );
        ModifyResponse modResp = adminConnection.modify( modReq );
        assertEquals( ResultCodeEnum.SUCCESS, modResp.getLdapResult().getResultCode() );

        userEntry = adminConnection.lookup( userDn, "*", "+" );
        assertEquals( modSubEntryDn, userEntry.get( "pwdPolicySubEntry" ).getString() );

        // try to modify the subentry as a non-admin
        adminConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
        adminConnection.bind( userDn.getName(), password );

        modResp = adminConnection.modify( modReq );
        modReq.replace( "pwdPolicySubEntry", userDn.getName() );
        assertEquals( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, modResp.getLdapResult().getResultCode() );
        adminConnection.close();
    }


    @Test
    public void testGraceAuth() throws Exception
    {
        policyConfig.setPwdMaxFailure( 2 );
        policyConfig.setPwdLockout( true );
        policyConfig.setPwdLockoutDuration( 0 );
        policyConfig.setPwdGraceAuthNLimit( 2 );
        policyConfig.setPwdFailureCountInterval( 60 );
        policyConfig.setPwdMaxAge( 1 );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=userGrace,ou=system" );

        addUser( adminConnection, "userGrace", "12345" );

        BindRequest bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( "12345" ); // grace login
        bindReq.addControl( PP_REQ_CTRL );

        LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );

        mockTimeProvider.addMillis( 2000 ); // let the password expire
        BindResponse bindResp = userConnection.bind( bindReq );
        assertTrue( userConnection.isAuthenticated() );
        PasswordPolicyResponse ppolicy = getPwdRespCtrl( bindResp );
        assertEquals( 1, ppolicy.getGraceAuthNRemaining() );

        Entry userEntry = adminConnection.lookup( userDn, "+" );
        Attribute pwdGraceAuthUseTime = userEntry.get( PasswordPolicySchemaConstants.PWD_GRACE_USE_TIME_AT );
        assertNotNull( pwdGraceAuthUseTime );

        Attribute pwdChangedTime = userEntry.get( PasswordPolicySchemaConstants.PWD_CHANGED_TIME_AT );

        ModifyRequest modReq = new ModifyRequestImpl();
        modReq.setName( userDn );
        modReq.replace( SchemaConstants.USER_PASSWORD_AT, "secret1" );
        ModifyResponse modResp = userConnection.modify( modReq );
        assertEquals( ResultCodeEnum.SUCCESS, modResp.getLdapResult().getResultCode() );

        userEntry = adminConnection.lookup( userDn, "+" );
        pwdGraceAuthUseTime = userEntry.get( PasswordPolicySchemaConstants.PWD_GRACE_USE_TIME_AT );
        assertNull( pwdGraceAuthUseTime );

        Attribute latestPwdChangedTime = userEntry.get( PasswordPolicySchemaConstants.PWD_CHANGED_TIME_AT );
        assertNotSame( pwdChangedTime.getString(), latestPwdChangedTime.getString() );

        userConnection.close();
        adminConnection.close();
    }


    @Test
    public void testPwdLockoutWithDuration() throws Exception
    {
        policyConfig.setPwdMaxFailure( 2 );
        policyConfig.setPwdLockout( true );
        policyConfig.setPwdLockoutDuration( 5 ); //5 sec
        policyConfig.setPwdGraceAuthNLimit( 2 );
        policyConfig.setPwdFailureCountInterval( 0 );

        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        Dn userDn = new Dn( "cn=userLockoutWithDuration,ou=system" );

        addUser( adminConnection, "userLockoutWithDuration", "12345" );

        BindRequest bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( "1234" ); // wrong password
        bindReq.addControl( PP_REQ_CTRL );

        LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );

        for ( int i = 0; i < 4; i++ )
        {
            mockTimeProvider.addMillis( 1 ); // make sure pwdFailureTime uses a different timestamp
            userConnection.bind( bindReq );
            assertFalse( userConnection.isAuthenticated() );
        }

        Entry userEntry = adminConnection.lookup( userDn, "+" );
        Attribute pwdAccountLockedTime = userEntry.get( PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT );
        assertNotNull( pwdAccountLockedTime );

        mockTimeProvider.addMillis( 10000 );
        bindReq = new BindRequestImpl();
        bindReq.setDn( userDn );
        bindReq.setCredentials( "12345" ); // correct password
        bindReq.addControl( PP_REQ_CTRL );
        userConnection.bind( bindReq );
        assertTrue( userConnection.isAuthenticated() );

        userConnection.close();
        adminConnection.close();
    }


    /**
     * Check that we can't try more than N times to login with a wrong password before
     * being locked.
     * @throws Exception
     */
    @Test
    public void testPwdLockoutWithNAttempts() throws Exception
    {
        policyConfig.setPwdMaxFailure( 3 );
        policyConfig.setPwdLockout( true );

        Dn userDn = new Dn( getService().getSchemaManager(), "cn=userLockout,ou=system" );
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        addUser( adminConnection, "userLockout", "12345" );

        LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );

        checkBind( userConnection, userDn, "badPassword", 3,
            "INVALID_CREDENTIALS: Bind failed: ERR_229 Cannot authenticate user cn=userLockout,ou=system" );

        checkBind( userConnection, userDn, "12345", 1,
            "INVALID_CREDENTIALS: Bind failed: account was permanently locked" );

        userConnection.close();

        Entry userEntry = adminConnection.lookup( userDn, "+" );
        Attribute pwdAccountLockedTime = userEntry.get( PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT );
        assertNotNull( pwdAccountLockedTime );
        adminConnection.close();
    }


    /**
     * Check that we can't try more than N times to login with a wrong password before
     * being locked. Also check that we have a delay before we can log again.
     */
    @Test
    public void testPwdLockoutWithNAttemptsAndLockoutDelay() throws Exception
    {
        policyConfig.setPwdLockout( true );
        policyConfig.setPwdMaxFailure( 3 );
        policyConfig.setPwdLockoutDuration( 5 );

        Dn userDn = new Dn( "cn=userLockout2,ou=system" );
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        addUser( adminConnection, "userLockout2", "12345" );

        LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );

        checkBind( userConnection, userDn, "badPassword", 3,
            "INVALID_CREDENTIALS: Bind failed: ERR_229 Cannot authenticate user cn=userLockout2,ou=system" );

        // Now, try to login until the delay is elapsed
        boolean success = false;
        int t = 0;

        for ( t = 0; t < 10; t++ )
        {
            try
            {
                userConnection.bind( userDn, "12345" );
                //System.out.println( "Attempt success " + ( t + 1 ) + " at " + new Date( System.currentTimeMillis() ) );
                success = true;
                break;
            }
            catch ( LdapException le )
            {
                //System.out.println( "Attempt failure " + ( t + 1 ) + " at " + new Date( System.currentTimeMillis() ) );
                Entry userEntry = adminConnection.lookup( userDn, "+" );
                Attribute pwdAccountLockedTime = userEntry
                    .get( PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT );
                assertNotNull( pwdAccountLockedTime );

                // Expected : wait 1 second before retrying
                mockTimeProvider.addMillis( 1000 );
            }
        }

        assertTrue( success );
        assertTrue( t >= 5 );
        userConnection.close();

        Entry userEntry = adminConnection.lookup( userDn, "+" );
        Attribute pwdAccountLockedTime = userEntry.get( PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT );
        assertNull( pwdAccountLockedTime );
        adminConnection.close();
    }


    /**
     * Check that the failure attempts are removed from the entry when the 
     * pwdFailureCountInterval attribute is set.
     */
    @Test
    public void testPwdLockoutFailureCountInterval() throws Exception
    {
        policyConfig.setPwdLockout( true );
        policyConfig.setPwdMaxFailure( 5 );
        policyConfig.setPwdFailureCountInterval( 2 );

        Dn userDn = new Dn( "cn=userLockout3,ou=system" );
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        addUser( adminConnection, "userLockout3", "12345" );

        LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );

        // First attempt
        checkBind( userConnection, userDn, "badPassword", 1,
            "INVALID_CREDENTIALS: Bind failed: ERR_229 Cannot authenticate user cn=userLockout3,ou=system" );

        Entry userEntry = adminConnection.lookup( userDn, "+" );
        Attribute pwdFailureTime = userEntry
            .get( PasswordPolicySchemaConstants.PWD_FAILURE_TIME_AT );
        assertNotNull( pwdFailureTime );
        assertEquals( 1, pwdFailureTime.size() );

        mockTimeProvider.addMillis( 1000 );

        // Second attempt
        checkBind( userConnection, userDn, "badPassword", 1,
            "INVALID_CREDENTIALS: Bind failed: ERR_229 Cannot authenticate user cn=userLockout3,ou=system" );

        userEntry = adminConnection.lookup( userDn, "+" );
        pwdFailureTime = userEntry
            .get( PasswordPolicySchemaConstants.PWD_FAILURE_TIME_AT );
        assertNotNull( pwdFailureTime );
        assertEquals( 2, pwdFailureTime.size() );

        mockTimeProvider.addMillis( 1000 );

        // Third attempt
        checkBind( userConnection, userDn, "badPassword", 1,
            "INVALID_CREDENTIALS: Bind failed: ERR_229 Cannot authenticate user cn=userLockout3,ou=system" );

        userEntry = adminConnection.lookup( userDn, "+" );
        pwdFailureTime = userEntry
            .get( PasswordPolicySchemaConstants.PWD_FAILURE_TIME_AT );
        assertNotNull( pwdFailureTime );
        assertEquals( 2, pwdFailureTime.size() );

        mockTimeProvider.addMillis( 1000 );

        // Forth attempt
        checkBind( userConnection, userDn, "badPassword", 1,
            "INVALID_CREDENTIALS: Bind failed: ERR_229 Cannot authenticate user cn=userLockout3,ou=system" );

        userEntry = adminConnection.lookup( userDn, "+" );
        pwdFailureTime = userEntry
            .get( PasswordPolicySchemaConstants.PWD_FAILURE_TIME_AT );
        assertNotNull( pwdFailureTime );

        // We should not have more than 2 attempts stored
        assertEquals( 2, pwdFailureTime.size() );

        userConnection.close();
        adminConnection.close();
    }


    /**
     * Check that we are delayed between each attempt
     * @throws Exception
     */
    @Test
    @Disabled
    public void testPwdAttempsDelayed() throws Exception
    {
        policyConfig.setPwdMinDelay( 200 );
        policyConfig.setPwdMaxDelay( 400 );
        policyConfig.setPwdLockout( true );

        Dn userDn = new Dn( "cn=userLockout,ou=system" );
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        addUser( adminConnection, "userLockout", "12345" );

        LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );

        // Do two attempts 
        checkBind( userConnection, userDn, "badPassword", 1,
            "INVALID_CREDENTIALS: Bind failed: ERR_229 Cannot authenticate user cn=userLockout,ou=system" );

        // Wait 1 second
        mockTimeProvider.addMillis( 1000L );

        // Retry with the correct password : we should get rejected because it's too early
        checkBind( userConnection, userDn, "12345", 1,
            "INVALID_CREDENTIALS: Bind failed: ERR_229 Cannot authenticate user cn=userLockout,ou=system" );

        // Wait 1 second and a bit more
        mockTimeProvider.addMillis( 1200L );

        // Retry : it should work
        userConnection.bind( userDn, "12345" );
        userConnection.close();
        adminConnection.close();
    }


    /**
     * Check the maxIdle : if the user does not bind for more than this delay,
     * the password is locked.
     */
    @Test
    public void testPwMaxIdle() throws Exception
    {
        policyConfig.setPwdMaxIdle( 5 );

        Dn userDn = new Dn( "cn=userLockout4,ou=system" );
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        addUser( adminConnection, "userLockout4", "12345" );

        // We should succeed
        try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            checkBindSuccess( userConnection, userDn, "12345" );
        }

        // Wait 5 seconds now
        mockTimeProvider.addMillis( 5000 );

        // We shpuld not be able to succeed now
        checkBindFailure( userDn, "12345" );

        adminConnection.close();
    }


    /**
     * Check the pwdAllowUserChange
     */
    @Test
    public void testPwdAllowUserChange() throws Exception
    {
        policyConfig.setPwdAllowUserChange( false );

        Dn userDn = new Dn( "cn=userAllowUserChange,ou=system" );
        LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

        addUser( adminConnection, "userAllowUserChange", "12345" );

        try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            // We should be able to bind
            checkBindSuccess( userConnection, userDn, "12345" );
        }

        try (LdapConnection userConnection = getNetworkConnectionAs( getLdapServer(), userDn.getName(), "12345" ))
        {
    
            // Now, try to change the password
            ModifyRequest modReq = new ModifyRequestImpl();
            modReq.setName( userDn );
            modReq.addControl( PP_REQ_CTRL );
            modReq.replace( "userPassword", "67890" );

            ModifyResponse modifyResponse = userConnection.modify( modReq );
    
            assertEquals( ResultCodeEnum.INSUFFICIENT_ACCESS_RIGHTS, modifyResponse.getLdapResult().getResultCode() );
    
            // Now, allow the user to change his password
            policyConfig.setPwdAllowUserChange( true );
    
            modifyResponse = userConnection.modify( modReq );
    
            assertEquals( ResultCodeEnum.SUCCESS, modifyResponse.getLdapResult().getResultCode() );
        }
    
        try (LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() ))
        {
            checkBindSuccess( userConnection, userDn, "67890" );
        }

        adminConnection.close();
    }


    /**
     * Check the pwdExpireWarning
     */
    @Test
    public void testPwdExpireWarning() throws Exception
    {
        policyConfig.setPwdGraceAuthNLimit( 0 );
        policyConfig.setPwdMaxAge( 3600 ); // 1 hour
        policyConfig.setPwdExpireWarning( 600 ); // 10 minutes
    
        LdapConnection adminConnection = null;
        LdapConnection userConnection = null;
        LdapConnection userConnection2 = null;
        
        try {
            String userCn = "userExpireWarningToo";
            Dn userDn = new Dn( "cn=" + userCn + ",ou=system" );
            String password = "12345";
            adminConnection = getAdminNetworkConnection( getLdapServer() );
            userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );
            userConnection2 = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, ldapServer.getPort() );

            addUser( adminConnection, userCn, password );

            BindRequest bindReq = new BindRequestImpl();
            bindReq.setDn( userDn );
            bindReq.setCredentials( "12345" );
            bindReq.addControl( PP_REQ_CTRL );
            BindResponse bindResponse = userConnection2.bind( bindReq );
            PasswordPolicyResponse respCtrl = getPwdRespCtrl( bindResponse );
            assertNotNull( respCtrl );

            // now modify change time to trigger warning
            ModifyRequest modifyRequest = new ModifyRequestImpl();
            modifyRequest.setName( userDn );
            modifyRequest.replace( "pwdChangedTime", DateUtils.getGeneralizedTime( new Date().getTime() - 3100000 ) );
            adminConnection.modify( modifyRequest );

            BindRequest bindReq2 = new BindRequestImpl();
            bindReq2.setDn( userDn );
            bindReq2.setCredentials( "12345" );
            bindReq2.addControl( new PasswordPolicyRequestImpl() );
            bindResponse = userConnection.bind( bindReq2 );
            respCtrl = getPwdRespCtrl( bindResponse );
            assertNotNull( respCtrl );
            assertNotNull( respCtrl );
            assertTrue( respCtrl.getTimeBeforeExpiration() > 0 );

            // now modify change time to trigger expired
            modifyRequest = new ModifyRequestImpl();
            modifyRequest.setName( userDn );
            modifyRequest.replace( "pwdChangedTime", DateUtils.getGeneralizedTime( new Date().getTime() - 3700000 ) );
            adminConnection.modify( modifyRequest );

            BindRequest bindReq3 = new BindRequestImpl();
            bindReq3.setDn( userDn );
            bindReq3.setCredentials( "12345" );
            bindReq3.addControl( new PasswordPolicyRequestImpl() );
            bindResponse = userConnection.bind( bindReq3 );
            assertEquals( ResultCodeEnum.INVALID_CREDENTIALS, bindResponse.getLdapResult().getResultCode() );
            respCtrl = getPwdRespCtrl( bindResponse );
            assertNotNull( respCtrl );
            assertNotNull( respCtrl );
            assertEquals( PasswordPolicyErrorEnum.PASSWORD_EXPIRED, respCtrl.getPasswordPolicyError() );
        }
        finally {
            safeCloseConnections( userConnection, userConnection2, adminConnection );
        }
    }   
    
    
    private void safeCloseConnections( LdapConnection... connections ) {
        for ( LdapConnection connection : connections ) {
            try {
                connection.close();
            }
            catch ( Exception e ) {
                // might want to log here...
            }
        }
    }


    /**
    * According to the <a href=
    * "http://tools.ietf.org/html/draft-behera-ldap-password-policy-10#section-7.8"
    * >rfc</a>:
    * <pre>
    * <b>7.8 Password Too Young Check</b>
    * 
    *   If the Section 7.2 check returned true then this check will return
    *   false, to allow the password to be changed.
    *   ...
    * </pre>
    * <pre>
    * <b>7.2 Password Must be Changed Now Check</b>
    * 
    *   A status of true is returned to indicate that the password must be
    *   changed if all of these conditions are met:
    *   
    *   o  The pwdMustChange attribute is set to TRUE.
    *   o  The pwdReset attribute is set to TRUE.
    *   
    *   Otherwise a status of false is returned.
    * </pre>
    * 
    * Therefore, if the admin sets the password, the user should be allowed
    * to change it even if pwdMinAge has not expired.
    */
   @Test
   public void testPwdMinAgeWithMustChange() throws Exception
   {
       policyConfig.setPwdMustChange( true );
       policyConfig.setPwdMinAge( 1 );

       LdapConnection adminConnection = getAdminNetworkConnection( getLdapServer() );

       Dn userDn = new Dn( "cn=userMinAgeMustChange,ou=system" );
       Entry userEntry = new DefaultEntry(
           userDn.toString(),
           "ObjectClass: top",
           "ObjectClass: person",
           "cn: userMinAgeMustChange",
           "sn: userMinAgeMustChange_sn",
           "userPassword: 12345" );
       adminConnection.add( userEntry );

       LdapConnection userConnection = new LdapNetworkConnection( Network.LOOPBACK_HOSTNAME, getLdapServer().getPort() );
       BindRequest bindRequest = new BindRequestImpl();
       bindRequest.setDn( userDn );
       bindRequest.setCredentials( "12345" );
       bindRequest.addControl( PP_REQ_CTRL );
       // successful bind but must require pwd reset as was set by admin
       BindResponse bindResponse = userConnection.bind( bindRequest );
       assertEquals( ResultCodeEnum.SUCCESS, bindResponse.getLdapResult().getResultCode() );
       assertEquals( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET, 
           getPwdRespCtrl( bindResponse ).getPasswordPolicyError() );

       // in this case all other operations must be disallowed
       // see section #8.1.2.2.  Password must be changed now
       Entry mustbeNull = userConnection.lookup( "ou=system" );
       assertNull(mustbeNull);
       
       ModifyRequest modifyRequest = new ModifyRequestImpl();
       modifyRequest.setName( userDn );
       modifyRequest.replace( "userPassword", "123456" );
       modifyRequest.addControl( PP_REQ_CTRL );
       // succeed because admin previously set password
       ModifyResponse modifyResponse = userConnection.modify( modifyRequest );
       assertEquals( ResultCodeEnum.SUCCESS, modifyResponse.getLdapResult().getResultCode() );

       modifyRequest = new ModifyRequestImpl();
       modifyRequest.setName( userDn );
       modifyRequest.replace( "userPassword", "1234567" );
       modifyRequest.addControl( PP_REQ_CTRL );
       // fail cause password is too young
       modifyResponse = userConnection.modify( modifyRequest );
       assertEquals( ResultCodeEnum.CONSTRAINT_VIOLATION, 
           modifyResponse.getLdapResult().getResultCode() );
       assertEquals( PasswordPolicyErrorEnum.PASSWORD_TOO_YOUNG, 
           getPwdRespCtrl( modifyResponse ).getPasswordPolicyError() );

       // Wait for the pwdMinAge delay to be over
       mockTimeProvider.addMillis( 1000 );

       // Now, we should be able to modify the password
       modifyResponse = userConnection.modify( modifyRequest );
       assertEquals( ResultCodeEnum.SUCCESS, modifyResponse.getLdapResult().getResultCode() );

       userConnection.close();
       adminConnection.close();
   }
}
