001/*
002 *  Licensed to the Apache Software Foundation (ASF) under one
003 *  or more contributor license agreements.  See the NOTICE file
004 *  distributed with this work for additional information
005 *  regarding copyright ownership.  The ASF licenses this file
006 *  to you under the Apache License, Version 2.0 (the
007 *  "License"); you may not use this file except in compliance
008 *  with the License.  You may obtain a copy of the License at
009 *
010 *    http://www.apache.org/licenses/LICENSE-2.0
011 *
012 *  Unless required by applicable law or agreed to in writing,
013 *  software distributed under the License is distributed on an
014 *  "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 *  KIND, either express or implied.  See the License for the
016 *  specific language governing permissions and limitations
017 *  under the License.
018 *
019 */
020package org.apache.directory.server.core.authn;
021
022
023import static org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum.INSUFFICIENT_PASSWORD_QUALITY;
024import static org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum.PASSWORD_TOO_SHORT;
025import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT;
026import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_CHANGED_TIME_AT;
027import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_END_TIME_AT;
028import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_FAILURE_TIME_AT;
029import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_GRACE_USE_TIME_AT;
030import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_HISTORY_AT;
031import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_LAST_SUCCESS_AT;
032import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_POLICY_SUBENTRY_AT;
033import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_RESET_AT;
034import static org.apache.directory.api.ldap.model.constants.PasswordPolicySchemaConstants.PWD_START_TIME_AT;
035import static org.apache.directory.api.ldap.model.entry.ModificationOperation.ADD_ATTRIBUTE;
036import static org.apache.directory.api.ldap.model.entry.ModificationOperation.REMOVE_ATTRIBUTE;
037import static org.apache.directory.api.ldap.model.entry.ModificationOperation.REPLACE_ATTRIBUTE;
038
039import java.io.IOException;
040import java.security.MessageDigest;
041import java.util.ArrayList;
042import java.util.Collection;
043import java.util.Collections;
044import java.util.EnumMap;
045import java.util.HashSet;
046import java.util.Iterator;
047import java.util.List;
048import java.util.Set;
049
050import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum;
051import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyRequest;
052import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyResponse;
053import org.apache.directory.api.ldap.extras.controls.ppolicy.PasswordPolicyResponseImpl;
054import org.apache.directory.api.ldap.model.constants.AuthenticationLevel;
055import org.apache.directory.api.ldap.model.constants.LdapSecurityConstants;
056import org.apache.directory.api.ldap.model.constants.SchemaConstants;
057import org.apache.directory.api.ldap.model.entry.Attribute;
058import org.apache.directory.api.ldap.model.entry.DefaultAttribute;
059import org.apache.directory.api.ldap.model.entry.DefaultModification;
060import org.apache.directory.api.ldap.model.entry.Entry;
061import org.apache.directory.api.ldap.model.entry.Modification;
062import org.apache.directory.api.ldap.model.entry.ModificationOperation;
063import org.apache.directory.api.ldap.model.entry.Value;
064import org.apache.directory.api.ldap.model.exception.LdapAuthenticationException;
065import org.apache.directory.api.ldap.model.exception.LdapException;
066import org.apache.directory.api.ldap.model.exception.LdapNoPermissionException;
067import org.apache.directory.api.ldap.model.exception.LdapOperationException;
068import org.apache.directory.api.ldap.model.exception.LdapOtherException;
069import org.apache.directory.api.ldap.model.exception.LdapUnwillingToPerformException;
070import org.apache.directory.api.ldap.model.message.ResultCodeEnum;
071import org.apache.directory.api.ldap.model.name.Dn;
072import org.apache.directory.api.ldap.model.password.PasswordUtil;
073import org.apache.directory.api.ldap.model.schema.AttributeType;
074import org.apache.directory.api.util.DateUtils;
075import org.apache.directory.api.util.Strings;
076import org.apache.directory.server.constants.ServerDNConstants;
077import org.apache.directory.server.core.api.CoreSession;
078import org.apache.directory.server.core.api.DirectoryService;
079import org.apache.directory.server.core.api.InterceptorEnum;
080import org.apache.directory.server.core.api.LdapPrincipal;
081import org.apache.directory.server.core.api.authn.ppolicy.CheckQualityEnum;
082import org.apache.directory.server.core.api.authn.ppolicy.DefaultPasswordValidator;
083import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyConfiguration;
084import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyException;
085import org.apache.directory.server.core.api.authn.ppolicy.PasswordValidator;
086import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
087import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
088import org.apache.directory.server.core.api.interceptor.Interceptor;
089import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
090import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
091import org.apache.directory.server.core.api.interceptor.context.CompareOperationContext;
092import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
093import org.apache.directory.server.core.api.interceptor.context.GetRootDseOperationContext;
094import org.apache.directory.server.core.api.interceptor.context.HasEntryOperationContext;
095import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
096import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
097import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
098import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
099import org.apache.directory.server.core.api.interceptor.context.OperationContext;
100import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
101import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
102import org.apache.directory.server.core.api.interceptor.context.UnbindOperationContext;
103import org.apache.directory.server.core.api.partition.Partition;
104import org.apache.directory.server.core.api.partition.PartitionTxn;
105import org.apache.directory.server.core.authn.ppolicy.PpolicyConfigContainer;
106import org.apache.directory.server.core.shared.DefaultCoreSession;
107import org.apache.directory.server.i18n.I18n;
108import org.slf4j.Logger;
109import org.slf4j.LoggerFactory;
110
111
112/**
113 * An {@link Interceptor} that authenticates users.
114 *
115 * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
116 */
117public class AuthenticationInterceptor extends BaseInterceptor
118{
119    private static final Logger LOG = LoggerFactory.getLogger( AuthenticationInterceptor.class );
120
121    /**
122     * Speedup for logs
123     */
124    private static final boolean IS_DEBUG = LOG.isDebugEnabled();
125
126    /** A Set of all the existing Authenticator to be used by the bind operation */
127    private Set<Authenticator> authenticators = new HashSet<>();
128
129    /** A map of authenticators associated with the authentication level required */
130    private final EnumMap<AuthenticationLevel, Collection<Authenticator>> authenticatorsMapByType = new EnumMap<>( AuthenticationLevel.class );
131
132    private CoreSession adminSession;
133
134    // pwdpolicy state attribute types
135    private AttributeType pwdResetAT;
136
137    private AttributeType pwdChangedTimeAT;
138
139    private AttributeType pwdHistoryAT;
140
141    private AttributeType pwdFailurTimeAT;
142
143    private AttributeType pwdAccountLockedTimeAT;
144
145    private AttributeType pwdLastSuccessAT;
146
147    private AttributeType pwdGraceUseTimeAT;
148
149    private AttributeType pwdPolicySubentryAT;
150
151    private AttributeType pwdStartTimeAT;
152
153    private AttributeType pwdEndTimeAT;
154
155    /** a container to hold all the ppolicies */
156    private PpolicyConfigContainer pwdPolicyContainer;
157
158
159    /**
160     * Creates an authentication service interceptor.
161     */
162    public AuthenticationInterceptor()
163    {
164        super( InterceptorEnum.AUTHENTICATION_INTERCEPTOR );
165    }
166
167
168    /**
169     * Registers and initializes all {@link Authenticator}s to this service.
170     */
171    @Override
172    public void init( DirectoryService directoryService ) throws LdapException
173    {
174        super.init( directoryService );
175
176        adminSession = directoryService.getAdminSession();
177
178        if ( ( authenticators == null ) || authenticators.isEmpty() )
179        {
180            setDefaultAuthenticators();
181        }
182
183        // Register all authenticators
184        for ( Authenticator authenticator : authenticators )
185        {
186            register( authenticator, directoryService );
187        }
188
189        loadPwdPolicyStateAttributeTypes();
190    }
191
192
193    /**
194     * Initialize the set of authenticators with some default values
195     */
196    private void setDefaultAuthenticators()
197    {
198        if ( authenticators == null )
199        {
200            authenticators = new HashSet<>();
201        }
202
203        authenticators.clear();
204        authenticators.add( new AnonymousAuthenticator( Dn.ROOT_DSE ) );
205        authenticators.add( new SimpleAuthenticator( Dn.ROOT_DSE ) );
206        authenticators.add( new StrongAuthenticator( Dn.ROOT_DSE ) );
207    }
208
209
210    public Set<Authenticator> getAuthenticators()
211    {
212        return authenticators;
213    }
214
215
216    /**
217     * @param authenticators authenticators to be used by this AuthenticationInterceptor
218     */
219    public void setAuthenticators( Set<Authenticator> authenticators )
220    {
221        if ( authenticators == null )
222        {
223            this.authenticators.clear();
224        }
225        else
226        {
227            this.authenticators = authenticators;
228        }
229    }
230
231
232    /**
233     * @param authenticators authenticators to be used by this AuthenticationInterceptor
234     */
235    public void setAuthenticators( Authenticator[] authenticators )
236    {
237        if ( authenticators == null )
238        {
239            throw new IllegalArgumentException( "The given authenticators set is null" );
240        }
241
242        this.authenticators.clear();
243        this.authenticatorsMapByType.clear();
244
245        for ( Authenticator authenticator : authenticators )
246        {
247            try
248            {
249                register( authenticator, directoryService );
250            }
251            catch ( LdapException le )
252            {
253                LOG.error( "Cannot register authenticator {}", authenticator );
254            }
255        }
256    }
257
258
259    /**
260     * Deinitializes and deregisters all {@link Authenticator}s from this service.
261     */
262    @Override
263    public void destroy()
264    {
265        authenticatorsMapByType.clear();
266        Set<Authenticator> copy = new HashSet<>( authenticators );
267        authenticators = new HashSet<>();
268
269        for ( Authenticator authenticator : copy )
270        {
271            authenticator.destroy();
272        }
273    }
274
275
276    /**
277     * Initializes the specified {@link Authenticator} and registers it to
278     * this service.
279     *
280     * @param authenticator Authenticator to initialize and register by type
281     * @param directoryService configuration info to supply to the Authenticator during initialization
282     * @throws javax.naming.Exception if initialization fails.
283     */
284    private void register( Authenticator authenticator, DirectoryService directoryService ) throws LdapException
285    {
286        authenticator.init( directoryService );
287        authenticators.add( authenticator );
288
289        Collection<Authenticator> authenticatorList = getAuthenticators( authenticator.getAuthenticatorType() );
290
291        if ( authenticatorList == null )
292        {
293            authenticatorList = new ArrayList<>();
294            authenticatorsMapByType.put( authenticator.getAuthenticatorType(), authenticatorList );
295        }
296
297        if ( !authenticatorList.contains( authenticator ) )
298        {
299            authenticatorList.add( authenticator );
300        }
301    }
302
303
304    /**
305     * Returns the list of {@link Authenticator}s with the specified type.
306     *
307     * @param type type of Authenticator sought
308     * @return A list of Authenticators of the requested type or <tt>null</tt> if no authenticator is found.
309     */
310    private Collection<Authenticator> getAuthenticators( AuthenticationLevel type )
311    {
312        Collection<Authenticator> result = authenticatorsMapByType.get( type );
313
314        if ( ( result != null ) && ( !result.isEmpty() ) )
315        {
316            return result;
317        }
318        else
319        {
320            return null;
321        }
322    }
323
324
325    /**
326     * {@inheritDoc}
327     */
328    @Override
329    public void add( AddOperationContext addContext ) throws LdapException
330    {
331        if ( IS_DEBUG )
332        {
333            LOG.debug( "Operation Context: {}", addContext );
334        }
335
336        checkAuthenticated( addContext );
337
338        Entry entry = addContext.getEntry();
339
340        if ( !directoryService.isPwdPolicyEnabled() || addContext.isReplEvent() )
341        {
342            next( addContext );
343            return;
344        }
345
346        PasswordPolicyConfiguration policyConfig = getPwdPolicy( entry );
347
348        boolean isPPolicyReqCtrlPresent = addContext.hasRequestControl( PasswordPolicyRequest.OID );
349
350        checkPwdReset( addContext );
351
352        // Get the password depending on the configuration
353        String passwordAttribute = SchemaConstants.USER_PASSWORD_AT;
354
355        if ( isPPolicyReqCtrlPresent )
356        {
357            passwordAttribute = policyConfig.getPwdAttribute();
358        }
359
360        Attribute userPasswordAttribute = entry.get( passwordAttribute );
361
362        if ( userPasswordAttribute != null )
363        {
364            Value userPassword = userPasswordAttribute.get();
365
366            try
367            {
368                check( addContext, entry, userPassword.getBytes(), policyConfig );
369            }
370            catch ( PasswordPolicyException e )
371            {
372                if ( isPPolicyReqCtrlPresent )
373                {
374                    PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
375                    responseControl.setPasswordPolicyError(
376                        PasswordPolicyErrorEnum.get( e.getErrorCode() ) );
377                    addContext.addResponseControl( responseControl );
378                }
379
380                // throw exception if userPassword quality checks fail
381                throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION, e.getMessage(), e );
382            }
383
384            String pwdChangedTime = DateUtils.getGeneralizedTime( directoryService.getTimeProvider() );
385
386            if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
387            {
388                // https://issues.apache.org/jira/browse/DIRSERVER-1978
389                if ( !addContext.getSession().isAnAdministrator()
390                    || entry.get( pwdChangedTimeAT ) == null )
391                {
392                    Attribute pwdChangedTimeAt = new DefaultAttribute( pwdChangedTimeAT );
393                    pwdChangedTimeAt.add( pwdChangedTime );
394                    entry.add( pwdChangedTimeAt );
395                }
396            }
397
398            if ( policyConfig.isPwdMustChange() && addContext.getSession().isAnAdministrator() )
399            {
400                Attribute pwdResetAt = new DefaultAttribute( pwdResetAT );
401                pwdResetAt.add( "TRUE" );
402                entry.add( pwdResetAt );
403            }
404
405            if ( policyConfig.getPwdInHistory() > 0 )
406            {
407                Attribute pwdHistoryAt = new DefaultAttribute( pwdHistoryAT );
408                byte[] pwdHistoryVal = new PasswordHistory( pwdChangedTime, userPassword.getBytes() ).getHistoryValue();
409                pwdHistoryAt.add( pwdHistoryVal );
410                entry.add( pwdHistoryAt );
411            }
412        }
413
414        next( addContext );
415    }
416
417
418    /**
419     * Return the selected authenticator given the DN and the level required.
420     */
421    private Authenticator selectAuthenticator( Dn bindDn, AuthenticationLevel level )
422        throws LdapUnwillingToPerformException, LdapAuthenticationException
423    {
424        Authenticator selectedAuthenticator = null;
425        Collection<Authenticator> levelAuthenticators = authenticatorsMapByType.get( level );
426
427        if ( ( levelAuthenticators == null ) || levelAuthenticators.isEmpty() )
428        {
429            // No authenticators associated with this level : get out
430            throw new LdapAuthenticationException( "Cannot Bind for Dn "
431                + bindDn.getName() + ", no authenticator for the requested level " + level );
432        }
433
434        if ( levelAuthenticators.size() == 1 )
435        {
436            // Just pick the existing one
437            for ( Authenticator authenticator : levelAuthenticators )
438            {
439                // Check that the bindDN fits
440                if ( authenticator.isValid( bindDn ) )
441                {
442                    return authenticator;
443                }
444                else
445                {
446                    throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
447                        "Cannot Bind for Dn " + bindDn.getName() 
448                        + ", its not a descendant of the authenticator base DN '" + authenticator.getBaseDn() + "'" );
449                }
450            }
451        }
452
453        // We have more than one authenticator. Let's loop on all of them and
454        // select the one that fits the bindDN
455        Dn innerDn = Dn.ROOT_DSE;
456
457        for ( Authenticator authenticator : levelAuthenticators )
458        {
459            if ( authenticator.isValid( bindDn ) )
460            {
461                // We have found a valid authenticator, let's check if it's the inner one
462                if ( innerDn.isAncestorOf( authenticator.getBaseDn() ) )
463                {
464                    innerDn = authenticator.getBaseDn();
465                    selectedAuthenticator = authenticator;
466                }
467            }
468        }
469
470        if ( selectedAuthenticator == null )
471        {
472            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM,
473                    "Cannot Bind for Dn " + bindDn.getName() + ", there is no authenticator for it" );
474        }
475        
476        return selectedAuthenticator;
477    }
478    
479    
480    private void internalModify( OperationContext opContext, ModifyOperationContext bindModCtx ) throws LdapException
481    {
482        Partition partition = opContext.getPartition();
483        bindModCtx.setPartition( partition );
484        PartitionTxn partitionTxn = null;
485
486        try
487        {
488            partitionTxn = partition.beginWriteTransaction();
489            bindModCtx.setTransaction( partitionTxn );
490
491            directoryService.getPartitionNexus().modify( bindModCtx );
492
493            partitionTxn.commit();
494        }
495        catch ( LdapException le )
496        {
497            try 
498            {
499                if ( partitionTxn != null )
500                {
501                    partitionTxn.abort();
502                }
503                
504                throw le;
505            }
506            catch ( IOException ioe )
507            {
508                throw new LdapOtherException( ioe.getMessage(), ioe );
509            }
510        }
511        catch ( IOException ioe )
512        {
513            try 
514            {
515                partitionTxn.abort();
516                
517                throw new LdapOtherException( ioe.getMessage(), ioe );
518            }
519            catch ( IOException ioe2 )
520            {
521                throw new LdapOtherException( ioe2.getMessage(), ioe2 );
522            }
523        }
524    }
525
526
527    /**
528     * {@inheritDoc}
529     */
530    @Override
531    public void bind( BindOperationContext bindContext ) throws LdapException
532    {
533        if ( IS_DEBUG )
534        {
535            LOG.debug( "Operation Context: {}", bindContext );
536        }
537
538        CoreSession session = bindContext.getSession();
539        Dn bindDn = bindContext.getDn();
540
541        if ( ( session != null )
542            && ( session.getEffectivePrincipal() != null )
543            && ( !session.isAnonymous() )
544            && ( !session.isAdministrator() ) )
545        {
546            // null out the credentials
547            bindContext.setCredentials( null );
548        }
549
550        // pick the first matching authenticator type
551        AuthenticationLevel level = bindContext.getAuthenticationLevel();
552
553        if ( level == AuthenticationLevel.UNAUTHENT )
554        {
555            // This is a case where the Bind request contains a Dn, but no password.
556            // We don't check the Dn, we just return a UnwillingToPerform error
557            // Cf RFC 4513, chap. 5.1.2
558            throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, "Cannot Bind for Dn "
559                + bindDn.getName() );
560        }
561
562        PasswordPolicyException ppe = null;
563        boolean isPPolicyReqCtrlPresent = bindContext.hasRequestControl( PasswordPolicyRequest.OID );
564        PasswordPolicyResponse pwdRespCtrl = new PasswordPolicyResponseImpl();
565        boolean authenticated = false;
566
567        Authenticator authenticator = selectAuthenticator( bindDn, level );
568
569        try
570        {
571            // perform the authentication
572            LdapPrincipal principal = authenticator.authenticate( bindContext );
573
574            if ( principal != null )
575            {
576                LdapPrincipal clonedPrincipal = ( LdapPrincipal ) ( principal.clone() );
577
578                // remove creds so there is no security risk
579                bindContext.setCredentials( null );
580                clonedPrincipal.setUserPassword( Strings.EMPTY_BYTES );
581
582                // authentication was successful
583                CoreSession newSession = new DefaultCoreSession( clonedPrincipal, directoryService );
584                bindContext.setSession( newSession );
585
586                authenticated = true;
587            }
588        }
589        catch ( PasswordPolicyException e )
590        {
591            ppe = e;
592        }
593        catch ( LdapAuthenticationException e )
594        {
595            // authentication failed, try the next authenticator
596            LOG.info( "Authenticator {} failed to authenticate: {}", authenticator, bindContext.getDn() );
597        }
598        catch ( Exception e )
599        {
600            // Log other exceptions than LdapAuthenticationException
601            LOG.info( "Unexpected failure for Authenticator {} : {}", authenticator, bindContext.getDn() );
602        }
603
604        if ( ppe != null )
605        {
606            if ( isPPolicyReqCtrlPresent )
607            {
608                pwdRespCtrl.setPasswordPolicyError( PasswordPolicyErrorEnum.get( ppe.getErrorCode() ) );
609                bindContext.addResponseControl( pwdRespCtrl );
610            }
611
612            throw ppe;
613        }
614
615        Entry userEntry = bindContext.getEntry();
616
617        PasswordPolicyConfiguration policyConfig = getPwdPolicy( userEntry );
618
619        // load the user entry again if ppolicy is enabled, cause the authenticator might have modified the entry
620        if ( policyConfig != null )
621        {
622            LookupOperationContext lookupContext = new LookupOperationContext( adminSession, bindDn,
623                SchemaConstants.ALL_ATTRIBUTES_ARRAY );
624            lookupContext.setPartition( bindContext.getPartition() );
625            lookupContext.setTransaction( bindContext.getTransaction() );
626            
627            userEntry = directoryService.getPartitionNexus().lookup( lookupContext );
628        }
629
630        // check if the user entry is null, it will be null
631        // in cases of anonymous bind
632        if ( authenticated && ( userEntry == null ) && directoryService.isAllowAnonymousAccess() )
633        {
634            return;
635        }
636
637        if ( !authenticated )
638        {
639            if ( LOG.isInfoEnabled() )
640            {
641                LOG.info( "Cannot bind to the server " );
642            }
643
644            if ( ( policyConfig != null ) && ( userEntry != null ) )
645            {
646                Attribute pwdFailTimeAt = userEntry.get( pwdFailurTimeAT );
647
648                if ( pwdFailTimeAt == null )
649                {
650                    pwdFailTimeAt = new DefaultAttribute( pwdFailurTimeAT );
651                }
652                else
653                {
654                    purgeFailureTimes( policyConfig, pwdFailTimeAt );
655                }
656
657                String failureTime = DateUtils.getGeneralizedTime( directoryService.getTimeProvider() );
658                pwdFailTimeAt.add( failureTime );
659                Modification pwdFailTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdFailTimeAt );
660
661                List<Modification> mods = new ArrayList<>();
662                mods.add( pwdFailTimeMod );
663
664                int numFailures = pwdFailTimeAt.size();
665
666                if ( policyConfig.isPwdLockout() && ( numFailures >= policyConfig.getPwdMaxFailure() ) )
667                {
668                    // Checking that we're not locking the admin user of the system partition
669                    // See DIRSERVER-1812 (The default admin account should never get locked forever)
670                    if ( !userEntry.getDn().equals( new Dn( schemaManager, ServerDNConstants.ADMIN_SYSTEM_DN ) ) )
671                    {
672                        Attribute pwdAccountLockedTimeAt = new DefaultAttribute( pwdAccountLockedTimeAT );
673
674                        // if zero, lockout permanently, only admin can unlock it
675                        if ( policyConfig.getPwdLockoutDuration() == 0 )
676                        {
677                            pwdAccountLockedTimeAt.add( "000001010000Z" );
678                        }
679                        else
680                        {
681                            pwdAccountLockedTimeAt.add( failureTime );
682                        }
683
684                        Modification pwdAccountLockedMod = new DefaultModification( REPLACE_ATTRIBUTE,
685                            pwdAccountLockedTimeAt );
686                        mods.add( pwdAccountLockedMod );
687
688                        pwdRespCtrl.setPasswordPolicyError( PasswordPolicyErrorEnum.ACCOUNT_LOCKED );
689                    }
690                }
691                else if ( policyConfig.getPwdMinDelay() > 0 )
692                {
693                    int numDelay = numFailures * policyConfig.getPwdMinDelay();
694                    int maxDelay = policyConfig.getPwdMaxDelay();
695
696                    if ( numDelay > maxDelay )
697                    {
698                        numDelay = maxDelay;
699                    }
700
701                    try
702                    {
703                        Thread.sleep( numDelay * 1000L );
704                    }
705                    catch ( InterruptedException e )
706                    {
707                        LOG.warn(
708                            "Interrupted while delaying to send the failed authentication response for the user {}",
709                            bindDn, e );
710                    }
711                }
712
713                if ( !mods.isEmpty() )
714                {
715                    String csnVal = directoryService.getCSN().toString();
716                    Modification csnMod = new DefaultModification( REPLACE_ATTRIBUTE, directoryService.getAtProvider()
717                        .getEntryCSN(), csnVal );
718                    mods.add( csnMod );
719                    ModifyOperationContext bindModCtx = new ModifyOperationContext( adminSession );
720                    bindModCtx.setDn( bindDn );
721                    bindModCtx.setEntry( userEntry );
722                    bindModCtx.setModItems( mods );
723                    bindModCtx.setPushToEvtInterceptor( true );
724
725                    internalModify( bindContext, bindModCtx );
726                }
727            }
728
729            String upDn = bindDn == null ? "" : bindDn.getName();
730            throw new LdapAuthenticationException( I18n.err( I18n.ERR_229, upDn ) );
731        }
732        else if ( policyConfig != null )
733        {
734            List<Modification> mods = new ArrayList<>();
735
736            if ( policyConfig.getPwdMaxIdle() > 0 )
737            {
738                Attribute pwdLastSuccesTimeAt = new DefaultAttribute( pwdLastSuccessAT );
739                pwdLastSuccesTimeAt.add( DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
740                Modification pwdLastSuccesTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdLastSuccesTimeAt );
741                mods.add( pwdLastSuccesTimeMod );
742            }
743
744            Attribute pwdFailTimeAt = userEntry.get( pwdFailurTimeAT );
745
746            if ( pwdFailTimeAt != null )
747            {
748                Modification pwdFailTimeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdFailTimeAt );
749                mods.add( pwdFailTimeMod );
750            }
751
752            Attribute pwdAccLockedTimeAt = userEntry.get( pwdAccountLockedTimeAT );
753
754            if ( pwdAccLockedTimeAt != null )
755            {
756                Modification pwdAccLockedTimeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdAccLockedTimeAt );
757                mods.add( pwdAccLockedTimeMod );
758            }
759
760            // checking the expiration time *after* performing authentication, do we need to care about millisecond precision?
761            if ( ( policyConfig.getPwdMaxAge() > 0 ) && ( policyConfig.getPwdGraceAuthNLimit() > 0 ) )
762            {
763                Attribute pwdChangeTimeAttr = userEntry.get( pwdChangedTimeAT );
764
765                if ( pwdChangeTimeAttr != null )
766                {
767                    boolean expired = PasswordUtil.isPwdExpired( pwdChangeTimeAttr.getString(),
768                        policyConfig.getPwdMaxAge(), directoryService.getTimeProvider() );
769
770                    if ( expired )
771                    {
772                        Attribute pwdGraceUseAttr = userEntry.get( pwdGraceUseTimeAT );
773                        int numGraceAuth;
774
775                        if ( pwdGraceUseAttr != null )
776                        {
777                            numGraceAuth = policyConfig.getPwdGraceAuthNLimit() - ( pwdGraceUseAttr.size() + 1 );
778                        }
779                        else
780                        {
781                            pwdGraceUseAttr = new DefaultAttribute( pwdGraceUseTimeAT );
782                            numGraceAuth = policyConfig.getPwdGraceAuthNLimit() - 1;
783                        }
784
785                        pwdRespCtrl.setGraceAuthNRemaining( numGraceAuth );
786
787                        pwdGraceUseAttr.add( DateUtils.getGeneralizedTime( directoryService.getTimeProvider() ) );
788                        Modification pwdGraceUseMod = new DefaultModification( ADD_ATTRIBUTE, pwdGraceUseAttr );
789                        mods.add( pwdGraceUseMod );
790                    }
791                }
792            }
793
794            if ( !mods.isEmpty() )
795            {
796                String csnVal = directoryService.getCSN().toString();
797                Modification csnMod = new DefaultModification( REPLACE_ATTRIBUTE, directoryService.getAtProvider()
798                    .getEntryCSN(), csnVal );
799                mods.add( csnMod );
800
801                ModifyOperationContext bindModCtx = new ModifyOperationContext( adminSession );
802                bindModCtx.setDn( bindDn );
803                bindModCtx.setEntry( userEntry );
804                bindModCtx.setModItems( mods );
805                bindModCtx.setPushToEvtInterceptor( true );
806                
807                internalModify( bindContext, bindModCtx );
808            }
809
810            if ( isPPolicyReqCtrlPresent )
811            {
812                int expiryWarnTime = getPwdTimeBeforeExpiry( userEntry, policyConfig );
813
814                if ( expiryWarnTime > 0 )
815                {
816                    pwdRespCtrl.setTimeBeforeExpiration( expiryWarnTime );
817                }
818
819                if ( isPwdMustReset( userEntry ) )
820                {
821                    pwdRespCtrl.setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
822                    bindContext.getSession().setPwdMustChange( true );
823                }
824
825                bindContext.addResponseControl( pwdRespCtrl );
826            }
827        }
828    }
829
830
831    /**
832     * {@inheritDoc}
833     */
834    @Override
835    public boolean compare( CompareOperationContext compareContext ) throws LdapException
836    {
837        if ( IS_DEBUG )
838        {
839            LOG.debug( "Operation Context: {}", compareContext );
840        }
841
842        checkAuthenticated( compareContext );
843        checkPwdReset( compareContext );
844        return next( compareContext );
845    }
846
847
848    /**
849     * {@inheritDoc}
850     */
851    @Override
852    public void delete( DeleteOperationContext deleteContext ) throws LdapException
853    {
854        if ( IS_DEBUG )
855        {
856            LOG.debug( "Operation Context: {}", deleteContext );
857        }
858
859        // Check that we can execute this operation
860        checkAuthenticated( deleteContext );
861
862        // propagate the call to the next interceptor
863        next( deleteContext );
864        
865        // if the deleted entry contains a password, then invalidate the associated caches
866        invalidateAuthenticatorCaches( deleteContext.getDn() );
867    }
868
869
870    /**
871     * {@inheritDoc}
872     */
873    @Override
874    public Entry getRootDse( GetRootDseOperationContext getRootDseContext ) throws LdapException
875    {
876        if ( IS_DEBUG )
877        {
878            LOG.debug( "Operation Context: {}", getRootDseContext );
879        }
880
881        checkAuthenticated( getRootDseContext );
882        checkPwdReset( getRootDseContext );
883
884        return next( getRootDseContext );
885    }
886
887
888    /**
889     * {@inheritDoc}
890     */
891    @Override
892    public boolean hasEntry( HasEntryOperationContext hasEntryContext ) throws LdapException
893    {
894        if ( IS_DEBUG )
895        {
896            LOG.debug( "Operation Context: {}", hasEntryContext );
897        }
898
899        checkAuthenticated( hasEntryContext );
900        checkPwdReset( hasEntryContext );
901
902        return next( hasEntryContext );
903    }
904
905
906    /**
907     * {@inheritDoc}
908     */
909    @Override
910    public Entry lookup( LookupOperationContext lookupContext ) throws LdapException
911    {
912        if ( IS_DEBUG )
913        {
914            LOG.debug( "Operation Context: {}", lookupContext );
915        }
916
917        checkAuthenticated( lookupContext );
918        checkPwdReset( lookupContext );
919
920        return next( lookupContext );
921    }
922
923
924    private void invalidateAuthenticatorCaches( Dn principalDn )
925    {
926        for ( AuthenticationLevel authMech : authenticatorsMapByType.keySet() )
927        {
928            // try each authenticator
929            for ( Authenticator authenticator : getAuthenticators( authMech ) )
930            {
931                authenticator.invalidateCache( principalDn );
932            }
933        }
934    }
935
936    
937    /**
938     * {@inheritDoc}
939     */
940    @Override
941    public void modify( ModifyOperationContext modifyContext ) throws LdapException
942    {
943        if ( IS_DEBUG )
944        {
945            LOG.debug( "Operation Context: {}", modifyContext );
946        }
947
948        checkAuthenticated( modifyContext );
949
950        if ( !directoryService.isPwdPolicyEnabled() || modifyContext.isReplEvent() )
951        {
952            processStandardModify( modifyContext );
953        }
954        else
955        {
956            processPasswordPolicydModify( modifyContext );
957        }
958    }
959
960    
961    /**
962     * Proceed with the Modification operation when the PasswordPolicy is not activated.
963     */
964    private void processStandardModify( ModifyOperationContext modifyContext ) throws LdapException
965    {
966        next( modifyContext );
967
968        List<Modification> modifications = modifyContext.getModItems();
969
970        for ( Modification modification : modifications )
971        {
972            if ( directoryService.getAtProvider().getUserPassword()
973                .equals( modification.getAttribute().getAttributeType() ) )
974            {
975                invalidateAuthenticatorCaches( modifyContext.getDn() );
976                break;
977            }
978        }
979    }
980
981    
982    /**
983     * Proceed with the Modification operation when the PasswordPolicy is activated.
984     */
985    private void processPasswordPolicydModify( ModifyOperationContext modifyContext ) throws LdapException
986    {
987        // handle the case where pwdPolicySubentry AT is about to be deleted in this modify()
988        PasswordPolicyConfiguration policyConfig = getPwdPolicy( modifyContext.getEntry() );
989
990        PwdModDetailsHolder pwdModDetails = getPwdModDetails( modifyContext, policyConfig );
991
992        if ( !pwdModDetails.isPwdModPresent() )
993        {
994            // We can going on, the password attribute is not present in the Modifications.
995            next( modifyContext );
996        }
997        else
998        {
999            // The password is present in the modifications. Deal with the various use cases.
1000            CoreSession userSession = modifyContext.getSession();
1001            boolean isPPolicyReqCtrlPresent = modifyContext.hasRequestControl( PasswordPolicyRequest.OID );
1002            
1003            // First, check if the password must be changed, and if the operation allows it
1004            checkPwdMustChange( modifyContext, userSession, pwdModDetails, isPPolicyReqCtrlPresent );
1005
1006            // Check the the old password is present if it's required by the PP config
1007            checkOldPwdRequired( modifyContext, policyConfig, pwdModDetails, isPPolicyReqCtrlPresent );
1008
1009            // Check that we can't update the password if it's not allowed
1010            checkChangePwdAllowed( modifyContext, policyConfig, isPPolicyReqCtrlPresent );
1011
1012            Entry entry = modifyContext.getEntry();
1013
1014            boolean removePwdReset = false;
1015
1016            List<Modification> mods = new ArrayList<>();
1017
1018            if ( pwdModDetails.isAddOrReplace() )
1019            {
1020                if ( isPwdTooYoung( modifyContext, entry, policyConfig ) )
1021                {
1022                    if ( isPPolicyReqCtrlPresent )
1023                    {
1024                        PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
1025                        responseControl.setPasswordPolicyError(
1026                            PasswordPolicyErrorEnum.PASSWORD_TOO_YOUNG );
1027                        modifyContext.addResponseControl( responseControl );
1028                    }
1029
1030                    throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION,
1031                        "password is too young to update" );
1032                }
1033
1034                byte[] newPassword = pwdModDetails.getNewPwd();
1035
1036                try
1037                {
1038                    check( modifyContext, entry, newPassword, policyConfig );
1039                }
1040                catch ( PasswordPolicyException e )
1041                {
1042                    if ( isPPolicyReqCtrlPresent )
1043                    {
1044                        PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
1045                        responseControl.setPasswordPolicyError(
1046                            PasswordPolicyErrorEnum.get( e.getErrorCode() ) );
1047                        modifyContext.addResponseControl( responseControl );
1048                    }
1049
1050                    // throw exception if userPassword quality checks fail
1051                    throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION, e.getMessage(), e );
1052                }
1053
1054                int histSize = policyConfig.getPwdInHistory();
1055                Modification pwdRemHistMod = null;
1056                Modification pwdAddHistMod = null;
1057                String pwdChangedTime = DateUtils.getGeneralizedTime( directoryService.getTimeProvider() );
1058
1059                if ( histSize > 0 )
1060                {
1061                    Attribute pwdHistoryAt = entry.get( pwdHistoryAT );
1062
1063                    if ( pwdHistoryAt == null )
1064                    {
1065                        pwdHistoryAt = new DefaultAttribute( pwdHistoryAT );
1066                    }
1067
1068                    // Build the Modification containing the password history
1069                    pwdRemHistMod = buildPwdHistory( modifyContext, pwdHistoryAt, histSize, 
1070                        newPassword, isPPolicyReqCtrlPresent );
1071
1072                    PasswordHistory newPwdHist = new PasswordHistory( pwdChangedTime, newPassword );
1073                    pwdHistoryAt.add( newPwdHist.getHistoryValue() );
1074                    pwdAddHistMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdHistoryAt );
1075                }
1076
1077                next( modifyContext );
1078
1079                invalidateAuthenticatorCaches( modifyContext.getDn() );
1080
1081                LookupOperationContext lookupContext = new LookupOperationContext( adminSession, modifyContext.getDn(),
1082                    SchemaConstants.ALL_ATTRIBUTES_ARRAY );
1083                lookupContext.setPartition( modifyContext.getPartition() );
1084                lookupContext.setTransaction( modifyContext.getTransaction() );
1085                
1086                entry = directoryService.getPartitionNexus().lookup( lookupContext );
1087
1088                if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
1089                {
1090                    Attribute pwdChangedTimeAt = new DefaultAttribute( pwdChangedTimeAT );
1091                    pwdChangedTimeAt.add( pwdChangedTime );
1092                    Modification pwdChangedTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdChangedTimeAt );
1093                    mods.add( pwdChangedTimeMod );
1094                }
1095
1096                if ( pwdAddHistMod != null )
1097                {
1098                    mods.add( pwdAddHistMod );
1099                }
1100
1101                if ( pwdRemHistMod != null )
1102                {
1103                    mods.add( pwdRemHistMod );
1104                }
1105
1106                if ( policyConfig.isPwdMustChange() )
1107                {
1108                    Attribute pwdMustChangeAt = new DefaultAttribute( pwdResetAT );
1109                    Modification pwdMustChangeMod;
1110
1111                    if ( modifyContext.getSession().isAnAdministrator() )
1112                    {
1113                        pwdMustChangeAt.add( "TRUE" );
1114                        pwdMustChangeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdMustChangeAt );
1115                    }
1116                    else
1117                    {
1118                        pwdMustChangeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdMustChangeAt );
1119                        removePwdReset = true;
1120                    }
1121
1122                    mods.add( pwdMustChangeMod );
1123                }
1124            }
1125
1126            // Add the attributes that have been modified following a Add/Replace password
1127            processModifyAddPwdAttributes( entry, mods, pwdModDetails );
1128
1129            String csnVal = directoryService.getCSN().toString();
1130            Modification csnMod = new DefaultModification( REPLACE_ATTRIBUTE, directoryService.getAtProvider()
1131                .getEntryCSN(), csnVal );
1132            mods.add( csnMod );
1133
1134            ModifyOperationContext internalModifyCtx = new ModifyOperationContext( adminSession );
1135            internalModifyCtx.setPushToEvtInterceptor( true );
1136            internalModifyCtx.setDn( modifyContext.getDn() );
1137            internalModifyCtx.setEntry( entry );
1138            internalModifyCtx.setModItems( mods );
1139
1140            internalModify( modifyContext, internalModifyCtx );
1141
1142            if ( removePwdReset || pwdModDetails.isDelete() )
1143            {
1144                userSession.setPwdMustChange( false );
1145            }
1146        }
1147    }
1148    
1149    
1150    /**
1151     * Build the list of passwordHistory
1152     */
1153    Modification buildPwdHistory( ModifyOperationContext modifyContext, Attribute pwdHistoryAt, 
1154        int histSize, byte[] newPassword, boolean isPPolicyReqCtrlPresent ) throws LdapOperationException
1155    {
1156        List<PasswordHistory> pwdHistLst = new ArrayList<>();
1157
1158        for ( Value value : pwdHistoryAt )
1159        {
1160            PasswordHistory pwdh = new PasswordHistory( Strings.utf8ToString( value.getBytes() ) );
1161
1162            // Admin user is exempt from history check
1163            // https://issues.apache.org/jira/browse/DIRSERVER-2084 
1164            if ( !modifyContext.getSession().isAnAdministrator() )
1165            {
1166                boolean matched = MessageDigest.isEqual( newPassword, pwdh.getPassword() );
1167
1168                if ( matched )
1169                {
1170                    if ( isPPolicyReqCtrlPresent )
1171                    {
1172                        PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
1173                        responseControl.setPasswordPolicyError(
1174                            PasswordPolicyErrorEnum.PASSWORD_IN_HISTORY );
1175                        modifyContext.addResponseControl( responseControl );
1176                    }
1177
1178                    throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION,
1179                        "invalid reuse of password present in password history" );
1180                }
1181            }
1182
1183            pwdHistLst.add( pwdh );
1184        }
1185 
1186        Modification pwdRemHistMod = null;
1187        
1188        if ( pwdHistLst.size() >= histSize )
1189        {
1190            // see the javadoc of PasswordHistory
1191            Collections.sort( pwdHistLst );
1192
1193            // remove the oldest value
1194            PasswordHistory remPwdHist = ( PasswordHistory ) pwdHistLst.toArray()[histSize - 1];
1195            Attribute tempAt = new DefaultAttribute( pwdHistoryAT );
1196            tempAt.add( remPwdHist.getHistoryValue() );
1197            pwdRemHistMod = new DefaultModification( REMOVE_ATTRIBUTE, tempAt );
1198        }
1199
1200        return pwdRemHistMod;
1201    }
1202    
1203    
1204    /**
1205     * Add the passwordPolicy related Attributes from the modified entry
1206     */
1207    private void processModifyAddPwdAttributes( Entry entry, List<Modification> mods, PwdModDetailsHolder pwdModDetails )
1208    {
1209        Attribute pwdFailureTimeAt = entry.get( pwdFailurTimeAT );
1210    
1211        if ( pwdFailureTimeAt != null )
1212        {
1213            mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdFailureTimeAt ) );
1214        }
1215    
1216        Attribute pwdGraceUseTimeAt = entry.get( pwdGraceUseTimeAT );
1217    
1218        if ( pwdGraceUseTimeAt != null )
1219        {
1220            mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdGraceUseTimeAt ) );
1221        }
1222    
1223        if ( pwdModDetails.isDelete() )
1224        {
1225            Attribute pwdHistory = entry.get( pwdHistoryAT );
1226            
1227            if ( pwdHistory != null )
1228            {
1229                mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdHistory ) );
1230            }
1231    
1232            Attribute pwdChangedTimeAt = entry.get( pwdChangedTimeAT );
1233            
1234            if ( pwdChangedTimeAt != null )
1235            {
1236                mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdChangedTimeAt ) );
1237            }
1238    
1239            Attribute pwdMustChangeAt = entry.get( pwdResetAT );
1240            
1241            if ( pwdMustChangeAt != null )
1242            {
1243                mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdMustChangeAt ) );
1244            }
1245    
1246            Attribute pwdAccountLockedTimeAt = entry.get( pwdAccountLockedTimeAT );
1247            
1248            if ( pwdAccountLockedTimeAt != null )
1249            {
1250                mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdAccountLockedTimeAt ) );
1251            }
1252        }
1253    }
1254
1255    
1256    /**
1257     * Check if the password has to be changed, but can't.
1258     */
1259    private void checkPwdMustChange( ModifyOperationContext modifyContext, CoreSession userSession, 
1260        PwdModDetailsHolder pwdModDetails, boolean isPPolicyReqCtrlPresent ) throws LdapNoPermissionException
1261    {
1262        if ( userSession.isPwdMustChange() && !pwdModDetails.isDelete() && pwdModDetails.isOtherModExists() )
1263       {
1264           if ( isPPolicyReqCtrlPresent )
1265           {
1266               PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
1267               responseControl.setPasswordPolicyError(
1268                   PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
1269               modifyContext.addResponseControl( responseControl );
1270           }
1271
1272           throw new LdapNoPermissionException(
1273               "Password should be reset before making any changes to this entry" );
1274       }
1275    }
1276    
1277    
1278    /**
1279     * If the PP config request it, the old password must be supplied in the modifications. Check that it 
1280     * is present.
1281     */
1282    private void checkOldPwdRequired( ModifyOperationContext modifyContext, PasswordPolicyConfiguration policyConfig,
1283        PwdModDetailsHolder pwdModDetails, boolean isPPolicyReqCtrlPresent ) throws LdapNoPermissionException
1284    {
1285        if ( policyConfig.isPwdSafeModify() && !pwdModDetails.isDelete() && pwdModDetails.isAddOrReplace() )
1286        {
1287            String msg = "trying to update password attribute without the supplying the old password";
1288            LOG.debug( msg );
1289
1290            if ( isPPolicyReqCtrlPresent )
1291            {
1292                PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
1293                responseControl.setPasswordPolicyError(
1294                    PasswordPolicyErrorEnum.MUST_SUPPLY_OLD_PASSWORD );
1295                modifyContext.addResponseControl( responseControl );
1296            }
1297
1298            throw new LdapNoPermissionException( msg );
1299        }
1300    }
1301    
1302    
1303    /**
1304     * check that if the password modification is allowed by the PP config, or if the session is 
1305     * the admin. 
1306     */
1307    private void checkChangePwdAllowed( ModifyOperationContext modifyContext, PasswordPolicyConfiguration policyConfig,
1308        boolean isPPolicyReqCtrlPresent ) throws LdapNoPermissionException
1309    {
1310        if ( !policyConfig.isPwdAllowUserChange() && !modifyContext.getSession().isAnAdministrator() )
1311             
1312        {
1313            if ( isPPolicyReqCtrlPresent )
1314            {
1315                PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
1316                responseControl.setPasswordPolicyError(
1317                    PasswordPolicyErrorEnum.PASSWORD_MOD_NOT_ALLOWED );
1318                modifyContext.addResponseControl( responseControl );
1319            }
1320
1321            throw new LdapNoPermissionException();
1322        }
1323    }
1324
1325    
1326    /**
1327     * {@inheritDoc}
1328     */
1329    @Override
1330    public void move( MoveOperationContext moveContext ) throws LdapException
1331    {
1332        if ( IS_DEBUG )
1333        {
1334            LOG.debug( "Operation Context: {}", moveContext );
1335        }
1336
1337        checkAuthenticated( moveContext );
1338        checkPwdReset( moveContext );
1339        next( moveContext );
1340        invalidateAuthenticatorCaches( moveContext.getDn() );
1341    }
1342
1343
1344    /**
1345     * {@inheritDoc}
1346     */
1347    @Override
1348    public void moveAndRename( MoveAndRenameOperationContext moveAndRenameContext ) throws LdapException
1349    {
1350        if ( IS_DEBUG )
1351        {
1352            LOG.debug( "Operation Context: {}", moveAndRenameContext );
1353        }
1354
1355        checkAuthenticated( moveAndRenameContext );
1356        checkPwdReset( moveAndRenameContext );
1357        next( moveAndRenameContext );
1358        invalidateAuthenticatorCaches( moveAndRenameContext.getDn() );
1359    }
1360
1361
1362    /**
1363     * {@inheritDoc}
1364     */
1365    @Override
1366    public void rename( RenameOperationContext renameContext ) throws LdapException
1367    {
1368        if ( IS_DEBUG )
1369        {
1370            LOG.debug( "Operation Context: {}", renameContext );
1371        }
1372
1373        checkAuthenticated( renameContext );
1374        checkPwdReset( renameContext );
1375        next( renameContext );
1376        invalidateAuthenticatorCaches( renameContext.getDn() );
1377    }
1378
1379
1380    /**
1381     * {@inheritDoc}
1382     */
1383    @Override
1384    public EntryFilteringCursor search( SearchOperationContext searchContext ) throws LdapException
1385    {
1386        if ( IS_DEBUG )
1387        {
1388            LOG.debug( "Operation Context: {}", searchContext );
1389        }
1390
1391        checkAuthenticated( searchContext );
1392        checkPwdReset( searchContext );
1393
1394        return next( searchContext );
1395    }
1396
1397
1398    /**
1399     * {@inheritDoc}
1400     */
1401    @Override
1402    public void unbind( UnbindOperationContext unbindContext ) throws LdapException
1403    {
1404        next( unbindContext );
1405    }
1406
1407
1408    /**
1409     * Check if the current operation has a valid PrincipalDN or not.
1410     *
1411     * @param operation the operation type
1412     * @throws Exception
1413     */
1414    private void checkAuthenticated( OperationContext operation ) throws LdapException
1415    {
1416        if ( operation.getSession().isAnonymous() && !directoryService.isAllowAnonymousAccess()
1417            && !operation.getDn().isEmpty() )
1418        {
1419            String msg = I18n.err( I18n.ERR_5, operation.getName() );
1420            LOG.error( msg );
1421            throw new LdapNoPermissionException( msg );
1422        }
1423    }
1424
1425
1426    /**
1427     * Initialize the PasswordPolicy attributeTypes
1428     * 
1429     * @throws LdapException If the initialization failed
1430     */
1431    public void loadPwdPolicyStateAttributeTypes() throws LdapException
1432    {
1433        pwdResetAT = schemaManager.lookupAttributeTypeRegistry( PWD_RESET_AT );
1434        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdResetAT );
1435
1436        pwdChangedTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_CHANGED_TIME_AT );
1437        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdChangedTimeAT );
1438
1439        pwdHistoryAT = schemaManager.lookupAttributeTypeRegistry( PWD_HISTORY_AT );
1440        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdHistoryAT );
1441
1442        pwdFailurTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_FAILURE_TIME_AT );
1443        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdFailurTimeAT );
1444
1445        pwdAccountLockedTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_ACCOUNT_LOCKED_TIME_AT );
1446        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdAccountLockedTimeAT );
1447
1448        pwdLastSuccessAT = schemaManager.lookupAttributeTypeRegistry( PWD_LAST_SUCCESS_AT );
1449        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdLastSuccessAT );
1450
1451        pwdGraceUseTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_GRACE_USE_TIME_AT );
1452        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdGraceUseTimeAT );
1453
1454        pwdPolicySubentryAT = schemaManager.lookupAttributeTypeRegistry( PWD_POLICY_SUBENTRY_AT );
1455        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdPolicySubentryAT );
1456
1457        pwdStartTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_START_TIME_AT );
1458        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdStartTimeAT );
1459
1460        pwdEndTimeAT = schemaManager.lookupAttributeTypeRegistry( PWD_END_TIME_AT );
1461        PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( pwdEndTimeAT );
1462    }
1463
1464
1465    // ---------- private methods ----------------
1466    private void check( OperationContext operationContext, Entry entry,
1467        byte[] password, PasswordPolicyConfiguration policyConfig )
1468        throws LdapException
1469    {
1470        // https://issues.apache.org/jira/browse/DIRSERVER-1928
1471        if ( operationContext.getSession().isAnAdministrator() )
1472        {
1473            return;
1474        }
1475        
1476        CheckQualityEnum qualityVal = policyConfig.getPwdCheckQuality();
1477
1478        if ( qualityVal == CheckQualityEnum.NO_CHECK )
1479        {
1480            return;
1481        }
1482
1483        LdapSecurityConstants secConst = PasswordUtil.findAlgorithm( password );
1484
1485        // do not perform quality check if the password is not plain text and
1486        // pwdCheckQuality value is set to 1
1487        if ( secConst != null )
1488        {
1489            if ( qualityVal == CheckQualityEnum.CHECK_ACCEPT )
1490            {
1491                return;
1492            }
1493            else
1494            {
1495                throw new PasswordPolicyException( "cannot verify the quality of the non-cleartext passwords",
1496                    INSUFFICIENT_PASSWORD_QUALITY.getValue() );
1497            }
1498        }
1499
1500        String strPassword = Strings.utf8ToString( password );
1501
1502        // perform the length validation
1503        validatePasswordLength( strPassword, policyConfig );
1504
1505        PasswordValidator passwordValidator = policyConfig.getPwdValidator();
1506        
1507        if ( passwordValidator == null )
1508        {
1509            // Use the default one
1510            passwordValidator = new DefaultPasswordValidator();
1511        }
1512        
1513        passwordValidator.validate( strPassword, entry );
1514    }
1515
1516
1517    /**
1518     * validates the length of the password
1519     */
1520    private void validatePasswordLength( String password, PasswordPolicyConfiguration policyConfig )
1521        throws PasswordPolicyException
1522    {
1523        int maxLen = policyConfig.getPwdMaxLength();
1524        int minLen = policyConfig.getPwdMinLength();
1525
1526        int pwdLen = password.length();
1527
1528        if ( ( maxLen > 0 ) && ( pwdLen > maxLen ) )
1529        {
1530            throw new PasswordPolicyException( "Password should not have more than " + maxLen + " characters",
1531                INSUFFICIENT_PASSWORD_QUALITY.getValue() );
1532        }
1533
1534        if ( ( minLen > 0 ) && ( pwdLen < minLen ) )
1535        {
1536            throw new PasswordPolicyException( "Password should have a minimum of " + minLen + " characters",
1537                PASSWORD_TOO_SHORT.getValue() );
1538        }
1539    }
1540
1541
1542    private int getPwdTimeBeforeExpiry( Entry userEntry, PasswordPolicyConfiguration policyConfig )
1543        throws LdapException
1544    {
1545        if ( policyConfig.getPwdMaxAge() == 0 )
1546        {
1547            return 0;
1548        }
1549
1550        int warningAge = policyConfig.getPwdExpireWarning();
1551
1552        if ( warningAge <= 0 )
1553        {
1554            return 0;
1555        }
1556
1557        Attribute pwdChangedTimeAt = userEntry.get( pwdChangedTimeAT );
1558        if ( pwdChangedTimeAt == null )
1559        {
1560            pwdChangedTimeAt = userEntry.get( directoryService.getAtProvider().getCreateTimestamp() );
1561        }
1562        long changedTime = DateUtils.getDate( pwdChangedTimeAt.getString() ).getTime();
1563
1564        long currentTime = directoryService.getTimeProvider().currentIimeMillis();
1565        long pwdAge = ( currentTime - changedTime ) / 1000;
1566
1567        if ( pwdAge > policyConfig.getPwdMaxAge() )
1568        {
1569            return 0;
1570        }
1571
1572        warningAge = policyConfig.getPwdMaxAge() - warningAge;
1573
1574        if ( pwdAge >= warningAge )
1575        {
1576            long timeBeforeExpiration = ( ( long ) policyConfig.getPwdMaxAge() ) - pwdAge;
1577
1578            if ( timeBeforeExpiration > Integer.MAX_VALUE )
1579            {
1580                timeBeforeExpiration = Integer.MAX_VALUE;
1581            }
1582
1583            return ( int ) timeBeforeExpiration;
1584        }
1585
1586        return 0;
1587    }
1588
1589
1590    /**
1591     * checks if the password is too young
1592     *
1593     * @param userEntry the user's entry
1594     * @return true if the password is young, false otherwise
1595     * @throws LdapException
1596     */
1597    private boolean isPwdTooYoung( OperationContext operationContext,
1598        Entry userEntry, PasswordPolicyConfiguration policyConfig ) throws LdapException
1599    {
1600        // https://issues.apache.org/jira/browse/DIRSERVER-1928
1601        if ( operationContext.getSession().isAnAdministrator() )
1602        {
1603            return false;
1604        }
1605        if ( policyConfig.getPwdMinAge() == 0 )
1606        {
1607            return false;
1608        }
1609
1610        CoreSession userSession = operationContext.getSession();
1611        
1612        // see sections 7.8 and 7.2 of the ppolicy draft
1613        if ( policyConfig.isPwdMustChange() && userSession.isPwdMustChange() )
1614        {
1615            return false;
1616        }
1617
1618        Attribute pwdChangedTimeAt = userEntry.get( pwdChangedTimeAT );
1619
1620        if ( pwdChangedTimeAt != null )
1621        {
1622            long changedTime = DateUtils.getDate( pwdChangedTimeAt.getString() ).getTime();
1623            changedTime += policyConfig.getPwdMinAge() * 1000L;
1624
1625            long currentTime = directoryService.getTimeProvider().currentIimeMillis();
1626
1627            if ( changedTime > currentTime )
1628            {
1629                return true;
1630            }
1631        }
1632
1633        return false;
1634    }
1635
1636
1637    /**
1638     * checks if the password must be changed after the initial bind
1639     *
1640     * @param userEntry the user's entry
1641     * @return true if must be changed, false otherwise
1642     * @throws LdapException
1643     */
1644    private boolean isPwdMustReset( Entry userEntry ) throws LdapException
1645    {
1646        boolean mustChange = false;
1647
1648        Attribute pwdResetAt = userEntry.get( pwdResetAT );
1649
1650        if ( pwdResetAt != null )
1651        {
1652            mustChange = Boolean.parseBoolean( pwdResetAt.getString() );
1653        }
1654
1655        return mustChange;
1656    }
1657
1658
1659    private PwdModDetailsHolder getPwdModDetails( ModifyOperationContext modifyContext,
1660        PasswordPolicyConfiguration policyConfig ) throws LdapException
1661    {
1662        PwdModDetailsHolder pwdModDetails = new PwdModDetailsHolder();
1663
1664        List<Modification> mods = modifyContext.getModItems();
1665
1666        for ( Modification mod : mods )
1667        {
1668            Attribute at = mod.getAttribute();
1669            AttributeType passwordAttribute = schemaManager.lookupAttributeTypeRegistry( policyConfig.getPwdAttribute() );
1670
1671            if ( at.getAttributeType().equals( passwordAttribute ) )
1672            {
1673                pwdModDetails.setPwdModPresent( true );
1674                ModificationOperation op = mod.getOperation();
1675
1676                if ( op == REMOVE_ATTRIBUTE )
1677                {
1678                    pwdModDetails.setDelete( true );
1679                }
1680                else if ( op == REPLACE_ATTRIBUTE || op == ADD_ATTRIBUTE )
1681                {
1682                    pwdModDetails.setAddOrReplace( true );
1683                    pwdModDetails.setNewPwd( at.getBytes() );
1684                }
1685
1686                switch ( op )
1687                {
1688                    case REMOVE_ATTRIBUTE:
1689                        pwdModDetails.setDelete( true );
1690                        break;
1691                        
1692                    case REPLACE_ATTRIBUTE:
1693                    case ADD_ATTRIBUTE:
1694                        pwdModDetails.setAddOrReplace( true );
1695                        pwdModDetails.setNewPwd( at.getBytes() );
1696                        break;
1697                        
1698                    default:
1699                        // nothing to do
1700                }
1701            }
1702            else
1703            {
1704                pwdModDetails.setOtherModExists( true );
1705            }
1706        }
1707
1708        return pwdModDetails;
1709    }
1710
1711
1712    /**
1713     * checks to see if the user's password should be changed before performing any operations
1714     * other than bind, password update, unbind, abandon or StartTLS
1715     *
1716     * @param opContext the operation's context
1717     * @throws LdapException
1718     */
1719    private void checkPwdReset( OperationContext opContext ) throws LdapException
1720    {
1721        if ( directoryService.isPwdPolicyEnabled() )
1722        {
1723            CoreSession session = opContext.getSession();
1724
1725            if ( session.isPwdMustChange() )
1726            {
1727                boolean isPPolicyReqCtrlPresent = opContext
1728                    .hasRequestControl( PasswordPolicyRequest.OID );
1729
1730                if ( isPPolicyReqCtrlPresent )
1731                {
1732                    PasswordPolicyResponse responseControl = new PasswordPolicyResponseImpl();
1733                    responseControl.setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
1734                    opContext.addResponseControl( responseControl );
1735                }
1736
1737                throw new LdapNoPermissionException( "password needs to be reset before performing this operation" );
1738            }
1739        }
1740    }
1741
1742    private static class PwdModDetailsHolder
1743    {
1744        private boolean pwdModPresent = false;
1745
1746        private boolean isDelete = false;
1747
1748        private boolean isAddOrReplace = false;
1749
1750        private boolean otherModExists = false;
1751
1752        private byte[] newPwd;
1753
1754
1755        public boolean isPwdModPresent()
1756        {
1757            return pwdModPresent;
1758        }
1759
1760
1761        public void setPwdModPresent( boolean pwdModPresent )
1762        {
1763            this.pwdModPresent = pwdModPresent;
1764        }
1765
1766
1767        public boolean isDelete()
1768        {
1769            return isDelete;
1770        }
1771
1772
1773        public void setDelete( boolean isDelete )
1774        {
1775            this.isDelete = isDelete;
1776        }
1777
1778
1779        public boolean isAddOrReplace()
1780        {
1781            return isAddOrReplace;
1782        }
1783
1784
1785        public void setAddOrReplace( boolean isAddOrReplace )
1786        {
1787            this.isAddOrReplace = isAddOrReplace;
1788        }
1789
1790
1791        public boolean isOtherModExists()
1792        {
1793            return otherModExists;
1794        }
1795
1796
1797        public void setOtherModExists( boolean otherModExists )
1798        {
1799            this.otherModExists = otherModExists;
1800        }
1801
1802
1803        public byte[] getNewPwd()
1804        {
1805            return newPwd;
1806        }
1807
1808
1809        public void setNewPwd( byte[] newPwd )
1810        {
1811            this.newPwd = newPwd;
1812        }
1813    }
1814
1815
1816    /**
1817     * Gets the effective password policy of the given entry.
1818     * If the entry has defined a custom password policy by setting "pwdPolicySubentry" attribute
1819     * then the password policy associated with the Dn specified at the above attribute's value will be returned.
1820     * Otherwise the default password policy will be returned (if present)
1821     * 
1822     * @param userEntry the user's entry
1823     * @return the associated password policy
1824     * @throws LdapException If we weren't able to ftech the password policy
1825     */
1826    public PasswordPolicyConfiguration getPwdPolicy( Entry userEntry ) throws LdapException
1827    {
1828        if ( pwdPolicyContainer == null )
1829        {
1830            return null;
1831        }
1832
1833        if ( userEntry == null )
1834        {
1835            return pwdPolicyContainer.getDefaultPolicy();
1836        }
1837
1838        if ( pwdPolicyContainer.hasCustomConfigs() )
1839        {
1840            Attribute pwdPolicySubentry = userEntry.get( pwdPolicySubentryAT );
1841
1842            if ( pwdPolicySubentry != null )
1843            {
1844                Dn configDn = dnFactory.create( pwdPolicySubentry.getString() );
1845
1846                PasswordPolicyConfiguration custom = pwdPolicyContainer.getPolicyConfig( configDn );
1847                
1848                if ( custom != null )
1849                {
1850                    return custom;
1851                }
1852                else
1853                {
1854                    LOG.warn(
1855                        "The custom password policy for the user entry {} is not found, returning default policy configuration",
1856                        userEntry.getDn() );
1857                }
1858            }
1859        }
1860
1861        return pwdPolicyContainer.getDefaultPolicy();
1862    }
1863
1864
1865    /**
1866     * set all the password policies to be used by the server.
1867     * This includes a default(i.e applicable to all entries) and custom(a.k.a per user) password policies
1868     * 
1869     * @param policyContainer the container holding all the password policies
1870     */
1871    public void setPwdPolicies( PpolicyConfigContainer policyContainer )
1872    {
1873        this.pwdPolicyContainer = policyContainer;
1874    }
1875
1876
1877    /**
1878     * {@inheritDoc}
1879     */
1880    public boolean isPwdPolicyEnabled()
1881    {
1882        return ( pwdPolicyContainer != null )
1883        && ( ( pwdPolicyContainer.getDefaultPolicy() != null )
1884        || ( pwdPolicyContainer.hasCustomConfigs() ) );
1885    }
1886
1887
1888    /**
1889     * @return the pwdPolicyContainer
1890     */
1891    public PpolicyConfigContainer getPwdPolicyContainer()
1892    {
1893        return pwdPolicyContainer;
1894    }
1895
1896
1897    /**
1898     * purges failure timestamps which are older than the configured interval
1899     * (section 7.6 in the draft)
1900     */
1901    private void purgeFailureTimes( PasswordPolicyConfiguration config, Attribute pwdFailTimeAt )
1902    {
1903        long interval = config.getPwdFailureCountInterval();
1904
1905        if ( interval == 0 )
1906        {
1907            return;
1908        }
1909
1910        interval *= 1000;
1911
1912        long currentTime = directoryService.getTimeProvider().currentIimeMillis();
1913
1914        Iterator<Value> itr = pwdFailTimeAt.iterator();
1915
1916        while ( itr.hasNext() )
1917        {
1918            Value value = itr.next();
1919            String failureTime = value.getString();
1920            long time = DateUtils.getDate( failureTime ).getTime();
1921            time += interval;
1922
1923            if ( currentTime >= time )
1924            {
1925                itr.remove();
1926            }
1927        }
1928    }
1929}