blob: a0df8687d49a79720bd0f04f679d3c87cebd4ac9 [file] [log] [blame]
/*
* 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.PasswordPolicyErrorEnum.INSUFFICIENT_PASSWORD_QUALITY;
import static org.apache.directory.shared.ldap.extras.controls.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_EXPIRE_WARNING_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.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.TreeSet;
import org.apache.directory.server.core.CoreSession;
import org.apache.directory.server.core.DefaultCoreSession;
import org.apache.directory.server.core.DirectoryService;
import org.apache.directory.server.core.LdapPrincipal;
import org.apache.directory.server.core.PasswordPolicyConfiguration;
import org.apache.directory.server.core.admin.AdministrativePointInterceptor;
import org.apache.directory.server.core.authz.AciAuthorizationInterceptor;
import org.apache.directory.server.core.authz.DefaultAuthorizationInterceptor;
import org.apache.directory.server.core.collective.CollectiveAttributeInterceptor;
import org.apache.directory.server.core.event.EventInterceptor;
import org.apache.directory.server.core.exception.ExceptionInterceptor;
import org.apache.directory.server.core.filtering.EntryFilteringCursor;
import org.apache.directory.server.core.interceptor.BaseInterceptor;
import org.apache.directory.server.core.interceptor.Interceptor;
import org.apache.directory.server.core.interceptor.NextInterceptor;
import org.apache.directory.server.core.interceptor.context.AddOperationContext;
import org.apache.directory.server.core.interceptor.context.BindOperationContext;
import org.apache.directory.server.core.interceptor.context.CompareOperationContext;
import org.apache.directory.server.core.interceptor.context.DeleteOperationContext;
import org.apache.directory.server.core.interceptor.context.EntryOperationContext;
import org.apache.directory.server.core.interceptor.context.GetRootDSEOperationContext;
import org.apache.directory.server.core.interceptor.context.ListOperationContext;
import org.apache.directory.server.core.interceptor.context.LookupOperationContext;
import org.apache.directory.server.core.interceptor.context.ModifyOperationContext;
import org.apache.directory.server.core.interceptor.context.MoveAndRenameOperationContext;
import org.apache.directory.server.core.interceptor.context.MoveOperationContext;
import org.apache.directory.server.core.interceptor.context.OperationContext;
import org.apache.directory.server.core.interceptor.context.RenameOperationContext;
import org.apache.directory.server.core.interceptor.context.SearchOperationContext;
import org.apache.directory.server.core.interceptor.context.UnbindOperationContext;
import org.apache.directory.server.core.normalization.NormalizationInterceptor;
import org.apache.directory.server.core.operational.OperationalAttributeInterceptor;
import org.apache.directory.server.core.schema.SchemaInterceptor;
import org.apache.directory.server.core.subtree.SubentryInterceptor;
import org.apache.directory.server.core.trigger.TriggerInterceptor;
import org.apache.directory.server.i18n.I18n;
import org.apache.directory.shared.ldap.extras.controls.PasswordPolicy;
import org.apache.directory.shared.ldap.extras.controls.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.BinaryValue;
import org.apache.directory.shared.ldap.model.entry.DefaultEntryAttribute;
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.EntryAttribute;
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.StringValue;
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.MutableAttributeTypeImpl;
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 MutableAttributeTypeImpl AT_PWD_RESET;
private MutableAttributeTypeImpl AT_PWD_CHANGED_TIME;
private MutableAttributeTypeImpl AT_PWD_HISTORY;
private MutableAttributeTypeImpl AT_PWD_FAILURE_TIME;
private MutableAttributeTypeImpl AT_PWD_ACCOUNT_LOCKED_TIME;
private MutableAttributeTypeImpl AT_PWD_LAST_SUCCESS;
private MutableAttributeTypeImpl AT_PWD_GRACE_USE_TIME;
/**
* 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.class.getName() );
c.add( AuthenticationInterceptor.class.getName() );
c.add( AciAuthorizationInterceptor.class.getName() );
c.add( AdministrativePointInterceptor.class.getName() );
c.add( DefaultAuthorizationInterceptor.class.getName() );
c.add( AdministrativePointInterceptor.class.getName() );
c.add( ExceptionInterceptor.class.getName() );
c.add( OperationalAttributeInterceptor.class.getName() );
c.add( SchemaInterceptor.class.getName() );
c.add( CollectiveAttributeInterceptor.class.getName() );
c.add( SubentryInterceptor.class.getName() );
c.add( EventInterceptor.class.getName() );
c.add( TriggerInterceptor.class.getName() );
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();
loadPwdPolicyStateAtributeTypes();
if ( ( authenticators == null ) || ( authenticators.size() == 0 ) )
{
setDefaultAuthenticators();
}
// Register all authenticators
for ( Authenticator authenticator : authenticators )
{
register( authenticator, directoryService );
}
}
/**
* 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 = directoryService.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();
if ( entry.get( SchemaConstants.CN_AT ) != null )
{
StringValue attr = ( StringValue ) entry.get( SchemaConstants.CN_AT ).get();
username = attr.getString();
}
try
{
check( username, userPassword.get(), policyConfig );
}
catch ( PasswordPolicyException e )
{
if ( isPPolicyReqCtrlPresent )
{
PasswordPolicyDecorator responseControl =
new PasswordPolicyDecorator( directoryService.getLdapCodecService() );
responseControl.getResponse().setPasswordPolicyError( e.getErrorCode() );
addContext.addResponseControl( responseControl );
}
// throw exception if userPassword quality checks fail
throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION, e.getMessage() );
}
String pwdChangedTime = DateUtils.getGeneralizedTime();
if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
{
EntryAttribute pwdChangedTimeAt = new DefaultEntryAttribute( AT_PWD_CHANGED_TIME );
pwdChangedTimeAt.add( pwdChangedTime );
entry.add( pwdChangedTimeAt );
}
if ( policyConfig.isPwdMustChange() && addContext.getSession().isAnAdministrator() )
{
EntryAttribute pwdResetAt = new DefaultEntryAttribute( AT_PWD_RESET );
pwdResetAt.add( "TRUE" );
entry.add( pwdResetAt );
}
if ( policyConfig.getPwdInHistory() > 0 )
{
EntryAttribute pwdHistoryAt = new DefaultEntryAttribute( AT_PWD_HISTORY );
byte[] pwdHistoryVal = new PasswordHistory( pwdChangedTime, userPassword.get() ).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 = directoryService.getPwdPolicy( modifyContext.getOriginalEntry() );
boolean isPPolicyReqCtrlPresent = modifyContext.hasRequestControl( PasswordPolicy.OID );
Dn userDn = modifyContext.getSession().getAuthenticatedPrincipal().getDn();
PwdModDetailsHolder pwdModDetails = null;
if ( policyConfig.isPwdSafeModify() || pwdResetSet.contains( userDn ) || ( policyConfig.getPwdMinAge() > 0 ) )
{
pwdModDetails = getPwdModDetails( modifyContext, policyConfig );
}
if ( ( pwdModDetails != null ) && pwdModDetails.isPwdModPresent() )
{
if ( pwdResetSet.contains( userDn ) )
{
if ( pwdModDetails.isOtherModExists() )
{
if ( isPPolicyReqCtrlPresent )
{
PasswordPolicyDecorator responseControl =
new PasswordPolicyDecorator( directoryService.getLdapCodecService() );
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() );
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() );
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() );
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 ) )
{
String userName = null;
if ( entry.get( SchemaConstants.CN_AT ) != null )
{
StringValue attr = ( StringValue ) entry.get( SchemaConstants.CN_AT ).get();
userName = attr.getString();
}
newPassword = pwdModDetails.getNewPwd();
try
{
check( userName, newPassword, policyConfig );
}
catch ( PasswordPolicyException e )
{
if ( isPPolicyReqCtrlPresent )
{
PasswordPolicyDecorator responseControl =
new PasswordPolicyDecorator( directoryService.getLdapCodecService() );
responseControl.getResponse().setPasswordPolicyError( e.getErrorCode() );
modifyContext.addResponseControl( responseControl );
}
// throw exception if userPassword quality checks fail
throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION, e.getMessage() );
}
}
int histSize = policyConfig.getPwdInHistory();
Modification pwdRemHistMod = null;
Modification pwdAddHistMod = null;
String pwdChangedTime = DateUtils.getGeneralizedTime();
if ( histSize > 0 )
{
EntryAttribute pwdHistoryAt = entry.get( PWD_HISTORY_AT );
Set<PasswordHistory> pwdHistSet = new TreeSet<PasswordHistory>();
Iterator<Value<?>> itr = pwdHistoryAt.getAll();
while ( itr.hasNext() )
{
Value<?> val = itr.next();
PasswordHistory pwdh = new PasswordHistory( Strings.utf8ToString(val.getBytes()) );
boolean matched = Arrays.equals( newPassword, pwdh.getPassword() );
if ( matched )
{
if ( isPPolicyReqCtrlPresent )
{
PasswordPolicyDecorator responseControl =
new PasswordPolicyDecorator( directoryService.getLdapCodecService() );
responseControl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.PASSWORD_IN_HISTORY );
modifyContext.addResponseControl( responseControl );
}
throw new LdapOperationException( ResultCodeEnum.CONSTRAINT_VIOLATION,
"invalid reuse of password present in password history" );
}
pwdHistSet.add( pwdh );
}
PasswordHistory newPwdHist = new PasswordHistory( pwdChangedTime, newPassword );
pwdHistSet.add( newPwdHist );
pwdHistoryAt = new DefaultEntryAttribute( pwdHistoryAt.getAttributeType() );
pwdHistoryAt.add( newPwdHist.getHistoryValue() );
pwdAddHistMod = new DefaultModification( ADD_ATTRIBUTE, pwdHistoryAt );
if ( pwdHistSet.size() > histSize )
{
pwdHistoryAt = new DefaultEntryAttribute( pwdHistoryAt.getAttributeType() );
PasswordHistory remPwdHist = ( PasswordHistory ) pwdHistSet.toArray()[histSize - 1];
pwdHistoryAt.add( remPwdHist.getHistoryValue() );
pwdRemHistMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdHistoryAt );
}
}
next.modify( modifyContext );
invalidateAuthenticatorCaches( modifyContext.getDn() );
List<Modification> mods = new ArrayList<Modification>();
if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
{
EntryAttribute pwdChangedTimeAt = new DefaultEntryAttribute( 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() )
{
EntryAttribute pwdMustChangeAt = new DefaultEntryAttribute( 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 );
}
EntryAttribute pwdFailureTimeAt = entry.get( PWD_FAILURE_TIME_AT );
if ( pwdFailureTimeAt != null )
{
mods.add( new DefaultModification( REMOVE_ATTRIBUTE, pwdFailureTimeAt ) );
}
EntryAttribute 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() );
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;
}
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( ppe.getErrorCode() );
bindContext.addResponseControl( pwdRespCtrl );
}
throw ppe;
}
Dn dn = bindContext.getDn();
Entry userEntry = bindContext.getEntry();
PasswordPolicyConfiguration policyConfig = directoryService.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 ) )
{
EntryAttribute pwdFailTimeAt = userEntry.get( PWD_FAILURE_TIME_AT );
if ( pwdFailTimeAt == null )
{
pwdFailTimeAt = new DefaultEntryAttribute( 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() ) )
{
EntryAttribute pwdAccountLockedTimeAt = new DefaultEntryAttribute( AT_PWD_ACCOUNT_LOCKED_TIME );
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( Collections.singletonList( pwdFailTimeMod ) );
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 )
{
EntryAttribute pwdLastSuccesTimeAt = new DefaultEntryAttribute( AT_PWD_LAST_SUCCESS );
pwdLastSuccesTimeAt.add( DateUtils.getGeneralizedTime() );
Modification pwdLastSuccesTimeMod = new DefaultModification( REPLACE_ATTRIBUTE, pwdLastSuccesTimeAt );
mods.add( pwdLastSuccesTimeMod );
}
EntryAttribute pwdFailTimeAt = userEntry.get( AT_PWD_FAILURE_TIME );
if ( pwdFailTimeAt != null )
{
Modification pwdFailTimeMod = new DefaultModification( REMOVE_ATTRIBUTE, pwdFailTimeAt );
mods.add( pwdFailTimeMod );
}
EntryAttribute 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 ) )
{
EntryAttribute pwdChangeTimeAttr = userEntry.get( PWD_CHANGED_TIME_AT );
if ( pwdChangeTimeAttr != null )
{
boolean expired = PasswordUtil.isPwdExpired( pwdChangeTimeAttr.getString(),
policyConfig.getPwdMaxAge() );
if ( expired )
{
EntryAttribute pwdGraceUseAttr = userEntry.get( PWD_GRACE_USE_TIME_AT );
if ( pwdGraceUseAttr != null )
{
pwdRespCtrl.getResponse().setGraceAuthNsRemaining( policyConfig.getPwdGraceAuthNLimit()
- ( pwdGraceUseAttr.size() + 1 ) );
}
else
{
pwdGraceUseAttr = new DefaultEntryAttribute( AT_PWD_GRACE_USE_TIME );
}
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 );
}
}
String strPassword = Strings.utf8ToString(password);
validatePasswordLength( strPassword, policyConfig );
checkUsernameSubstring( username, strPassword, policyConfig );
// checkPasswordChars( strPassword );
}
/**
* 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 );
}
}
if ( minLen > 0 )
{
if ( pwdLen < minLen )
{
throw new PasswordPolicyException( "Password should have a minmum of " + minLen + " characters",
PASSWORD_TOO_SHORT );
}
}
}
/**
* The password does not contain three letter (or more) tokens from the user's account name.
*
* If the account name is less than three characters long, this check is not performed
* because the rate at which passwords would be rejected is too high. For each token that is
* three or more characters long, that token is searched for in the password; if it is present,
* the password change is rejected. For example, the name "First M. Last" would be split into
* three tokens: "First", "M", and "Last". Because the second token is only one character long,
* it would be ignored. Therefore, this user could not have a password that included either
* "first" or "last" as a substring anywhere in the password. All of these checks are
* case-insensitive.
*/
private void checkUsernameSubstring( String username, String password, PasswordPolicyConfiguration policyConfig ) throws PasswordPolicyException
{
if ( username == null || username.trim().length() == 0 )
{
return;
}
String[] tokens = username.split( "[^a-zA-Z]" );
for ( int ii = 0; ii < tokens.length; ii++ )
{
if ( password.matches( "(?i).*" + tokens[ii] + ".*" ) )
{
throw new PasswordPolicyException( "Password shouldn't contain parts of the username",
INSUFFICIENT_PASSWORD_QUALITY );
}
}
}
private int getPwdTimeBeforeExpiry( Entry userEntry, PasswordPolicyConfiguration policyConfig ) throws LdapException
{
if ( policyConfig.getPwdMaxAge() == 0 )
{
return 0;
}
EntryAttribute pwdExpireWarningAt = userEntry.get( PWD_EXPIRE_WARNING_AT );
if ( pwdExpireWarningAt == null )
{
return 0;
}
EntryAttribute pwdChangedTimeAt = userEntry.get( PWD_CHANGED_TIME_AT );
long changedTime = DateUtils.getDate(pwdChangedTimeAt.getString()).getTime();
int pwdAge = ( int ) ( System.currentTimeMillis() - changedTime ) / 1000;
if ( pwdAge > policyConfig.getPwdMaxAge() )
{
return 0;
}
int warningAge = ( int ) ( DateUtils.getDate( pwdExpireWarningAt.getString() ).getTime() ) / 1000;
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;
}
EntryAttribute pwdChangedTimeAt = userEntry.get( PWD_CHANGED_TIME_AT );
long changedTime = DateUtils.getDate( pwdChangedTimeAt.getString() ).getTime();
changedTime += policyConfig.getPwdMinAge() * 1000;
if ( changedTime > System.currentTimeMillis() )
{
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;
EntryAttribute 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 )
{
EntryAttribute 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() );
pwdRespCtrl.getResponse().setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
opContext.addResponseControl( pwdRespCtrl );
}
throw new LdapNoPermissionException( "password needs to be reset before performing this operation" );
}
}
}
private 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;
}
}
}