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