mirror of https://github.com/OpenIdentityPlatform/OpenDJ.git

Matthew Swift
20.29.2011 87a32e534959a6ffaf12c6d69ce98197f7bee596
Issue OPENDJ-262: Implement pass through authentication (PTA)

Add support for administrative account lockout via the ds-pwp-account-disable operational attribute. The mechanism works in exactly the same way as for password policies and is supported by the proxy authorization controls and account usable response control. No support is provided for the password modify extended operation since this operation cannot be used with PTA accounts, nor is there support for the password policy state extended operation since this operation is very much targeted at password policy based accounts.
9 files modified
779 ■■■■■ changed files
opends/src/server/org/opends/server/api/AuthenticationPolicyState.java 295 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/controls/ProxiedAuthV1Control.java 21 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/controls/ProxiedAuthV2Control.java 21 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/PasswordPolicyState.java 241 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/core/SearchOperationBasis.java 40 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java 4 ●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/PlainSASLMechanismHandler.java 12 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendBindOperation.java 8 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/api/AuthenticationPolicyTestCase.java 137 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/api/AuthenticationPolicyState.java
@@ -29,9 +29,20 @@
import org.opends.server.types.ByteString;
import org.opends.server.types.DirectoryException;
import org.opends.server.types.Entry;
import static org.opends.messages.CoreMessages.*;
import static org.opends.server.config.ConfigConstants.OP_ATTR_ACCOUNT_DISABLED;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
import static org.opends.server.util.StaticUtils.stackTraceToSingleLineString;
import static org.opends.server.util.StaticUtils.toLowerCase;
import java.util.List;
import org.opends.messages.Message;
import org.opends.server.core.DirectoryServer;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.schema.GeneralizedTimeSyntax;
import org.opends.server.types.*;
@@ -43,6 +54,13 @@
public abstract class AuthenticationPolicyState
{
  /**
   * The tracer object for the debug logger.
   */
  private static final DebugTracer TRACER = getTracer();
  /**
   * Returns the authentication policy state for the user provided user. This
   * method is equivalent to the following:
   *
@@ -68,10 +86,10 @@
   *           policy for the user.
   * @see AuthenticationPolicy#forUser(Entry, boolean)
   */
  public final static AuthenticationPolicyState forUser(Entry userEntry,
      boolean useDefaultOnError) throws DirectoryException
  public final static AuthenticationPolicyState forUser(final Entry userEntry,
      final boolean useDefaultOnError) throws DirectoryException
  {
    AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry,
    final AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry,
        useDefaultOnError);
    return policy.createAuthenticationPolicyState(userEntry);
  }
@@ -79,37 +97,184 @@
  /**
   * Creates a new abstract authentication policy context.
   * A utility method which may be used by implementations in order to obtain
   * the value of the specified attribute from the provided entry as a boolean.
   *
   * @param entry
   *          The entry whose attribute is to be parsed as a boolean.
   * @param attributeType
   *          The attribute type whose value should be parsed as a boolean.
   * @return The attribute's value represented as a ConditionResult value, or
   *         ConditionResult.UNDEFINED if the specified attribute does not exist
   *         in the entry.
   * @throws DirectoryException
   *           If the value cannot be decoded as a boolean.
   */
  protected AuthenticationPolicyState()
  protected static final ConditionResult getBoolean(final Entry entry,
      final AttributeType attributeType) throws DirectoryException
  {
    // No implementation required.
    final List<Attribute> attrList = entry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (final Attribute a : attrList)
      {
        if (a.isEmpty())
        {
          continue;
        }
        final String valueString = toLowerCase(a.iterator().next().getValue()
            .toString());
        if (valueString.equals("true") || valueString.equals("yes")
            || valueString.equals("on") || valueString.equals("1"))
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Attribute %s resolves to true for user entry "
                + "%s", attributeType.getNameOrOID(), entry.getDN().toString());
          }
          return ConditionResult.TRUE;
        }
        if (valueString.equals("false") || valueString.equals("no")
            || valueString.equals("off") || valueString.equals("0"))
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Attribute %s resolves to false for user "
                + "entry %s", attributeType.getNameOrOID(), entry.getDN()
                .toString());
          }
          return ConditionResult.FALSE;
        }
        if (debugEnabled())
        {
          TRACER.debugError("Unable to resolve value %s for attribute %s "
              + "in user entry %s as a Boolean.", valueString,
              attributeType.getNameOrOID(), entry.getDN().toString());
        }
        final Message message = ERR_PWPSTATE_CANNOT_DECODE_BOOLEAN
            .get(valueString, attributeType.getNameOrOID(), entry.getDN()
                .toString());
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
            message);
      }
    }
    if (debugEnabled())
    {
      TRACER.debugInfo("Returning %s because attribute %s does not exist "
          + "in user entry %s", ConditionResult.UNDEFINED.toString(),
          attributeType.getNameOrOID(), entry.getDN().toString());
    }
    return ConditionResult.UNDEFINED;
  }
  /**
   * Returns {@code true} if the provided password value matches any of the
   * user's passwords.
   * A utility method which may be used by implementations in order to obtain
   * the value of the specified attribute from the provided entry as a time in
   * generalized time format.
   *
   * @param password
   *          The user-provided password to verify.
   * @return {@code true} if the provided password value matches any of the
   *         user's passwords.
   * @param entry
   *          The entry whose attribute is to be parsed as a boolean.
   * @param attributeType
   *          The attribute type whose value should be parsed as a generalized
   *          time value.
   * @return The requested time, or -1 if it could not be determined.
   * @throws DirectoryException
   *           If verification unexpectedly failed.
   *           If a problem occurs while attempting to decode the value as a
   *           generalized time.
   */
  public abstract boolean passwordMatches(ByteString password)
      throws DirectoryException;
  protected static final long getGeneralizedTime(final Entry entry,
      final AttributeType attributeType) throws DirectoryException
  {
    long timeValue = -1;
    final List<Attribute> attrList = entry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (final Attribute a : attrList)
      {
        if (a.isEmpty())
        {
          continue;
        }
        final AttributeValue v = a.iterator().next();
        try
        {
          timeValue = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(v
              .getNormalizedValue());
        }
        catch (final Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
            TRACER.debugWarning("Unable to decode value %s for attribute %s "
                + "in user entry %s: %s", v.getValue().toString(),
                attributeType.getNameOrOID(), entry.getDN().toString(),
                stackTraceToSingleLineString(e));
          }
          final Message message = ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME
              .get(v.getValue().toString(), attributeType.getNameOrOID(), entry
                  .getDN().toString(), String.valueOf(e));
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
              message, e);
        }
        break;
      }
    }
    if (timeValue == -1)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning -1 because attribute %s does not "
            + "exist in user entry %s", attributeType.getNameOrOID(), entry
            .getDN().toString());
      }
    }
    // FIXME: else to be consistent...
    return timeValue;
  }
  /**
   * Returns the authentication policy associated with this state.
   *
   * @return The authentication policy associated with this state.
   * A boolean indicating whether or not the account associated with this
   * authentication state has been administratively disabled.
   */
  public abstract AuthenticationPolicy getAuthenticationPolicy();
  protected ConditionResult isDisabled = ConditionResult.UNDEFINED;
  /**
   * The user entry associated with this authentication policy state.
   */
  protected final Entry userEntry;
  /**
   * Creates a new abstract authentication policy context.
   *
   * @param userEntry
   *          The user's entry.
   */
  protected AuthenticationPolicyState(final Entry userEntry)
  {
    this.userEntry = userEntry;
  }
@@ -129,6 +294,76 @@
  /**
   * Returns the authentication policy associated with this state.
   *
   * @return The authentication policy associated with this state.
   */
  public abstract AuthenticationPolicy getAuthenticationPolicy();
  /**
   * Returns {@code true} if this authentication policy state is associated with
   * a user whose account has been administratively disabled.
   * <p>
   * The default implementation is use the value of the "ds-pwp-account-disable"
   * attribute in the user's entry.
   *
   * @return {@code true} if this authentication policy state is associated with
   *         a user whose account has been administratively disabled.
   */
  public boolean isDisabled()
  {
    final AttributeType type = DirectoryServer.getAttributeType(
        OP_ATTR_ACCOUNT_DISABLED, true);
    try
    {
      isDisabled = getBoolean(userEntry, type);
    }
    catch (final Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      isDisabled = ConditionResult.TRUE;
      if (debugEnabled())
      {
        TRACER.debugWarning("User %s is considered administratively "
            + "disabled because an error occurred while "
            + "attempting to make the determination: %s.", userEntry.getDN()
            .toString(), stackTraceToSingleLineString(e));
      }
      return true;
    }
    if (isDisabled == ConditionResult.UNDEFINED)
    {
      isDisabled = ConditionResult.FALSE;
      if (debugEnabled())
      {
        TRACER.debugInfo("User %s is not administratively disabled since "
            + "the attribute \"%s\" is not present in the entry.", userEntry
            .getDN().toString(), OP_ATTR_ACCOUNT_DISABLED);
      }
      return false;
    }
    if (debugEnabled())
    {
      TRACER.debugInfo("User %s %s administratively disabled.", userEntry
          .getDN().toString(), ((isDisabled == ConditionResult.TRUE) ? " is"
          : " is not"));
    }
    return isDisabled == ConditionResult.TRUE;
  }
  /**
   * Returns {@code true} if this authentication policy state is associated with
   * a password policy and the method {@link #getAuthenticationPolicy} will
   * return a {@code PasswordPolicy}.
@@ -140,4 +375,20 @@
  {
    return getAuthenticationPolicy().isPasswordPolicy();
  }
  /**
   * Returns {@code true} if the provided password value matches any of the
   * user's passwords.
   *
   * @param password
   *          The user-provided password to verify.
   * @return {@code true} if the provided password value matches any of the
   *         user's passwords.
   * @throws DirectoryException
   *           If verification unexpectedly failed.
   */
  public abstract boolean passwordMatches(ByteString password)
      throws DirectoryException;
}
opends/src/server/org/opends/server/controls/ProxiedAuthV1Control.java
@@ -32,7 +32,7 @@
import java.util.concurrent.locks.Lock;
import java.io.IOException;
import org.opends.server.api.AuthenticationPolicy;
import org.opends.server.api.AuthenticationPolicyState;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PasswordPolicyState;
import org.opends.server.protocols.asn1.*;
@@ -325,13 +325,20 @@
      // FIXME -- We should provide some mechanism for enabling debug
      // processing.
      AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry,
          false);
      if (policy.isPasswordPolicy())
      AuthenticationPolicyState state = AuthenticationPolicyState.forUser(
          userEntry, false);
      if (state.isDisabled())
      {
        PasswordPolicyState pwpState = (PasswordPolicyState) policy
            .createAuthenticationPolicyState(userEntry);
        if (pwpState.isDisabled() || pwpState.isAccountExpired() ||
        Message message = ERR_PROXYAUTH1_UNUSABLE_ACCOUNT.get(String
            .valueOf(userEntry.getDN()));
        throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, message);
      }
      if (state.isPasswordPolicy())
      {
        PasswordPolicyState pwpState = (PasswordPolicyState) state;
        if (pwpState.isAccountExpired() ||
            pwpState.lockedDueToFailures() ||
            pwpState.lockedDueToIdleInterval() ||
            pwpState.lockedDueToMaximumResetAge() ||
opends/src/server/org/opends/server/controls/ProxiedAuthV2Control.java
@@ -33,7 +33,7 @@
import java.util.concurrent.locks.Lock;
import java.io.IOException;
import org.opends.server.api.AuthenticationPolicy;
import org.opends.server.api.AuthenticationPolicyState;
import org.opends.server.api.IdentityMapper;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.PasswordPolicyState;
@@ -333,13 +333,20 @@
  private void checkAccountIsUsable(Entry userEntry)
      throws DirectoryException
  {
    AuthenticationPolicy policy = AuthenticationPolicy.forUser(userEntry,
        false);
    if (policy.isPasswordPolicy())
    AuthenticationPolicyState state = AuthenticationPolicyState.forUser(
        userEntry, false);
    if (state.isDisabled())
    {
      PasswordPolicyState pwpState = (PasswordPolicyState) policy
          .createAuthenticationPolicyState(userEntry);
      if (pwpState.isDisabled() || pwpState.isAccountExpired() ||
      Message message = ERR_PROXYAUTH2_UNUSABLE_ACCOUNT.get(String
          .valueOf(userEntry.getDN()));
      throw new DirectoryException(ResultCode.AUTHORIZATION_DENIED, message);
    }
    if (state.isPasswordPolicy())
    {
      PasswordPolicyState pwpState = (PasswordPolicyState) state;
      if (pwpState.isAccountExpired() ||
          pwpState.lockedDueToFailures() ||
          pwpState.lockedDueToIdleInterval() ||
          pwpState.lockedDueToMaximumResetAge() ||
opends/src/server/org/opends/server/core/PasswordPolicyState.java
@@ -77,9 +77,6 @@
  // The user entry with which this state information is associated.
  private final Entry userEntry;
  // The string representation of the user's DN.
  private final String userDNString;
@@ -169,7 +166,7 @@
   */
  PasswordPolicyState(PasswordPolicy policy, Entry userEntry, long currentTime)
  {
    this.userEntry   = userEntry;
    super(userEntry);
    this.currentTime = currentTime;
    this.userDNString     = userEntry.getDN().toString();
    this.passwordPolicy   = policy;
@@ -225,74 +222,6 @@
  /**
   * Retrieves the value of the specified attribute from the user's entry as a
   * time in generalized time format.
   *
   * @param  attributeType  The attribute type whose value should be parsed as a
   *                        generalized time value.
   *
   * @return  The requested time, or -1 if it could not be determined.
   *
   * @throws  DirectoryException  If a problem occurs while attempting to
   *                              decode the value as a generalized time.
   */
  private long getGeneralizedTime(AttributeType attributeType)
          throws DirectoryException
  {
    long timeValue = -1 ;
    List<Attribute> attrList = userEntry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        if (a.isEmpty()) continue;
        AttributeValue v = a.iterator().next();
        try
        {
          timeValue = GeneralizedTimeSyntax.decodeGeneralizedTimeValue(
                          v.getNormalizedValue());
        }
        catch (Exception e)
        {
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
            TRACER.debugWarning("Unable to decode value %s for attribute %s " +
                "in user entry %s: %s",
                v.getValue().toString(), attributeType.getNameOrOID(),
                userDNString, stackTraceToSingleLineString(e));
          }
          Message message = ERR_PWPSTATE_CANNOT_DECODE_GENERALIZED_TIME.
              get(v.getValue().toString(), attributeType.getNameOrOID(),
                  userDNString, String.valueOf(e));
          throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
                                       message, e);
        }
        break ;
      }
    }
    if (timeValue == -1)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning -1 because attribute %s does not " +
            "exist in user entry %s",
            attributeType.getNameOrOID(), userDNString);
      }
    }
    // FIXME: else to be consistent...
    return timeValue;
  }
  /**
   * Retrieves the set of values of the specified attribute from the user's
   * entry in generalized time format.
   *
@@ -359,84 +288,6 @@
  /**
   * Retrieves the value of the specified attribute from the user's entry as a
   * Boolean.
   *
   * @param  attributeType  The attribute type whose value should be parsed as a
   *                        Boolean.
   *
   * @return  The attribute's value represented as a ConditionResult value, or
   *          ConditionResult.UNDEFINED if the specified attribute does not
   *          exist in the entry.
   *
   * @throws  DirectoryException  If the value cannot be decoded as a Boolean.
   */
  private ConditionResult getBoolean(AttributeType attributeType)
          throws DirectoryException
  {
    List<Attribute> attrList = userEntry.getAttribute(attributeType);
    if (attrList != null)
    {
      for (Attribute a : attrList)
      {
        if (a.isEmpty()) continue;
        String valueString
             = toLowerCase(a.iterator().next().getValue().toString());
        if (valueString.equals("true") || valueString.equals("yes") ||
            valueString.equals("on") || valueString.equals("1"))
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Attribute %s resolves to true for user entry " +
                "%s", attributeType.getNameOrOID(), userDNString);
          }
          return ConditionResult.TRUE;
        }
        if (valueString.equals("false") || valueString.equals("no") ||
                 valueString.equals("off") || valueString.equals("0"))
        {
          if (debugEnabled())
          {
            TRACER.debugInfo("Attribute %s resolves to false for user " +
                "entry %s", attributeType.getNameOrOID(), userDNString);
          }
          return ConditionResult.FALSE;
        }
        if(debugEnabled())
        {
          TRACER.debugError("Unable to resolve value %s for attribute %s " +
              "in user entry %s as a Boolean.",
              valueString, attributeType.getNameOrOID(),
              userDNString);
        }
        Message message = ERR_PWPSTATE_CANNOT_DECODE_BOOLEAN.get(
            valueString, attributeType.getNameOrOID(), userDNString);
        throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX,
            message);
      }
    }
    if (debugEnabled())
    {
      TRACER.debugInfo("Returning %s because attribute %s does not exist " +
          "in user entry %s",
          ConditionResult.UNDEFINED.toString(),
          attributeType.getNameOrOID(), userDNString);
    }
    return ConditionResult.UNDEFINED;
  }
  /**
   * {@inheritDoc}
   */
  public PasswordPolicy getAuthenticationPolicy()
@@ -461,7 +312,7 @@
      try
      {
        passwordChangedTime = getGeneralizedTime(type);
        passwordChangedTime = getGeneralizedTime(userEntry, type);
      }
      catch (DirectoryException e)
      {
@@ -481,7 +332,7 @@
            OP_ATTR_CREATE_TIMESTAMP_LC, true);
        try
        {
          passwordChangedTime = getGeneralizedTime(createTimeType);
          passwordChangedTime = getGeneralizedTime(userEntry, createTimeType);
        }
        catch (DirectoryException e)
        {
@@ -626,7 +477,7 @@
         DirectoryServer.getAttributeType(OP_ATTR_CREATE_TIMESTAMP_LC, true);
    try
    {
      passwordChangedTime = getGeneralizedTime(createTimeType);
      passwordChangedTime = getGeneralizedTime(userEntry, createTimeType);
      if (passwordChangedTime < 0)
      {
        passwordChangedTime = 0;
@@ -640,81 +491,13 @@
  /**
   * Indicates whether the user account has been administratively disabled.
   *
   * @return  <CODE>true</CODE> if the user account has been administratively
   *          disabled, or <CODE>false</CODE> otherwise.
   */
  public boolean isDisabled()
  {
    if (isDisabled != ConditionResult.UNDEFINED)
    {
      if (debugEnabled())
      {
        TRACER.debugInfo("Returning stored result of %b for user %s",
            (isDisabled == ConditionResult.TRUE), userDNString);
      }
      return isDisabled == ConditionResult.TRUE;
    }
    AttributeType type =
         DirectoryServer.getAttributeType(OP_ATTR_ACCOUNT_DISABLED, true);
    try
    {
      isDisabled = getBoolean(type);
    }
    catch (Exception e)
    {
      if (debugEnabled())
      {
        TRACER.debugCaught(DebugLogLevel.ERROR, e);
      }
      isDisabled = ConditionResult.TRUE;
      if (debugEnabled())
      {
          TRACER.debugWarning("User %s is considered administratively " +
              "disabled because an error occurred while attempting to make " +
              "the determination: %s.",
                       userDNString, stackTraceToSingleLineString(e));
      }
      return true;
    }
    if (isDisabled == ConditionResult.UNDEFINED)
    {
      isDisabled = ConditionResult.FALSE;
      if (debugEnabled())
      {
        TRACER.debugInfo("User %s is not administratively disabled since " +
            "the attribute \"%s\" is not present in the entry.",
            userDNString, OP_ATTR_ACCOUNT_DISABLED);
      }
      return false;
    }
    if (debugEnabled())
    {
      TRACER.debugInfo("User %s %s administratively disabled.",
          userDNString,
          ((isDisabled == ConditionResult.TRUE) ? " is" : " is not"));
    }
    return isDisabled == ConditionResult.TRUE;
  }
  /**
   * Updates the user entry to indicate whether user account has been
   * administratively disabled.
   *
   * @param  isDisabled  Indicates whether the user account has been
   *                     administratively disabled.
   * @param isDisabled
   *          Indicates whether the user account has been administratively
   *          disabled.
   */
  public void setDisabled(boolean isDisabled)
  {
@@ -775,7 +558,7 @@
    try
    {
      accountExpirationTime = getGeneralizedTime(type);
      accountExpirationTime = getGeneralizedTime(userEntry, type);
     }
    catch (Exception e)
    {
@@ -1216,7 +999,7 @@
    try
    {
      failureLockedTime = getGeneralizedTime(type);
      failureLockedTime = getGeneralizedTime(userEntry, type);
    }
    catch (Exception e)
    {
@@ -1811,7 +1594,7 @@
    try
    {
      mustChangePassword = getBoolean(type);
      mustChangePassword = getBoolean(userEntry, type);
    }
    catch (Exception e)
    {
@@ -2335,7 +2118,7 @@
    try
    {
      requiredChangeTime = getGeneralizedTime(type);
      requiredChangeTime = getGeneralizedTime(userEntry, type);
    }
    catch (Exception e)
    {
@@ -2449,7 +2232,7 @@
           DirectoryServer.getAttributeType(OP_ATTR_PWPOLICY_WARNED_TIME, true);
      try
      {
        warnedTime = getGeneralizedTime(type);
        warnedTime = getGeneralizedTime(userEntry, type);
      }
      catch (Exception e)
      {
opends/src/server/org/opends/server/core/SearchOperationBasis.java
@@ -37,7 +37,7 @@
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import org.opends.server.api.AuthenticationPolicy;
import org.opends.server.api.AuthenticationPolicyState;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.plugin.PluginResult;
import org.opends.server.controls.AccountUsableResponseControl;
@@ -645,15 +645,19 @@
    // create it now.
    if (isIncludeUsableControl())
    {
      if (controls == null)
      {
        controls = new ArrayList<Control>(1);
      }
      try
      {
        // FIXME -- Need a way to enable PWP debugging.
        AuthenticationPolicy policy = AuthenticationPolicy
            .forUser(entry, false);
        if (policy.isPasswordPolicy())
        AuthenticationPolicyState state = AuthenticationPolicyState.forUser(
            entry, false);
        if (state.isPasswordPolicy())
        {
          PasswordPolicyState pwpState = (PasswordPolicyState) policy
              .createAuthenticationPolicyState(entry);
          PasswordPolicyState pwpState = (PasswordPolicyState) state;
          boolean isInactive = pwpState.isDisabled()
              || pwpState.isAccountExpired();
@@ -667,12 +671,6 @@
          {
            int secondsBeforeUnlock = pwpState.getSecondsUntilUnlock();
            int remainingGraceLogins = pwpState.getGraceLoginsRemaining();
            if (controls == null)
            {
              controls = new ArrayList<Control>(1);
            }
            controls
                .add(new AccountUsableResponseControl(isInactive, isReset,
                    isExpired, remainingGraceLogins, isLocked,
@@ -680,16 +678,24 @@
          }
          else
          {
            if (controls == null)
            {
              controls = new ArrayList<Control>(1);
            }
            int secondsBeforeExpiration = pwpState.getSecondsUntilExpiration();
            controls.add(new AccountUsableResponseControl(
                secondsBeforeExpiration));
          }
        }
        else
        {
          // Another type of authentication policy (e.g. PTA).
          if (state.isDisabled())
          {
            controls.add(new AccountUsableResponseControl(false, false, false,
                -1, true, -1));
          }
          else
          {
            controls.add(new AccountUsableResponseControl(-1));
          }
        }
      }
      catch (Exception e)
      {
opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -73,7 +73,6 @@
  // TODO: handle password policy response controls? AD?
  // TODO: custom aliveness pings
  // TODO: manage account lockout
  // TODO: cache password
  /**
@@ -1555,14 +1554,13 @@
    private final class StateImpl extends AuthenticationPolicyState
    {
      private final Entry userEntry;
      private ByteString cachedPassword = null;
      private StateImpl(final Entry userEntry)
      {
        this.userEntry = userEntry;
        super(userEntry);
      }
opends/src/server/org/opends/server/extensions/PlainSASLMechanismHandler.java
@@ -29,6 +29,7 @@
import static org.opends.messages.CoreMessages.*;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.loggers.debug.DebugLogger.getTracer;
@@ -504,6 +505,17 @@
      // the user's entry when the bind completes.
      AuthenticationPolicyState authState = AuthenticationPolicyState.forUser(
          userEntry, false);
      if (authState.isDisabled())
      {
        // Check to see if the user is administratively disabled or locked.
        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
        Message message = ERR_BIND_OPERATION_ACCOUNT_DISABLED.get(String
            .valueOf(userEntry.getDN()));
        bindOperation.setAuthFailureReason(message);
        return;
      }
      if (!authState.passwordMatches(ByteString.valueOf(password)))
      {
        bindOperation.setResultCode(ResultCode.INVALID_CREDENTIALS);
opends/src/server/org/opends/server/workflowelement/localbackend/LocalBackendBindOperation.java
@@ -675,6 +675,14 @@
      }
      else
      {
        // Check to see if the user is administratively disabled or locked.
        if (authPolicyState.isDisabled())
        {
          throw new DirectoryException(ResultCode.INVALID_CREDENTIALS,
              ERR_BIND_OPERATION_ACCOUNT_DISABLED.get(String.valueOf(userEntry
                  .getDN())));
        }
        // Invoke pre-operation plugins.
        if (!invokePreOpPlugins())
        {
opends/tests/unit-tests-testng/src/server/org/opends/server/api/AuthenticationPolicyTestCase.java
@@ -30,6 +30,7 @@
import static org.testng.Assert.assertEquals;
import static org.testng.Assert.assertFalse;
import static org.testng.Assert.assertNull;
import static org.testng.Assert.assertTrue;
import org.opends.server.TestCaseUtils;
@@ -38,6 +39,7 @@
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.types.*;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -54,6 +56,8 @@
   */
  private final class MockPolicy extends AuthenticationPolicy
  {
    private final boolean isDisabled;
    private boolean isPolicyFinalized = false;
    private boolean isStateFinalized = false;
@@ -93,9 +97,9 @@
     *
     * @return The password which was tested.
     */
    public String getMatchedPassword()
    public ByteString getMatchedPassword()
    {
      return matchedPassword.toString();
      return matchedPassword;
    }
@@ -105,10 +109,13 @@
     *
     * @param matches
     *          The result to always return from {@code passwordMatches}.
     * @param isDisabled
     *          The result to return from {@code isDisabled}.
     */
    public MockPolicy(boolean matches)
    public MockPolicy(boolean matches, boolean isDisabled)
    {
      this.matches = matches;
      this.isDisabled = isDisabled;
    }
@@ -129,7 +136,7 @@
    public AuthenticationPolicyState createAuthenticationPolicyState(
        Entry userEntry, long time) throws DirectoryException
    {
      return new AuthenticationPolicyState()
      return new AuthenticationPolicyState(userEntry)
      {
        /**
@@ -147,6 +154,16 @@
        /**
         * {@inheritDoc}
         */
        public boolean isDisabled()
        {
          return MockPolicy.this.isDisabled;
        }
        /**
         * {@inheritDoc}
         */
        public void finalizeStateAfterBind() throws DirectoryException
        {
          isStateFinalized = true;
@@ -202,29 +219,22 @@
  /**
   * Test simple authentication where password validation succeeds.
   * Returns test data for the simple/sasl tests.
   *
   * @throws Exception
   *           If an unexpected exception occurred.
   * @return Test data for the simple/sasl tests.
   */
  @Test
  public void testSimpleBindAllowed() throws Exception
  @DataProvider
  public Object[][] testBindData()
  {
    testSimpleBind(true);
  }
  /**
   * Test simple authentication where password validation fails.
   *
   * @throws Exception
   *           If an unexpected exception occurred.
   */
  @Test
  public void testSimpleBindRefused() throws Exception
  {
    testSimpleBind(false);
    // @formatter:off
    return new Object[][] {
        /* password matches, account is disabled */
        { false, false },
        { false,  true },
        {  true, false },
        {  true,  true },
    };
    // @formatter:on
  }
@@ -232,34 +242,18 @@
  /**
   * Test simple authentication where password validation succeeds.
   *
   * @param matches
   *          The result to always return from {@code passwordMatches}.
   * @param isDisabled
   *          The result to return from {@code isDisabled}.
   * @throws Exception
   *           If an unexpected exception occurred.
   */
  @Test
  public void testSASLPLAINBindAllowed() throws Exception
  @Test(dataProvider = "testBindData")
  public void testSimpleBind(boolean matches, boolean isDisabled)
      throws Exception
  {
    testSASLPLAINBind(true);
  }
  /**
   * Test simple authentication where password validation fails.
   *
   * @throws Exception
   *           If an unexpected exception occurred.
   */
  @Test
  public void testSASLPLAINBindRefused() throws Exception
  {
    testSASLPLAINBind(false);
  }
  private void testSimpleBind(boolean allow) throws Exception
  {
    MockPolicy policy = new MockPolicy(allow);
    MockPolicy policy = new MockPolicy(matches, isDisabled);
    DirectoryServer.registerAuthenticationPolicy(policyDN, policy);
    try
    {
@@ -287,13 +281,24 @@
      BindOperation bind = conn.processSimpleBind(userDNString, "password");
      // Check authentication result.
      assertEquals(bind.getResultCode(), allow ? ResultCode.SUCCESS
      assertEquals(bind.getResultCode(),
          matches & !isDisabled ? ResultCode.SUCCESS
          : ResultCode.INVALID_CREDENTIALS);
      // Verify interaction with the policy/state.
      assertTrue(policy.isStateFinalized());
      assertFalse(policy.isPolicyFinalized());
      assertEquals(policy.getMatchedPassword(), "password");
      if (!isDisabled)
      {
        assertEquals(policy.getMatchedPassword().toString(), "password");
      }
      else
      {
        // If the account is disabled then the password should not have been
        // checked. This is important because we want to avoid potentially
        // expensive password fetches (e.g. PTA).
        assertNull(policy.getMatchedPassword());
      }
    }
    finally
    {
@@ -304,9 +309,21 @@
  private void testSASLPLAINBind(boolean allow) throws Exception
  /**
   * Test simple authentication where password validation succeeds.
   *
   * @param matches
   *          The result to always return from {@code passwordMatches}.
   * @param isDisabled
   *          The result to return from {@code isDisabled}.
   * @throws Exception
   *           If an unexpected exception occurred.
   */
  @Test(dataProvider = "testBindData")
  public void testSASLPLAINBind(boolean matches, boolean isDisabled)
      throws Exception
  {
    MockPolicy policy = new MockPolicy(allow);
    MockPolicy policy = new MockPolicy(matches, isDisabled);
    DirectoryServer.registerAuthenticationPolicy(policyDN, policy);
    try
    {
@@ -342,13 +359,24 @@
          credentials.toByteString());
      // Check authentication result.
      assertEquals(bind.getResultCode(), allow ? ResultCode.SUCCESS
      assertEquals(bind.getResultCode(),
          matches & !isDisabled ? ResultCode.SUCCESS
          : ResultCode.INVALID_CREDENTIALS);
      // Verify interaction with the policy/state.
      assertTrue(policy.isStateFinalized());
      assertFalse(policy.isPolicyFinalized());
      assertEquals(policy.getMatchedPassword(), "password");
      if (!isDisabled)
      {
        assertEquals(policy.getMatchedPassword().toString(), "password");
      }
      else
      {
        // If the account is disabled then the password should not have been
        // checked. This is important because we want to avoid potentially
        // expensive password fetches (e.g. PTA).
        assertNull(policy.getMatchedPassword());
      }
    }
    finally
    {
@@ -356,4 +384,5 @@
      assertTrue(policy.isPolicyFinalized());
    }
  }
}