001/*
002 * Copyright 2011-2024 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2011-2024 Ping Identity Corporation
007 *
008 * Licensed under the Apache License, Version 2.0 (the "License");
009 * you may not use this file except in compliance with the License.
010 * You may obtain a copy of the License at
011 *
012 *    http://www.apache.org/licenses/LICENSE-2.0
013 *
014 * Unless required by applicable law or agreed to in writing, software
015 * distributed under the License is distributed on an "AS IS" BASIS,
016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
017 * See the License for the specific language governing permissions and
018 * limitations under the License.
019 */
020/*
021 * Copyright (C) 2011-2024 Ping Identity Corporation
022 *
023 * This program is free software; you can redistribute it and/or modify
024 * it under the terms of the GNU General Public License (GPLv2 only)
025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only)
026 * as published by the Free Software Foundation.
027 *
028 * This program is distributed in the hope that it will be useful,
029 * but WITHOUT ANY WARRANTY; without even the implied warranty of
030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
031 * GNU General Public License for more details.
032 *
033 * You should have received a copy of the GNU General Public License
034 * along with this program; if not, see <http://www.gnu.org/licenses>.
035 */
036package com.unboundid.ldap.listener;
037
038
039
040import java.security.SecureRandom;
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.HashSet;
044import java.util.List;
045
046import com.unboundid.asn1.ASN1OctetString;
047import com.unboundid.ldap.sdk.Control;
048import com.unboundid.ldap.sdk.DN;
049import com.unboundid.ldap.sdk.Entry;
050import com.unboundid.ldap.sdk.ExtendedRequest;
051import com.unboundid.ldap.sdk.ExtendedResult;
052import com.unboundid.ldap.sdk.LDAPException;
053import com.unboundid.ldap.sdk.Modification;
054import com.unboundid.ldap.sdk.ModificationType;
055import com.unboundid.ldap.sdk.ResultCode;
056import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedRequest;
057import com.unboundid.ldap.sdk.extensions.PasswordModifyExtendedResult;
058import com.unboundid.ldap.sdk.unboundidds.controls.NoOpRequestControl;
059import com.unboundid.util.Debug;
060import com.unboundid.util.NotMutable;
061import com.unboundid.util.NotNull;
062import com.unboundid.util.StaticUtils ;
063import com.unboundid.util.ThreadLocalSecureRandom;
064import com.unboundid.util.ThreadSafety;
065import com.unboundid.util.ThreadSafetyLevel;
066
067import static com.unboundid.ldap.listener.ListenerMessages.*;
068
069
070
071/**
072 * This class provides an implementation of an extended operation handler for
073 * the in-memory directory server that can be used to process the password
074 * modify extended operation as defined in
075 * <A HREF="http://www.ietf.org/rfc/rfc3062.txt">RFC 3062</A>.
076 */
077@NotMutable()
078@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
079public final class PasswordModifyExtendedOperationHandler
080       extends InMemoryExtendedOperationHandler
081{
082  /**
083   * Creates a new instance of this extended operation handler.
084   */
085  public PasswordModifyExtendedOperationHandler()
086  {
087    // No initialization is required.
088  }
089
090
091
092  /**
093   * {@inheritDoc}
094   */
095  @Override()
096  @NotNull()
097  public String getExtendedOperationHandlerName()
098  {
099    return "Password Modify";
100  }
101
102
103
104  /**
105   * {@inheritDoc}
106   */
107  @Override()
108  @NotNull()
109  public List<String> getSupportedExtendedRequestOIDs()
110  {
111    return Collections.singletonList(
112         PasswordModifyExtendedRequest.PASSWORD_MODIFY_REQUEST_OID);
113  }
114
115
116
117  /**
118   * {@inheritDoc}
119   */
120  @Override()
121  @NotNull()
122  public ExtendedResult processExtendedOperation(
123                             @NotNull final InMemoryRequestHandler handler,
124                             final int messageID,
125                             @NotNull final ExtendedRequest request)
126  {
127    // This extended operation handler supports the no operation control.  If
128    // any other control is present, then reject it if it's critical.
129    boolean noOperation = false;
130    for (final Control c : request.getControls())
131    {
132      if (c.getOID().equalsIgnoreCase(NoOpRequestControl.NO_OP_REQUEST_OID))
133      {
134        noOperation = true;
135      }
136      else if (c.isCritical())
137      {
138        return new ExtendedResult(messageID,
139             ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
140             ERR_PW_MOD_EXTOP_UNSUPPORTED_CONTROL.get(c.getOID()),
141             null, null, null, null, null);
142      }
143    }
144
145
146    // Decode the request.
147    final PasswordModifyExtendedRequest pwModRequest;
148    try
149    {
150      pwModRequest = new PasswordModifyExtendedRequest(request);
151    }
152    catch (final LDAPException le)
153    {
154      Debug.debugException(le);
155      return new ExtendedResult(messageID, le.getResultCode(),
156           le.getDiagnosticMessage(), le.getMatchedDN(), le.getReferralURLs(),
157           null, null, null);
158    }
159
160
161    // Get the elements of the request.
162    final String userIdentity = pwModRequest.getUserIdentity();
163    final byte[] oldPWBytes = pwModRequest.getOldPasswordBytes();
164    final byte[] newPWBytes = pwModRequest.getNewPasswordBytes();
165
166
167    // Determine the DN of the target user.
168    final DN targetDN;
169    if (userIdentity == null)
170    {
171      targetDN = handler.getAuthenticatedDN();
172    }
173    else
174    {
175      // The user identity should generally be a DN, but we'll also allow an
176      // authorization ID.
177      final String lowerUserIdentity = StaticUtils.toLowerCase(userIdentity);
178      if (lowerUserIdentity.startsWith("dn:") ||
179           lowerUserIdentity.startsWith("u:"))
180      {
181        try
182        {
183          targetDN = handler.getDNForAuthzID(userIdentity);
184        }
185        catch (final LDAPException le)
186        {
187          Debug.debugException(le);
188          return new PasswordModifyExtendedResult(messageID,
189               le.getResultCode(), le.getMessage(), le.getMatchedDN(),
190               le.getReferralURLs(), null, le.getResponseControls());
191        }
192      }
193      else
194      {
195        try
196        {
197          targetDN = new DN(userIdentity);
198        }
199        catch (final LDAPException le)
200        {
201          Debug.debugException(le);
202          return new PasswordModifyExtendedResult(messageID,
203               ResultCode.INVALID_DN_SYNTAX,
204               ERR_PW_MOD_EXTOP_CANNOT_PARSE_USER_IDENTITY.get(userIdentity),
205               null, null, null, null);
206        }
207      }
208    }
209
210    if ((targetDN == null) || targetDN.isNullDN())
211    {
212      return new PasswordModifyExtendedResult(messageID,
213           ResultCode.UNWILLING_TO_PERFORM, ERR_PW_MOD_NO_IDENTITY.get(),
214           null, null, null, null);
215    }
216
217    final Entry userEntry = handler.getEntry(targetDN);
218    if (userEntry == null)
219    {
220      return new PasswordModifyExtendedResult(messageID,
221           ResultCode.UNWILLING_TO_PERFORM,
222           ERR_PW_MOD_EXTOP_CANNOT_GET_USER_ENTRY.get(targetDN.toString()),
223           null, null, null, null);
224    }
225
226
227    // Make sure that the server is configured with at least one password
228    // attribute.
229    final List<String> passwordAttributes = handler.getPasswordAttributes();
230    if (passwordAttributes.isEmpty())
231    {
232      return new PasswordModifyExtendedResult(messageID,
233           ResultCode.UNWILLING_TO_PERFORM, ERR_PW_MOD_EXTOP_NO_PW_ATTRS.get(),
234           null, null, null, null);
235    }
236
237
238    // If an old password was provided, then validate it.  If not, then
239    // determine whether it is acceptable for no password to have been given.
240    if (oldPWBytes == null)
241    {
242      if (handler.getAuthenticatedDN().isNullDN())
243      {
244        return new PasswordModifyExtendedResult(messageID,
245             ResultCode.UNWILLING_TO_PERFORM,
246             ERR_PW_MOD_EXTOP_NO_AUTHENTICATION.get(), null, null, null, null);
247      }
248    }
249    else
250    {
251      final List<InMemoryDirectoryServerPassword> passwordList =
252           handler.getPasswordsInEntry(userEntry,
253                pwModRequest.getRawOldPassword());
254      if (passwordList.isEmpty())
255      {
256        return new PasswordModifyExtendedResult(messageID,
257             ResultCode.INVALID_CREDENTIALS, null, null, null, null, null);
258      }
259    }
260
261
262    // If no new password was provided, then generate a random password to use.
263    final byte[] pwBytes;
264    final ASN1OctetString genPW;
265    if (newPWBytes == null)
266    {
267      final SecureRandom random = ThreadLocalSecureRandom.get();
268      final byte[] pwAlphabet = StaticUtils.getBytes(
269           "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
270      pwBytes = new byte[8];
271      for (int i=0; i < pwBytes.length; i++)
272      {
273        pwBytes[i] = pwAlphabet[random.nextInt(pwAlphabet.length)];
274      }
275      genPW = new ASN1OctetString(pwBytes);
276    }
277    else
278    {
279      genPW   = null;
280      pwBytes = newPWBytes;
281    }
282
283
284    // Construct the set of modifications to apply to the user entry.  Iterate
285    // through the passwords
286
287    final List<InMemoryDirectoryServerPassword> existingPasswords =
288         handler.getPasswordsInEntry(userEntry, null);
289    final ArrayList<Modification> mods =
290         new ArrayList<>(existingPasswords.size()+1);
291    if (existingPasswords.isEmpty())
292    {
293      mods.add(new Modification(ModificationType.REPLACE,
294           passwordAttributes.get(0), pwBytes));
295    }
296    else
297    {
298      final HashSet<String> usedPWAttrs = new HashSet<>(
299           StaticUtils.computeMapCapacity(existingPasswords.size()));
300      for (final InMemoryDirectoryServerPassword p : existingPasswords)
301      {
302        final String attr = StaticUtils.toLowerCase(p.getAttributeName());
303        if (usedPWAttrs.isEmpty())
304        {
305          usedPWAttrs.add(attr);
306          mods.add(new Modification(ModificationType.REPLACE,
307               p.getAttributeName(), pwBytes));
308        }
309        else if (! usedPWAttrs.contains(attr))
310        {
311          usedPWAttrs.add(attr);
312          mods.add(new Modification(ModificationType.REPLACE,
313               p.getAttributeName()));
314        }
315      }
316    }
317
318
319    // If the no operation request control was provided, then return an
320    // appropriate result now.
321    if (noOperation)
322    {
323      return new PasswordModifyExtendedResult(messageID,
324           ResultCode.NO_OPERATION, INFO_PW_MOD_EXTOP_NO_OP.get(), null, null,
325           genPW, null);
326    }
327
328
329    // Attempt to modify the user password.
330    try
331    {
332      handler.modifyEntry(userEntry.getDN(), mods);
333      return new PasswordModifyExtendedResult(messageID, ResultCode.SUCCESS,
334           null, null, null, genPW, null);
335    }
336    catch (final LDAPException le)
337    {
338      Debug.debugException(le);
339      return new PasswordModifyExtendedResult(messageID, le.getResultCode(),
340           ERR_PW_MOD_EXTOP_CANNOT_CHANGE_PW.get(userEntry.getDN(),
341                le.getMessage()),
342           le.getMatchedDN(), le.getReferralURLs(), null, null);
343    }
344  }
345}