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