Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@
*
* Copyright 2008 Sun Microsystems, Inc.
* Portions Copyright 2012-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.extensions;

import org.forgerock.i18n.LocalizableMessage;

import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import java.util.Set;

import org.forgerock.opendj.config.server.ConfigurationChangeListener;
Expand Down Expand Up @@ -81,17 +84,28 @@ public void finalizePasswordValidator()
private boolean containsSubstring(String password, int minSubstringLength,
Attribute a)
{
// Clamp to at least 1 so an empty substring never matches unconditionally.
final int minLen = Math.max(1, minSubstringLength);
final int passwordLength = password.length();

// Precompute the lowercase password once to avoid repeated conversions.
final String passwordLower = password.toLowerCase(Locale.ROOT);

// Precompute lowercase attribute values once, outside the substring loops.
final List<String> attrValuesLower = new ArrayList<>(a.size());
for (ByteString val : a)
{
attrValuesLower.add(val.toString().toLowerCase(Locale.ROOT));
}

for (int i = 0; i < passwordLength; i++)
{
for (int j = i + minSubstringLength; j <= passwordLength; j++)
for (int j = i + minLen; j <= passwordLength; j++)
{
Attribute substring = Attributes.create(a.getAttributeDescription().getAttributeType(),
password.substring(i, j));
for (ByteString val : a)
final String pwdSubstring = passwordLower.substring(i, j);
for (String attrValueLower : attrValuesLower)
{
if (substring.contains(val))
if (attrValueLower.contains(pwdSubstring))
{
return true;
}
Expand Down Expand Up @@ -141,7 +155,8 @@ public boolean passwordIsAcceptable(ByteString newPassword,
if (a.contains(vf) ||
(config.isTestReversedPassword() && a.contains(vr)) ||
(config.isCheckSubstrings() &&
containsSubstring(password, minSubstringLength, a)))
(containsSubstring(password, minSubstringLength, a) ||
(config.isTestReversedPassword() && containsSubstring(reversed, minSubstringLength, a)))))
{
invalidReason.append(ERR_ATTRVALUE_VALIDATOR_PASSWORD_IN_ENTRY.get());
return false;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -585,11 +585,15 @@ public void searchInChangeNumberModeOnOneSuffixMultipleTimes() throws Exception
// write 4 changes starting from changenumber 1, and search them
String testName = "Multiple/1";
CSN[] csns = generateAndPublishUpdateMsgForEachOperationType(testName, false);
// Wait until changenumber 4 is visible before searching
assertChangelogAttributesInRootDSE(1, 4);
searchChangesForEachOperationTypeUsingChangeNumberMode(1, csns, testName);

// write 4 more changes starting from changenumber 5, and search them
testName = "Multiple/5";
csns = generateAndPublishUpdateMsgForEachOperationType(testName, false);
// Wait until changenumber 8 is visible before searching
assertChangelogAttributesInRootDSE(1, 8);
searchChangesForEachOperationTypeUsingChangeNumberMode(5, csns, testName);

// search from the provided change number: 6 (should be the add msg)
Expand Down Expand Up @@ -851,7 +855,7 @@ private List<SearchResultEntry> assertChangelogAttributesInRootDSE(
final int expectedFirstChangeNumber, final int expectedLastChangeNumber) throws Exception
{
TestTimer timer = new TestTimer.Builder()
.maxSleep(3, SECONDS)
.maxSleep(10, SECONDS)
.sleepTimes(100, MILLISECONDS)
.toTimer();
return timer.repeatUntilSuccess(new Callable<List<SearchResultEntry>>()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2012-2016 ForgeRock AS.
* Portions Copyright 2026 3A Systems, LLC.
*/
package org.opends.server.extensions;

Expand Down Expand Up @@ -370,6 +371,131 @@ public Object[][] getTestData()



/**
* Retrieves test data for substring and reversed-password substring checks
* using a user entry with uid=USN123.
*
* @throws Exception If an unexpected problem occurs.
*/
@DataProvider(name = "substringTestData")
public Object[][] getSubstringTestData()
throws Exception
{
Entry configEntry = TestCaseUtils.makeEntry(
"dn: cn=Attribute Value,cn=Password Validators,cn=config",
"objectClass: top",
"objectClass: ds-cfg-password-validator",
"objectClass: ds-cfg-attribute-value-password-validator",
"cn: Attribute Value",
"ds-cfg-java-class: org.opends.server.extensions." +
"AttributeValuePasswordValidator",
"ds-cfg-enabled: true",
"ds-cfg-match-attribute: uid",
"ds-cfg-check-substrings: true",
"ds-cfg-min-substring-length: 3",
"ds-cfg-test-reversed-password: true");

return new Object[][]
{
// BLOCK: forward match "N12" in "USN123"
new Object[] { configEntry, "USN123aa", false },
// BLOCK: forward match "N12" in "USN123"
new Object[] { configEntry, "aaUSN123", false },
// BLOCK: forward match "123" in "USN123"
new Object[] { configEntry, "U1sn123b", false },
// BLOCK: reverse-password match "123" — reversed("NsU321ab")="ba123UsN" contains "123"
new Object[] { configEntry, "NsU321ab", false },
// BLOCK: forward match "N12" in "USN123"
new Object[] { configEntry, "A9USN12z", false },
// BLOCK: forward match "USN" in "USN123"
new Object[] { configEntry, "xx123USN", false },
// BLOCK: reverse-password match "USN" — reversed("NSU123xy")="yx321USN" contains "USN"
new Object[] { configEntry, "NSU123xy", false },
// BLOCK: forward match "N12" in "USN123"
new Object[] { configEntry, "z9nUSN12", false },
// BLOCK: reverse-password match "123" — reversed("usN321AA")="AA123Nsu" contains "123"
new Object[] { configEntry, "usN321AA", false },
// BLOCK: forward match "USN" in "USN123"
new Object[] { configEntry, "1USN2abc", false },

// PASS: no username substrings detected
new Object[] { configEntry, "Sun3RiseA", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Rock7fall", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Tree9Bark", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Wave4Deep", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Glow5Star", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Rain8Drop", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Fire6Ash", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Mist2Hill", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Frog1Lake", true },
// PASS: no username substrings detected
new Object[] { configEntry, "Dust7Moon", true },
};
}



/**
* Tests substring and reversed-password substring checks against a user
* entry with uid=USN123.
*
* @param configEntry The configuration entry to use for the password
* validator.
* @param password The password to test with the validator.
* @param acceptable Indicates whether the provided password should be
* considered acceptable.
*
* @throws Exception If an unexpected problem occurs.
*/
@Test(dataProvider = "substringTestData")
public void testSubstringPasswordIsAcceptable(Entry configEntry,
String password,
boolean acceptable)
throws Exception
{
TestCaseUtils.initializeTestBackend(true);
Entry userEntry = TestCaseUtils.makeEntry(
"dn: uid=USN123,o=test",
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: USN123",
"givenName: USN",
"sn: 123",
"cn: USN 123",
"userPassword: doesntmatter");

AttributeValuePasswordValidator validator = initializePasswordValidator(configEntry);

ByteString pwOS = ByteString.valueOfUtf8(password);
ArrayList<Modification> mods = CollectionUtils.newArrayList(
new Modification(ModificationType.REPLACE, Attributes.create("userpassword", password)));

ModifyOperationBasis modifyOperation =
new ModifyOperationBasis(getRootConnection(), nextOperationID(), nextMessageID(),
new ArrayList<Control>(),
DN.valueOf("uid=USN123,o=test"), mods);

LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder();
assertEquals(validator.passwordIsAcceptable(pwOS,
new HashSet<ByteString>(0), modifyOperation,
userEntry, invalidReason),
acceptable, invalidReason.toString());

validator.finalizePasswordValidator();
}



/**
* Tests the {@code passwordIsAcceptable} method using the provided
* information.
Expand Down
Loading