001/*
002 * Copyright 2011-2020 Ping Identity Corporation
003 * All Rights Reserved.
004 */
005/*
006 * Copyright 2011-2020 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-2020 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.ThreadSafety;
064import com.unboundid.util.ThreadSafetyLevel;
065
066import static com.unboundid.ldap.listener.ListenerMessages.*;
067
068
069
070/**
071 * This class provides an implementation of an extended operation handler for
072 * the in-memory directory server that can be used to process the password
073 * modify extended operation as defined in
074 * <A HREF="http://www.ietf.org/rfc/rfc3062.txt">RFC 3062</A>.
075 */
076@NotMutable()
077@ThreadSafety(level=ThreadSafetyLevel.COMPLETELY_THREADSAFE)
078public final class PasswordModifyExtendedOperationHandler
079       extends InMemoryExtendedOperationHandler
080{
081  /**
082   * Creates a new instance of this extended operation handler.
083   */
084  public PasswordModifyExtendedOperationHandler()
085  {
086    // No initialization is required.
087  }
088
089
090
091  /**
092   * {@inheritDoc}
093   */
094  @Override()
095  @NotNull()
096  public String getExtendedOperationHandlerName()
097  {
098    return "Password Modify";
099  }
100
101
102
103  /**
104   * {@inheritDoc}
105   */
106  @Override()
107  @NotNull()
108  public List<String> getSupportedExtendedRequestOIDs()
109  {
110    return Collections.singletonList(
111         PasswordModifyExtendedRequest.PASSWORD_MODIFY_REQUEST_OID);
112  }
113
114
115
116  /**
117   * {@inheritDoc}
118   */
119  @Override()
120  @NotNull()
121  public ExtendedResult processExtendedOperation(
122                             @NotNull final InMemoryRequestHandler handler,
123                             final int messageID,
124                             @NotNull final ExtendedRequest request)
125  {
126    // This extended operation handler supports the no operation control.  If
127    // any other control is present, then reject it if it's critical.
128    boolean noOperation = false;
129    for (final Control c : request.getControls())
130    {
131      if (c.getOID().equalsIgnoreCase(NoOpRequestControl.NO_OP_REQUEST_OID))
132      {
133        noOperation = true;
134      }
135      else if (c.isCritical())
136      {
137        return new ExtendedResult(messageID,
138             ResultCode.UNAVAILABLE_CRITICAL_EXTENSION,
139             ERR_PW_MOD_EXTOP_UNSUPPORTED_CONTROL.get(c.getOID()),
140             null, null, null, null, null);
141      }
142    }
143
144
145    // Decode the request.
146    final PasswordModifyExtendedRequest pwModRequest;
147    try
148    {
149      pwModRequest = new PasswordModifyExtendedRequest(request);
150    }
151    catch (final LDAPException le)
152    {
153      Debug.debugException(le);
154      return new ExtendedResult(messageID, le.getResultCode(),
155           le.getDiagnosticMessage(), le.getMatchedDN(), le.getReferralURLs(),
156           null, null, null);
157    }
158
159
160    // Get the elements of the request.
161    final String userIdentity = pwModRequest.getUserIdentity();
162    final byte[] oldPWBytes = pwModRequest.getOldPasswordBytes();
163    final byte[] newPWBytes = pwModRequest.getNewPasswordBytes();
164
165
166    // Determine the DN of the target user.
167    final DN targetDN;
168    if (userIdentity == null)
169    {
170      targetDN = handler.getAuthenticatedDN();
171    }
172    else
173    {
174      // The user identity should generally be a DN, but we'll also allow an
175      // authorization ID.
176      final String lowerUserIdentity = StaticUtils.toLowerCase(userIdentity);
177      if (lowerUserIdentity.startsWith("dn:") ||
178           lowerUserIdentity.startsWith("u:"))
179      {
180        try
181        {
182          targetDN = handler.getDNForAuthzID(userIdentity);
183        }
184        catch (final LDAPException le)
185        {
186          Debug.debugException(le);
187          return new PasswordModifyExtendedResult(messageID,
188               le.getResultCode(), le.getMessage(), le.getMatchedDN(),
189               le.getReferralURLs(), null, le.getResponseControls());
190        }
191      }
192      else
193      {
194        try
195        {
196          targetDN = new DN(userIdentity);
197        }
198        catch (final LDAPException le)
199        {
200          Debug.debugException(le);
201          return new PasswordModifyExtendedResult(messageID,
202               ResultCode.INVALID_DN_SYNTAX,
203               ERR_PW_MOD_EXTOP_CANNOT_PARSE_USER_IDENTITY.get(userIdentity),
204               null, null, null, null);
205        }
206      }
207    }
208
209    if ((targetDN == null) || targetDN.isNullDN())
210    {
211      return new PasswordModifyExtendedResult(messageID,
212           ResultCode.UNWILLING_TO_PERFORM, ERR_PW_MOD_NO_IDENTITY.get(),
213           null, null, null, null);
214    }
215
216    final Entry userEntry = handler.getEntry(targetDN);
217    if (userEntry == null)
218    {
219      return new PasswordModifyExtendedResult(messageID,
220           ResultCode.UNWILLING_TO_PERFORM,
221           ERR_PW_MOD_EXTOP_CANNOT_GET_USER_ENTRY.get(targetDN.toString()),
222           null, null, null, null);
223    }
224
225
226    // Make sure that the server is configured with at least one password
227    // attribute.
228    final List<String> passwordAttributes = handler.getPasswordAttributes();
229    if (passwordAttributes.isEmpty())
230    {
231      return new PasswordModifyExtendedResult(messageID,
232           ResultCode.UNWILLING_TO_PERFORM, ERR_PW_MOD_EXTOP_NO_PW_ATTRS.get(),
233           null, null, null, null);
234    }
235
236
237    // If an old password was provided, then validate it.  If not, then
238    // determine whether it is acceptable for no password to have been given.
239    if (oldPWBytes == null)
240    {
241      if (handler.getAuthenticatedDN().isNullDN())
242      {
243        return new PasswordModifyExtendedResult(messageID,
244             ResultCode.UNWILLING_TO_PERFORM,
245             ERR_PW_MOD_EXTOP_NO_AUTHENTICATION.get(), null, null, null, null);
246      }
247    }
248    else
249    {
250      final List<InMemoryDirectoryServerPassword> passwordList =
251           handler.getPasswordsInEntry(userEntry,
252                pwModRequest.getRawOldPassword());
253      if (passwordList.isEmpty())
254      {
255        return new PasswordModifyExtendedResult(messageID,
256             ResultCode.INVALID_CREDENTIALS, null, null, null, null, null);
257      }
258    }
259
260
261    // If no new password was provided, then generate a random password to use.
262    final byte[] pwBytes;
263    final ASN1OctetString genPW;
264    if (newPWBytes == null)
265    {
266      final SecureRandom random = new SecureRandom();
267      final byte[] pwAlphabet = StaticUtils.getBytes(
268           "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789");
269      pwBytes = new byte[8];
270      for (int i=0; i < pwBytes.length; i++)
271      {
272        pwBytes[i] = pwAlphabet[random.nextInt(pwAlphabet.length)];
273      }
274      genPW = new ASN1OctetString(pwBytes);
275    }
276    else
277    {
278      genPW   = null;
279      pwBytes = newPWBytes;
280    }
281
282
283    // Construct the set of modifications to apply to the user entry.  Iterate
284    // through the passwords
285
286    final List<InMemoryDirectoryServerPassword> existingPasswords =
287         handler.getPasswordsInEntry(userEntry, null);
288    final ArrayList<Modification> mods =
289         new ArrayList<>(existingPasswords.size()+1);
290    if (existingPasswords.isEmpty())
291    {
292      mods.add(new Modification(ModificationType.REPLACE,
293           passwordAttributes.get(0), pwBytes));
294    }
295    else
296    {
297      final HashSet<String> usedPWAttrs = new HashSet<>(
298           StaticUtils.computeMapCapacity(existingPasswords.size()));
299      for (final InMemoryDirectoryServerPassword p : existingPasswords)
300      {
301        final String attr = StaticUtils.toLowerCase(p.getAttributeName());
302        if (usedPWAttrs.isEmpty())
303        {
304          usedPWAttrs.add(attr);
305          mods.add(new Modification(ModificationType.REPLACE,
306               p.getAttributeName(), pwBytes));
307        }
308        else if (! usedPWAttrs.contains(attr))
309        {
310          usedPWAttrs.add(attr);
311          mods.add(new Modification(ModificationType.REPLACE,
312               p.getAttributeName()));
313        }
314      }
315    }
316
317
318    // If the no operation request control was provided, then return an
319    // appropriate result now.
320    if (noOperation)
321    {
322      return new PasswordModifyExtendedResult(messageID,
323           ResultCode.NO_OPERATION, INFO_PW_MOD_EXTOP_NO_OP.get(), null, null,
324           genPW, null);
325    }
326
327
328    // Attempt to modify the user password.
329    try
330    {
331      handler.modifyEntry(userEntry.getDN(), mods);
332      return new PasswordModifyExtendedResult(messageID, ResultCode.SUCCESS,
333           null, null, null, genPW, null);
334    }
335    catch (final LDAPException le)
336    {
337      Debug.debugException(le);
338      return new PasswordModifyExtendedResult(messageID, le.getResultCode(),
339           ERR_PW_MOD_EXTOP_CANNOT_CHANGE_PW.get(userEntry.getDN(),
340                le.getMessage()),
341           le.getMatchedDN(), le.getReferralURLs(), null, null);
342    }
343  }
344}