/*
* CDDL HEADER START
*
* The contents of this file are subject to the terms of the
* Common Development and Distribution License, Version 1.0 only
* (the "License"). You may not use this file except in compliance
* with the License.
*
* You can obtain a copy of the license at legal-notices/CDDLv1_0.txt
* or http://forgerock.org/license/CDDLv1.0.html.
* See the License for the specific language governing permissions
* and limitations under the License.
*
* When distributing Covered Code, include this CDDL HEADER in each
* file and include the License file at legal-notices/CDDLv1_0.txt.
* If applicable, add the following below this CDDL HEADER, with the
* fields enclosed by brackets "[]" replaced with your own identifying
* information:
* Portions Copyright [yyyy] [name of copyright owner]
*
* CDDL HEADER END
*
*
* Copyright 2006-2008 Sun Microsystems, Inc.
* Portions Copyright 2014-2015 ForgeRock AS
*/
package org.opends.server.extensions;
import static org.opends.messages.ExtensionMessages.*;
import static org.opends.server.util.StaticUtils.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.SortedSet;
import java.util.StringTokenizer;
import org.forgerock.i18n.LocalizableMessage;
import org.forgerock.i18n.slf4j.LocalizedLogger;
import org.forgerock.opendj.config.server.ConfigChangeResult;
import org.forgerock.opendj.config.server.ConfigException;
import org.forgerock.opendj.ldap.ByteString;
import org.forgerock.opendj.ldap.ResultCode;
import org.opends.server.admin.server.ConfigurationChangeListener;
import org.opends.server.admin.std.server.PasswordGeneratorCfg;
import org.opends.server.admin.std.server.RandomPasswordGeneratorCfg;
import org.opends.server.api.PasswordGenerator;
import org.opends.server.core.DirectoryServer;
import org.opends.server.types.*;
/**
* This class provides an implementation of a Directory Server password
* generator that will create random passwords based on fixed-length strings
* built from one or more character sets.
*/
public class RandomPasswordGenerator
extends PasswordGenerator<RandomPasswordGeneratorCfg>
implements ConfigurationChangeListener<RandomPasswordGeneratorCfg>
{
private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
/** The current configuration for this password validator. */
private RandomPasswordGeneratorCfg currentConfig;
/** The encoded list of character sets defined for this password generator. */
private SortedSet<String> encodedCharacterSets;
/** The DN of the configuration entry for this password generator. */
private DN configEntryDN;
/** The total length of the password that will be generated. */
private int totalLength;
/**
* The numbers of characters of each type that should be used to generate the
* passwords.
*/
private int[] characterCounts;
/** The character sets that should be used to generate the passwords. */
private NamedCharacterSet[] characterSets;
/**
* The lock to use to ensure that the character sets and counts are not
* altered while a password is being generated.
*/
private Object generatorLock;
/** The character set format string for this password generator. */
private String formatString;
/** {@inheritDoc} */
@Override
public void initializePasswordGenerator(
RandomPasswordGeneratorCfg configuration)
throws ConfigException, InitializationException
{
this.configEntryDN = configuration.dn();
generatorLock = new Object();
// Get the character sets for use in generating the password. At least one
// must have been provided.
HashMap<String,NamedCharacterSet> charsets = new HashMap<>();
try
{
encodedCharacterSets = configuration.getPasswordCharacterSet();
if (encodedCharacterSets.isEmpty())
{
LocalizableMessage message = ERR_RANDOMPWGEN_NO_CHARSETS.get(configEntryDN);
throw new ConfigException(message);
}
for (NamedCharacterSet s : NamedCharacterSet
.decodeCharacterSets(encodedCharacterSets))
{
if (charsets.containsKey(s.getName()))
{
LocalizableMessage message = ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(configEntryDN, s.getName());
throw new ConfigException(message);
}
else
{
charsets.put(s.getName(), s);
}
}
}
catch (ConfigException ce)
{
throw ce;
}
catch (Exception e)
{
logger.traceException(e);
LocalizableMessage message =
ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(getExceptionMessage(e));
throw new InitializationException(message, e);
}
// Get the value that describes which character set(s) and how many
// characters from each should be used.
try
{
formatString = configuration.getPasswordFormat();
StringTokenizer tokenizer = new StringTokenizer(formatString, ", ");
ArrayList<NamedCharacterSet> setList = new ArrayList<>();
ArrayList<Integer> countList = new ArrayList<>();
while (tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
try
{
int colonPos = token.indexOf(':');
String name = token.substring(0, colonPos);
int count = Integer.parseInt(token.substring(colonPos + 1));
NamedCharacterSet charset = charsets.get(name);
if (charset == null)
{
throw new ConfigException(ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(formatString, name));
}
else
{
setList.add(charset);
countList.add(count);
}
}
catch (ConfigException ce)
{
throw ce;
}
catch (Exception e)
{
logger.traceException(e);
LocalizableMessage message = ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(formatString);
throw new ConfigException(message, e);
}
}
characterSets = new NamedCharacterSet[setList.size()];
characterCounts = new int[characterSets.length];
totalLength = 0;
for (int i = 0; i < characterSets.length; i++)
{
characterSets[i] = setList.get(i);
characterCounts[i] = countList.get(i);
totalLength += characterCounts[i];
}
}
catch (ConfigException ce)
{
throw ce;
}
catch (Exception e)
{
logger.traceException(e);
LocalizableMessage message =
ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(getExceptionMessage(e));
throw new InitializationException(message, e);
}
configuration.addRandomChangeListener(this) ;
currentConfig = configuration;
}
/** {@inheritDoc} */
@Override
public void finalizePasswordGenerator()
{
currentConfig.removeRandomChangeListener(this);
}
/**
* Generates a password for the user whose account is contained in the
* specified entry.
*
* @param userEntry The entry for the user for whom the password is to be
* generated.
*
* @return The password that has been generated for the user.
*
* @throws DirectoryException If a problem occurs while attempting to
* generate the password.
*/
@Override
public ByteString generatePassword(Entry userEntry)
throws DirectoryException
{
StringBuilder buffer = new StringBuilder(totalLength);
synchronized (generatorLock)
{
for (int i=0; i < characterSets.length; i++)
{
characterSets[i].getRandomCharacters(buffer, characterCounts[i]);
}
}
return ByteString.valueOfUtf8(buffer);
}
/** {@inheritDoc} */
@Override
public boolean isConfigurationAcceptable(PasswordGeneratorCfg configuration,
List<LocalizableMessage> unacceptableReasons)
{
RandomPasswordGeneratorCfg config =
(RandomPasswordGeneratorCfg) configuration;
return isConfigurationChangeAcceptable(config, unacceptableReasons);
}
/** {@inheritDoc} */
@Override
public boolean isConfigurationChangeAcceptable(
RandomPasswordGeneratorCfg configuration,
List<LocalizableMessage> unacceptableReasons)
{
DN cfgEntryDN = configuration.dn();
// Get the character sets for use in generating the password.
// At least one must have been provided.
HashMap<String,NamedCharacterSet> charsets = new HashMap<>();
try
{
SortedSet<String> currentPasSet = configuration.getPasswordCharacterSet();
if (currentPasSet.isEmpty())
{
throw new ConfigException(ERR_RANDOMPWGEN_NO_CHARSETS.get(cfgEntryDN));
}
for (NamedCharacterSet s : NamedCharacterSet
.decodeCharacterSets(currentPasSet))
{
if (charsets.containsKey(s.getName()))
{
unacceptableReasons.add(ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(cfgEntryDN, s.getName()));
return false;
}
else
{
charsets.put(s.getName(), s);
}
}
}
catch (ConfigException ce)
{
unacceptableReasons.add(ce.getMessageObject());
return false;
}
catch (Exception e)
{
logger.traceException(e);
LocalizableMessage message = ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(
getExceptionMessage(e));
unacceptableReasons.add(message);
return false;
}
// Get the value that describes which character set(s) and how many
// characters from each should be used.
try
{
String formatString = configuration.getPasswordFormat() ;
StringTokenizer tokenizer = new StringTokenizer(formatString, ", ");
while (tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
try
{
int colonPos = token.indexOf(':');
String name = token.substring(0, colonPos);
NamedCharacterSet charset = charsets.get(name);
if (charset == null)
{
unacceptableReasons.add(ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(formatString, name));
return false;
}
}
catch (Exception e)
{
logger.traceException(e);
unacceptableReasons.add(ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(formatString));
return false;
}
}
}
catch (Exception e)
{
logger.traceException(e);
LocalizableMessage message = ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(
getExceptionMessage(e));
unacceptableReasons.add(message);
return false;
}
// If we've gotten here, then everything looks OK.
return true;
}
/** {@inheritDoc} */
@Override
public ConfigChangeResult applyConfigurationChange(
RandomPasswordGeneratorCfg configuration)
{
final ConfigChangeResult ccr = new ConfigChangeResult();
// Get the character sets for use in generating the password. At least one
// must have been provided.
SortedSet<String> newEncodedCharacterSets = null;
HashMap<String,NamedCharacterSet> charsets = new HashMap<>();
try
{
newEncodedCharacterSets = configuration.getPasswordCharacterSet();
if (newEncodedCharacterSets.isEmpty())
{
ccr.addMessage(ERR_RANDOMPWGEN_NO_CHARSETS.get(configEntryDN));
ccr.setResultCodeIfSuccess(ResultCode.OBJECTCLASS_VIOLATION);
}
else
{
for (NamedCharacterSet s :
NamedCharacterSet.decodeCharacterSets(newEncodedCharacterSets))
{
if (charsets.containsKey(s.getName()))
{
ccr.addMessage(ERR_RANDOMPWGEN_CHARSET_NAME_CONFLICT.get(configEntryDN, s.getName()));
ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
}
else
{
charsets.put(s.getName(), s);
}
}
}
}
catch (ConfigException ce)
{
ccr.addMessage(ce.getMessageObject());
ccr.setResultCodeIfSuccess(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
}
catch (Exception e)
{
logger.traceException(e);
ccr.addMessage(ERR_RANDOMPWGEN_CANNOT_DETERMINE_CHARSETS.get(getExceptionMessage(e)));
ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
}
// Get the value that describes which character set(s) and how many
// characters from each should be used.
ArrayList<NamedCharacterSet> newSetList = new ArrayList<>();
ArrayList<Integer> newCountList = new ArrayList<>();
String newFormatString = null;
try
{
newFormatString = configuration.getPasswordFormat();
StringTokenizer tokenizer = new StringTokenizer(newFormatString, ", ");
while (tokenizer.hasMoreTokens())
{
String token = tokenizer.nextToken();
try
{
int colonPos = token.indexOf(':');
String name = token.substring(0, colonPos);
int count = Integer.parseInt(token.substring(colonPos + 1));
NamedCharacterSet charset = charsets.get(name);
if (charset == null)
{
ccr.addMessage(ERR_RANDOMPWGEN_UNKNOWN_CHARSET.get(newFormatString, name));
ccr.setResultCodeIfSuccess(ResultCode.CONSTRAINT_VIOLATION);
}
else
{
newSetList.add(charset);
newCountList.add(count);
}
}
catch (Exception e)
{
logger.traceException(e);
ccr.addMessage(ERR_RANDOMPWGEN_INVALID_PWFORMAT.get(newFormatString));
ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
}
}
}
catch (Exception e)
{
logger.traceException(e);
ccr.addMessage(ERR_RANDOMPWGEN_CANNOT_DETERMINE_PWFORMAT.get(getExceptionMessage(e)));
ccr.setResultCodeIfSuccess(DirectoryServer.getServerErrorResultCode());
}
// If everything looks OK, then apply the changes.
if (ccr.getResultCode() == ResultCode.SUCCESS)
{
synchronized (generatorLock)
{
encodedCharacterSets = newEncodedCharacterSets;
formatString = newFormatString;
characterSets = new NamedCharacterSet[newSetList.size()];
characterCounts = new int[characterSets.length];
totalLength = 0;
for (int i=0; i < characterCounts.length; i++)
{
characterSets[i] = newSetList.get(i);
characterCounts[i] = newCountList.get(i);
totalLength += characterCounts[i];
}
}
}
return ccr;
}
}