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

Matthew Swift
29.50.2011 94af7fde4bf372b77568f47e066925073748cb1f
Issue OPENDJ-262: Implement pass through authentication (PTA)

Implement LDAP PTA password caching and enable tests.



4 files modified
380 ■■■■ changed files
opends/resource/schema/02-config.ldif 14 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/config/ConfigConstants.java 20 ●●●●● patch | view | raw | blame | history
opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java 230 ●●●●● patch | view | raw | blame | history
opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java 116 ●●●●● patch | view | raw | blame | history
opends/resource/schema/02-config.ldif
@@ -2646,6 +2646,20 @@
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.15
  SINGLE-VALUE
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.26
  NAME 'ds-pta-cached-password'
  SYNTAX 1.3.6.1.4.1.26027.1.3.1
  SINGLE-VALUE
  NO-USER-MODIFICATION
  USAGE directoryOperation
  X-ORIGIN 'OpenDJ Directory Server' )
attributeTypes: ( 1.3.6.1.4.1.36733.2.1.1.27
  NAME 'ds-pta-cached-password-time'
  SYNTAX 1.3.6.1.4.1.1466.115.121.1.24
  SINGLE-VALUE
  NO-USER-MODIFICATION
  USAGE directoryOperation
  X-ORIGIN 'OpenDJ Directory Server' )
objectClasses: ( 1.3.6.1.4.1.26027.1.2.1
  NAME 'ds-cfg-access-control-handler'
  SUP top
opends/src/server/org/opends/server/config/ConfigConstants.java
@@ -23,6 +23,7 @@
 *
 *
 *      Copyright 2006-2010 Sun Microsystems, Inc.
 *      Portions copyright 2011 ForgeRock AS
 */
package org.opends.server.config;
@@ -3685,6 +3686,25 @@
  /**
   * The name of the operational attribute which will be put in user's entry in
   * order to cache a copy of their password for pass through authentication.
   */
  public static final String OP_ATTR_PTAPOLICY_CACHED_PASSWORD =
       "ds-pta-cached-password";
  /**
   * The name of the operational attribute which will be put in user's entry in
   * order to record the time at which their password was cached for pass
   * through authentication.
   */
  public static final String OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME =
       "ds-pta-cached-password-time";
  /**
   * The name of the attribute option used to indicate that a configuration
   * attribute has one or more pending values.
   */
opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
@@ -30,6 +30,7 @@
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.config.ConfigConstants.*;
import static org.opends.server.loggers.debug.DebugLogger.debugEnabled;
import static org.opends.server.protocols.ldap.LDAPConstants.*;
import static org.opends.server.util.StaticUtils.getExceptionMessage;
@@ -54,13 +55,18 @@
import org.opends.server.api.*;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyOperation;
import org.opends.server.loggers.debug.DebugLogger;
import org.opends.server.loggers.debug.DebugTracer;
import org.opends.server.protocols.asn1.ASN1Exception;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.ldap.*;
import org.opends.server.schema.GeneralizedTimeSyntax;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.tools.LDAPReader;
import org.opends.server.tools.LDAPWriter;
import org.opends.server.types.*;
import org.opends.server.util.TimeThread;
@@ -73,7 +79,6 @@
  // TODO: handle password policy response controls? AD?
  // TODO: custom aliveness pings
  // TODO: cache password
  // TODO: improve debug logging and error messages.
  /**
@@ -1527,12 +1532,23 @@
    /**
     * Returns the current time in milli-seconds in order to perform cached
     * password expiration checks.
     * Returns the current time in order to perform cached password expiration
     * checks. The returned string will be formatted as a a generalized time
     * string
     *
     * @return The current time in milli-seconds.
     * @return The current time.
     */
    long getCurrentTimeMillis();
    String getCurrentTime();
    /**
     * Returns the current time in order to perform cached password expiration
     * checks.
     *
     * @return The current time in MS.
     */
    long getCurrentTimeMS();
  }
@@ -1616,13 +1632,22 @@
    private final class StateImpl extends AuthenticationPolicyState
    {
      private ByteString cachedPassword = null;
      private final AttributeType cachedPasswordAttribute;
      private final AttributeType cachedPasswordTimeAttribute;
      private ByteString newCachedPassword = null;
      private StateImpl(final Entry userEntry)
      {
        super(userEntry);
        this.cachedPasswordAttribute = DirectoryServer.getAttributeType(
            OP_ATTR_PTAPOLICY_CACHED_PASSWORD, true);
        this.cachedPasswordTimeAttribute = DirectoryServer.getAttributeType(
            OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME, true);
      }
@@ -1633,10 +1658,52 @@
      @Override
      public void finalizeStateAfterBind() throws DirectoryException
      {
        if (cachedPassword != null)
        sharedLock.lock();
        try
        {
          // TODO: persist cached password if needed.
          cachedPassword = null;
          if (cfg.isUsePasswordCaching() && newCachedPassword != null)
          {
            // Update the user's entry to contain the cached password and
            // time stamp.
            ByteString encodedPassword = pwdStorageScheme
                .encodePasswordWithScheme(newCachedPassword);
            List<RawModification> modifications =
              new ArrayList<RawModification>(2);
            modifications.add(RawModification.create(ModificationType.REPLACE,
                OP_ATTR_PTAPOLICY_CACHED_PASSWORD, encodedPassword));
            modifications.add(RawModification.create(ModificationType.REPLACE,
                OP_ATTR_PTAPOLICY_CACHED_PASSWORD_TIME,
                provider.getCurrentTime()));
            InternalClientConnection conn = InternalClientConnection
                .getRootConnection();
            ModifyOperation internalModify = conn.processModify(userEntry
                .getDN().toString(), modifications);
            ResultCode resultCode = internalModify.getResultCode();
            if (resultCode != ResultCode.SUCCESS)
            {
              // The modification failed for some reason. This should not
              // prevent the bind from succeeded since we are only updating
              // cache data. However, the performance of the server may be
              // impacted, so log a debug warning message.
              if (debugEnabled())
              {
                TRACER.debugWarning(
                    "An error occurred while trying to update the LDAP PTA "
                        + "cached password for user %s: %s", userEntry.getDN()
                        .toString(), String.valueOf(internalModify
                        .getErrorMessage()));
              }
            }
            newCachedPassword = null;
          }
        }
        finally
        {
          sharedLock.unlock();
        }
      }
@@ -1663,8 +1730,13 @@
        sharedLock.lock();
        try
        {
          // First of determine the user name to use when binding to the remote
          // directory.
          // First check the cached password if enabled and available.
          if (passwordMatchesCachedPassword(password))
          {
            return true;
          }
          // The cache lookup failed, so perform full PTA.
          ByteString username = null;
          switch (cfg.getMappingPolicy())
@@ -1820,6 +1892,11 @@
          {
            connection = bindFactory.getConnection();
            connection.simpleBind(username, password);
            // The password matched, so cache it, it will be stored in the
            // user's entry when the state is finalized and only if caching is
            // enabled.
            newCachedPassword = password;
            return true;
          }
          catch (final DirectoryException e)
@@ -1852,6 +1929,118 @@
          sharedLock.unlock();
        }
      }
      private boolean passwordMatchesCachedPassword(ByteString password)
      {
        if (!cfg.isUsePasswordCaching())
        {
          return false;
        }
        // First determine if the cached password time is present and valid.
        boolean foundValidCachedPasswordTime = false;
        List<Attribute> cptlist = userEntry
            .getAttribute(cachedPasswordTimeAttribute);
        if (cptlist != null && !cptlist.isEmpty())
        {
          foundCachedPasswordTime:
          {
            for (Attribute attribute : cptlist)
            {
              // Ignore any attributes with options.
              if (!attribute.hasOptions())
              {
                for (AttributeValue value : attribute)
                {
                  try
                  {
                    long cachedPasswordTime = GeneralizedTimeSyntax
                        .decodeGeneralizedTimeValue(value.getNormalizedValue());
                    long currentTime = provider.getCurrentTimeMS();
                    long expiryTime = cachedPasswordTime
                        + (cfg.getCachedPasswordTTL() * 1000);
                    foundValidCachedPasswordTime = (expiryTime > currentTime);
                  }
                  catch (DirectoryException e)
                  {
                    // Fall-through and give up immediately.
                    if (debugEnabled())
                    {
                      TRACER.debugCaught(DebugLogLevel.ERROR, e);
                    }
                  }
                  break foundCachedPasswordTime;
                }
              }
            }
          }
        }
        if (!foundValidCachedPasswordTime)
        {
          // The cached password time was not found or it has expired, so give
          // up immediately.
          return false;
        }
        // Next determine if there is a cached password.
        ByteString cachedPassword = null;
        List<Attribute> cplist = userEntry
            .getAttribute(cachedPasswordAttribute);
        if (cplist != null && !cplist.isEmpty())
        {
          foundCachedPassword:
          {
            for (Attribute attribute : cplist)
            {
              // Ignore any attributes with options.
              if (!attribute.hasOptions())
              {
                for (AttributeValue value : attribute)
                {
                  cachedPassword = value.getValue();
                  break foundCachedPassword;
                }
              }
            }
          }
        }
        if (cachedPassword == null)
        {
          // The cached password was not found, so give up immediately.
          return false;
        }
        // Decode the password and match it according to its storage scheme.
        try
        {
          String[] userPwComponents = UserPasswordSyntax
              .decodeUserPassword(cachedPassword.toString());
          PasswordStorageScheme<?> scheme = DirectoryServer
              .getPasswordStorageScheme(userPwComponents[0]);
          if (scheme != null)
          {
            return scheme.passwordMatches(password,
                ByteString.valueOf(userPwComponents[1]));
          }
        }
        catch (DirectoryException e)
        {
          // Unable to decode the cached password, so give up.
          if (debugEnabled())
          {
            TRACER.debugCaught(DebugLogLevel.ERROR, e);
          }
        }
        return false;
      }
    }
@@ -1867,6 +2056,8 @@
    private ConnectionFactory searchFactory = null;
    private ConnectionFactory bindFactory = null;
    private PasswordStorageScheme<?> pwdStorageScheme = null;
    private PolicyImpl(
@@ -2062,6 +2253,12 @@
        bindFactory = new FailoverLoadBalancer(primaryBindLoadBalancer,
            secondaryBindLoadBalancer, scheduler);
      }
      if (cfg.isUsePasswordCaching())
      {
        pwdStorageScheme = DirectoryServer.getPasswordStorageScheme(cfg
            .getCachedPasswordStorageSchemeDN());
      }
    }
@@ -2137,9 +2334,16 @@
    public long getCurrentTimeMillis()
    public String getCurrentTime()
    {
      return System.currentTimeMillis();
      return TimeThread.getGMTTime();
    }
    public long getCurrentTimeMS()
    {
      return TimeThread.getTime();
    }
  };
opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
@@ -56,10 +56,12 @@
import org.opends.server.protocols.ldap.*;
import org.opends.server.schema.DirectoryStringSyntax;
import org.opends.server.schema.GeneralizedTimeSyntax;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.tools.LDAPReader;
import org.opends.server.tools.LDAPWriter;
import org.opends.server.types.*;
import org.opends.server.util.StaticUtils;
import org.opends.server.util.TimeThread;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
@@ -877,7 +879,7 @@
    private final Queue<Event<?>> expectedEvents = new LinkedList<Event<?>>();
    private final List<MockScheduledFuture> monitorRunnables = new LinkedList<MockScheduledFuture>();
    private long currentTimeMS = System.currentTimeMillis();
    private String currentTime = TimeThread.getGMTTime();
    // All methods unused excepted scheduleWithFixedDelay.
    private final ScheduledExecutorService mockScheduler = new ScheduledExecutorService()
@@ -1062,9 +1064,27 @@
    /**
     * {@inheritDoc}
     */
    public long getCurrentTimeMillis()
    public String getCurrentTime()
    {
      return currentTimeMS;
      return currentTime;
    }
    /**
     * {@inheritDoc}
     */
    public long getCurrentTimeMS()
    {
      try
      {
        return GeneralizedTimeSyntax.decodeGeneralizedTimeValue(ByteString
            .valueOf(currentTime));
      }
      catch (DirectoryException e)
      {
        throw new RuntimeException(e);
      }
    }
@@ -1101,9 +1121,9 @@
    MockProvider withCurrentTime(final long currentTimeMS)
    MockProvider withCurrentTime(final String currentTime)
    {
      this.currentTimeMS = currentTimeMS;
      this.currentTime = currentTime;
      return this;
    }
@@ -4271,7 +4291,7 @@
   * @throws Exception
   *           If an unexpected exception occurred.
   */
  @Test(enabled = false, dataProvider = "testPasswordCachingData")
  @Test(enabled = true, dataProvider = "testPasswordCachingData")
  public void testPasswordCaching(String cacheState, boolean matchesCache,
      boolean matchesReal) throws Exception
  {
@@ -4279,20 +4299,23 @@
    TestCaseUtils.initializeTestBackend(true);
    // Choose arbitrary date.
    final GregorianCalendar testTime = new GregorianCalendar(2010, 0, 1);
    final long testTimeMS = testTime.getTimeInMillis();
    final String testCurrentTimeGMT = "20100621120000Z";
    final String testCachedPassword = "{SSHA}N8QSu9kXHkODFxFtwtwVEqM5XMCfnSaq/5gWew==";
    final boolean expectPTA;
    final boolean expectCacheInfo;
    final boolean expectCacheUpdate;
    final boolean expectedBindResultIsSuccess;
    final Entry testUser;
    final String testCachedPassword = "{SSHA}N8QSu9kXHkODFxFtwtwVEqM5XMCfnSaq/5gWew==";
    final String testCachedPasswordTime;
    if (cacheState.equals("notPresent"))
    {
      expectPTA = true;
      expectCacheInfo = matchesReal;
      expectCacheUpdate = matchesReal;
      expectedBindResultIsSuccess = matchesReal;
      testCachedPasswordTime = null;
      testUser = TestCaseUtils.makeEntry(
          /* @formatter:off */
@@ -4306,9 +4329,11 @@
    }
    else if (cacheState.equals("presentValid"))
    {
      expectPTA = false;
      expectPTA = !matchesCache;
      expectCacheInfo = true;
      expectCacheUpdate = !matchesCache && matchesReal;
      expectedBindResultIsSuccess = matchesCache;
      expectedBindResultIsSuccess = matchesCache | matchesReal;
      testCachedPasswordTime = "20100621110000Z"; // 1 hour old
      // Create an entry whose cached password is 10s old.
      testUser = TestCaseUtils.makeEntry(
@@ -4319,7 +4344,7 @@
          "sn: user",
          "cn: test user",
          "ds-pta-cached-password: " + testCachedPassword,
          "ds-pta-cached-password-time: " + GeneralizedTimeSyntax.format(testTimeMS - 10000)
          "ds-pta-cached-password-time: " + testCachedPasswordTime
          /* @formatter:on */
      );
    }
@@ -4327,8 +4352,10 @@
    {
      // presentExpired
      expectPTA = true;
      expectCacheInfo = true;
      expectCacheUpdate = matchesReal;
      expectedBindResultIsSuccess = matchesReal;
      testCachedPasswordTime = "20100620110000Z"; // 1 day + 1 hour old
      // Create an entry whose cached password is more than 1 day old.
      testUser = TestCaseUtils.makeEntry(
@@ -4339,7 +4366,7 @@
          "sn: user",
          "cn: test user",
          "ds-pta-cached-password: " + testCachedPassword,
          "ds-pta-cached-password-time: " + GeneralizedTimeSyntax.format(testTimeMS - 100000)
          "ds-pta-cached-password-time: " + testCachedPasswordTime
          /* @formatter:on */
      );
    }
@@ -4359,24 +4386,25 @@
    // Mock configuration.
    final LDAPPassThroughAuthenticationPolicyCfg cfg = mockCfg()
        .withPrimaryServer(phost1).withMappedAttribute("uid")
        .withBaseDN("o=ad").withUsePasswordCaching(true);
        .withPrimaryServer(phost1).withUsePasswordCaching(true);
    // Create all the events.
    final MockProvider provider = new MockProvider();
    final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
        phost1, cfg);
    final MockProvider provider = new MockProvider().withCurrentTime(
        testCurrentTimeGMT).expectEvent(fe);
    if (expectPTA)
    {
      final GetLDAPConnectionFactoryEvent fe = new GetLDAPConnectionFactoryEvent(
          phost1, cfg);
      provider.expectEvent(fe);
      // Get connection then bind twice creating two pooled connections.
      final GetConnectionEvent ce = new GetConnectionEvent(fe);
      provider
          .expectEvent(ce)
          .expectEvent(
              new SimpleBindEvent(ce, "cn=test user,o=test", presentedPassword))
              new SimpleBindEvent(ce, "cn=test user,o=test", presentedPassword,
                  expectedBindResultIsSuccess ? ResultCode.SUCCESS
                      : ResultCode.INVALID_CREDENTIALS))
          .expectEvent(new CloseEvent(ce));
    }
@@ -4388,28 +4416,46 @@
    // Perform the authentication.
    final AuthenticationPolicyState state = policy
        .createAuthenticationPolicyState(userEntry);
        .createAuthenticationPolicyState(testUser);
    assertEquals(state.getAuthenticationPolicy(), policy);
    assertEquals(state.passwordMatches(ByteString.valueOf(presentedPassword)),
        expectedBindResultIsSuccess);
    state.finalizeStateAfterBind();
    // Check that the password has been cached if needed.
    if (expectCacheUpdate)
    Entry updatedTestUser = DirectoryServer.getEntry(DN
        .decode("cn=test user,o=test"));
    String newCachedPassword = updatedTestUser.getAttributeValue(
        DirectoryServer.getAttributeType("ds-pta-cached-password"),
        DirectoryStringSyntax.DECODER);
    String newCachedPasswordTime = updatedTestUser.getAttributeValue(
        DirectoryServer.getAttributeType("ds-pta-cached-password-time"),
        DirectoryStringSyntax.DECODER);
    if (expectCacheInfo)
    {
      Entry updatedTestUser = DirectoryServer.getEntry(DN
          .decode("cn=test user,o=test"));
      assertNotNull(newCachedPassword);
      assertNotNull(newCachedPasswordTime);
      String newCachedPassword = updatedTestUser.getAttributeValue(
          DirectoryServer.getAttributeType("ds-pta-cached-password"),
          DirectoryStringSyntax.DECODER);
      assertFalse(newCachedPassword.equals(testCachedPassword));
      String newCachedPasswordTime = updatedTestUser.getAttributeValue(
          DirectoryServer.getAttributeType("ds-pta-cached-password-time"),
          DirectoryStringSyntax.DECODER);
      assertEquals(newCachedPasswordTime,
          GeneralizedTimeSyntax.format(testTimeMS));
      if (expectCacheUpdate)
      {
        assertFalse(newCachedPassword.equals(testCachedPassword));
        assertTrue(UserPasswordSyntax.isEncoded(ByteString
            .valueOf(newCachedPassword)));
        assertEquals(newCachedPasswordTime, testCurrentTimeGMT);
      }
      else
      {
        assertEquals(newCachedPassword, testCachedPassword);
        assertEquals(newCachedPasswordTime, testCachedPasswordTime);
      }
    }
    else
    {
      assertNull(newCachedPassword);
      assertNull(newCachedPasswordTime);
    }
    // Tear down and check final state.