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); assertFalse(newCachedPassword.equals(testCachedPassword)); String newCachedPasswordTime = updatedTestUser.getAttributeValue( DirectoryServer.getAttributeType("ds-pta-cached-password-time"), DirectoryStringSyntax.DECODER); assertEquals(newCachedPasswordTime, GeneralizedTimeSyntax.format(testTimeMS)); if (expectCacheInfo) { assertNotNull(newCachedPassword); assertNotNull(newCachedPasswordTime); 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.