/*
 *  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.ACCOUNT_LOCKED;
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_END_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_LAST_SUCCESS_AT;
import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_START_TIME_AT;

import java.io.IOException;
import java.util.Collections;
import java.util.Date;

import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
import org.apache.directory.api.ldap.model.entry.Attribute;
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.exception.LdapException;
import org.apache.directory.api.ldap.model.exception.LdapOtherException;
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.server.core.api.DirectoryService;
import org.apache.directory.server.core.api.InterceptorEnum;
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.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.api.partition.Partition;
import org.apache.directory.server.core.api.partition.PartitionTxn;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


/**
 * Base class for all Authenticators.
 *
 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
 */
public abstract class AbstractAuthenticator implements Authenticator
{
    /** A logger for the extending classes */
    protected static final Logger LOG = LoggerFactory.getLogger( AbstractAuthenticator.class );

    /** The associated DirectoryService */
    private DirectoryService directoryService;

    /** authenticator type */
    private final AuthenticationLevel authenticatorType;

    /** The base DN which will be the starting point from which we use the authenticator */
    private Dn baseDn;


    /**
     * Creates a new instance.
     *
     * @param type the type of this authenticator (e.g. <tt>'simple'</tt>, <tt>'none'</tt>...)
     */
    protected AbstractAuthenticator( AuthenticationLevel type )
    {
        this.authenticatorType = type;
        this.baseDn = Dn.ROOT_DSE;
    }


    /**
     * Creates a new instance.
     *
     * @param type the type of this authenticator (e.g. <tt>'simple'</tt>, <tt>'none'</tt>...)
     * @param baseDn The base DN for this authenticator
     */
    protected AbstractAuthenticator( AuthenticationLevel type, Dn baseDn )
    {
        this.authenticatorType = type;
        this.baseDn = baseDn;
    }


    /**
     * Returns {@link DirectoryService} for this authenticator.
     * @return the directory service core
     */
    public DirectoryService getDirectoryService()
    {
        return directoryService;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public AuthenticationLevel getAuthenticatorType()
    {
        return authenticatorType;
    }


    /**
     * Initializes (<tt>directoryService</tt> and and calls {@link #doInit()} method.
     * Please put your initialization code into {@link #doInit()}.
     * @param directoryService the directory core for this authenticator
     * @throws LdapException if there is a problem starting up the authenticator
     */
    @Override
    public final void init( DirectoryService directoryService ) throws LdapException
    {
        this.directoryService = directoryService;
        doInit();
    }


    /**
     * Implement your initialization code here.
     */
    protected void doInit()
    {
    }


    /**
     * Calls {@link #doDestroy()} method, and clears default properties
     * (<tt>factoryConfiguration</tt> and <tt>configuration</tt>).
     * Please put your deinitialization code into {@link #doDestroy()}.
     */
    @Override
    public final void destroy()
    {
        try
        {
            doDestroy();
        }
        finally
        {
            this.directoryService = null;
        }
    }


    /**
     * Implement your deinitialization code here.
     */
    protected void doDestroy()
    {
    }


    /**
     * Does nothing leaving it so subclasses can override.
     */
    @Override
    public void invalidateCache( Dn bindDn )
    {
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public boolean isValid( Dn bindDn )
    {
        // The authenticator is valid if the baseDn is null or if it's a parent of the bindDn
        return ( baseDn == null ) || ( baseDn.isAncestorOf( bindDn ) );
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public Dn getBaseDn()
    {
        return baseDn;
    }


    /**
     * {@inheritDoc}
     */
    @Override
    public void setBaseDn( Dn baseDn )
    {
        this.baseDn = baseDn;
    }
    
    
    private void internalModify( ModifyOperationContext modContext ) throws LdapException
    {
        Partition partition = directoryService.getPartitionNexus().getPartition( modContext.getDn() );
        modContext.setPartition( partition );
        PartitionTxn partitionTxn = null;

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

            directoryService.getPartitionNexus().modify( modContext );

            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 checkPwdPolicy( Entry userEntry ) throws LdapException
    {
        if ( !directoryService.isPwdPolicyEnabled() )
        {
            return;
        }

        AuthenticationInterceptor authenticationInterceptor = ( AuthenticationInterceptor ) directoryService
            .getInterceptor(
            InterceptorEnum.AUTHENTICATION_INTERCEPTOR.getName() );
        PasswordPolicyConfiguration pPolicyConfig = authenticationInterceptor.getPwdPolicy( userEntry );

        // check for locked out account
        if ( pPolicyConfig.isPwdLockout() )
        {
            LOG.debug( "checking if account with the Dn {} is locked", userEntry.getDn() );

            Attribute accountLockAttr = userEntry.get( PWD_ACCOUNT_LOCKED_TIME_AT );

            if ( accountLockAttr != null )
            {
                String lockedTime = accountLockAttr.getString();
                
                if ( "000001010000Z".equals( lockedTime ) )
                {
                    throw new PasswordPolicyException( "account was permanently locked", ACCOUNT_LOCKED.getValue() );
                }
                else
                {
                    Date lockedDate = DateUtils.getDate( lockedTime );
                    long unlockTime = pPolicyConfig.getPwdLockoutDuration() * 1000L;
                    unlockTime += lockedDate.getTime();

                    Date unlockDate = new Date( unlockTime );
                    Date now = new Date( directoryService.getTimeProvider().currentIimeMillis() );

                    if ( unlockDate.after( now ) )
                    {
                        throw new PasswordPolicyException( "account will remain locked till " + unlockDate,
                            ACCOUNT_LOCKED.getValue() );
                    }
                    else
                    {
                        // remove pwdAccountLockedTime attribute
                        Modification pwdAccountLockMod = new DefaultModification(
                            ModificationOperation.REMOVE_ATTRIBUTE, accountLockAttr );
                        ModifyOperationContext modContext = new ModifyOperationContext(
                            directoryService.getAdminSession() );
                        modContext.setDn( userEntry.getDn() );
                        modContext.setModItems( Collections.singletonList( pwdAccountLockMod ) );

                        internalModify( modContext );
                    }
                }
            }
        }

        Attribute pwdStartTimeAttr = userEntry.get( PWD_START_TIME_AT );

        if ( pwdStartTimeAttr != null )
        {
            Date pwdStartTime = DateUtils.getDate( pwdStartTimeAttr.getString() );

            if ( System.currentTimeMillis() < pwdStartTime.getTime() )
            {
                throw new PasswordPolicyException( "account is locked, will be activated after " + pwdStartTime,
                    ACCOUNT_LOCKED.getValue() );
            }
        }

        Attribute pwdEndTimeAttr = userEntry.get( PWD_END_TIME_AT );

        if ( pwdEndTimeAttr != null )
        {
            Date pwdEndTime = DateUtils.getDate( pwdEndTimeAttr.getString() );

            if ( System.currentTimeMillis() >= pwdEndTime.getTime() )
            {
                throw new PasswordPolicyException(
                    "password end time reached, will be locked till administrator activates it",
                    ACCOUNT_LOCKED.getValue() );
            }
        }

        if ( pPolicyConfig.getPwdMaxIdle() > 0 )
        {
            Attribute pwdLastSuccessTimeAttr = userEntry.get( PWD_LAST_SUCCESS_AT );

            // Let's be sure that the user has already logged in
            if ( pwdLastSuccessTimeAttr != null )
            {
                long time = pPolicyConfig.getPwdMaxIdle() * 1000L;
                time += DateUtils.getDate( pwdLastSuccessTimeAttr.getString() ).getTime();

                if ( directoryService.getTimeProvider().currentIimeMillis() >= time )
                {
                    throw new PasswordPolicyException(
                        "account locked due to the max idle time of the password was exceeded",
                        ACCOUNT_LOCKED.getValue() );
                }
            }
        }

        // Check that the password is not too old and need to be disabled
        if ( pPolicyConfig.getPwdMaxAge() > 0 )
        {
            // In case we have a grace number of attempts
            if ( pPolicyConfig.getPwdGraceAuthNLimit() > 0 )
            {
                Attribute pwdGraceUseAttr = userEntry.get( PWD_GRACE_USE_TIME_AT );

                // check for grace authentication count
                if ( ( pwdGraceUseAttr != null ) && ( pwdGraceUseAttr.size() >= pPolicyConfig.getPwdGraceAuthNLimit() ) )
                {
                    throw new PasswordPolicyException( "password expired and max grace logins were used",
                        PASSWORD_EXPIRED.getValue() );
                }
            }
            else
            {
                // No grace attempt : check if the password has expired or not
                Attribute pwdChangeTimeAttr = userEntry.get( PWD_CHANGED_TIME_AT );

                // If the attr is null, this is the admin user. We don't block it
                if ( pwdChangeTimeAttr != null )
                {
                    boolean expired = PasswordUtil.isPwdExpired( pwdChangeTimeAttr.getString(),
                        pPolicyConfig.getPwdMaxAge(), directoryService.getTimeProvider() );

                    if ( expired )
                    {
                        throw new PasswordPolicyException( "password expired", PASSWORD_EXPIRED.getValue() );
                    }
                }
            }
        }
    }
}
