From 85e5ab92ac335ce41ed775364459a9629b2c77c4 Mon Sep 17 00:00:00 2001
From: Matthew Swift <matthew.swift@forgerock.com>
Date: Thu, 29 Sep 2011 09:50:04 +0000
Subject: [PATCH] Issue OPENDJ-262: Implement pass through authentication (PTA)

---
 opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java                          |  230 +++++++++++++++++++++++++++++++-
 opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java                                                         |   20 ++
 opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java |  116 +++++++++++-----
 opendj-sdk/opends/resource/schema/02-config.ldif                                                                                   |   14 ++
 4 files changed, 332 insertions(+), 48 deletions(-)

diff --git a/opendj-sdk/opends/resource/schema/02-config.ldif b/opendj-sdk/opends/resource/schema/02-config.ldif
index 6458a2c..42625ff 100644
--- a/opendj-sdk/opends/resource/schema/02-config.ldif
+++ b/opendj-sdk/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
diff --git a/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java b/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
index 18cd876..232f815 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/config/ConfigConstants.java
+++ b/opendj-sdk/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.
    */
diff --git a/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java b/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
index 8539712..500f1e1 100644
--- a/opendj-sdk/opends/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyFactory.java
+++ b/opendj-sdk/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();
     }
 
   };
diff --git a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java b/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
index 6689eca..801995d 100644
--- a/opendj-sdk/opends/tests/unit-tests-testng/src/server/org/opends/server/extensions/LDAPPassThroughAuthenticationPolicyTestCase.java
+++ b/opendj-sdk/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.

--
Gitblit v1.10.0