001    /*
002     * CDDL HEADER START
003     *
004     * The contents of this file are subject to the terms of the
005     * Common Development and Distribution License, Version 1.0 only
006     * (the "License").  You may not use this file except in compliance
007     * with the License.
008     *
009     * You can obtain a copy of the license at
010     * trunk/opends/resource/legal-notices/OpenDS.LICENSE
011     * or https://OpenDS.dev.java.net/OpenDS.LICENSE.
012     * See the License for the specific language governing permissions
013     * and limitations under the License.
014     *
015     * When distributing Covered Code, include this CDDL HEADER in each
016     * file and include the License file at
017     * trunk/opends/resource/legal-notices/OpenDS.LICENSE.  If applicable,
018     * add the following below this CDDL HEADER, with the fields enclosed
019     * by brackets "[]" replaced with your own identifying information:
020     *      Portions Copyright [yyyy] [name of copyright owner]
021     *
022     * CDDL HEADER END
023     *
024     *
025     *      Copyright 2006-2008 Sun Microsystems, Inc.
026     */
027    package org.opends.server.extensions;
028    import org.opends.messages.Message;
029    
030    
031    
032    import java.util.ArrayList;
033    import java.util.Arrays;
034    import java.util.LinkedHashSet;
035    import java.util.List;
036    import java.util.HashSet;
037    import java.util.Set;
038    import java.util.concurrent.locks.Lock;
039    
040    import org.opends.server.admin.server.ConfigurationChangeListener;
041    import org.opends.server.admin.std.server.ExtendedOperationHandlerCfg;
042    import org.opends.server.admin.std.server.
043                PasswordModifyExtendedOperationHandlerCfg;
044    import org.opends.server.api.ClientConnection;
045    import org.opends.server.api.ExtendedOperationHandler;
046    import org.opends.server.api.IdentityMapper;
047    import org.opends.server.api.PasswordStorageScheme;
048    import org.opends.server.config.ConfigException;
049    import org.opends.server.controls.PasswordPolicyResponseControl;
050    import org.opends.server.controls.PasswordPolicyWarningType;
051    import org.opends.server.controls.PasswordPolicyErrorType;
052    import org.opends.server.core.DirectoryServer;
053    import org.opends.server.core.ExtendedOperation;
054    import org.opends.server.core.ModifyOperation;
055    import org.opends.server.core.PasswordPolicyState;
056    import org.opends.server.loggers.debug.DebugTracer;
057    import org.opends.server.protocols.asn1.ASN1Element;
058    import org.opends.server.protocols.asn1.ASN1Exception;
059    import org.opends.server.protocols.asn1.ASN1OctetString;
060    import org.opends.server.protocols.asn1.ASN1Sequence;
061    import org.opends.server.protocols.internal.InternalClientConnection;
062    import org.opends.server.schema.AuthPasswordSyntax;
063    import org.opends.server.schema.UserPasswordSyntax;
064    import org.opends.server.types.Attribute;
065    import org.opends.server.types.AttributeType;
066    import org.opends.server.types.AttributeValue;
067    import org.opends.server.types.AuthenticationInfo;
068    import org.opends.server.types.ByteString;
069    import org.opends.server.types.ConfigChangeResult;
070    import org.opends.server.types.Control;
071    import org.opends.server.types.DebugLogLevel;
072    import org.opends.server.types.DirectoryException;
073    import org.opends.server.types.DN;
074    import org.opends.server.types.Entry;
075    import org.opends.server.types.InitializationException;
076    import org.opends.server.types.LockManager;
077    import org.opends.server.types.Modification;
078    import org.opends.server.types.ModificationType;
079    import org.opends.server.types.Privilege;
080    import org.opends.server.types.ResultCode;
081    
082    import static org.opends.server.extensions.ExtensionsConstants.*;
083    import static org.opends.server.loggers.debug.DebugLogger.*;
084    import org.opends.server.loggers.ErrorLogger;
085    import static org.opends.messages.ExtensionMessages.*;
086    
087    import org.opends.messages.MessageBuilder;
088    import static org.opends.server.util.ServerConstants.*;
089    import static org.opends.server.util.StaticUtils.*;
090    
091    
092    /**
093     * This class implements the password modify extended operation defined in RFC
094     * 3062.  It includes support for requiring the user's current password as well
095     * as for generating a new password if none was provided.
096     */
097    public class PasswordModifyExtendedOperation
098           extends ExtendedOperationHandler<
099                        PasswordModifyExtendedOperationHandlerCfg>
100           implements ConfigurationChangeListener<
101                        PasswordModifyExtendedOperationHandlerCfg>
102    {
103      /**
104       * The tracer object for the debug logger.
105       */
106      private static final DebugTracer TRACER = getTracer();
107    
108    
109    
110      // The current configuration state.
111      private PasswordModifyExtendedOperationHandlerCfg currentConfig;
112    
113      // The DN of the identity mapper.
114      private DN identityMapperDN;
115    
116      // The reference to the identity mapper.
117      private IdentityMapper identityMapper;
118    
119      // The default set of supported control OIDs for this extended
120      private Set<String> supportedControlOIDs = new HashSet<String>(0);
121    
122    
123    
124      /**
125       * Create an instance of this password modify extended operation.  All
126       * initialization should be performed in the
127       * <CODE>initializeExtendedOperationHandler</CODE> method.
128       */
129      public PasswordModifyExtendedOperation()
130      {
131        super();
132    
133      }
134    
135    
136    
137    
138      /**
139       * Initializes this extended operation handler based on the information in the
140       * provided configuration.  It should also register itself with the
141       * Directory Server for the particular kinds of extended operations that it
142       * will process.
143       *
144       * @param   config      The configuration that contains the information
145       *                      to use to initialize this extended operation handler.
146       *
147       * @throws  ConfigException  If an unrecoverable problem arises in the
148       *                           process of performing the initialization.
149       *
150       * @throws  InitializationException  If a problem occurs during initialization
151       *                                   that is not related to the server
152       *                                   configuration.
153       */
154      public void initializeExtendedOperationHandler(
155           PasswordModifyExtendedOperationHandlerCfg config)
156             throws ConfigException, InitializationException
157      {
158        try
159        {
160          identityMapperDN = config.getIdentityMapperDN();
161          identityMapper = DirectoryServer.getIdentityMapper(identityMapperDN);
162          if (identityMapper == null)
163          {
164            Message message = ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(
165                String.valueOf(identityMapperDN), String.valueOf(config.dn()));
166            throw new ConfigException(message);
167          }
168        }
169        catch (Exception e)
170        {
171          if (debugEnabled())
172          {
173            TRACER.debugCaught(DebugLogLevel.ERROR, e);
174          }
175          Message message = ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get(
176              String.valueOf(config.dn()), getExceptionMessage(e));
177          throw new InitializationException(message, e);
178        }
179    
180    
181        supportedControlOIDs = new HashSet<String>();
182        supportedControlOIDs.add(OID_LDAP_NOOP_OPENLDAP_ASSIGNED);
183        supportedControlOIDs.add(OID_PASSWORD_POLICY_CONTROL);
184    
185    
186        // Save this configuration for future reference.
187        currentConfig = config;
188    
189        // Register this as a change listener.
190        config.addPasswordModifyChangeListener(this);
191    
192        DirectoryServer.registerSupportedExtension(OID_PASSWORD_MODIFY_REQUEST,
193                                                   this);
194    
195        registerControlsAndFeatures();
196      }
197    
198    
199    
200      /**
201       * Performs any finalization that may be necessary for this extended
202       * operation handler.  By default, no finalization is performed.
203       */
204      public void finalizeExtendedOperationHandler()
205      {
206        currentConfig.removePasswordModifyChangeListener(this);
207    
208        DirectoryServer.deregisterSupportedExtension(OID_PASSWORD_MODIFY_REQUEST);
209    
210        deregisterControlsAndFeatures();
211      }
212    
213    
214    
215      /**
216       * {@inheritDoc}
217       */
218      @Override()
219      public Set<String> getSupportedControls()
220      {
221        return supportedControlOIDs;
222      }
223    
224    
225    
226      /**
227       * Processes the provided extended operation.
228       *
229       * @param  operation  The extended operation to be processed.
230       */
231      public void processExtendedOperation(ExtendedOperation operation)
232      {
233        // Initialize the variables associated with components that may be included
234        // in the request.
235        ByteString userIdentity = null;
236        ByteString oldPassword  = null;
237        ByteString newPassword  = null;
238    
239    
240        // Look at the set of controls included in the request, if there are any.
241        boolean                   noOpRequested        = false;
242        boolean                   pwPolicyRequested    = false;
243        int                       pwPolicyWarningValue = 0;
244        PasswordPolicyErrorType   pwPolicyErrorType    = null;
245        PasswordPolicyWarningType pwPolicyWarningType  = null;
246        List<Control> controls = operation.getRequestControls();
247        if (controls != null)
248        {
249          for (Control c : controls)
250          {
251            String oid = c.getOID();
252            if (oid.equals(OID_LDAP_NOOP_OPENLDAP_ASSIGNED))
253            {
254              noOpRequested = true;
255            }
256            else if (oid.equals(OID_PASSWORD_POLICY_CONTROL))
257            {
258              pwPolicyRequested = true;
259            }
260          }
261        }
262    
263    
264        // Parse the encoded request, if there is one.
265        ByteString requestValue = operation.getRequestValue();
266        if (requestValue != null)
267        {
268          try
269          {
270            ASN1Sequence requestSequence =
271                 ASN1Sequence.decodeAsSequence(requestValue.value());
272    
273            for (ASN1Element e : requestSequence.elements())
274            {
275              switch (e.getType())
276              {
277                case TYPE_PASSWORD_MODIFY_USER_ID:
278                  userIdentity = e.decodeAsOctetString();
279                  break;
280                case TYPE_PASSWORD_MODIFY_OLD_PASSWORD:
281                  oldPassword = e.decodeAsOctetString();
282                  break;
283                case TYPE_PASSWORD_MODIFY_NEW_PASSWORD:
284                  newPassword = e.decodeAsOctetString();
285                  break;
286                default:
287                  operation.setResultCode(ResultCode.PROTOCOL_ERROR);
288    
289    
290                  operation.appendErrorMessage(
291                          ERR_EXTOP_PASSMOD_ILLEGAL_REQUEST_ELEMENT_TYPE.get(
292                                  byteToHex(e.getType())));
293                  return;
294              }
295            }
296          }
297          catch (ASN1Exception ae)
298          {
299            if (debugEnabled())
300            {
301              TRACER.debugCaught(DebugLogLevel.ERROR, ae);
302            }
303    
304            operation.setResultCode(ResultCode.PROTOCOL_ERROR);
305    
306            Message message = ERR_EXTOP_PASSMOD_CANNOT_DECODE_REQUEST.get(
307                    getExceptionMessage(ae));
308            operation.appendErrorMessage(message);
309    
310            return;
311          }
312        }
313    
314    
315        // Get the entry for the user that issued the request.
316        Entry requestorEntry = operation.getAuthorizationEntry();
317    
318    
319        // See if a user identity was provided.  If so, then try to resolve it to
320        // an actual user.
321        DN    userDN    = null;
322        Entry userEntry;
323        Lock  userLock  = null;
324    
325        try
326        {
327          if (userIdentity == null)
328          {
329            // This request must be targeted at changing the password for the
330            // currently-authenticated user.  Make sure that the user actually is
331            // authenticated.
332            ClientConnection   clientConnection = operation.getClientConnection();
333            AuthenticationInfo authInfo = clientConnection.getAuthenticationInfo();
334            if ((! authInfo.isAuthenticated()) || (requestorEntry == null))
335            {
336              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
337    
338              operation.appendErrorMessage(
339                      ERR_EXTOP_PASSMOD_NO_AUTH_OR_USERID.get());
340    
341              return;
342            }
343    
344    
345            // Retrieve a write lock on that user's entry.
346            userDN = requestorEntry.getDN();
347    
348            for (int i=0; i < 3; i++)
349            {
350              userLock = LockManager.lockWrite(userDN);
351    
352              if (userLock != null)
353              {
354                break;
355              }
356            }
357    
358            if (userLock == null)
359            {
360              operation.setResultCode(DirectoryServer.getServerErrorResultCode());
361    
362              Message message =
363                      ERR_EXTOP_PASSMOD_CANNOT_LOCK_USER_ENTRY.get(
364                              String.valueOf(userDN));
365              operation.appendErrorMessage(message);
366    
367              return;
368            }
369    
370    
371            userEntry = requestorEntry;
372          }
373          else
374          {
375            // There was a userIdentity section in the request.  It should have
376            // started with either "dn:" to indicate that it contained a DN, or
377            // "u:" to indicate that it contained a user ID.
378            String authzIDStr      = userIdentity.stringValue();
379            String lowerAuthzIDStr = toLowerCase(authzIDStr);
380            if (lowerAuthzIDStr.startsWith("dn:"))
381            {
382              try
383              {
384                userDN = DN.decode(authzIDStr.substring(3));
385              }
386              catch (DirectoryException de)
387              {
388                if (debugEnabled())
389                {
390                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
391                }
392    
393                operation.setResultCode(ResultCode.INVALID_DN_SYNTAX);
394    
395                operation.appendErrorMessage(
396                        ERR_EXTOP_PASSMOD_CANNOT_DECODE_AUTHZ_DN.get(authzIDStr));
397    
398                return;
399              }
400    
401              // If the provided DN is an alternate DN for a root user, then replace
402              // it with the actual root DN.
403              DN actualRootDN = DirectoryServer.getActualRootBindDN(userDN);
404              if (actualRootDN != null)
405              {
406                userDN = actualRootDN;
407              }
408    
409              userEntry = getEntryByDN(operation, userDN);
410              if (userEntry == null)
411              {
412                return;
413              }
414            }
415            else if (lowerAuthzIDStr.startsWith("u:"))
416            {
417              try
418              {
419                userEntry = identityMapper.getEntryForID(authzIDStr.substring(2));
420                if (userEntry == null)
421                {
422                  if (oldPassword == null)
423                  {
424                    operation.setResultCode(ResultCode.NO_SUCH_OBJECT);
425    
426                    operation.appendErrorMessage(
427                            ERR_EXTOP_PASSMOD_CANNOT_MAP_USER.get(authzIDStr));
428                  }
429                  else
430                  {
431                    operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
432    
433                    operation.appendAdditionalLogMessage(
434                            ERR_EXTOP_PASSMOD_CANNOT_MAP_USER.get(authzIDStr));
435                  }
436    
437                  return;
438                }
439                else
440                {
441                  userDN = userEntry.getDN();
442                }
443              }
444              catch (DirectoryException de)
445              {
446                if (debugEnabled())
447                {
448                  TRACER.debugCaught(DebugLogLevel.ERROR, de);
449                }
450    
451                if (oldPassword == null)
452                {
453                  operation.setResultCode(de.getResultCode());
454    
455                  operation.appendErrorMessage(ERR_EXTOP_PASSMOD_ERROR_MAPPING_USER
456                          .get(authzIDStr,de.getMessageObject()));
457                }
458                else
459                {
460                  operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
461    
462                  operation.appendAdditionalLogMessage(
463                          ERR_EXTOP_PASSMOD_ERROR_MAPPING_USER.get(
464                                  authzIDStr,
465                                  de.getMessageObject()));
466                }
467    
468                return;
469              }
470            }
471            else
472            {
473              // The authorization ID was in an illegal format.
474              operation.setResultCode(ResultCode.PROTOCOL_ERROR);
475    
476              operation.appendErrorMessage(
477                      ERR_EXTOP_PASSMOD_INVALID_AUTHZID_STRING.get(authzIDStr));
478    
479              return;
480            }
481          }
482    
483    
484          // At this point, we should have the user entry.  Get the associated
485          // password policy.
486          PasswordPolicyState pwPolicyState;
487          try
488          {
489            pwPolicyState = new PasswordPolicyState(userEntry, false);
490          }
491          catch (DirectoryException de)
492          {
493            if (debugEnabled())
494            {
495              TRACER.debugCaught(DebugLogLevel.ERROR, de);
496            }
497    
498            operation.setResultCode(DirectoryServer.getServerErrorResultCode());
499    
500            operation.appendErrorMessage(
501                    ERR_EXTOP_PASSMOD_CANNOT_GET_PW_POLICY.get(
502                            String.valueOf(userDN),
503                            de.getMessageObject()));
504            return;
505          }
506    
507    
508          // Determine whether the user is changing his own password or if it's an
509          // administrative reset.  If it's an administrative reset, then the
510          // requester must have the PASSWORD_RESET privilege.
511          boolean selfChange;
512          if (userIdentity == null)
513          {
514            selfChange = true;
515          }
516          else if (requestorEntry == null)
517          {
518            selfChange = (oldPassword != null);
519          }
520          else
521          {
522            selfChange = userDN.equals(requestorEntry.getDN());
523          }
524    
525          if (! selfChange)
526          {
527            ClientConnection clientConnection = operation.getClientConnection();
528            if (! clientConnection.hasPrivilege(Privilege.PASSWORD_RESET,
529                                                operation))
530            {
531              operation.appendErrorMessage(
532                      ERR_EXTOP_PASSMOD_INSUFFICIENT_PRIVILEGES.get());
533              operation.setResultCode(ResultCode.INSUFFICIENT_ACCESS_RIGHTS);
534              return;
535            }
536          }
537    
538    
539          // See if the account is locked.  If so, then reject the request.
540          if (pwPolicyState.isDisabled())
541          {
542            if (pwPolicyRequested)
543            {
544              pwPolicyErrorType =
545                   PasswordPolicyErrorType.ACCOUNT_LOCKED;
546              operation.addResponseControl(
547                   new PasswordPolicyResponseControl(pwPolicyWarningType,
548                                                     pwPolicyWarningValue,
549                                                     pwPolicyErrorType));
550            }
551    
552            Message message = ERR_EXTOP_PASSMOD_ACCOUNT_DISABLED.get();
553    
554            if (oldPassword == null)
555            {
556              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
557              operation.appendErrorMessage(message);
558            }
559            else
560            {
561              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
562              operation.appendAdditionalLogMessage(message);
563            }
564    
565            return;
566          }
567          else if (selfChange &&
568                   (pwPolicyState.lockedDueToFailures() ||
569                    pwPolicyState.lockedDueToIdleInterval() ||
570                    pwPolicyState.lockedDueToMaximumResetAge()))
571          {
572            if (pwPolicyRequested)
573            {
574              pwPolicyErrorType =
575                   PasswordPolicyErrorType.ACCOUNT_LOCKED;
576              operation.addResponseControl(
577                   new PasswordPolicyResponseControl(pwPolicyWarningType,
578                                                     pwPolicyWarningValue,
579                                                     pwPolicyErrorType));
580            }
581    
582            Message message = ERR_EXTOP_PASSMOD_ACCOUNT_LOCKED.get();
583    
584            if (oldPassword == null)
585            {
586              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
587              operation.appendErrorMessage(message);
588            }
589            else
590            {
591              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
592              operation.appendAdditionalLogMessage(message);
593            }
594    
595            return;
596          }
597    
598    
599          // If the current password was provided, then we'll need to verify whether
600          // it was correct.  If it wasn't provided but this is a self change, then
601          // make sure that's OK.
602          if (oldPassword == null)
603          {
604            if (selfChange && pwPolicyState.getPolicy().requireCurrentPassword())
605            {
606              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
607    
608              operation.appendErrorMessage(
609                      ERR_EXTOP_PASSMOD_REQUIRE_CURRENT_PW.get());
610    
611              if (pwPolicyRequested)
612              {
613                pwPolicyErrorType =
614                     PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD;
615                operation.addResponseControl(
616                     new PasswordPolicyResponseControl(pwPolicyWarningType,
617                                                       pwPolicyWarningValue,
618                                                       pwPolicyErrorType));
619              }
620    
621              return;
622            }
623          }
624          else
625          {
626            if (pwPolicyState.getPolicy().requireSecureAuthentication() &&
627                (! operation.getClientConnection().isSecure()))
628            {
629              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
630    
631              operation.appendAdditionalLogMessage(
632                      ERR_EXTOP_PASSMOD_SECURE_AUTH_REQUIRED.get());
633              return;
634            }
635    
636            if (pwPolicyState.passwordMatches(oldPassword))
637            {
638              pwPolicyState.setLastLoginTime();
639            }
640            else
641            {
642              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
643    
644              operation.appendAdditionalLogMessage(
645                      ERR_EXTOP_PASSMOD_INVALID_OLD_PASSWORD.get());
646    
647              pwPolicyState.updateAuthFailureTimes();
648              List<Modification> mods = pwPolicyState.getModifications();
649              if (! mods.isEmpty())
650              {
651                InternalClientConnection conn =
652                     InternalClientConnection.getRootConnection();
653                conn.processModify(userDN, mods);
654              }
655    
656              return;
657            }
658          }
659    
660    
661          // If it is a self password change and we don't allow that, then reject
662          // the request.
663          if (selfChange &&
664               (! pwPolicyState.getPolicy().allowUserPasswordChanges()))
665          {
666            if (pwPolicyRequested)
667            {
668              pwPolicyErrorType =
669                   PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED;
670              operation.addResponseControl(
671                   new PasswordPolicyResponseControl(pwPolicyWarningType,
672                                                     pwPolicyWarningValue,
673                                                     pwPolicyErrorType));
674            }
675    
676            if (oldPassword == null)
677            {
678              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
679    
680              operation.appendErrorMessage(
681                      ERR_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED.get());
682            }
683            else
684            {
685              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
686    
687              operation.appendAdditionalLogMessage(
688                      ERR_EXTOP_PASSMOD_USER_PW_CHANGES_NOT_ALLOWED.get());
689            }
690    
691            return;
692          }
693    
694    
695          // If we require secure password changes and the connection isn't secure,
696          // then reject the request.
697          if (pwPolicyState.getPolicy().requireSecurePasswordChanges() &&
698              (! operation.getClientConnection().isSecure()))
699          {
700            if (oldPassword == null)
701            {
702              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
703    
704              operation.appendErrorMessage(
705                      ERR_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED.get());
706            }
707            else
708            {
709              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
710    
711              operation.appendAdditionalLogMessage(
712                      ERR_EXTOP_PASSMOD_SECURE_CHANGES_REQUIRED.get());
713            }
714    
715            return;
716          }
717    
718    
719          // If it's a self-change request and the user is within the minimum age,
720          // then reject it.
721          if (selfChange && pwPolicyState.isWithinMinimumAge())
722          {
723            if (pwPolicyRequested)
724            {
725              pwPolicyErrorType =
726                   PasswordPolicyErrorType.PASSWORD_TOO_YOUNG;
727              operation.addResponseControl(
728                   new PasswordPolicyResponseControl(pwPolicyWarningType,
729                                                     pwPolicyWarningValue,
730                                                     pwPolicyErrorType));
731            }
732    
733            if (oldPassword == null)
734            {
735              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
736    
737              operation.appendErrorMessage(ERR_EXTOP_PASSMOD_IN_MIN_AGE.get());
738            }
739            else
740            {
741              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
742    
743              operation.appendAdditionalLogMessage(
744                      ERR_EXTOP_PASSMOD_IN_MIN_AGE.get());
745            }
746    
747            return;
748          }
749    
750    
751          // If the user's password is expired and it's a self-change request, then
752          // see if that's OK.
753          if ((selfChange && pwPolicyState.isPasswordExpired() &&
754              (! pwPolicyState.getPolicy().allowExpiredPasswordChanges())))
755          {
756            if (pwPolicyRequested)
757            {
758              pwPolicyErrorType =
759                   PasswordPolicyErrorType.PASSWORD_EXPIRED;
760              operation.addResponseControl(
761                   new PasswordPolicyResponseControl(pwPolicyWarningType,
762                                                     pwPolicyWarningValue,
763                                                     pwPolicyErrorType));
764            }
765    
766            if (oldPassword == null)
767            {
768              operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
769    
770              operation.appendErrorMessage(
771                      ERR_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED.get());
772            }
773            else
774            {
775              operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
776    
777              operation.appendAdditionalLogMessage(
778                      ERR_EXTOP_PASSMOD_PASSWORD_IS_EXPIRED.get());
779            }
780    
781            return;
782          }
783    
784    
785    
786          // If the a new password was provided, then peform any appropriate
787          // validation on it.  If not, then see if we can generate one.
788          boolean generatedPassword = false;
789          boolean isPreEncoded      = false;
790          if (newPassword == null)
791          {
792            try
793            {
794              newPassword = pwPolicyState.generatePassword();
795              if (newPassword == null)
796              {
797                if (oldPassword == null)
798                {
799                  operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
800    
801                  operation.appendErrorMessage(
802                          ERR_EXTOP_PASSMOD_NO_PW_GENERATOR.get());
803                }
804                else
805                {
806                  operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
807    
808                  operation.appendAdditionalLogMessage(
809                          ERR_EXTOP_PASSMOD_NO_PW_GENERATOR.get());
810                }
811    
812                return;
813              }
814              else
815              {
816                generatedPassword = true;
817              }
818            }
819            catch (DirectoryException de)
820            {
821              if (debugEnabled())
822              {
823                TRACER.debugCaught(DebugLogLevel.ERROR, de);
824              }
825    
826              if (oldPassword == null)
827              {
828                operation.setResultCode(de.getResultCode());
829    
830                operation.appendErrorMessage(
831                        ERR_EXTOP_PASSMOD_CANNOT_GENERATE_PW.get(
832                                de.getMessageObject()));
833              }
834              else
835              {
836                operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
837    
838                operation.appendAdditionalLogMessage(
839                        ERR_EXTOP_PASSMOD_CANNOT_GENERATE_PW.get(
840                                de.getMessageObject()));
841              }
842    
843              return;
844            }
845          }
846          else
847          {
848            if (pwPolicyState.passwordIsPreEncoded(newPassword))
849            {
850              // The password modify extended operation isn't intended to be invoked
851              // by an internal operation or during synchronization, so we don't
852              // need to check for those cases.
853              isPreEncoded = true;
854              if (! pwPolicyState.getPolicy().allowPreEncodedPasswords())
855              {
856                if (oldPassword == null)
857                {
858                  operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
859    
860                  operation.appendErrorMessage(
861                          ERR_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED.get());
862                }
863                else
864                {
865                  operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
866    
867                  operation.appendAdditionalLogMessage(
868                          ERR_EXTOP_PASSMOD_PRE_ENCODED_NOT_ALLOWED.get());
869                }
870    
871                return;
872              }
873            }
874            else
875            {
876              // Run the new password through the set of password validators.
877              if (selfChange ||
878                   (! pwPolicyState.getPolicy().skipValidationForAdministrators()))
879              {
880                HashSet<ByteString> clearPasswords;
881                if (oldPassword == null)
882                {
883                  clearPasswords =
884                       new HashSet<ByteString>(pwPolicyState.getClearPasswords());
885                }
886                else
887                {
888                  clearPasswords = new HashSet<ByteString>();
889                  clearPasswords.add(oldPassword);
890                  for (ByteString pw : pwPolicyState.getClearPasswords())
891                  {
892                    if (! Arrays.equals(pw.value(), oldPassword.value()))
893                    {
894                      clearPasswords.add(pw);
895                    }
896                  }
897                }
898    
899                MessageBuilder invalidReason = new MessageBuilder();
900                if (! pwPolicyState.passwordIsAcceptable(operation, userEntry,
901                                                         newPassword,
902                                                         clearPasswords,
903                                                         invalidReason))
904                {
905                  if (pwPolicyRequested)
906                  {
907                    pwPolicyErrorType =
908                         PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY;
909                    operation.addResponseControl(
910                         new PasswordPolicyResponseControl(pwPolicyWarningType,
911                                                           pwPolicyWarningValue,
912                                                           pwPolicyErrorType));
913                  }
914    
915                  if (oldPassword == null)
916                  {
917                    operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
918    
919                    operation.appendErrorMessage(
920                            ERR_EXTOP_PASSMOD_UNACCEPTABLE_PW.get(
921                                    String.valueOf(invalidReason)));
922                  }
923                  else
924                  {
925                    operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
926    
927                    operation.appendAdditionalLogMessage(
928                            ERR_EXTOP_PASSMOD_UNACCEPTABLE_PW.get(
929                                    String.valueOf(invalidReason)));
930                  }
931    
932                  return;
933                }
934              }
935    
936    
937              // Prepare to update the password history, if necessary.
938              if (pwPolicyState.maintainHistory())
939              {
940                if (pwPolicyState.isPasswordInHistory(newPassword))
941                {
942                  if (oldPassword == null)
943                  {
944                    operation.setResultCode(ResultCode.UNWILLING_TO_PERFORM);
945    
946                    operation.appendErrorMessage(
947                            ERR_EXTOP_PASSMOD_PW_IN_HISTORY.get());
948                  }
949                  else
950                  {
951                    operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
952    
953                    operation.appendAdditionalLogMessage(
954                            ERR_EXTOP_PASSMOD_PW_IN_HISTORY.get());
955                  }
956                }
957                else
958                {
959                  pwPolicyState.updatePasswordHistory();
960                }
961              }
962            }
963          }
964    
965    
966          // Get the encoded forms of the new password.
967          List<ByteString> encodedPasswords;
968          if (isPreEncoded)
969          {
970            encodedPasswords = new ArrayList<ByteString>(1);
971            encodedPasswords.add(newPassword);
972          }
973          else
974          {
975            try
976            {
977              encodedPasswords = pwPolicyState.encodePassword(newPassword);
978            }
979            catch (DirectoryException de)
980            {
981              if (debugEnabled())
982              {
983                TRACER.debugCaught(DebugLogLevel.ERROR, de);
984              }
985    
986              if (oldPassword == null)
987              {
988                operation.setResultCode(de.getResultCode());
989    
990                operation.appendErrorMessage(
991                        ERR_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD.get(
992                                de.getMessageObject()));
993              }
994              else
995              {
996                operation.setResultCode(ResultCode.INVALID_CREDENTIALS);
997    
998                operation.appendAdditionalLogMessage(
999                        ERR_EXTOP_PASSMOD_CANNOT_ENCODE_PASSWORD.get(
1000                                de.getMessageObject()));
1001              }
1002    
1003              return;
1004            }
1005          }
1006    
1007    
1008          // If the current password was provided, then remove all matching values
1009          // from the user's entry and replace them with the new password.
1010          // Otherwise replace all password values.
1011          AttributeType attrType = pwPolicyState.getPolicy().getPasswordAttribute();
1012          List<Modification> modList = new ArrayList<Modification>();
1013          if (oldPassword != null)
1014          {
1015            // Remove all existing encoded values that match the old password.
1016            LinkedHashSet<AttributeValue> existingValues =
1017                 pwPolicyState.getPasswordValues();
1018            LinkedHashSet<AttributeValue> deleteValues =
1019                 new LinkedHashSet<AttributeValue>(existingValues.size());
1020            if (pwPolicyState.getPolicy().usesAuthPasswordSyntax())
1021            {
1022              for (AttributeValue v : existingValues)
1023              {
1024                try
1025                {
1026                  StringBuilder[] components =
1027                       AuthPasswordSyntax.decodeAuthPassword(v.getStringValue());
1028                  PasswordStorageScheme scheme =
1029                       DirectoryServer.getAuthPasswordStorageScheme(
1030                            components[0].toString());
1031                  if (scheme == null)
1032                  {
1033                    // The password is encoded using an unknown scheme.  Remove it
1034                    // from the user's entry.
1035                    deleteValues.add(v);
1036                  }
1037                  else
1038                  {
1039                    if (scheme.authPasswordMatches(oldPassword,
1040                                                   components[1].toString(),
1041                                                   components[2].toString()))
1042                    {
1043                      deleteValues.add(v);
1044                    }
1045                  }
1046                }
1047                catch (DirectoryException de)
1048                {
1049                  if (debugEnabled())
1050                  {
1051                    TRACER.debugCaught(DebugLogLevel.ERROR, de);
1052                  }
1053    
1054                  // We couldn't decode the provided password value, so remove it
1055                  // from the user's entry.
1056                  deleteValues.add(v);
1057                }
1058              }
1059            }
1060            else
1061            {
1062              for (AttributeValue v : existingValues)
1063              {
1064                try
1065                {
1066                  String[] components =
1067                       UserPasswordSyntax.decodeUserPassword(v.getStringValue());
1068                  PasswordStorageScheme scheme =
1069                       DirectoryServer.getPasswordStorageScheme(
1070                            toLowerCase(components[0]));
1071                  if (scheme == null)
1072                  {
1073                    // The password is encoded using an unknown scheme.  Remove it
1074                    // from the user's entry.
1075                    deleteValues.add(v);
1076                  }
1077                  else
1078                  {
1079                    if (scheme.passwordMatches(oldPassword,
1080                                               new ASN1OctetString(components[1])))
1081                    {
1082                      deleteValues.add(v);
1083                    }
1084                  }
1085                }
1086                catch (DirectoryException de)
1087                {
1088                  if (debugEnabled())
1089                  {
1090                    TRACER.debugCaught(DebugLogLevel.ERROR, de);
1091                  }
1092    
1093                  // We couldn't decode the provided password value, so remove it
1094                  // from the user's entry.
1095                  deleteValues.add(v);
1096                }
1097              }
1098            }
1099    
1100            Attribute deleteAttr = new Attribute(attrType, attrType.getNameOrOID(),
1101                                                 deleteValues);
1102            modList.add(new Modification(ModificationType.DELETE, deleteAttr));
1103    
1104    
1105            // Add the new encoded values.
1106            LinkedHashSet<AttributeValue> addValues =
1107                 new LinkedHashSet<AttributeValue>(encodedPasswords.size());
1108            for (ByteString s : encodedPasswords)
1109            {
1110              addValues.add(new AttributeValue(attrType, s));
1111            }
1112    
1113            Attribute addAttr = new Attribute(attrType, attrType.getNameOrOID(),
1114                                              addValues);
1115            modList.add(new Modification(ModificationType.ADD, addAttr));
1116          }
1117          else
1118          {
1119            LinkedHashSet<AttributeValue> replaceValues =
1120                 new LinkedHashSet<AttributeValue>(encodedPasswords.size());
1121            for (ByteString s : encodedPasswords)
1122            {
1123              replaceValues.add(new AttributeValue(attrType, s));
1124            }
1125    
1126            Attribute addAttr = new Attribute(attrType, attrType.getNameOrOID(),
1127                                              replaceValues);
1128            modList.add(new Modification(ModificationType.REPLACE, addAttr));
1129          }
1130    
1131    
1132          // Update the password changed time for the user entry.
1133          pwPolicyState.setPasswordChangedTime();
1134    
1135    
1136          // If the password was changed by an end user, then clear any reset flag
1137          // that might exist.  If the password was changed by an administrator,
1138          // then see if we need to set the reset flag.
1139          if (selfChange)
1140          {
1141            pwPolicyState.setMustChangePassword(false);
1142          }
1143          else
1144          {
1145            pwPolicyState.setMustChangePassword(
1146                 pwPolicyState.getPolicy().forceChangeOnReset());
1147          }
1148    
1149    
1150          // Clear any record of grace logins, auth failures, and expiration
1151          // warnings.
1152          pwPolicyState.clearFailureLockout();
1153          pwPolicyState.clearGraceLoginTimes();
1154          pwPolicyState.clearWarnedTime();
1155    
1156    
1157          // If the LDAP no-op control was included in the request, then set the
1158          // appropriate response.  Otherwise, process the operation.
1159          if (noOpRequested)
1160          {
1161            operation.appendErrorMessage(WARN_EXTOP_PASSMOD_NOOP.get());
1162    
1163            operation.setResultCode(ResultCode.NO_OPERATION);
1164          }
1165          else
1166          {
1167            if (selfChange && (requestorEntry == null))
1168            {
1169              requestorEntry = userEntry;
1170            }
1171    
1172            // Get an internal connection and use it to perform the modification.
1173            boolean isRoot = DirectoryServer.isRootDN(requestorEntry.getDN());
1174            AuthenticationInfo authInfo = new AuthenticationInfo(requestorEntry,
1175                                                                 isRoot);
1176            InternalClientConnection internalConnection = new
1177                 InternalClientConnection(authInfo);
1178    
1179            ModifyOperation modifyOperation =
1180                 internalConnection.processModify(userDN, modList);
1181            ResultCode resultCode = modifyOperation.getResultCode();
1182            if (resultCode != ResultCode.SUCCESS)
1183            {
1184              operation.setResultCode(resultCode);
1185              operation.setErrorMessage(modifyOperation.getErrorMessage());
1186              operation.setReferralURLs(modifyOperation.getReferralURLs());
1187              return;
1188            }
1189    
1190    
1191            // If there were any password policy state changes, we need to apply
1192            // them using a root connection because the end user may not have
1193            // sufficient access to apply them.  This is less efficient than
1194            // doing them all in the same modification, but it's safer.
1195            List<Modification> pwPolicyMods = pwPolicyState.getModifications();
1196            if (! pwPolicyMods.isEmpty())
1197            {
1198              InternalClientConnection rootConnection =
1199                   InternalClientConnection.getRootConnection();
1200              ModifyOperation modOp =
1201                   rootConnection.processModify(userDN, pwPolicyMods);
1202              if (modOp.getResultCode() != ResultCode.SUCCESS)
1203              {
1204                // At this point, the user's password is already changed so there's
1205                // not much point in returning a non-success result.  However, we
1206                // should at least log that something went wrong.
1207                Message message = WARN_EXTOP_PASSMOD_CANNOT_UPDATE_PWP_STATE.get(
1208                        String.valueOf(userDN),
1209                        String.valueOf(modOp.getResultCode()),
1210                        modOp.getErrorMessage());
1211                ErrorLogger.logError(message);
1212              }
1213            }
1214    
1215    
1216            // If we've gotten here, then everything is OK, so indicate that the
1217            // operation was successful.  If a password was generated, then include
1218            // it in the response.
1219            operation.setResultCode(ResultCode.SUCCESS);
1220    
1221            if (generatedPassword)
1222            {
1223              ArrayList<ASN1Element> valueElements = new ArrayList<ASN1Element>(1);
1224    
1225              ASN1OctetString newPWString =
1226                   new ASN1OctetString(TYPE_PASSWORD_MODIFY_GENERATED_PASSWORD,
1227                                       newPassword.value());
1228              valueElements.add(newPWString);
1229    
1230              ASN1Sequence valueSequence = new ASN1Sequence(valueElements);
1231              operation.setResponseValue(new ASN1OctetString(
1232                                                  valueSequence.encode()));
1233            }
1234    
1235    
1236            // If this was a self password change, and the client is authenticated
1237            // as the user whose password was changed, then clear the "must change
1238            // password" flag in the client connection.  Note that we're using the
1239            // authentication DN rather than the authorization DN in this case to
1240            // avoid mistakenly clearing the flag for the wrong user.
1241            if (selfChange && (authInfo.getAuthenticationDN() != null) &&
1242                (authInfo.getAuthenticationDN().equals(userDN)))
1243            {
1244              operation.getClientConnection().setMustChangePassword(false);
1245            }
1246    
1247    
1248            // If the password policy control was requested, then add the
1249            // appropriate response control.
1250            if (pwPolicyRequested)
1251            {
1252              operation.addResponseControl(
1253                   new PasswordPolicyResponseControl(pwPolicyWarningType,
1254                                                     pwPolicyWarningValue,
1255                                                     pwPolicyErrorType));
1256            }
1257          }
1258        }
1259        finally
1260        {
1261          if (userLock != null)
1262          {
1263            LockManager.unlock(userDN, userLock);
1264          }
1265        }
1266      }
1267    
1268    
1269    
1270      /**
1271       * Retrieves the entry for the specified user based on the provided DN.  If
1272       * any problem is encountered or the requested entry does not exist, then the
1273       * provided operation will be updated with appropriate result information and
1274       * this method will return <CODE>null</CODE>.  The caller must hold a write
1275       * lock on the specified entry.
1276       *
1277       * @param  operation  The extended operation being processed.
1278       * @param  entryDN    The DN of the user entry to retrieve.
1279       *
1280       * @return  The requested entry, or <CODE>null</CODE> if there was no such
1281       *          entry or it could not be retrieved.
1282       */
1283      private Entry getEntryByDN(ExtendedOperation operation, DN entryDN)
1284      {
1285        // Retrieve the user's entry from the directory.  If it does not exist, then
1286        // fail.
1287        try
1288        {
1289          Entry userEntry = DirectoryServer.getEntry(entryDN);
1290    
1291          if (userEntry == null)
1292          {
1293            operation.setResultCode(ResultCode.NO_SUCH_OBJECT);
1294    
1295            operation.appendErrorMessage(
1296                    ERR_EXTOP_PASSMOD_NO_USER_ENTRY_BY_AUTHZID.get(
1297                            String.valueOf(entryDN)));
1298    
1299            // See if one of the entry's ancestors exists.
1300            DN parentDN = entryDN.getParentDNInSuffix();
1301            while (parentDN != null)
1302            {
1303              try
1304              {
1305                if (DirectoryServer.entryExists(parentDN))
1306                {
1307                  operation.setMatchedDN(parentDN);
1308                  break;
1309                }
1310              }
1311              catch (Exception e)
1312              {
1313                if (debugEnabled())
1314                {
1315                  TRACER.debugCaught(DebugLogLevel.ERROR, e);
1316                }
1317                break;
1318              }
1319    
1320              parentDN = parentDN.getParentDNInSuffix();
1321            }
1322    
1323            return null;
1324          }
1325    
1326          return userEntry;
1327        }
1328        catch (DirectoryException de)
1329        {
1330          if (debugEnabled())
1331          {
1332            TRACER.debugCaught(DebugLogLevel.ERROR, de);
1333          }
1334    
1335          operation.setResultCode(de.getResultCode());
1336          operation.appendErrorMessage(de.getMessageObject());
1337          operation.setMatchedDN(de.getMatchedDN());
1338          operation.setReferralURLs(de.getReferralURLs());
1339    
1340          return null;
1341        }
1342      }
1343    
1344    
1345    
1346      /**
1347       * {@inheritDoc}
1348       */
1349      @Override()
1350      public boolean isConfigurationAcceptable(ExtendedOperationHandlerCfg
1351                                                    configuration,
1352                                               List<Message> unacceptableReasons)
1353      {
1354        PasswordModifyExtendedOperationHandlerCfg config =
1355             (PasswordModifyExtendedOperationHandlerCfg) configuration;
1356        return isConfigurationChangeAcceptable(config, unacceptableReasons);
1357      }
1358    
1359    
1360    
1361      /**
1362       * Indicates whether the provided configuration entry has an acceptable
1363       * configuration for this component.  If it does not, then detailed
1364       * information about the problem(s) should be added to the provided list.
1365       *
1366       * @param  config          The configuration entry for which to make the
1367       *                              determination.
1368       * @param  unacceptableReasons  A list that can be used to hold messages about
1369       *                              why the provided entry does not have an
1370       *                              acceptable configuration.
1371       *
1372       * @return  <CODE>true</CODE> if the provided entry has an acceptable
1373       *          configuration for this component, or <CODE>false</CODE> if not.
1374       */
1375      public boolean isConfigurationChangeAcceptable(
1376           PasswordModifyExtendedOperationHandlerCfg config,
1377           List<Message> unacceptableReasons)
1378      {
1379        // Make sure that the specified identity mapper is OK.
1380        try
1381        {
1382          DN mapperDN = config.getIdentityMapperDN();
1383          IdentityMapper mapper = DirectoryServer.getIdentityMapper(mapperDN);
1384          if (mapper == null)
1385          {
1386            Message message = ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(
1387                    String.valueOf(mapperDN),
1388                    String.valueOf(config.dn()));
1389            unacceptableReasons.add(message);
1390            return false;
1391          }
1392        }
1393        catch (Exception e)
1394        {
1395          if (debugEnabled())
1396          {
1397            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1398          }
1399    
1400          Message message = ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get(
1401                  String.valueOf(config.dn()),
1402                  getExceptionMessage(e));
1403          unacceptableReasons.add(message);
1404          return false;
1405        }
1406    
1407    
1408        // If we've gotten here, then everything is OK.
1409        return true;
1410      }
1411    
1412    
1413    
1414      /**
1415       * Makes a best-effort attempt to apply the configuration contained in the
1416       * provided entry.  Information about the result of this processing should be
1417       * added to the provided message list.  Information should always be added to
1418       * this list if a configuration change could not be applied.  If detailed
1419       * results are requested, then information about the changes applied
1420       * successfully (and optionally about parameters that were not changed) should
1421       * also be included.
1422       *
1423       * @param  config      The entry containing the new configuration to
1424       *                          apply for this component.
1425       *
1426       * @return  Information about the result of the configuration update.
1427       */
1428      public ConfigChangeResult applyConfigurationChange(
1429           PasswordModifyExtendedOperationHandlerCfg config)
1430      {
1431        ResultCode        resultCode          = ResultCode.SUCCESS;
1432        boolean           adminActionRequired = false;
1433        ArrayList<Message> messages            = new ArrayList<Message>();
1434    
1435    
1436        // Make sure that the specified identity mapper is OK.
1437        DN             mapperDN = null;
1438        IdentityMapper mapper   = null;
1439        try
1440        {
1441          mapperDN = config.getIdentityMapperDN();
1442          mapper   = DirectoryServer.getIdentityMapper(mapperDN);
1443          if (mapper == null)
1444          {
1445            resultCode = ResultCode.CONSTRAINT_VIOLATION;
1446    
1447            messages.add(ERR_EXTOP_PASSMOD_NO_SUCH_ID_MAPPER.get(
1448                    String.valueOf(mapperDN),
1449                    String.valueOf(config.dn())));
1450          }
1451        }
1452        catch (Exception e)
1453        {
1454          if (debugEnabled())
1455          {
1456            TRACER.debugCaught(DebugLogLevel.ERROR, e);
1457          }
1458    
1459          resultCode = DirectoryServer.getServerErrorResultCode();
1460    
1461          messages.add(ERR_EXTOP_PASSMOD_CANNOT_DETERMINE_ID_MAPPER.get(
1462                  String.valueOf(config.dn()),
1463                  getExceptionMessage(e)));
1464        }
1465    
1466    
1467        // If all of the changes were acceptable, then apply them.
1468        if (resultCode == ResultCode.SUCCESS)
1469        {
1470          if (! identityMapperDN.equals(mapperDN))
1471          {
1472            identityMapper   = mapper;
1473            identityMapperDN = mapperDN;
1474          }
1475        }
1476    
1477    
1478        // Save this configuration for future reference.
1479        currentConfig = config;
1480    
1481        return new ConfigChangeResult(resultCode, adminActionRequired, messages);
1482      }
1483    }
1484