You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@directory.apache.org by ka...@apache.org on 2010/07/13 20:49:50 UTC
svn commit: r963815 -
/directory/apacheds/trunk/core/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java
Author: kayyagari
Date: Tue Jul 13 18:49:50 2010
New Revision: 963815
URL: http://svn.apache.org/viewvc?rev=963815&view=rev
Log:
o included the ppolicy related checks (99% of the policy related checks are done here)
Modified:
directory/apacheds/trunk/core/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java
Modified: directory/apacheds/trunk/core/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java
URL: http://svn.apache.org/viewvc/directory/apacheds/trunk/core/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java?rev=963815&r1=963814&r2=963815&view=diff
==============================================================================
--- directory/apacheds/trunk/core/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java (original)
+++ directory/apacheds/trunk/core/src/main/java/org/apache/directory/server/core/authn/AuthenticationInterceptor.java Tue Jul 13 18:49:50 2010
@@ -20,12 +20,29 @@
package org.apache.directory.server.core.authn;
+import static org.apache.directory.shared.ldap.codec.controls.ppolicy.PasswordPolicyErrorEnum.INSUFFICIENT_PASSWORD_QUALITY;
+import static org.apache.directory.shared.ldap.codec.controls.ppolicy.PasswordPolicyErrorEnum.PASSWORD_TOO_SHORT;
+import static org.apache.directory.shared.ldap.constants.PasswordPolicySchemaConstants.PWD_ACCOUNT_LOCKED_TIME_AT;
+import static org.apache.directory.shared.ldap.constants.PasswordPolicySchemaConstants.PWD_CHANGED_TIME_AT;
+import static org.apache.directory.shared.ldap.constants.PasswordPolicySchemaConstants.PWD_EXPIRE_WARNING_AT;
+import static org.apache.directory.shared.ldap.constants.PasswordPolicySchemaConstants.PWD_FAILURE_TIME_AT;
+import static org.apache.directory.shared.ldap.constants.PasswordPolicySchemaConstants.PWD_GRACE_USE_TIME_AT;
+import static org.apache.directory.shared.ldap.constants.PasswordPolicySchemaConstants.PWD_HISTORY_AT;
+import static org.apache.directory.shared.ldap.constants.PasswordPolicySchemaConstants.PWD_LAST_SUCCESS_AT;
+import static org.apache.directory.shared.ldap.constants.PasswordPolicySchemaConstants.PWD_RESET_AT;
+
import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.BitSet;
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;
@@ -49,15 +66,32 @@ import org.apache.directory.server.core.
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.i18n.I18n;
+import org.apache.directory.shared.ldap.codec.controls.ppolicy.PasswordPolicyErrorEnum;
+import org.apache.directory.shared.ldap.codec.controls.ppolicy.PasswordPolicyRequestControl;
+import org.apache.directory.shared.ldap.codec.controls.ppolicy.PasswordPolicyResponseControl;
import org.apache.directory.shared.ldap.constants.AuthenticationLevel;
+import org.apache.directory.shared.ldap.constants.LdapSecurityConstants;
+import org.apache.directory.shared.ldap.constants.SchemaConstants;
+import org.apache.directory.shared.ldap.entry.BinaryValue;
+import org.apache.directory.shared.ldap.entry.DefaultEntryAttribute;
+import org.apache.directory.shared.ldap.entry.DefaultModification;
import org.apache.directory.shared.ldap.entry.Entry;
+import org.apache.directory.shared.ldap.entry.EntryAttribute;
+import org.apache.directory.shared.ldap.entry.Modification;
+import org.apache.directory.shared.ldap.entry.ModificationOperation;
+import org.apache.directory.shared.ldap.entry.StringValue;
+import org.apache.directory.shared.ldap.entry.Value;
import org.apache.directory.shared.ldap.exception.LdapAuthenticationException;
import org.apache.directory.shared.ldap.exception.LdapException;
import org.apache.directory.shared.ldap.exception.LdapNoPermissionException;
+import org.apache.directory.shared.ldap.exception.LdapOperationException;
import org.apache.directory.shared.ldap.exception.LdapUnwillingToPerformException;
import org.apache.directory.shared.ldap.message.ResultCodeEnum;
import org.apache.directory.shared.ldap.name.DN;
+import org.apache.directory.shared.ldap.schema.SchemaManager;
+import org.apache.directory.shared.ldap.util.DateUtils;
import org.apache.directory.shared.ldap.util.StringTools;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -81,8 +115,18 @@ public class AuthenticationInterceptor e
private final Map<AuthenticationLevel, Collection<Authenticator>> authenticatorsMapByType = new HashMap<AuthenticationLevel, Collection<Authenticator>>();
private DirectoryService directoryService;
-
-
+
+ private PasswordPolicyConfiguration policyConfig;
+
+ private SchemaManager schemaManager;
+
+ private CoreSession adminSession;
+
+ private boolean pwdPolicyEnabled = false;
+
+ private Set<DN> pwdResetSet = new HashSet<DN>();
+
+
/**
* Creates an authentication service interceptor.
*/
@@ -90,14 +134,23 @@ public class AuthenticationInterceptor e
{
}
-
+
/**
* Registers and initializes all {@link Authenticator}s to this service.
*/
public void init( DirectoryService directoryService ) throws LdapException
{
this.directoryService = directoryService;
-
+
+ schemaManager = directoryService.getSchemaManager();
+
+ adminSession = directoryService.getAdminSession();
+
+ if ( policyConfig != null )
+ {
+ pwdPolicyEnabled = true;
+ }
+
if ( authenticators == null )
{
setDefaultAuthenticators();
@@ -109,7 +162,7 @@ public class AuthenticationInterceptor e
}
}
-
+
private void setDefaultAuthenticators()
{
Set<Authenticator> set = new HashSet<Authenticator>();
@@ -126,7 +179,7 @@ public class AuthenticationInterceptor e
return authenticators;
}
-
+
/**
* @param authenticators authenticators to be used by this AuthenticationInterceptor
*/
@@ -135,7 +188,7 @@ public class AuthenticationInterceptor e
this.authenticators = authenticators;
}
-
+
/**
* Deinitializes and deregisters all {@link Authenticator}s from this service.
*/
@@ -150,7 +203,7 @@ public class AuthenticationInterceptor e
}
}
-
+
/**
* Initializes the specified {@link Authenticator} and registers it to
* this service.
@@ -162,6 +215,7 @@ public class AuthenticationInterceptor e
private void register( Authenticator authenticator, DirectoryService directoryService ) throws LdapException
{
authenticator.init( directoryService );
+ authenticator.setPwdPolicyConfig( policyConfig );
Collection<Authenticator> authenticatorList = getAuthenticators( authenticator.getAuthenticatorType() );
@@ -188,7 +242,7 @@ public class AuthenticationInterceptor e
if ( ( result != null ) && ( result.size() > 0 ) )
{
return result;
- }
+ }
else
{
return null;
@@ -204,6 +258,75 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( addContext );
+
+ if ( policyConfig == null )
+ {
+ next.add( addContext );
+ return;
+ }
+
+ boolean isPPolicyReqCtrlPresent = addContext.hasRequestControl( PasswordPolicyRequestControl.CONTROL_OID );
+
+ checkPwdReset( addContext );
+
+ Entry entry = addContext.getEntry();
+
+ 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() );
+ }
+ catch( PasswordPolicyException e )
+ {
+ if( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyResponseControl responseControl = new PasswordPolicyResponseControl();
+ responseControl.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(
+ schemaManager.lookupAttributeTypeRegistry( PWD_CHANGED_TIME_AT ) );
+ pwdChangedTimeAt.add( pwdChangedTime );
+ entry.add( pwdChangedTimeAt );
+ }
+
+ if ( policyConfig.isPwdMustChange() && addContext.getSession().isAnAdministrator() )
+ {
+ EntryAttribute pwdMustChangeAt = new DefaultEntryAttribute(
+ schemaManager.lookupAttributeTypeRegistry( PWD_RESET_AT ) );
+ pwdMustChangeAt.add( "TRUE" );
+ entry.add( pwdMustChangeAt );
+ }
+
+ if ( policyConfig.getPwdInHistory() > 0 )
+ {
+ EntryAttribute pwdHistoryAt = new DefaultEntryAttribute(
+ schemaManager.lookupAttributeTypeRegistry( PWD_HISTORY_AT ) );
+ byte[] pwdHistoryVal = new PasswordHistory( pwdChangedTime, userPassword.get() ).getHistoryValue();
+ pwdHistoryAt.add( pwdHistoryVal );
+ entry.add( pwdHistoryAt );
+ }
+ }
+
next.add( addContext );
}
@@ -216,6 +339,7 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( deleteContext );
+ checkPwdReset( deleteContext );
next.delete( deleteContext );
invalidateAuthenticatorCaches( deleteContext.getDn() );
}
@@ -229,6 +353,7 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( getRootDseContext );
+ checkPwdReset( getRootDseContext );
return next.getRootDSE( getRootDseContext );
}
@@ -241,6 +366,7 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( hasEntryContext );
+ checkPwdReset( hasEntryContext );
return next.hasEntry( hasEntryContext );
}
@@ -253,6 +379,7 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( listContext );
+ checkPwdReset( listContext );
return next.list( listContext );
}
@@ -265,10 +392,11 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( lookupContext );
+ checkPwdReset( lookupContext );
return next.lookup( lookupContext );
}
-
+
private void invalidateAuthenticatorCaches( DN principalDn )
{
for ( AuthenticationLevel authMech : authenticatorsMapByType.keySet() )
@@ -284,6 +412,8 @@ public class AuthenticationInterceptor e
}
+ //FIXME crappy code, lots of if-else constructs related to pwdpolicy checks
+ // will be restructured after committing
public void modify( NextInterceptor next, ModifyOperationContext modifyContext ) throws LdapException
{
if ( IS_DEBUG )
@@ -292,8 +422,229 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( modifyContext );
+
+ boolean isPPolicyReqCtrlPresent = modifyContext.hasRequestControl( PasswordPolicyRequestControl.CONTROL_OID );
+
+ if ( policyConfig == null )
+ {
+ next.modify( modifyContext );
+ invalidateAuthenticatorCaches( modifyContext.getDn() );
+ return;
+ }
+
+ DN userDn = modifyContext.getSession().getAuthenticatedPrincipal().getDN();
+
+ PwdModDetailsHolder pwdModDetails = null;
+ if ( policyConfig.isPwdSafeModify() || pwdResetSet.contains( userDn ) || ( policyConfig.getPwdMinAge() > 0 ) )
+ {
+ pwdModDetails = getPwdModDetails( modifyContext );
+ }
+
+ if ( pwdResetSet.contains( userDn ) && pwdModDetails.isPwdModPresent() )
+ {
+ if ( !pwdModDetails.isPwdModPresent() || pwdModDetails.isOtherModExists() )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyResponseControl responseControl = new PasswordPolicyResponseControl();
+ responseControl.setPasswordPolicyError( PasswordPolicyErrorEnum.CHANGE_AFTER_RESET );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ throw new LdapNoPermissionException();
+ }
+ }
+
+ if ( policyConfig.isPwdSafeModify() && pwdModDetails.isPwdModPresent() )
+ {
+ if ( pwdModDetails.isAddOrReplace() && !pwdModDetails.isDelete() )
+ {
+ LOG.debug( "trying to update password attribute without the supplying the old password" );
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyResponseControl responseControl = new PasswordPolicyResponseControl();
+ responseControl.setPasswordPolicyError( PasswordPolicyErrorEnum.MUST_SUPPLY_OLD_PASSWORD );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ throw new LdapNoPermissionException();
+ }
+ }
+
+ if ( !policyConfig.isPwdAllowUserChange() && pwdModDetails.isPwdModPresent()
+ && !modifyContext.getSession().isAnAdministrator() )
+ {
+
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyResponseControl responseControl = new PasswordPolicyResponseControl();
+ responseControl.setPasswordPolicyError( PasswordPolicyErrorEnum.PASSWORD_MOD_NOT_ALLOWED );
+ modifyContext.addResponseControl( responseControl );
+ }
+
+ throw new LdapNoPermissionException();
+ }
+
+ Entry entry = modifyContext.getEntry();
+
+ if ( isPwdTooYoung( entry ) && pwdModDetails.isPwdModPresent() )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyResponseControl responseControl = new PasswordPolicyResponseControl();
+ responseControl.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 ) && pwdModDetails.isPwdModPresent() )
+ {
+ 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 );
+ }
+ catch ( PasswordPolicyException e )
+ {
+ if( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyResponseControl responseControl = new PasswordPolicyResponseControl();
+ responseControl.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 ( ( pwdModDetails != null ) && ( histSize > 0 ) && pwdModDetails.isPwdModPresent() )
+ {
+ 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( StringTools.utf8ToString( val.getBytes() ) );
+
+ boolean matched = Arrays.equals( newPassword, pwdh.getPassword() );
+
+ if ( matched )
+ {
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyResponseControl responseControl = new PasswordPolicyResponseControl();
+ responseControl.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( ModificationOperation.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( ModificationOperation.REMOVE_ATTRIBUTE, pwdHistoryAt );
+ }
+ }
+
next.modify( modifyContext );
invalidateAuthenticatorCaches( modifyContext.getDn() );
+
+ if ( ( pwdModDetails != null ) && pwdModDetails.isPwdModPresent() )
+ {
+ List<Modification> mods = new ArrayList<Modification>();
+ if ( ( policyConfig.getPwdMinAge() > 0 ) || ( policyConfig.getPwdMaxAge() > 0 ) )
+ {
+ EntryAttribute pwdChangedTimeAt = new DefaultEntryAttribute(
+ schemaManager.lookupAttributeTypeRegistry( PWD_CHANGED_TIME_AT ) );
+ pwdChangedTimeAt.add( pwdChangedTime );
+ Modification pwdChangedTimeMod = new DefaultModification( ModificationOperation.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(
+ schemaManager.lookupAttributeTypeRegistry( PWD_RESET_AT ) );
+ Modification pwdMustChangeMod = null;
+
+ if ( modifyContext.getSession().isAnAdministrator() )
+ {
+ pwdMustChangeAt.add( "TRUE" );
+ pwdMustChangeMod = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
+ pwdMustChangeAt );
+ }
+ else
+ {
+ pwdMustChangeMod = new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE, pwdMustChangeAt );
+ removeFromPwdResetSet = true;
+ }
+
+ mods.add( pwdMustChangeMod );
+ }
+
+ EntryAttribute pwdFailureTimeAt = entry.get( PWD_FAILURE_TIME_AT );
+ if ( pwdFailureTimeAt != null )
+ {
+ mods.add( new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE, pwdFailureTimeAt ) );
+ }
+
+ EntryAttribute pwdGraceUseTimeAt = entry.get( PWD_GRACE_USE_TIME_AT );
+ if ( pwdGraceUseTimeAt != null )
+ {
+ mods.add( new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE, pwdGraceUseTimeAt ) );
+ }
+
+ directoryService.getAdminSession().modify( modifyContext.getDn(), mods );
+
+ if ( removeFromPwdResetSet )
+ {
+ pwdResetSet.remove( userDn );
+ }
+ }
}
@@ -305,11 +656,12 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( renameContext );
+ checkPwdReset( renameContext );
next.rename( renameContext );
invalidateAuthenticatorCaches( renameContext.getDn() );
}
-
+
/**
* {@inheritDoc}
*/
@@ -321,15 +673,16 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( compareContext );
+ checkPwdReset( compareContext );
boolean result = next.compare( compareContext );
invalidateAuthenticatorCaches( compareContext.getDn() );
-
+
return result;
}
public void moveAndRename( NextInterceptor next, MoveAndRenameOperationContext moveAndRenameContext )
- throws LdapException
+ throws LdapException
{
if ( IS_DEBUG )
{
@@ -337,6 +690,7 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( moveAndRenameContext );
+ checkPwdReset( moveAndRenameContext );
next.moveAndRename( moveAndRenameContext );
invalidateAuthenticatorCaches( moveAndRenameContext.getDn() );
}
@@ -353,12 +707,14 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( moveContext );
+ checkPwdReset( moveContext );
next.move( moveContext );
invalidateAuthenticatorCaches( moveContext.getDn() );
}
- public EntryFilteringCursor search( NextInterceptor next, SearchOperationContext searchContext ) throws LdapException
+ public EntryFilteringCursor search( NextInterceptor next, SearchOperationContext searchContext )
+ throws LdapException
{
if ( IS_DEBUG )
{
@@ -366,6 +722,7 @@ public class AuthenticationInterceptor e
}
checkAuthenticated( searchContext );
+ checkPwdReset( searchContext );
return next.search( searchContext );
}
@@ -378,7 +735,7 @@ public class AuthenticationInterceptor e
*/
private void checkAuthenticated( OperationContext operation ) throws LdapException
{
- if ( operation.getSession().isAnonymous() && !directoryService.isAllowAnonymousAccess()
+ if ( operation.getSession().isAnonymous() && !directoryService.isAllowAnonymousAccess()
&& !operation.getDn().isEmpty() )
{
String msg = I18n.err( I18n.ERR_5, operation.getName() );
@@ -400,16 +757,17 @@ public class AuthenticationInterceptor e
// 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() );
+ throw new LdapUnwillingToPerformException( ResultCodeEnum.UNWILLING_TO_PERFORM, "Cannot Bind for DN "
+ + bindContext.getDn().getName() );
}
Collection<Authenticator> authenticators = getAuthenticators( level );
@@ -434,6 +792,12 @@ public class AuthenticationInterceptor e
return;
}
+ boolean isPPolicyReqCtrlPresent = bindContext.hasRequestControl( PasswordPolicyRequestControl.CONTROL_OID );
+ PasswordPolicyResponseControl pwdRespCtrl = new PasswordPolicyResponseControl();
+
+ boolean authenticated = false;
+ PasswordPolicyException ppe = null;
+
// TODO : we should refactor that.
// try each authenticator
for ( Authenticator authenticator : authenticators )
@@ -442,8 +806,8 @@ public class AuthenticationInterceptor e
{
// perform the authentication
LdapPrincipal principal = authenticator.authenticate( bindContext );
-
- LdapPrincipal clonedPrincipal = (LdapPrincipal)(principal.clone());
+
+ LdapPrincipal clonedPrincipal = ( LdapPrincipal ) ( principal.clone() );
// remove creds so there is no security risk
bindContext.setCredentials( null );
@@ -453,7 +817,12 @@ public class AuthenticationInterceptor e
CoreSession session = new DefaultCoreSession( clonedPrincipal, directoryService );
bindContext.setSession( session );
- return;
+ authenticated = true;
+ }
+ catch ( PasswordPolicyException e )
+ {
+ ppe = e;
+ break;
}
catch ( LdapAuthenticationException e )
{
@@ -473,13 +842,546 @@ public class AuthenticationInterceptor e
}
}
- if ( LOG.isInfoEnabled() )
+ if ( ppe != null )
{
- LOG.info( "Cannot bind to the server " );
+ if ( isPPolicyReqCtrlPresent )
+ {
+ pwdRespCtrl.setPasswordPolicyError( ppe.getErrorCode() );
+ bindContext.addResponseControl( pwdRespCtrl );
+ }
+
+ throw ppe;
}
DN dn = bindContext.getDn();
- String upDn = ( dn == null ? "" : dn.getName() );
- throw new LdapAuthenticationException( I18n.err( I18n.ERR_229, upDn ) );
+ Entry userEntry = bindContext.getEntry();
+
+ if ( !authenticated )
+ {
+ if ( LOG.isInfoEnabled() )
+ {
+ LOG.info( "Cannot bind to the server " );
+ }
+
+ if ( pwdPolicyEnabled )
+ {
+ EntryAttribute pwdFailTimeAt = bindContext.getEntry().get( PWD_FAILURE_TIME_AT );
+ if ( pwdFailTimeAt == null )
+ {
+ pwdFailTimeAt = new DefaultEntryAttribute(
+ schemaManager.lookupAttributeTypeRegistry( PWD_FAILURE_TIME_AT ) );
+ }
+ else
+ {
+ PasswordUtil.purgeFailureTimes( policyConfig, pwdFailTimeAt );
+ }
+
+ String failureTime = DateUtils.getGeneralizedTime();
+ pwdFailTimeAt.add( failureTime );
+ Modification pwdFailTimeMod = new DefaultModification( ModificationOperation.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(
+ schemaManager.lookupAttributeTypeRegistry( PWD_ACCOUNT_LOCKED_TIME_AT ) );
+ pwdAccountLockedTimeAt.add( failureTime );
+ Modification pwdAccountLockedMod = new DefaultModification( ModificationOperation.ADD_ATTRIBUTE,
+ pwdAccountLockedTimeAt );
+ mods.add( pwdAccountLockedMod );
+
+ pwdRespCtrl.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 ) );
+ }
+
+ String upDn = ( dn == null ? "" : dn.getName() );
+ throw new LdapAuthenticationException( I18n.err( I18n.ERR_229, upDn ) );
+ }
+ else if ( pwdPolicyEnabled )
+ {
+ List<Modification> mods = new ArrayList<Modification>();
+
+ if ( policyConfig.getPwdMaxIdle() > 0 )
+ {
+ EntryAttribute pwdLastSuccesTimeAt = new DefaultEntryAttribute(
+ schemaManager.lookupAttributeTypeRegistry( PWD_LAST_SUCCESS_AT ) );
+ pwdLastSuccesTimeAt.add( DateUtils.getGeneralizedTime() );
+ Modification pwdLastSuccesTimeMod = new DefaultModification( ModificationOperation.REPLACE_ATTRIBUTE,
+ pwdLastSuccesTimeAt );
+ mods.add( pwdLastSuccesTimeMod );
+ }
+
+ EntryAttribute pwdFailTimeAt = new DefaultEntryAttribute(
+ schemaManager.lookupAttributeTypeRegistry( PWD_FAILURE_TIME_AT ) );
+ Modification pwdFailTimeMod = new DefaultModification( ModificationOperation.REMOVE_ATTRIBUTE,
+ pwdFailTimeAt );
+ mods.add( pwdFailTimeMod );
+
+ EntryAttribute pwdAccLockedTimeAt = new DefaultEntryAttribute(
+ schemaManager.lookupAttributeTypeRegistry( PWD_ACCOUNT_LOCKED_TIME_AT ) );
+ Modification pwdAccLockedTimeMod = new DefaultModification( ModificationOperation.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 );
+ boolean expired = PasswordUtil
+ .isPwdExpired( pwdChangeTimeAttr.getString(), policyConfig.getPwdMaxAge() );
+ if ( expired )
+ {
+ EntryAttribute pwdGraceUseAttr = userEntry.get( PWD_GRACE_USE_TIME_AT );
+ if ( pwdGraceUseAttr != null )
+ {
+ pwdRespCtrl.setGraceAuthNsRemaining( policyConfig.getPwdGraceAuthNLimit()
+ - ( pwdGraceUseAttr.size() + 1 ) );
+ }
+ else
+ {
+ pwdGraceUseAttr = new DefaultEntryAttribute(
+ schemaManager.lookupAttributeTypeRegistry( PWD_GRACE_USE_TIME_AT ) );
+ }
+
+ pwdGraceUseAttr.add( DateUtils.getGeneralizedTime() );
+ Modification pwdGraceUseMod = new DefaultModification( ModificationOperation.ADD_ATTRIBUTE,
+ pwdGraceUseAttr );
+ mods.add( pwdGraceUseMod );
+ }
+ }
+
+ adminSession.modify( dn, mods );
+
+ if ( isPPolicyReqCtrlPresent )
+ {
+ int expiryWarnTime = getPwdTimeBeforeExpiry( userEntry );
+ if ( expiryWarnTime > 0 )
+ {
+ pwdRespCtrl.setTimeBeforeExpiration( expiryWarnTime );
+ }
+
+ if ( isPwdMustReset( userEntry ) )
+ {
+ pwdRespCtrl.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
+ if ( ( policyConfig != null ) && ( policyConfig.isPwdMustChange() ) )
+ {
+ pwdResetSet.remove( unbindContext.getDn() );
+ }
+ }
+
+
+ public void setPwdPolicyConfig( PasswordPolicyConfiguration policyConfig )
+ {
+ this.policyConfig = policyConfig;
+ }
+
+
+ // ---------- private methods ----------------
+
+ private void check( String username, byte[] password ) 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 = StringTools.utf8ToString( password );
+ validatePasswordLength( strPassword );
+ checkUsernameSubstring( username, strPassword );
+ // checkPasswordChars( strPassword );
+ }
+
+
+ /**
+ * validates the length of the password
+ */
+ private void validatePasswordLength( String password ) 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 contains characters from at least three of the following four categories:
+ * English uppercase characters (A - Z)
+ * English lowercase characters (a - z)
+ * Base 10 digits (0 - 9)
+ * Any non-alphanumeric character (for example: !, $, #, or %)
+ */
+ private void checkPasswordChars( String password ) throws PasswordPolicyException
+ {
+ BitSet bitSet = new BitSet( 4 );
+
+ char[] characters = password.toCharArray();
+
+ for ( char character : characters )
+ {
+ if ( Character.isLowerCase( character ) )
+ {
+ bitSet.set( 0 );
+ }
+ else
+ {
+ if ( Character.isUpperCase( character ) )
+ {
+ bitSet.set( 1 );
+ }
+ else
+ {
+ if ( Character.isDigit( character ) )
+ {
+ bitSet.set( 2 );
+ }
+ else
+ {
+ if ( !Character.isLetterOrDigit( character ) )
+ {
+ bitSet.set( 3 );
+ }
+ }
+ }
+ }
+ }
+
+ if ( bitSet.cardinality() != bitSet.size() )
+ {
+ throw new PasswordPolicyException(
+ "Password should contain a mix of uppercase, lowercase, digits and non-alphanumeric characters",
+ INSUFFICIENT_PASSWORD_QUALITY );
+ }
+ }
+
+
+ /**
+ * 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 ) 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 ) 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 ) 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 ) 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 == ModificationOperation.REMOVE_ATTRIBUTE )
+ {
+ pwdModDetails.setDelete( true );
+ }
+ else if ( op == ModificationOperation.REPLACE_ATTRIBUTE || op == ModificationOperation.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 ( policyConfig != null )
+ {
+ CoreSession session = opContext.getSession();
+
+ DN userDn = session.getAuthenticatedPrincipal().getDN();
+
+ if ( pwdResetSet.contains( userDn ) )
+ {
+ boolean isPPolicyReqCtrlPresent = opContext
+ .hasRequestControl( PasswordPolicyRequestControl.CONTROL_OID );
+ if ( isPPolicyReqCtrlPresent )
+ {
+ PasswordPolicyResponseControl pwdRespCtrl = new PasswordPolicyResponseControl();
+ pwdRespCtrl.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;
+ }
}
}