You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by el...@apache.org on 2011/10/20 21:42:13 UTC
svn commit: r1187013 [7/12] - in /directory/apacheds/branches/apacheds-txns:
./ all/ apache-felix/ apache-felix/bundle/ apache-felix/src/
apache-felix/src/main/ apache-felix/src/main/resources/ core-annotations/
core-annotations/src/main/java/org/apach...
Added: directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java
URL: http://svn.apache.org/viewvc/directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java?rev=1187013&view=auto
==============================================================================
--- directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java (added)
+++ directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java Thu Oct 20 19:41:49 2011
@@ -0,0 +1,1542 @@
+/*
+ * 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.shared.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum.INSUFFICIENT_PASSWORD_QUALITY;
+import static org.apache.directory.shared.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum.PASSWORD_TOO_SHORT;
+import static org.apache.directory.shared.ldap.model.constants.PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT;
+import static org.apache.directory.shared.ldap.model.constants.PasswordPolicySchemaConstants.PWD_CHANGED_TIME_AT;
+import static org.apache.directory.shared.ldap.model.constants.PasswordPolicySchemaConstants.PWD_FAILURE_TIME_AT;
+import static org.apache.directory.shared.ldap.model.constants.PasswordPolicySchemaConstants.PWD_GRACE_USE_TIME_AT;
+import static org.apache.directory.shared.ldap.model.constants.PasswordPolicySchemaConstants.PWD_HISTORY_AT;
+import static org.apache.directory.shared.ldap.model.constants.PasswordPolicySchemaConstants.PWD_LAST_SUCCESS_AT;
+import static org.apache.directory.shared.ldap.model.constants.PasswordPolicySchemaConstants.PWD_RESET_AT;
+import static org.apache.directory.shared.ldap.model.entry.ModificationOperation.ADD_ATTRIBUTE;
+import static org.apache.directory.shared.ldap.model.entry.ModificationOperation.REMOVE_ATTRIBUTE;
+import static org.apache.directory.shared.ldap.model.entry.ModificationOperation.REPLACE_ATTRIBUTE;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import org.apache.directory.server.core.shared.DefaultCoreSession;
+import org.apache.directory.server.core.api.CoreSession;
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.LdapPrincipal;
+import org.apache.directory.server.core.api.filtering.EntryFilteringCursor;
+import org.apache.directory.server.core.api.interceptor.BaseInterceptor;
+import org.apache.directory.server.core.api.interceptor.Interceptor;
+import org.apache.directory.server.core.api.interceptor.NextInterceptor;
+import org.apache.directory.server.core.api.interceptor.context.AddOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.CompareOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.DeleteOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.EntryOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.GetRootDSEOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.ListOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.LookupOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.ModifyOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.MoveAndRenameOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.MoveOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.OperationContext;
+import org.apache.directory.server.core.api.interceptor.context.RenameOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.SearchOperationContext;
+import org.apache.directory.server.core.api.interceptor.context.UnbindOperationContext;
+import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyConfiguration;
+import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyException;
+import org.apache.directory.server.core.authn.ppolicy.PpolicyConfigContainer;
+import org.apache.directory.server.i18n.I18n;
+import org.apache.directory.shared.ldap.extras.controls.ppolicy.PasswordPolicy;
+import org.apache.directory.shared.ldap.extras.controls.ppolicy.PasswordPolicyErrorEnum;
+import org.apache.directory.shared.ldap.extras.controls.ppolicy_impl.PasswordPolicyDecorator;
+import org.apache.directory.shared.ldap.model.constants.AuthenticationLevel;
+import org.apache.directory.shared.ldap.model.constants.LdapSecurityConstants;
+import org.apache.directory.shared.ldap.model.constants.SchemaConstants;
+import org.apache.directory.shared.ldap.model.entry.Attribute;
+import org.apache.directory.shared.ldap.model.entry.BinaryValue;
+import org.apache.directory.shared.ldap.model.entry.DefaultAttribute;
+import org.apache.directory.shared.ldap.model.entry.DefaultModification;
+import org.apache.directory.shared.ldap.model.entry.Entry;
+import org.apache.directory.shared.ldap.model.entry.Modification;
+import org.apache.directory.shared.ldap.model.entry.ModificationOperation;
+import org.apache.directory.shared.ldap.model.entry.Value;
+import org.apache.directory.shared.ldap.model.exception.LdapAuthenticationException;
+import org.apache.directory.shared.ldap.model.exception.LdapException;
+import org.apache.directory.shared.ldap.model.exception.LdapNoPermissionException;
+import org.apache.directory.shared.ldap.model.exception.LdapOperationException;
+import org.apache.directory.shared.ldap.model.exception.LdapUnwillingToPerformException;
+import org.apache.directory.shared.ldap.model.message.ResultCodeEnum;
+import org.apache.directory.shared.ldap.model.name.Dn;
+import org.apache.directory.shared.ldap.model.schema.AttributeType;
+import org.apache.directory.shared.util.DateUtils;
+import org.apache.directory.shared.util.StringConstants;
+import org.apache.directory.shared.util.Strings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/**
+ * An {@link Interceptor} that authenticates users.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class AuthenticationInterceptor extends BaseInterceptor
+{
+ private static final Logger LOG = LoggerFactory.getLogger( AuthenticationInterceptor.class );
+
+ /**
+ * Speedup for logs
+ */
+ private static final boolean IS_DEBUG = LOG.isDebugEnabled();
+
+ /** A Set of all the existing Authenticator to be used by the bind operation */
+ private Set<Authenticator> authenticators = new HashSet<Authenticator>();
+
+ /** A map of authenticators associated with the authentication level required */
+ private final Map<AuthenticationLevel, Collection<Authenticator>> authenticatorsMapByType = new HashMap<AuthenticationLevel, Collection<Authenticator>>();
+
+ private CoreSession adminSession;
+
+ private Set<Dn> pwdResetSet = new HashSet<Dn>();
+
+ // pwdpolicy state attribute types
+ private AttributeType AT_PWD_RESET;
+
+ private AttributeType AT_PWD_CHANGED_TIME;
+
+ private AttributeType AT_PWD_HISTORY;
+
+ private AttributeType AT_PWD_FAILURE_TIME;
+
+ private AttributeType AT_PWD_ACCOUNT_LOCKED_TIME;
+
+ private AttributeType AT_PWD_LAST_SUCCESS;
+
+ private AttributeType AT_PWD_GRACE_USE_TIME;
+
+ /** a container to hold all the ppolicies */
+ private PpolicyConfigContainer pwdPolicyContainer;
+
+ /** the pwdPolicySubentry AT */
+ private AttributeType pwdPolicySubentryAT;
+
+
+ /**
+ * the set of interceptors we should *not* go through when pwdpolicy state information is being updated
+ */
+ private static final Collection<String> BYPASS_INTERCEPTORS;
+
+ static
+ {
+ Set<String> c = new HashSet<String>();
+ c.add( "NormalizationInterceptor" );
+ c.add( "AuthenticationInterceptor" );
+ c.add( "AciAuthorizationInterceptor" );
+ c.add( "AdministrativePointInterceptor" );
+ c.add( "DefaultAuthorizationInterceptor" );
+ c.add( "AdministrativePointInterceptor" );
+ c.add( "ExceptionInterceptor" );
+ c.add( "OperationalAttributeInterceptor" );
+ c.add( "SchemaInterceptor" );
+ c.add( "CollectiveAttributeInterceptor" );
+ c.add( "SubentryInterceptor" );
+ c.add( "EventInterceptor" );
+ c.add( "TriggerInterceptor" );
+ BYPASS_INTERCEPTORS = Collections.unmodifiableCollection( c );
+ }
+
+
+ /**
+ * Creates an authentication service interceptor.
+ */
+ public AuthenticationInterceptor()
+ {
+ }
+
+
+ /**
+ * Registers and initializes all {@link Authenticator}s to this service.
+ */
+ public void init( DirectoryService directoryService ) throws LdapException
+ {
+ super.init( directoryService );
+
+ adminSession = directoryService.getAdminSession();
+ pwdPolicySubentryAT = schemaManager.lookupAttributeTypeRegistry( "pwdPolicySubentry" );
+
+ if ( ( authenticators == null ) || ( authenticators.size() == 0 ) )
+ {
+ setDefaultAuthenticators();
+ }
+
+ // Register all authenticators
+ for ( Authenticator authenticator : authenticators )
+ {
+ register( authenticator, directoryService );
+ }
+
+ loadPwdPolicyStateAtributeTypes();
+ }
+
+
+ /**
+ * Initialize the set of authenticators with some default values
+ */
+ private void setDefaultAuthenticators()
+ {
+ if ( authenticators == null )
+ {
+ authenticators = new HashSet<Authenticator>();
+ }
+
+ authenticators.clear();
+ authenticators.add( new AnonymousAuthenticator() );
+ authenticators.add( new SimpleAuthenticator() );
+ authenticators.add( new StrongAuthenticator() );
+ }
+
+
+ public Set<Authenticator> getAuthenticators()
+ {
+ return authenticators;
+ }
+
+
+ /**
+ * @param authenticators authenticators to be used by this AuthenticationInterceptor
+ */
+ public void setAuthenticators( Set<Authenticator> authenticators )
+ {
+ if ( authenticators == null )
+ {
+ this.authenticators.clear();
+ }
+ else
+ {
+ this.authenticators = authenticators;
+ }
+ }
+
+
+ /**
+ * @param authenticators authenticators to be used by this AuthenticationInterceptor
+ */
+ public void setAuthenticators( Authenticator[] authenticators )
+ {
+ if ( authenticators == null )
+ {
+ throw new IllegalArgumentException( "The given authenticators set is null" );
+ }
+
+ this.authenticators.clear();
+
+ for (Authenticator authenticator : authenticators)
+ {
+ this.authenticators.add( authenticator );
+ }
+ }
+
+
+ /**
+ * Deinitializes and deregisters all {@link Authenticator}s from this service.
+ */
+ public void destroy()
+ {
+ authenticatorsMapByType.clear();
+ Set<Authenticator> copy = new HashSet<Authenticator>( authenticators );
+ authenticators = null;
+ for ( Authenticator authenticator : copy )
+ {
+ authenticator.destroy();
+ }
+ }
+
+
+ /**
+ * Initializes the specified {@link Authenticator} and registers it to
+ * this service.
+ *
+ * @param authenticator Authenticator to initialize and register by type
+ * @param directoryService configuration info to supply to the Authenticator during initialization
+ * @throws javax.naming.Exception if initialization fails.
+ */
+ private void register( Authenticator authenticator, DirectoryService directoryService ) throws LdapException
+ {
+ authenticator.init( directoryService );
+
+ Collection<Authenticator> authenticatorList = getAuthenticators( authenticator.getAuthenticatorType() );
+
+ if ( authenticatorList == null )
+ {
+ authenticatorList = new ArrayList<Authenticator>();
+ authenticatorsMapByType.put( authenticator.getAuthenticatorType(), authenticatorList );
+ }
+
+ authenticatorList.add( authenticator );
+ }
+
+
+ /**
+ * Returns the list of {@link Authenticator}s with the specified type.
+ *
+ * @param type type of Authenticator sought
+ * @return A list of Authenticators of the requested type or <tt>null</tt> if no authenticator is found.
+ */
+ private Collection<Authenticator> getAuthenticators( AuthenticationLevel type )
+ {
+ Collection<Authenticator> result = authenticatorsMapByType.get( type );
+
+ if ( ( result != null ) && ( result.size() > 0 ) )
+ {
+ return result;
+ }
+ else
+ {
+ return null;
+ }
+ }
+
+
+ public void add( NextInterceptor next, AddOperationContext addContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", addContext );
+ }
+
+ checkAuthenticated( addContext );
+
+ Entry entry = addContext.getEntry();
+
+
+ if ( !directoryService.isPwdPolicyEnabled() )
+ {
+ next.add( addContext );
+ return;
+ }
+
+ PasswordPolicyConfiguration policyConfig = getPwdPolicy( entry );
+
+ boolean isPPolicyReqCtrlPresent = addContext.hasRequestControl( PasswordPolicy.OID );
+
+ checkPwdReset( addContext );
+
+ if ( entry.get( SchemaConstants.USER_PASSWORD_AT ) != null )
+ {
+ String username = null;
+
+ BinaryValue userPassword = ( BinaryValue ) entry.get( SchemaConstants.USER_PASSWORD_AT ).get();
+
+ try
+ {
+ username = entry.getDn().getRdn().getUpValue().getString();
+ check( username, userPassword.getValue(), policyConfig );
+ }
+ catch ( PasswordPolicyException e )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyDecorator responseControl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+ responseControl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.get( e.getErrorCode() ) );
+ addContext.addResponseControl( responseControl );
+ }
+
+ // throw exception if userPassword quality checks fail
+ throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION, e.getMessage(), e );
+ }
+
+ String pwdChangedTime = DateUtils.getGeneralizedTime();
+ if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
+ {
+ Attribute pwdChangedTimeAt = new DefaultAttribute( AT_PWD_CHANGED_TIME );
+ pwdChangedTimeAt.add( pwdChangedTime );
+ entry.add( pwdChangedTimeAt );
+ }
+
+ if ( policyConfig.isPwdMustChange() && addContext.getSession().isAnAdministrator() )
+ {
+ Attribute pwdResetAt = new DefaultAttribute( AT_PWD_RESET );
+ pwdResetAt.add( "TRUE" );
+ entry.add( pwdResetAt );
+ }
+
+ if ( policyConfig.getPwdInHistory() > 0 )
+ {
+ Attribute pwdHistoryAt = new DefaultAttribute( AT_PWD_HISTORY );
+ byte[] pwdHistoryVal = new PasswordHistory( pwdChangedTime, userPassword.getValue() ).getHistoryValue();
+ pwdHistoryAt.add( pwdHistoryVal );
+ entry.add( pwdHistoryAt );
+ }
+ }
+
+ next.add( addContext );
+ }
+
+
+ public void delete( NextInterceptor next, DeleteOperationContext deleteContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", deleteContext );
+ }
+
+ checkAuthenticated( deleteContext );
+ checkPwdReset( deleteContext );
+ next.delete( deleteContext );
+ invalidateAuthenticatorCaches( deleteContext.getDn() );
+ }
+
+
+ public Entry getRootDSE( NextInterceptor next, GetRootDSEOperationContext getRootDseContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", getRootDseContext );
+ }
+
+ checkAuthenticated( getRootDseContext );
+ checkPwdReset( getRootDseContext );
+ return next.getRootDSE( getRootDseContext );
+ }
+
+
+ public boolean hasEntry( NextInterceptor next, EntryOperationContext hasEntryContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", hasEntryContext );
+ }
+
+ checkAuthenticated( hasEntryContext );
+ checkPwdReset( hasEntryContext );
+ return next.hasEntry( hasEntryContext );
+ }
+
+
+ public EntryFilteringCursor list( NextInterceptor next, ListOperationContext listContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", listContext );
+ }
+
+ checkAuthenticated( listContext );
+ checkPwdReset( listContext );
+ return next.list( listContext );
+ }
+
+
+ public Entry lookup( NextInterceptor next, LookupOperationContext lookupContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", lookupContext );
+ }
+
+ checkAuthenticated( lookupContext );
+ checkPwdReset( lookupContext );
+
+ return next.lookup( lookupContext );
+ }
+
+
+ private void invalidateAuthenticatorCaches( Dn principalDn )
+ {
+ for ( AuthenticationLevel authMech : authenticatorsMapByType.keySet() )
+ {
+ Collection<Authenticator> authenticators = getAuthenticators( authMech );
+
+ // try each authenticator
+ for ( Authenticator authenticator : authenticators )
+ {
+ authenticator.invalidateCache( principalDn );
+ }
+ }
+ }
+
+
+ public void modify( NextInterceptor next, ModifyOperationContext modifyContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", modifyContext );
+ }
+
+ checkAuthenticated( modifyContext );
+
+
+ if ( ! directoryService.isPwdPolicyEnabled() )
+ {
+ next.modify( modifyContext );
+ invalidateAuthenticatorCaches( modifyContext.getDn() );
+ return;
+ }
+
+ // handle the case where pwdPolicySubentry AT is about to be deleted in thid modify()
+ PasswordPolicyConfiguration policyConfig = getPwdPolicy( modifyContext.getEntry() );
+
+ boolean isPPolicyReqCtrlPresent = modifyContext.hasRequestControl( PasswordPolicy.OID );
+ Dn userDn = modifyContext.getSession().getAuthenticatedPrincipal().getDn();
+
+ PwdModDetailsHolder pwdModDetails = null;
+
+ pwdModDetails = getPwdModDetails( modifyContext, policyConfig );
+
+ if ( pwdModDetails.isPwdModPresent() )
+ {
+ if ( pwdResetSet.contains( userDn ) )
+ {
+ if ( pwdModDetails.isOtherModExists() )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyDecorator responseControl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+ responseControl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ throw new LdapNoPermissionException();
+ }
+ }
+
+ if ( policyConfig.isPwdSafeModify() )
+ {
+ if ( pwdModDetails.isAddOrReplace() && !pwdModDetails.isDelete() )
+ {
+ LOG.debug( "trying to update password attribute without the supplying the old password" );
+
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyDecorator responseControl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+ responseControl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.MUST_SUPPLY_OLD_PASSWORD );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ throw new LdapNoPermissionException();
+ }
+ }
+
+ if ( !policyConfig.isPwdAllowUserChange() && !modifyContext.getSession().isAnAdministrator() )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyDecorator responseControl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+ responseControl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.PASSWORD_MOD_NOT_ALLOWED );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ throw new LdapNoPermissionException();
+ }
+
+ Entry entry = modifyContext.getEntry();
+
+ if ( isPwdTooYoung( entry, policyConfig ) )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyDecorator responseControl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+ responseControl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.PASSWORD_TOO_YOUNG );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION,
+ "password is too young to update" );
+ }
+
+ byte[] newPassword = null;
+
+ if ( ( pwdModDetails != null ) )
+ {
+ newPassword = pwdModDetails.getNewPwd();
+
+ try
+ {
+ String userName = entry.getDn().getRdn().getUpValue().getString();
+ check( userName, newPassword, policyConfig );
+ }
+ catch ( PasswordPolicyException e )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyDecorator responseControl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+ responseControl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.get( e.getErrorCode() ) );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ // throw exception if userPassword quality checks fail
+ throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION, e.getMessage(), e );
+ }
+ }
+
+ int histSize = policyConfig.getPwdInHistory();
+ Modification pwdRemHistMod = null;
+ Modification pwdAddHistMod = null;
+ String pwdChangedTime = DateUtils.getGeneralizedTime();
+
+ if ( histSize > 0 )
+ {
+ Attribute pwdHistoryAt = entry.get( PWD_HISTORY_AT );
+
+ if ( pwdHistoryAt == null )
+ {
+ pwdHistoryAt = new DefaultAttribute( AT_PWD_HISTORY );
+ }
+
+ List<PasswordHistory> pwdHistLst = new ArrayList<PasswordHistory>();
+
+ for ( Value<?> value : pwdHistoryAt )
+ {
+ PasswordHistory pwdh = new PasswordHistory( Strings.utf8ToString( value.getBytes() ) );
+
+ boolean matched = Arrays.equals( newPassword, pwdh.getPassword() );
+
+ if ( matched )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyDecorator responseControl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+ responseControl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.PASSWORD_IN_HISTORY );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION,
+ "invalid reuse of password present in password history" );
+ }
+
+ pwdHistLst.add( pwdh );
+ }
+
+ if ( pwdHistLst.size() >= histSize )
+ {
+ // see the javadoc of PasswordHistory
+ Collections.sort( pwdHistLst );
+
+ // remove the oldest value
+ PasswordHistory remPwdHist = ( PasswordHistory ) pwdHistLst.toArray()[histSize - 1];
+ Attribute tempAt = new DefaultAttribute( AT_PWD_HISTORY );
+ tempAt.add( remPwdHist.getHistoryValue() );
+ pwdRemHistMod = new DefaultModification( REMOVE_ATTRIBUTE, tempAt );
+ }
+
+ pwdHistoryAt.clear();
+ PasswordHistory newPwdHist = new PasswordHistory( pwdChangedTime, newPassword );
+ pwdHistoryAt.clear();
+ pwdHistoryAt.add( newPwdHist.getHistoryValue() );
+ pwdAddHistMod = new DefaultModification( ADD_ATTRIBUTE, pwdHistoryAt );
+ }
+
+ next.modify( modifyContext );
+
+ invalidateAuthenticatorCaches( modifyContext.getDn() );
+
+ List<Modification> mods = new ArrayList<Modification>();
+
+ if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
+ {
+ Attribute pwdChangedTimeAt = new DefaultAttribute( AT_PWD_CHANGED_TIME );
+ pwdChangedTimeAt.add( pwdChangedTime );
+ Modification pwdChangedTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdChangedTimeAt );
+ mods.add( pwdChangedTimeMod );
+ }
+
+ if ( pwdAddHistMod != null )
+ {
+ mods.add( pwdAddHistMod );
+ }
+
+ if ( pwdRemHistMod != null )
+ {
+ mods.add( pwdRemHistMod );
+ }
+
+ boolean removeFromPwdResetSet = false;
+
+ if ( policyConfig.isPwdMustChange() )
+ {
+ Attribute pwdMustChangeAt = new DefaultAttribute( AT_PWD_RESET );
+ Modification pwdMustChangeMod = null;
+
+ if ( modifyContext.getSession().isAnAdministrator() )
+ {
+ pwdMustChangeAt.add( "TRUE" );
+ pwdMustChangeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdMustChangeAt );
+ }
+ else
+ {
+ pwdMustChangeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdMustChangeAt );
+ removeFromPwdResetSet = true;
+ }
+
+ mods.add( pwdMustChangeMod );
+ }
+
+ Attribute pwdFailureTimeAt = entry.get( PWD_FAILURE_TIME_AT );
+
+ if ( pwdFailureTimeAt != null )
+ {
+ mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdFailureTimeAt ) );
+ }
+
+ Attribute pwdGraceUseTimeAt = entry.get( PWD_GRACE_USE_TIME_AT );
+
+ if ( pwdGraceUseTimeAt != null )
+ {
+ mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdGraceUseTimeAt ) );
+ }
+
+ directoryService.getAdminSession().modify( modifyContext.getDn(), mods );
+
+ if ( removeFromPwdResetSet )
+ {
+ pwdResetSet.remove( userDn );
+ }
+ }
+ else
+ {
+ next.modify( modifyContext );
+ invalidateAuthenticatorCaches( modifyContext.getDn() );
+ }
+ }
+
+
+ public void rename( NextInterceptor next, RenameOperationContext renameContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", renameContext );
+ }
+
+ checkAuthenticated( renameContext );
+ checkPwdReset( renameContext );
+ next.rename( renameContext );
+ invalidateAuthenticatorCaches( renameContext.getDn() );
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean compare( NextInterceptor next, CompareOperationContext compareContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", compareContext );
+ }
+
+ checkAuthenticated( compareContext );
+ checkPwdReset( compareContext );
+ boolean result = next.compare( compareContext );
+ invalidateAuthenticatorCaches( compareContext.getDn() );
+
+ return result;
+ }
+
+
+ public void moveAndRename( NextInterceptor next, MoveAndRenameOperationContext moveAndRenameContext )
+ throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", moveAndRenameContext );
+ }
+
+ checkAuthenticated( moveAndRenameContext );
+ checkPwdReset( moveAndRenameContext );
+ next.moveAndRename( moveAndRenameContext );
+ invalidateAuthenticatorCaches( moveAndRenameContext.getDn() );
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public void move( NextInterceptor next, MoveOperationContext moveContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", moveContext );
+ }
+
+ checkAuthenticated( moveContext );
+ checkPwdReset( moveContext );
+ next.move( moveContext );
+ invalidateAuthenticatorCaches( moveContext.getDn() );
+ }
+
+
+ public EntryFilteringCursor search( NextInterceptor next, SearchOperationContext searchContext )
+ throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", searchContext );
+ }
+
+ checkAuthenticated( searchContext );
+ checkPwdReset( searchContext );
+ return next.search( searchContext );
+ }
+
+
+ /**
+ * Check if the current operation has a valid PrincipalDN or not.
+ *
+ * @param operation the operation type
+ * @throws Exception
+ */
+ private void checkAuthenticated( OperationContext operation ) throws LdapException
+ {
+ if ( operation.getSession().isAnonymous() && !directoryService.isAllowAnonymousAccess()
+ && !operation.getDn().isEmpty() )
+ {
+ String msg = I18n.err( I18n.ERR_5, operation.getName() );
+ LOG.error( msg );
+ throw new LdapNoPermissionException( msg );
+ }
+ }
+
+
+ public void bind( NextInterceptor next, BindOperationContext bindContext ) throws LdapException
+ {
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Operation Context: {}", bindContext );
+ }
+
+ if ( ( bindContext.getSession() != null ) && ( bindContext.getSession().getEffectivePrincipal() != null ) )
+ {
+ // null out the credentials
+ bindContext.setCredentials( null );
+ }
+
+ // pick the first matching authenticator type
+ AuthenticationLevel level = bindContext.getAuthenticationLevel();
+
+ if ( level == AuthenticationLevel.UNAUTHENT )
+ {
+ // This is a case where the Bind request contains a Dn, but no password.
+ // We don't check the Dn, we just return a UnwillingToPerform error
+ // Cf RFC 4513, chap. 5.1.2
+ throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, "Cannot Bind for Dn "
+ + bindContext.getDn().getName() );
+ }
+
+ Collection<Authenticator> authenticators = getAuthenticators( level );
+
+ if ( authenticators == null )
+ {
+ LOG.debug( "No authenticators found, delegating bind to the nexus." );
+
+ // as a last resort try binding via the nexus
+ next.bind( bindContext );
+
+ LOG.debug( "Nexus succeeded on bind operation." );
+
+ // bind succeeded if we got this far
+ // TODO - authentication level not being set
+ LdapPrincipal principal = new LdapPrincipal( schemaManager, bindContext.getDn(), AuthenticationLevel.SIMPLE );
+ CoreSession session = new DefaultCoreSession( principal, directoryService );
+ bindContext.setSession( session );
+
+ // remove creds so there is no security risk
+ bindContext.setCredentials( null );
+ return;
+ }
+
+ boolean isPPolicyReqCtrlPresent = bindContext.hasRequestControl( PasswordPolicy.OID );
+ PasswordPolicyDecorator pwdRespCtrl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+
+ boolean authenticated = false;
+ PasswordPolicyException ppe = null;
+
+ // TODO : we should refactor that.
+ // try each authenticator
+ for ( Authenticator authenticator : authenticators )
+ {
+ try
+ {
+ // perform the authentication
+ LdapPrincipal principal = authenticator.authenticate( bindContext );
+
+ LdapPrincipal clonedPrincipal = ( LdapPrincipal ) ( principal.clone() );
+
+ // remove creds so there is no security risk
+ bindContext.setCredentials( null );
+ clonedPrincipal.setUserPassword( StringConstants.EMPTY_BYTES );
+
+ // authentication was successful
+ CoreSession session = new DefaultCoreSession( clonedPrincipal, directoryService );
+ bindContext.setSession( session );
+
+ authenticated = true;
+
+ // break out of the loop if the authentication succeeded
+ break;
+ }
+ catch ( PasswordPolicyException e )
+ {
+ ppe = e;
+ break;
+ }
+ catch ( LdapAuthenticationException e )
+ {
+ // authentication failed, try the next authenticator
+ if ( LOG.isInfoEnabled() )
+ {
+ LOG.info( "Authenticator {} failed to authenticate: {}", authenticator, bindContext );
+ }
+ }
+ catch ( Exception e )
+ {
+ // Log other exceptions than LdapAuthenticationException
+ if ( LOG.isWarnEnabled() )
+ {
+ LOG.info( "Unexpected failure for Authenticator {} : {}", authenticator, bindContext );
+ }
+ }
+ }
+
+ if ( ppe != null )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ pwdRespCtrl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.get( ppe.getErrorCode() ) );
+ bindContext.addResponseControl( pwdRespCtrl );
+ }
+
+ throw ppe;
+ }
+
+ Dn dn = bindContext.getDn();
+ Entry userEntry = bindContext.getEntry();
+
+ PasswordPolicyConfiguration policyConfig = getPwdPolicy( userEntry );
+
+ // check if the user entry is null, it will be null
+ // in cases of anonymous bind
+ if ( authenticated && ( userEntry == null ) && directoryService.isAllowAnonymousAccess() )
+ {
+ return;
+ }
+
+ if ( !authenticated )
+ {
+ if ( LOG.isInfoEnabled() )
+ {
+ LOG.info( "Cannot bind to the server " );
+ }
+
+ if ( ( policyConfig != null ) && ( userEntry != null ) )
+ {
+ Attribute pwdFailTimeAt = userEntry.get( PWD_FAILURE_TIME_AT );
+ if ( pwdFailTimeAt == null )
+ {
+ pwdFailTimeAt = new DefaultAttribute( AT_PWD_FAILURE_TIME );
+ }
+ else
+ {
+ PasswordUtil.purgeFailureTimes( policyConfig, pwdFailTimeAt );
+ }
+
+ String failureTime = DateUtils.getGeneralizedTime();
+ pwdFailTimeAt.add( failureTime );
+ Modification pwdFailTimeMod = new DefaultModification( ADD_ATTRIBUTE, pwdFailTimeAt );
+
+ List<Modification> mods = new ArrayList<Modification>();
+ mods.add( pwdFailTimeMod );
+
+ int numFailures = pwdFailTimeAt.size();
+
+ if ( policyConfig.isPwdLockout() && ( numFailures >= policyConfig.getPwdMaxFailure() ) )
+ {
+ Attribute pwdAccountLockedTimeAt = new DefaultAttribute( AT_PWD_ACCOUNT_LOCKED_TIME );
+
+ // if zero, lockout permanently, only admin can unlock it
+ if ( policyConfig.getPwdLockoutDuration() == 0 )
+ {
+ pwdAccountLockedTimeAt.add( "000001010000Z" );
+ }
+ else
+ {
+ pwdAccountLockedTimeAt.add( failureTime );
+ }
+
+ Modification pwdAccountLockedMod = new DefaultModification( ADD_ATTRIBUTE, pwdAccountLockedTimeAt );
+ mods.add( pwdAccountLockedMod );
+
+ pwdRespCtrl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.ACCOUNT_LOCKED );
+ }
+ else if ( policyConfig.getPwdMinDelay() > 0 )
+ {
+ int numDelay = numFailures * policyConfig.getPwdMinDelay();
+ int maxDelay = policyConfig.getPwdMaxDelay();
+ if ( numDelay > maxDelay )
+ {
+ numDelay = maxDelay;
+ }
+
+ try
+ {
+ Thread.sleep( numDelay * 1000 );
+ }
+ catch ( InterruptedException e )
+ {
+ LOG.warn(
+ "Interrupted while delaying to send the failed authentication response for the user {}",
+ dn, e );
+ }
+ }
+
+ //adminSession.modify( dn, Collections.singletonList( pwdFailTimeMod ) );
+ ModifyOperationContext bindModCtx = new ModifyOperationContext( adminSession );
+ bindModCtx.setByPassed( BYPASS_INTERCEPTORS );
+ bindModCtx.setDn( dn );
+ bindModCtx.setModItems( mods );
+ directoryService.getOperationManager().modify( bindModCtx );
+ }
+
+ String upDn = ( dn == null ? "" : dn.getName() );
+ throw new LdapAuthenticationException( I18n.err( I18n.ERR_229, upDn ) );
+ }
+ else if ( policyConfig != null )
+ {
+ List<Modification> mods = new ArrayList<Modification>();
+
+ if ( policyConfig.getPwdMaxIdle() > 0 )
+ {
+ Attribute pwdLastSuccesTimeAt = new DefaultAttribute( AT_PWD_LAST_SUCCESS );
+ pwdLastSuccesTimeAt.add( DateUtils.getGeneralizedTime() );
+ Modification pwdLastSuccesTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdLastSuccesTimeAt );
+ mods.add( pwdLastSuccesTimeMod );
+ }
+
+ Attribute pwdFailTimeAt = userEntry.get( AT_PWD_FAILURE_TIME );
+ if ( pwdFailTimeAt != null )
+ {
+ Modification pwdFailTimeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdFailTimeAt );
+ mods.add( pwdFailTimeMod );
+ }
+
+ Attribute pwdAccLockedTimeAt = userEntry.get( AT_PWD_ACCOUNT_LOCKED_TIME );
+ if ( pwdAccLockedTimeAt != null )
+ {
+ Modification pwdAccLockedTimeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdAccLockedTimeAt );
+ mods.add( pwdAccLockedTimeMod );
+ }
+
+ // checking the expiration time *after* performing authentication, do we need to care about millisecond precision?
+ if ( ( policyConfig.getPwdMaxAge() > 0 ) && ( policyConfig.getPwdGraceAuthNLimit() > 0 ) )
+ {
+ Attribute pwdChangeTimeAttr = userEntry.get( PWD_CHANGED_TIME_AT );
+
+ if ( pwdChangeTimeAttr != null )
+ {
+ boolean expired = PasswordUtil.isPwdExpired( pwdChangeTimeAttr.getString(),
+ policyConfig.getPwdMaxAge() );
+
+ if ( expired )
+ {
+ Attribute pwdGraceUseAttr = userEntry.get( PWD_GRACE_USE_TIME_AT );
+ int numGraceAuth = 0;
+
+ if ( pwdGraceUseAttr != null )
+ {
+ numGraceAuth = policyConfig.getPwdGraceAuthNLimit() - ( pwdGraceUseAttr.size() + 1 );
+ }
+ else
+ {
+ pwdGraceUseAttr = new DefaultAttribute( AT_PWD_GRACE_USE_TIME );
+ numGraceAuth = policyConfig.getPwdGraceAuthNLimit() - 1;
+ }
+
+ pwdRespCtrl.getResponse().setGraceAuthNsRemaining( numGraceAuth );
+
+ pwdGraceUseAttr.add( DateUtils.getGeneralizedTime() );
+ Modification pwdGraceUseMod = new DefaultModification( ADD_ATTRIBUTE, pwdGraceUseAttr );
+ mods.add( pwdGraceUseMod );
+ }
+ }
+ }
+
+ if ( !mods.isEmpty() )
+ {
+ //adminSession.modify( dn, mods );
+ ModifyOperationContext bindModCtx = new ModifyOperationContext( adminSession );
+ bindModCtx.setByPassed( BYPASS_INTERCEPTORS );
+ bindModCtx.setDn( dn );
+ bindModCtx.setModItems( mods );
+ directoryService.getOperationManager().modify( bindModCtx );
+ }
+
+ if ( isPPolicyReqCtrlPresent )
+ {
+ int expiryWarnTime = getPwdTimeBeforeExpiry( userEntry, policyConfig );
+
+ if ( expiryWarnTime > 0 )
+ {
+ pwdRespCtrl.getResponse().setTimeBeforeExpiration( expiryWarnTime );
+ }
+
+ if ( isPwdMustReset( userEntry ) )
+ {
+ pwdRespCtrl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
+ pwdResetSet.add( dn );
+ }
+
+ bindContext.addResponseControl( pwdRespCtrl );
+ }
+ }
+ }
+
+
+ @Override
+ public void unbind( NextInterceptor next, UnbindOperationContext unbindContext ) throws LdapException
+ {
+ super.unbind( next, unbindContext );
+
+ // remove the Dn from the password reset Set
+ // we do not perform a check to see if the reset flag in the associated ppolicy is enabled
+ // cause that requires fetching the ppolicy first, which requires a lookup for user entry
+ if ( !directoryService.isPwdPolicyEnabled() )
+ {
+ pwdResetSet.remove( unbindContext.getDn() );
+ }
+ }
+
+
+ /**
+ * Initialize the PasswordPolicy attributeTypes
+ *
+ * @throws LdapException If the initialization failed
+ */
+ public void loadPwdPolicyStateAtributeTypes() throws LdapException
+ {
+ if ( directoryService.isPwdPolicyEnabled() )
+ {
+ AT_PWD_RESET = schemaManager.lookupAttributeTypeRegistry( PWD_RESET_AT );
+ PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( AT_PWD_RESET );
+
+ AT_PWD_CHANGED_TIME = schemaManager.lookupAttributeTypeRegistry( PWD_CHANGED_TIME_AT );
+ PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( AT_PWD_CHANGED_TIME );
+
+ AT_PWD_HISTORY = schemaManager.lookupAttributeTypeRegistry( PWD_HISTORY_AT );
+ PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( AT_PWD_HISTORY );
+
+ AT_PWD_FAILURE_TIME = schemaManager.lookupAttributeTypeRegistry( PWD_FAILURE_TIME_AT );
+ PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( AT_PWD_FAILURE_TIME );
+
+ AT_PWD_ACCOUNT_LOCKED_TIME = schemaManager.lookupAttributeTypeRegistry( PWD_ACCOUNT_LOCKED_TIME_AT );
+ PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( AT_PWD_ACCOUNT_LOCKED_TIME );
+
+ AT_PWD_LAST_SUCCESS = schemaManager.lookupAttributeTypeRegistry( PWD_LAST_SUCCESS_AT );
+ PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( AT_PWD_LAST_SUCCESS );
+
+ AT_PWD_GRACE_USE_TIME = schemaManager.lookupAttributeTypeRegistry( PWD_GRACE_USE_TIME_AT );
+ PWD_POLICY_STATE_ATTRIBUTE_TYPES.add( AT_PWD_GRACE_USE_TIME );
+ }
+ }
+
+
+ // ---------- private methods ----------------
+
+ private void check( String username, byte[] password, PasswordPolicyConfiguration policyConfig ) throws LdapException
+ {
+ final int qualityVal = policyConfig.getPwdCheckQuality();
+
+ if ( qualityVal == 0 )
+ {
+ return;
+ }
+
+ LdapSecurityConstants secConst = PasswordUtil.findAlgorithm( password );
+
+ // do not perform quality check if the password is not plain text and
+ // pwdCheckQuality value is set to 1
+ if ( secConst != null )
+ {
+ if ( qualityVal == 1 )
+ {
+ return;
+ }
+ else
+ {
+ throw new PasswordPolicyException( "cannot verify the quality of the non-cleartext passwords",
+ INSUFFICIENT_PASSWORD_QUALITY.getValue() );
+ }
+ }
+
+ String strPassword = Strings.utf8ToString(password);
+
+ // perform the length validation
+ validatePasswordLength( strPassword, policyConfig );
+
+ policyConfig.getPwdValidator().validate( strPassword, username );
+ }
+
+
+ /**
+ * validates the length of the password
+ */
+ private void validatePasswordLength( String password, PasswordPolicyConfiguration policyConfig ) throws PasswordPolicyException
+ {
+ int maxLen = policyConfig.getPwdMaxLength();
+ int minLen = policyConfig.getPwdMinLength();
+
+ int pwdLen = password.length();
+
+ if ( maxLen > 0 )
+ {
+ if ( pwdLen > maxLen )
+ {
+ throw new PasswordPolicyException( "Password should not have more than " + maxLen + " characters",
+ INSUFFICIENT_PASSWORD_QUALITY.getValue() );
+ }
+ }
+
+ if ( minLen > 0 )
+ {
+ if ( pwdLen < minLen )
+ {
+ throw new PasswordPolicyException( "Password should have a minmum of " + minLen + " characters",
+ PASSWORD_TOO_SHORT.getValue() );
+ }
+ }
+ }
+
+
+ private int getPwdTimeBeforeExpiry( Entry userEntry, PasswordPolicyConfiguration policyConfig ) throws LdapException
+ {
+ if ( policyConfig.getPwdMaxAge() == 0 )
+ {
+ return 0;
+ }
+
+ int warningAge = policyConfig.getPwdExpireWarning();
+
+ if ( warningAge <= 0 )
+ {
+ return 0;
+ }
+
+ Attribute pwdChangedTimeAt = userEntry.get( PWD_CHANGED_TIME_AT );
+ long changedTime = DateUtils.getDate(pwdChangedTimeAt.getString()).getTime();
+
+ long currentTime = DateUtils.getDate( DateUtils.getGeneralizedTime() ).getTime();
+ int pwdAge = ( int ) ( currentTime - changedTime ) / 1000;
+
+ if ( pwdAge > policyConfig.getPwdMaxAge() )
+ {
+ return 0;
+ }
+
+ warningAge = policyConfig.getPwdMaxAge() - warningAge;
+
+ if ( pwdAge >= warningAge )
+ {
+ return policyConfig.getPwdMaxAge() - pwdAge;
+ }
+
+ return 0;
+ }
+
+
+ /**
+ * checks if the password is too young
+ *
+ * @param userEntry the user's entry
+ * @return true if the password is young, false otherwise
+ * @throws LdapException
+ */
+ private boolean isPwdTooYoung( Entry userEntry, PasswordPolicyConfiguration policyConfig ) throws LdapException
+ {
+ if ( policyConfig.getPwdMinAge() == 0 )
+ {
+ return false;
+ }
+
+ Attribute pwdChangedTimeAt = userEntry.get( PWD_CHANGED_TIME_AT );
+
+ if ( pwdChangedTimeAt != null )
+ {
+ long changedTime = DateUtils.getDate( pwdChangedTimeAt.getString() ).getTime();
+ changedTime += policyConfig.getPwdMinAge() * 1000;
+
+ long currentTime = DateUtils.getDate( DateUtils.getGeneralizedTime() ).getTime();
+
+ if ( changedTime > currentTime )
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * checks if the password must be changed after the initial bind
+ *
+ * @param userEntry the user's entry
+ * @return true if must be changed, false otherwise
+ * @throws LdapException
+ */
+ private boolean isPwdMustReset( Entry userEntry ) throws LdapException
+ {
+ boolean mustChange = false;
+
+ Attribute pwdResetAt = userEntry.get( PWD_RESET_AT );
+ if ( pwdResetAt != null )
+ {
+ mustChange = Boolean.parseBoolean( pwdResetAt.getString() );
+ }
+
+ return mustChange;
+ }
+
+
+ private PwdModDetailsHolder getPwdModDetails( ModifyOperationContext modifyContext, PasswordPolicyConfiguration policyConfig ) throws LdapException
+ {
+ PwdModDetailsHolder pwdModDetails = new PwdModDetailsHolder();
+
+ List<Modification> mods = modifyContext.getModItems();
+ for ( Modification m : mods )
+ {
+ Attribute at = m.getAttribute();
+
+ if ( at.getUpId().equalsIgnoreCase( policyConfig.getPwdAttribute() ) )
+ {
+ pwdModDetails.setPwdModPresent( true );
+ ModificationOperation op = m.getOperation();
+
+ if ( op == REMOVE_ATTRIBUTE )
+ {
+ pwdModDetails.setDelete( true );
+ }
+ else if ( op == REPLACE_ATTRIBUTE || op == ADD_ATTRIBUTE )
+ {
+ pwdModDetails.setAddOrReplace( true );
+ pwdModDetails.setNewPwd( at.getBytes() );
+ }
+ }
+ else
+ {
+ pwdModDetails.setOtherModExists( true );
+ }
+ }
+
+ return pwdModDetails;
+ }
+
+
+ /**
+ * checks to see if the user's password should be changed before performing any operations
+ * other than bind, password update, unbind, abandon or StartTLS
+ *
+ * @param opContext the operation's context
+ * @throws LdapException
+ */
+ private void checkPwdReset( OperationContext opContext ) throws LdapException
+ {
+ if ( ! directoryService.isPwdPolicyEnabled() )
+ {
+ CoreSession session = opContext.getSession();
+
+ Dn userDn = session.getAuthenticatedPrincipal().getDn();
+
+ if ( pwdResetSet.contains( userDn ) )
+ {
+ boolean isPPolicyReqCtrlPresent = opContext
+ .hasRequestControl( PasswordPolicy.OID );
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyDecorator pwdRespCtrl =
+ new PasswordPolicyDecorator( directoryService.getLdapCodecService(), true );
+ pwdRespCtrl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
+ opContext.addResponseControl( pwdRespCtrl );
+ }
+
+ throw new LdapNoPermissionException( "password needs to be reset before performing this operation" );
+ }
+ }
+ }
+
+
+ private static class PwdModDetailsHolder
+ {
+ private boolean pwdModPresent = false;
+
+ private boolean isDelete = false;
+
+ private boolean isAddOrReplace = false;
+
+ private boolean otherModExists = false;
+
+ private byte[] newPwd;
+
+
+ public boolean isPwdModPresent()
+ {
+ return pwdModPresent;
+ }
+
+
+ public void setPwdModPresent( boolean pwdModPresent )
+ {
+ this.pwdModPresent = pwdModPresent;
+ }
+
+
+ public boolean isDelete()
+ {
+ return isDelete;
+ }
+
+
+ public void setDelete( boolean isDelete )
+ {
+ this.isDelete = isDelete;
+ }
+
+
+ public boolean isAddOrReplace()
+ {
+ return isAddOrReplace;
+ }
+
+
+ public void setAddOrReplace( boolean isAddOrReplace )
+ {
+ this.isAddOrReplace = isAddOrReplace;
+ }
+
+
+ public boolean isOtherModExists()
+ {
+ return otherModExists;
+ }
+
+
+ public void setOtherModExists( boolean otherModExists )
+ {
+ this.otherModExists = otherModExists;
+ }
+
+
+ public byte[] getNewPwd()
+ {
+ return newPwd;
+ }
+
+
+ public void setNewPwd( byte[] newPwd )
+ {
+ this.newPwd = newPwd;
+ }
+ }
+
+
+ /**
+ * Gets the effective password policy of the given entry.
+ * If the entry has defined a custom password policy by setting "pwdPolicySubentry" attribute
+ * then the password policy associated with the Dn specified at the above attribute's value will be returned.
+ * Otherwise the default password policy will be returned (if present)
+ *
+ * @param userEntry the user's entry
+ * @return the associated password policy
+ * @throws LdapException
+ */
+ public PasswordPolicyConfiguration getPwdPolicy( Entry userEntry ) throws LdapException
+ {
+ if ( pwdPolicyContainer == null )
+ {
+ return null;
+ }
+
+ if ( pwdPolicyContainer.hasCustomConfigs() )
+ {
+ Attribute pwdPolicySubentry = userEntry.get( pwdPolicySubentryAT );
+
+ if ( pwdPolicySubentry != null )
+ {
+ Dn configDn = adminSession.getDirectoryService().getDnFactory().create( pwdPolicySubentry.getString() );
+
+ return pwdPolicyContainer.getPolicyConfig( configDn );
+ }
+ }
+
+ return pwdPolicyContainer.getDefaultPolicy();
+ }
+
+
+ /**
+ * set all the password policies to be used by the server.
+ * This includes a default(i.e applicable to all entries) and custom(a.k.a per user) password policies
+ *
+ * @param policyContainer the container holding all the password policies
+ */
+ public void setPwdPolicies( PpolicyConfigContainer policyContainer )
+ {
+ this.pwdPolicyContainer = policyContainer;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public boolean isPwdPolicyEnabled()
+ {
+ return ( ( pwdPolicyContainer != null )
+ && ( ( pwdPolicyContainer.getDefaultPolicy() != null )
+ || ( pwdPolicyContainer.hasCustomConfigs() ) ) );
+ }
+
+
+ /**
+ * @return the pwdPolicyContainer
+ */
+ public PpolicyConfigContainer getPwdPolicyContainer()
+ {
+ return pwdPolicyContainer;
+ }
+
+
+ /**
+ * @param pwdPolicyContainer the pwdPolicyContainer to set
+ */
+ public void setPwdPolicyContainer( PpolicyConfigContainer pwdPolicyContainer )
+ {
+ this.pwdPolicyContainer = pwdPolicyContainer;
+ }
+}
Added: directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/Authenticator.java
URL: http://svn.apache.org/viewvc/directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/Authenticator.java?rev=1187013&view=auto
==============================================================================
--- directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/Authenticator.java (added)
+++ directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/Authenticator.java Thu Oct 20 19:41:49 2011
@@ -0,0 +1,110 @@
+/*
+ * 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 javax.naming.Context;
+
+import org.apache.directory.server.core.api.DirectoryService;
+import org.apache.directory.server.core.api.LdapPrincipal;
+import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
+import org.apache.directory.server.core.api.authn.ppolicy.PasswordPolicyException;
+import org.apache.directory.server.core.shared.partition.DefaultPartitionNexus;
+import org.apache.directory.shared.ldap.model.constants.AuthenticationLevel;
+import org.apache.directory.shared.ldap.model.entry.Entry;
+import org.apache.directory.shared.ldap.model.exception.LdapException;
+import org.apache.directory.shared.ldap.model.name.Dn;
+
+
+/**
+ * Authenticates users who access {@link DefaultPartitionNexus}.
+ * <p>
+ * {@link Authenticator}s are registered to and configured by
+ * {@link AuthenticationInterceptor} interceptor.
+ * <p>
+ * {@link AuthenticationInterceptor} authenticates users by calling
+ * {@link #authenticate(BindOperationContext)}, and then {@link Authenticator}
+ * checks JNDI {@link Context} environment properties
+ * ({@link Context#SECURITY_PRINCIPAL} and {@link Context#SECURITY_CREDENTIALS})
+ * of current {@link Context}.
+ *
+ * @see AbstractAuthenticator
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public interface Authenticator
+{
+ /**
+ * Returns the type of this authenticator (e.g. <tt>'simple'</tt>,
+ * <tt>'none'</tt>,...).
+ */
+ AuthenticationLevel getAuthenticatorType();
+
+
+ /**
+ * Called by {@link AuthenticationInterceptor} to indicate that this
+ * authenticator is being placed into service.
+ */
+ void init( DirectoryService directoryService ) throws LdapException;
+
+
+ /**
+ * Called by {@link AuthenticationInterceptor} to indicate that this
+ * authenticator is being removed from service.
+ */
+ void destroy();
+
+
+ /**
+ * Callback used to respond to password changes by invalidating a password
+ * cache if implemented. This is an additional feature of an authenticator
+ * which need not be implemented: empty implementation is sufficient. This
+ * is called on every del, modify, and modifyRdn operation.
+ *
+ * @param bindDn the already normalized distinguished name of the bind principal
+ */
+ void invalidateCache( Dn bindDn );
+
+
+ /**
+ * Performs authentication and returns the principal if succeeded.
+ *
+ * @param bindContext The Bind context
+ * @exception If the authentication failed
+ */
+ LdapPrincipal authenticate( BindOperationContext bindContext ) throws Exception;
+
+
+ /**
+ * performs checks on the given entry based on the specified password policy configuration
+ *
+ * @param userEntry the user entry to be checked for authentication
+ * @throws PasswordPolicyException
+ */
+ void checkPwdPolicy( Entry userEntry ) throws LdapException;
+
+
+ /**
+ * Performs an unbind on the given context
+ *
+ * @param unbindContext The Unbind context
+ * @exception If the unbind failed
+ */
+ //void unbind( UnbindOperationContext unbindContext ) throws Exception;
+}
Added: directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/DelegatingAuthenticator.java
URL: http://svn.apache.org/viewvc/directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/DelegatingAuthenticator.java?rev=1187013&view=auto
==============================================================================
--- directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/DelegatingAuthenticator.java (added)
+++ directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/DelegatingAuthenticator.java Thu Oct 20 19:41:49 2011
@@ -0,0 +1,189 @@
+/*
+ * 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 java.net.SocketAddress;
+
+import org.apache.directory.ldap.client.api.LdapConnection;
+import org.apache.directory.ldap.client.api.LdapConnectionFactory;
+import org.apache.directory.server.core.api.LdapPrincipal;
+import org.apache.directory.server.core.api.interceptor.context.BindOperationContext;
+import org.apache.directory.server.i18n.I18n;
+import org.apache.directory.shared.ldap.model.constants.AuthenticationLevel;
+import org.apache.directory.shared.ldap.model.entry.Entry;
+import org.apache.directory.shared.ldap.model.exception.LdapAuthenticationException;
+import org.apache.directory.shared.ldap.model.exception.LdapException;
+import org.apache.directory.shared.ldap.model.name.Dn;
+import org.apache.directory.shared.util.Strings;
+import org.apache.mina.core.session.IoSession;
+
+
+/**
+ * Authenticator delegating to another LDAP server.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class DelegatingAuthenticator extends AbstractAuthenticator
+{
+ /** A speedup for logger in debug mode */
+ private static final boolean IS_DEBUG = LOG.isDebugEnabled();
+
+ /** The host in charge of delegated authentication */
+ private String delegateHost;
+
+ /** The associated port */
+ private int delegatePort;
+
+ /**
+ * Creates a new instance.
+ * @see AbstractAuthenticator
+ */
+ public DelegatingAuthenticator()
+ {
+ super( AuthenticationLevel.SIMPLE );
+ }
+
+
+ /**
+ * Creates a new instance, for a specific authentication level.
+ * @see AbstractAuthenticator
+ * @param type The relevant AuthenticationLevel
+ */
+ protected DelegatingAuthenticator( AuthenticationLevel type )
+ {
+ super( type );
+ }
+
+
+ /**
+ * @return the delegateHost
+ */
+ public String getDelegateHost()
+ {
+ return delegateHost;
+ }
+
+
+ /**
+ * @param delegateHost the delegateHost to set
+ */
+ public void setDelegateHost( String delegateHost )
+ {
+ this.delegateHost = delegateHost;
+ }
+
+
+ /**
+ * @return the delegatePort
+ */
+ public int getDelegatePort()
+ {
+ return delegatePort;
+ }
+
+
+ /**
+ * @param delegatePort the delegatePort to set
+ */
+ public void setDelegatePort( int delegatePort )
+ {
+ this.delegatePort = delegatePort;
+ }
+
+
+ /**
+ * {@inheritDoc}
+ */
+ public LdapPrincipal authenticate( BindOperationContext bindContext )
+ throws Exception
+ {
+ LdapPrincipal principal = null;
+
+ if ( IS_DEBUG )
+ {
+ LOG.debug( "Authenticating {}", bindContext.getDn() );
+ }
+
+ // Create a connection on the remote host
+ LdapConnection ldapConnection = LdapConnectionFactory.getNetworkConnection( delegateHost, delegatePort );
+
+ try
+ {
+ // Try to bind
+ try
+ {
+ ldapConnection.bind( bindContext.getDn(),
+ Strings.utf8ToString(bindContext.getCredentials()) );
+
+ // no need to remain bound to delegate host
+ ldapConnection.unBind();
+ }
+ catch ( LdapException le )
+ {
+ String message = I18n.err( I18n.ERR_230, bindContext.getDn().getName() );
+ LOG.info( message );
+ throw new LdapAuthenticationException( message );
+ }
+
+ // Create the new principal
+ principal = new LdapPrincipal( getDirectoryService().getSchemaManager(), bindContext.getDn(), AuthenticationLevel.SIMPLE,
+ bindContext.getCredentials() );
+
+ IoSession session = bindContext.getIoSession();
+
+ if ( session != null )
+ {
+ SocketAddress clientAddress = session.getRemoteAddress();
+ principal.setClientAddress( clientAddress );
+ SocketAddress serverAddress = session.getServiceAddress();
+ principal.setServerAddress( serverAddress );
+ }
+
+ return principal;
+
+ }
+ catch ( LdapException e )
+ {
+ // Bad password ...
+ String message = I18n.err( I18n.ERR_230, bindContext.getDn().getName() );
+ LOG.info( message );
+ throw new LdapAuthenticationException( message );
+ }
+ }
+
+
+ /**
+ * We don't handle any password policy when using a delegated authentication
+ */
+ public void checkPwdPolicy( Entry userEntry ) throws LdapException
+ {
+ // no check for delegating authentication
+ }
+
+
+ /**
+ * We don't handle any cache when using a delegated authentication
+ */
+ public void invalidateCache( Dn bindDn )
+ {
+ // cache is not implemented here
+ }
+}
Added: directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/EncryptionMethod.java
URL: http://svn.apache.org/viewvc/directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/EncryptionMethod.java?rev=1187013&view=auto
==============================================================================
--- directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/EncryptionMethod.java (added)
+++ directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/EncryptionMethod.java Thu Oct 20 19:41:49 2011
@@ -0,0 +1,92 @@
+/*
+ * 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 org.apache.directory.shared.ldap.model.constants.LdapSecurityConstants;
+import org.apache.directory.shared.util.Strings;
+
+
+/**
+ * A class to store all informations about the existing
+ * password found in the cache or get from the backend.
+ *
+ * This is necessary as we have to compute :
+ * - the used algorithm
+ * - the salt if any
+ * - the password itself.
+ *
+ * If we have a on-way encrypted password, it is stored using this
+ * format :
+ * {<algorithm>}<encrypted password>
+ * where the encrypted password format can be :
+ * - MD5/SHA : base64(<password>)
+ * - SMD5/SSH : base64(<salted-password-digest><salt (4 or 8 bytes)>)
+ * - crypt : <salt (2 btytes)><password>
+ *
+ * Algorithm are currently MD5, SMD5, SHA, SSHA, SHA2, SSHA-2 (except SHA-224), CRYPT and empty
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ */
+public class EncryptionMethod
+{
+ private byte[] salt;
+ private LdapSecurityConstants algorithm;
+
+
+ /** package protected */EncryptionMethod( LdapSecurityConstants algorithm, byte[] salt )
+ {
+ this.algorithm = algorithm;
+ this.salt = salt;
+ }
+
+
+ public LdapSecurityConstants getAlgorithm()
+ {
+ return algorithm;
+ }
+
+
+ public byte[] getSalt()
+ {
+ return salt;
+ }
+
+
+ /** package protected */ void setSalt( byte[] salt )
+ {
+ // just to make this class immutable, though we have a setter
+ if ( this.salt != null )
+ {
+ throw new IllegalStateException( "salt will only be allowed to set once" );
+ }
+
+ this.salt = salt;
+ }
+
+
+ @Override
+ public String toString()
+ {
+ return "EncryptionMethod [algorithm=" + algorithm.getName().toUpperCase() + ", salt=" + Strings.dumpBytes(salt) + "]";
+ }
+
+
+}
Added: directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/PasswordHistory.java
URL: http://svn.apache.org/viewvc/directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/PasswordHistory.java?rev=1187013&view=auto
==============================================================================
--- directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/PasswordHistory.java (added)
+++ directory/apacheds/branches/apacheds-txns/interceptors/authn/src/main/java/org/apache/directory/server/core/authn/PasswordHistory.java Thu Oct 20 19:41:49 2011
@@ -0,0 +1,160 @@
+/*
+ * 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 org.apache.directory.shared.ldap.model.constants.SchemaConstants;
+import org.apache.directory.shared.util.Base64;
+import org.apache.directory.shared.util.Strings;
+
+
+/**
+ * A class to hold the data of historical passwords of a entry.
+ * Note: This class's natural ordering is inconsistent with the equals() method
+ * hence it is advised not to use this in any implementations of sorted sets
+ * Instead use Collections.sort() to sort the collection of PasswordHistory objects.
+ *
+ * @author <a href="mailto:dev@directory.apache.org">Apache Directory Project</a>
+ * @version $Rev$, $Date$
+ */
+public class PasswordHistory implements Comparable<PasswordHistory>
+{
+ /** time when password was last changed */
+ private String time;
+
+ /** the syntax OID that is to be used on the password data */
+ private String syntaxOID = SchemaConstants.OCTET_STRING_SYNTAX;
+
+ /** the length of the password data */
+ private int length;
+
+ /** password octet string */
+ private String data;
+
+ private static final char DELIMITER = '#';
+
+
+ public PasswordHistory( String pwdHistoryVal )
+ {
+ int pos = pwdHistoryVal.indexOf( DELIMITER );
+ time = pwdHistoryVal.substring( 0, pos );
+
+ pos++;
+ int nextPos = pwdHistoryVal.indexOf( DELIMITER, pos );
+ syntaxOID = pwdHistoryVal.substring( pos, nextPos );
+
+ nextPos++;
+ pos = pwdHistoryVal.indexOf( DELIMITER, nextPos );
+ length = Integer.parseInt( pwdHistoryVal.substring( nextPos, pos ) );
+
+ data = pwdHistoryVal.substring( pos + 1 );
+ }
+
+
+ public PasswordHistory( String time, byte[] password )
+ {
+ this.time = time;
+ this.data = String.valueOf( Base64.encode( password ) );
+ this.length = data.length();
+ }
+
+
+ public byte[] getHistoryValue()
+ {
+ StringBuilder sb = new StringBuilder();
+
+ sb.append( time ).append( DELIMITER );
+
+ sb.append( syntaxOID ).append( DELIMITER );
+
+ sb.append( length ).append( DELIMITER );
+
+ sb.append( data );
+
+ return Strings.getBytesUtf8(sb.toString());
+ }
+
+
+ public String getTime()
+ {
+ return time;
+ }
+
+
+ public String getSyntaxOID()
+ {
+ return syntaxOID;
+ }
+
+
+ public int getLength()
+ {
+ return length;
+ }
+
+
+ public byte[] getPassword()
+ {
+ return Base64.decode( data.toCharArray() );
+ }
+
+
+ public int compareTo( PasswordHistory o )
+ {
+ return o.getTime().compareTo( time );
+ }
+
+
+ @Override
+ public boolean equals( Object o )
+ {
+ if ( !( o instanceof PasswordHistory ) )
+ {
+ return false;
+ }
+
+ PasswordHistory other = ( PasswordHistory ) o;
+
+ return this.getTime().equals( other.getTime() ) &&
+ this.data.equals( other.data );
+ }
+
+
+ @Override
+ public int hashCode()
+ {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ( ( data == null ) ? 0 : data.hashCode() );
+ result = prime * result + length;
+ result = prime * result + ( ( syntaxOID == null ) ? 0 : syntaxOID.hashCode() );
+ result = prime * result + ( ( time == null ) ? 0 : time.hashCode() );
+ return result;
+ }
+
+
+ @Override
+ public String toString()
+ {
+ return "PasswordHistory [time=" + time + ", syntaxOID=" + syntaxOID + ", length=" + length + ", data=" + data
+ + "]";
+ }
+}