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