/*
* 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
* trunk/opends/resource/legal-notices/OpenDS.LICENSE
* or https://OpenDS.dev.java.net/OpenDS.LICENSE.
* 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
* trunk/opends/resource/legal-notices/OpenDS.LICENSE. 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
*
*
* Portions Copyright 2006 Sun Microsystems, Inc.
*/
package org.opends.server.extensions;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.concurrent.locks.Lock;
import org.opends.server.api.ClientConnection;
import org.opends.server.api.ExtendedOperationHandler;
import org.opends.server.config.ConfigEntry;
import org.opends.server.config.ConfigException;
import org.opends.server.core.DirectoryException;
import org.opends.server.core.DirectoryServer;
import org.opends.server.core.ExtendedOperation;
import org.opends.server.core.InitializationException;
import org.opends.server.core.LockManager;
import org.opends.server.core.ModifyOperation;
import org.opends.server.protocols.asn1.ASN1Element;
import org.opends.server.protocols.asn1.ASN1Exception;
import org.opends.server.protocols.asn1.ASN1OctetString;
import org.opends.server.protocols.asn1.ASN1Sequence;
import org.opends.server.protocols.internal.InternalClientConnection;
import org.opends.server.protocols.internal.InternalSearchOperation;
import org.opends.server.protocols.ldap.LDAPAttribute;
import org.opends.server.protocols.ldap.LDAPFilter;
import org.opends.server.protocols.ldap.LDAPModification;
import org.opends.server.types.Attribute;
import org.opends.server.types.AttributeType;
import org.opends.server.types.AttributeValue;
import org.opends.server.types.AuthenticationInfo;
import org.opends.server.types.ByteString;
import org.opends.server.types.DN;
import org.opends.server.types.Entry;
import org.opends.server.types.ModificationType;
import org.opends.server.types.ResultCode;
import org.opends.server.types.SearchResultEntry;
import org.opends.server.types.SearchScope;
import static org.opends.server.extensions.ExtensionsConstants.*;
import static org.opends.server.loggers.Debug.*;
import static org.opends.server.messages.ExtensionsMessages.*;
import static org.opends.server.messages.MessageHandler.*;
import static org.opends.server.util.ServerConstants.*;
import static org.opends.server.util.StaticUtils.*;
/**
* This class implements the password modify extended operation defined in RFC
* 3062. It includes support for requiring the user's current password as well
* as for generating a new password if none was provided.
*/
public class PasswordModifyExtendedOperation
extends ExtendedOperationHandler
{
/**
* The fully-qualified name of this class for debugging purposes.
*/
private static final String CLASS_NAME =
"org.opends.server.extensions.PasswordModifyExtendedOperation";
/**
* Create an instance of this password modify extended operation. All
* initialization should be performed in the
* initializeExtendedOperationHandler method.
*/
public PasswordModifyExtendedOperation()
{
super();
assert debugConstructor(CLASS_NAME);
}
/**
* Initializes this extended operation handler based on the information in the
* provided configuration entry. It should also register itself with the
* Directory Server for the particular kinds of extended operations that it
* will process.
*
* @param configEntry The configuration entry that contains the information
* to use to initialize this extended operation handler.
*
* @throws ConfigException If an unrecoverable problem arises in the
* process of performing the initialization.
*
* @throws InitializationException If a problem occurs during initialization
* that is not related to the server
* configuration.
*/
public void initializeExtendedOperationHandler(ConfigEntry configEntry)
throws ConfigException, InitializationException
{
assert debugEnter(CLASS_NAME, "initializeExtendedOperationHandler",
String.valueOf(configEntry));
// NYI -- parse the config entry for any settings that might be defined
// This can include:
// - Whether to require the old password
// - Whether to allow automatic generation of a new password
// - The class name for an algorithm to generate new passwords
DirectoryServer.registerSupportedExtension(OID_PASSWORD_MODIFY_REQUEST,
this);
}
/**
* Performs any finalization that may be necessary for this extended
* operation handler. By default, no finalization is performed.
*/
public void finalizeExtendedOperationHandler()
{
assert debugEnter(CLASS_NAME, "finalizeExtendedOperationHandler");
DirectoryServer.deregisterSupportedExtension(OID_PASSWORD_MODIFY_REQUEST);
}
/**
* Processes the provided extended operation.
*
* @param operation The extended operation to be processed.
*/
public void processExtendedOperation(ExtendedOperation operation)
{
assert debugEnter(CLASS_NAME, "processExtendedOperation",
String.valueOf(operation));
// Initialize the variables associated with components that may be included
// in the request.
ASN1OctetString userIdentity = null;
ASN1OctetString oldPassword = null;
ASN1OctetString newPassword = null;
// Parse the encoded request, if there is one.
ByteString requestValue = operation.getRequestValue();
if (requestValue != null)
{
try
{
ASN1Sequence requestSequence =
ASN1Sequence.decodeAsSequence(requestValue.value());
for (ASN1Element e : requestSequence.elements())
{
switch (e.getType())
{
case TYPE_PASSWORD_MODIFY_USER_ID:
userIdentity = e.decodeAsOctetString();
break;
case TYPE_PASSWORD_MODIFY_OLD_PASSWORD:
oldPassword = e.decodeAsOctetString();
break;
case TYPE_PASSWORD_MODIFY_NEW_PASSWORD:
newPassword = e.decodeAsOctetString();
break;
default:
operation.setResultCode(ResultCode.PROTOCOL_ERROR);
int msgID = MSGID_EXTOP_PASSMOD_ILLEGAL_REQUEST_ELEMENT_TYPE;
operation.appendErrorMessage(getMessage(msgID,
byteToHex(e.getType())));
return;
}
}
}
catch (ASN1Exception ae)
{
assert debugException(CLASS_NAME, "processExtendedOperation", ae);
operation.setResultCode(ResultCode.PROTOCOL_ERROR);
int msgID = MSGID_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST;
String message = getMessage(msgID, stackTraceToSingleLineString(ae));
operation.appendErrorMessage(message);
return;
}
}
// Get the DN of the user that issued the request.
DN requestorDN = operation.getAuthorizationDN();
// See if a user identity was provided. If so, then try to resolve it to
// an actual user.
DN userDN = null;
Entry userEntry = null;
Lock userLock = null;
try
{
if (userIdentity == null)
{
// This request must be targeted at changing the password for the
// currently-authenticated user. Make sure that the user actually is
// authenticated.
ClientConnection clientConnection = operation.getClientConnection();
AuthenticationInfo authInfo = clientConnection.getAuthenticationInfo();
if ((! authInfo.isAuthenticated()) || (requestorDN == null) ||
(requestorDN.isNullDN()))
{
operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
int msgID = MSGID_EXTOP_PASSMOD_NO_AUTH_OR_USERID;
operation.appendErrorMessage(getMessage(msgID));
return;
}
// If the user is connected over an insecure channel, then determine
// whether we should attempt to proceed.
if (! clientConnection.isSecure())
{
// NYI
}
// Retrieve a write lock on that user's entry.
userDN = requestorDN;
for (int i=0; i < 3; i++)
{
userLock = LockManager.lockWrite(userDN);
if (userLock != null)
{
break;
}
}
if (userLock == null)
{
operation.setResultCode(DirectoryServer.getServerErrorResultCode());
int msgID = MSGID_EXTOP_PASSMOD_CANNOT_LOCK_USER_ENTRY;
String message = getMessage(msgID, String.valueOf(userDN));
operation.appendErrorMessage(message);
return;
}
userEntry = getEntryByDN(operation, userDN);
if (userEntry == null)
{
return;
}
}
else
{
// There was a userIdentity section in the request. It should have
// started with either "dn:" to indicate that it contained a DN, or
// "u:" to indicate that it contained a user ID.
String authzIDStr = userIdentity.stringValue();
String lowerAuthzIDStr = toLowerCase(authzIDStr);
if (lowerAuthzIDStr.startsWith("dn:"))
{
try
{
userDN = DN.decode(authzIDStr.substring(3));
}
catch (DirectoryException de)
{
assert debugException(CLASS_NAME, "processExtendedOperation", de);
operation.setResultCode(ResultCode.INVALID_DN_SYNTAX);
int msgID = MSGID_EXTOP_PASSMOD_CANNOT_DECODE_AUTHZ_DN;
operation.appendErrorMessage(getMessage(msgID, authzIDStr));
return;
}
userEntry = getEntryByDN(operation, userDN);
if (userEntry == null)
{
return;
}
}
else if (lowerAuthzIDStr.startsWith("u:"))
{
userDN = getDNByUserID(operation, authzIDStr.substring(2));
if (userDN == null)
{
return;
}
userEntry = getEntryByDN(operation, userDN);
if (userEntry == null)
{
return;
}
}
else
{
// The authorization ID was in an illegal format.
operation.setResultCode(ResultCode.PROTOCOL_ERROR);
int msgID = MSGID_EXTOP_PASSMOD_INVALID_AUTHZID_STRING;
operation.appendErrorMessage(getMessage(msgID, authzIDStr));
return;
}
}
// At this point, we should have the user entry. If a current password
// was provided, then validate it. If not, then see if that's OK.
AttributeType pwType = DirectoryServer.getAttributeType("userpassword");
if (pwType == null)
{
pwType = DirectoryServer.getDefaultAttributeType("userPassword");
}
if (oldPassword == null)
{
// NYI -- Confirm that this is allowed.
}
else
{
// FIXME -- Use a more generic check to determine the correct attribute
List pwAttrList = userEntry.getAttribute(pwType);
if ((pwAttrList == null) || pwAttrList.isEmpty())
{
// There were no existing passwords, so the validation will fail.
operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
int msgID = MSGID_EXTOP_PASSMOD_INVALID_OLD_PASSWORD;
operation.appendErrorMessage(getMessage(msgID));
return;
}
AttributeValue matchValue = new AttributeValue(pwType, oldPassword);
boolean matchFound = false;
for (Attribute a : pwAttrList)
{
if (a.hasValue(matchValue))
{
matchFound = true;
break;
}
}
if (! matchFound)
{
// None of the password values matched what the user provided.
operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
int msgID = MSGID_EXTOP_PASSMOD_INVALID_OLD_PASSWORD;
operation.appendErrorMessage(getMessage(msgID));
return;
}
}
// See if a new password was provided. If not, then generate one.
boolean generatedPassword = false;
if (newPassword == null)
{
// FIXME -- use an extensible algorithm for generating the new password.
newPassword = new ASN1OctetString("newpassword");
generatedPassword = true;
}
ArrayList newPWValues =
new ArrayList(1);
newPWValues.add(newPassword);
// Create the modification to update the user's password.
LDAPAttribute pwAttr = new LDAPAttribute("userPassword", newPWValues);
ArrayList mods = new ArrayList(1);
mods.add(new LDAPModification(ModificationType.REPLACE, pwAttr));
// Get an internal connection and use it to perform the modification.
// FIXME -- Make a better determination here.
AuthenticationInfo authInfo = new AuthenticationInfo(requestorDN, false);
InternalClientConnection internalConnection = new
InternalClientConnection(authInfo);
ModifyOperation modifyOperation =
internalConnection.processModify(
new ASN1OctetString(userDN.toString()), mods);
ResultCode resultCode = modifyOperation.getResultCode();
if (resultCode != resultCode.SUCCESS)
{
operation.setResultCode(resultCode);
operation.setErrorMessage(modifyOperation.getErrorMessage());
operation.setReferralURLs(modifyOperation.getReferralURLs());
return;
}
// If we've gotten here, then everything is OK, so indicate that the
// operation was successful. If a password was generated, then include
// it in the response.
operation.setResultCode(ResultCode.SUCCESS);
if (generatedPassword)
{
ArrayList valueElements = new ArrayList(1);
newPassword.setType(TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD);
valueElements.add(newPassword);
ASN1Sequence valueSequence = new ASN1Sequence(valueElements);
operation.setResponseValue(new ASN1OctetString(valueSequence.encode()));
}
}
finally
{
if (userLock != null)
{
LockManager.unlock(userDN, userLock);
}
}
}
/**
* Retrieves the entry for the specified user based on the provided DN. If
* any problem is encountered or the requested entry does not exist, then the
* provided operation will be updated with appropriate result information and
* this method will return null. The caller must hold a write
* lock on the specified entry.
*
* @param operation The extended operation being processed.
* @param entryDN The DN of the user entry to retrieve.
*
* @return The requested entry, or null if there was no such
* entry or it could not be retrieved.
*/
private Entry getEntryByDN(ExtendedOperation operation, DN entryDN)
{
assert debugEnter(CLASS_NAME, "getEntryByDN", String.valueOf(operation),
String.valueOf(entryDN));
// Retrieve the user's entry from the directory. If it does not exist, then
// fail.
try
{
Entry userEntry = DirectoryServer.getEntry(entryDN);
if (userEntry == null)
{
operation.setResultCode(ResultCode.NO_SUCH_OBJECT);
int msgID = MSGID_EXTOP_PASSMOD_NO_USER_ENTRY_BY_AUTHZID;
operation.appendErrorMessage(getMessage(msgID,
String.valueOf(entryDN)));
// See if one of the entry's ancestors exists.
DN parentDN = entryDN.getParent();
while (parentDN != null)
{
try
{
if (DirectoryServer.entryExists(parentDN))
{
operation.setMatchedDN(parentDN);
break;
}
}
catch (Exception e)
{
assert debugException(CLASS_NAME, "getEntryByDN", e);
break;
}
parentDN = parentDN.getParent();
}
return null;
}
return userEntry;
}
catch (DirectoryException de)
{
assert debugException(CLASS_NAME, "getEntryByDN", de);
operation.setResultCode(de.getResultCode());
operation.appendErrorMessage(de.getErrorMessage());
operation.setMatchedDN(de.getMatchedDN());
operation.setReferralURLs(de.getReferralURLs());
return null;
}
}
/**
* Retrieves the DN of the user with the provided user ID. The DN will be
* obtained by performing a subtree search with a base of the null DN (i.e.,
* the root DSE) and therefore potentially searching across multiple backends.
* If any problem is encountered or the requested entry does not exist, then
* the provided operation will be updated with appropriate result information
* and this method will return null. The caller is not required
* to hold any locks.
*
* @param operation The extended operation being processed.
* @param userID The user ID for which to retrieve the DN.
*
* @return The requested DN, or null if there was no such entry
* or if a problem was encountered.
*/
private DN getDNByUserID(ExtendedOperation operation, String userID)
{
assert debugEnter(CLASS_NAME, "getDNByUserID", String.valueOf(operation),
String.valueOf(userID));
InternalClientConnection internalConnection =
InternalClientConnection.getRootConnection();
LDAPFilter rawFilter =
LDAPFilter.createEqualityFilter("uid", new ASN1OctetString(userID));
InternalSearchOperation internalSearch =
internalConnection.processSearch(new ASN1OctetString(),
SearchScope.WHOLE_SUBTREE, rawFilter);
ResultCode resultCode = internalSearch.getResultCode();
if (resultCode != ResultCode.SUCCESS)
{
operation.setResultCode(resultCode);
operation.setErrorMessage(internalSearch.getErrorMessage());
operation.setMatchedDN(internalSearch.getMatchedDN());
return null;
}
LinkedList entryList = internalSearch.getSearchEntries();
if ((entryList == null) || entryList.isEmpty())
{
operation.setResultCode(ResultCode.NO_SUCH_OBJECT);
int msgID = MSGID_EXTOP_PASSMOD_NO_DN_BY_AUTHZID;
operation.appendErrorMessage(getMessage(msgID, String.valueOf(userID)));
return null;
}
if (entryList.size() > 1)
{
operation.setResultCode(ResultCode.CONSTRAINT_VIOLATION);
int msgID = MSGID_EXTOP_PASSMOD_MULTIPLE_ENTRIES_BY_AUTHZID;
operation.appendErrorMessage(getMessage(msgID, String.valueOf(userID)));
return null;
}
return entryList.get(0).getDN();
}
}