Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
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