/*
* The contents of this file are subject to the terms of the Common Development and
* Distribution License (the License). You may not use this file except in compliance with the
* License.
*
* You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the
* specific language governing permission and limitations under the License.
*
* When distributing Covered Software, include this CDDL Header Notice in each file and include
* the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL
* Header, with the fields enclosed by brackets [] replaced by your own identifying
* information: "Portions Copyright [year] [name of copyright owner]".
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2010-2016 ForgeRock AS.
*/
package org.opends.server.extensions;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.DN;
import org.forgerock.opendj.ldap.ResultCode;
import org.forgerock.opendj.ldap.requests.ModifyRequest;
import org.opends.server.TestCaseUtils;
import org.opends.server.api.PasswordStorageScheme;
import org.opends.server.types.Entry;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ModifyOperation;
import org.opends.server.core.PasswordPolicy;
import org.opends.server.schema.AuthPasswordSyntax;
import org.opends.server.schema.UserPasswordSyntax;
import org.opends.server.types.DirectoryException;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.DataProvider;
import org.testng.annotations.Test;
import static org.forgerock.opendj.ldap.ModificationType.*;
import static org.forgerock.opendj.ldap.requests.Requests.*;
import static org.opends.server.protocols.internal.InternalClientConnection.*;
import static org.testng.Assert.*;
/** A set of generic test cases for password storage schemes. */
@SuppressWarnings("javadoc")
public abstract class PasswordStorageSchemeTestCase
extends ExtensionsTestCase
{
/** The configuration entry for this password storage scheme. */
protected Entry configEntry;
/**
* The string representation of the DN of the configuration entry for this
* password storage scheme.
*/
private String configDNString;
/**
* Creates a new instance of this password storage scheme test case with the
* provided information.
*
* @param configDNString The string representation of the DN of the
* configuration entry, or null if there
* is none.
*/
protected PasswordStorageSchemeTestCase(String configDNString)
{
super();
this.configDNString = configDNString;
this.configEntry = null;
}
/**
* Ensures that the Directory Server is started before running any of these
* tests.
*/
@BeforeClass
public void startServer() throws Exception
{
TestCaseUtils.startServer();
if (configDNString != null)
{
configEntry = DirectoryServer.getEntry(DN.valueOf(configDNString));
}
}
/**
* Retrieves a set of passwords that may be used to test the password storage
* scheme.
*
* @return A set of passwords that may be used to test the password storage
* scheme.
*/
@DataProvider(name = "testPasswords")
public Object[][] getTestPasswords()
{
return getTestPasswordsStatic();
}
static Object[][] getTestPasswordsStatic()
{
return new Object[][]
{
new Object[] { ByteString.empty() },
new Object[] { ByteString.valueOfUtf8("") },
new Object[] { ByteString.valueOfUtf8("\u0000") },
new Object[] { ByteString.valueOfUtf8("\t") },
new Object[] { ByteString.valueOfUtf8("\n") },
new Object[] { ByteString.valueOfUtf8("\r\n") },
new Object[] { ByteString.valueOfUtf8(" ") },
new Object[] { ByteString.valueOfUtf8("Test1\tTest2\tTest3") },
new Object[] { ByteString.valueOfUtf8("Test1\nTest2\nTest3") },
new Object[] { ByteString.valueOfUtf8("Test1\r\nTest2\r\nTest3") },
new Object[] { ByteString.valueOfUtf8("a") },
new Object[] { ByteString.valueOfUtf8("ab") },
new Object[] { ByteString.valueOfUtf8("abc") },
new Object[] { ByteString.valueOfUtf8("abcd") },
new Object[] { ByteString.valueOfUtf8("abcde") },
new Object[] { ByteString.valueOfUtf8("abcdef") },
new Object[] { ByteString.valueOfUtf8("abcdefg") },
new Object[] { ByteString.valueOfUtf8("abcdefgh") },
new Object[] { ByteString.valueOfUtf8("The Quick Brown Fox Jumps Over " +
"The Lazy Dog") },
new Object[] { ByteString.valueOfUtf8("\u00BFD\u00F3nde est\u00E1 el " +
"ba\u00F1o?") }
};
}
/**
* Creates an instance of the password storage scheme, uses it to encode the
* provided password, and ensures that the encoded value is correct.
*
* @param plaintext The plain-text version of the password to encode.
* @throws Exception If an unexpected problem occurs.
*/
@Test(dataProvider = "testPasswords")
public void testStorageScheme(ByteString plaintext)
throws Exception
{
testStorageScheme(plaintext, getScheme());
}
static void testStorageScheme(ByteString plaintext,
PasswordStorageScheme> scheme) throws Exception
{
assertNotNull(scheme);
assertNotNull(scheme.getStorageSchemeName());
ByteString encodedPassword = scheme.encodePassword(plaintext);
assertNotNull(encodedPassword);
assertTrue(scheme.passwordMatches(plaintext, encodedPassword));
assertFalse(scheme.passwordMatches(plaintext,
ByteString.valueOfUtf8("garbage")));
ByteString schemeEncodedPassword =
scheme.encodePasswordWithScheme(plaintext);
String[] pwComponents = UserPasswordSyntax.decodeUserPassword(
schemeEncodedPassword.toString());
assertNotNull(pwComponents);
if (scheme.supportsAuthPasswordSyntax())
{
assertNotNull(scheme.getAuthPasswordSchemeName());
ByteString encodedAuthPassword = scheme.encodeAuthPassword(plaintext);
String[] authPWComponents = AuthPasswordSyntax.decodeAuthPassword(encodedAuthPassword.toString());
assertTrue(scheme.authPasswordMatches(plaintext, authPWComponents[1], authPWComponents[2]));
assertFalse(scheme.authPasswordMatches(plaintext, ",", "foo"));
assertFalse(scheme.authPasswordMatches(plaintext, "foo", ","));
}
else
{
try
{
scheme.encodeAuthPassword(plaintext);
throw new Exception("Expected encodedAuthPassword to fail for scheme " +
scheme.getStorageSchemeName() +
" because it doesn't support auth passwords.");
}
catch (DirectoryException de)
{
// This was expected.
}
assertFalse(scheme.authPasswordMatches(plaintext, "foo", "bar"));
}
if (scheme.isReversible())
{
assertEquals(scheme.getPlaintextValue(encodedPassword), plaintext);
}
else
{
try
{
scheme.getPlaintextValue(encodedPassword);
throw new Exception("Expected getPlaintextValue to fail for scheme " +
scheme.getStorageSchemeName() +
" because it is not reversible.");
}
catch (DirectoryException de)
{
// This was expected.
}
}
scheme.isStorageSchemeSecure();
}
@DataProvider
public static Object[][] passwordsForBinding()
{
return new Object[][]
{
// In the case of a clear-text password, these values will be shoved
// un-excaped into an LDIF file, so make sure they don't include \n
// or other characters that will cause LDIF parsing errors.
// We really don't need many test cases here, since that functionality
// is tested above.
new Object[] { ByteString.valueOfUtf8("a") },
new Object[] { ByteString.valueOfUtf8("abcdefgh") },
new Object[] { ByteString.valueOfUtf8("abcdefghi") },
};
}
/**
* An end-to-end test that verifies that we can set a pre-encoded password
* in a user entry, and then bind as that user using the cleartext password.
*/
@Test(dataProvider = "passwordsForBinding")
public void testSettingEncodedPassword(ByteString plainPassword) throws Exception
{
testSettingEncodedPassword(plainPassword, getScheme());
}
static void testSettingEncodedPassword(ByteString plainPassword,
PasswordStorageScheme> scheme) throws Exception, DirectoryException
{
// Start/clear-out the memory backend
TestCaseUtils.initializeTestBackend(true);
boolean allowPreencodedDefault = setAllowPreencodedPasswords(true);
try {
ByteString schemeEncodedPassword =
scheme.encodePasswordWithScheme(plainPassword);
// This code creates a user with the encoded password,
// and then verifies that they can bind with the raw password.
Entry userEntry = TestCaseUtils.makeEntry(
"dn: uid=test.user,o=test",
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: test.user",
"givenName: Test",
"sn: User",
"cn: Test User",
"ds-privilege-name: bypass-acl",
"userPassword: " + schemeEncodedPassword);
TestCaseUtils.addEntry(userEntry);
assertTrue(TestCaseUtils.canBind("uid=test.user,o=test",
plainPassword.toString()),
"Failed to bind when pre-encoded password = \"" +
schemeEncodedPassword + "\" and " +
"plaintext password = \"" +
plainPassword + "\"");
} finally {
setAllowPreencodedPasswords(allowPreencodedDefault);
}
}
/**
* Sets whether to allow pre-encoded password values for the
* current password storage scheme and returns the previous value so that
* it can be restored.
*
* @param allowPreencoded whether to allow pre-encoded passwords
* @return the previous value for the allow preencoded passwords
*/
protected static boolean setAllowPreencodedPasswords(boolean allowPreencoded)
throws Exception
{
// This code was borrowed from
// PasswordPolicyTestCase.testAllowPreEncodedPasswordsAuth
try {
DN dn = DN.valueOf("cn=Default Password Policy,cn=Password Policies,cn=config");
PasswordPolicy p = (PasswordPolicy) DirectoryServer.getAuthenticationPolicy(dn);
final boolean previousValue = p.isAllowPreEncodedPasswords();
ModifyRequest modifyRequest = newModifyRequest("cn=Default Password Policy,cn=Password Policies,cn=config")
.addModification(REPLACE, "ds-cfg-allow-pre-encoded-passwords", allowPreencoded);
ModifyOperation modifyOperation = getRootConnection().processModify(modifyRequest);
assertEquals(modifyOperation.getResultCode(), ResultCode.SUCCESS);
p = (PasswordPolicy) DirectoryServer.getAuthenticationPolicy(dn);
assertEquals(p.isAllowPreEncodedPasswords(), allowPreencoded);
return previousValue;
} catch (Exception e) {
System.err.println("Failed to set ds-cfg-allow-pre-encoded-passwords " +
" to " + allowPreencoded);
e.printStackTrace();
throw e;
}
}
protected static void testAuthPasswords(final String upperName,
String plaintextPassword, String encodedPassword) throws Exception
{
// Start/clear-out the memory backend
TestCaseUtils.initializeTestBackend(true);
boolean allowPreencodedDefault = setAllowPreencodedPasswords(true);
try
{
final String lowerName =
Character.toLowerCase(upperName.charAt(0)) + upperName.substring(1);
Entry userEntry = TestCaseUtils.makeEntry(
"dn: uid=" + lowerName + ".user,o=test",
"objectClass: top",
"objectClass: person",
"objectClass: organizationalPerson",
"objectClass: inetOrgPerson",
"uid: " + lowerName + ".user",
"givenName: " + upperName,
"sn: User",
"cn: " + upperName + " User",
"userPassword: " + encodedPassword);
TestCaseUtils.addEntry(userEntry);
assertTrue(TestCaseUtils.canBind(
"uid=" + lowerName + ".user,o=test", plaintextPassword),
"Failed to bind when pre-encoded password = \"" + encodedPassword
+ "\" and " + "plaintext password = \"" + plaintextPassword + "\"");
}
finally
{
setAllowPreencodedPasswords(allowPreencodedDefault);
}
}
/**
* Tests the encodeOffline method.
*
* @param plaintext
* The plaintext password to use for the test.
* @throws Exception
* If an unexpected problem occurs.
*/
@Test(dataProvider = "testPasswords")
public void testEncodeOffline(ByteString plaintext) throws Exception
{
PasswordStorageScheme> scheme = getScheme();
String passwordString = encodeOffline(plaintext.toByteArray());
if (passwordString != null)
{
String[] pwComps = UserPasswordSyntax.decodeUserPassword(passwordString);
ByteString encodedPassword = ByteString.valueOfUtf8(pwComps[1]);
assertTrue(scheme.passwordMatches(plaintext, encodedPassword));
}
}
/**
* Retrieves an initialized instance of this password storage scheme.
*
* @return An initialized instance of this password storage scheme.
* @throws Exception
* If an unexpected problem occurs.
*/
protected abstract PasswordStorageScheme> getScheme() throws Exception;
/**
* Encodes the provided plaintext password while offline.
*
* @param plaintextBytes
* The plaintext password in bytes to use for the test.
* @throws DirectoryException
* If an unexpected problem occurs.
*/
protected String encodeOffline(byte[] plaintextBytes) throws DirectoryException
{
return null;
}
}