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 legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2008-2011 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.server.workflowelement.localbackend; 028 029import java.util.HashSet; 030import java.util.LinkedList; 031import java.util.List; 032import java.util.ListIterator; 033 034import org.forgerock.i18n.LocalizableMessage; 035import org.forgerock.i18n.LocalizableMessageBuilder; 036import org.forgerock.i18n.LocalizableMessageDescriptor.Arg3; 037import org.forgerock.i18n.LocalizableMessageDescriptor.Arg4; 038import org.forgerock.i18n.slf4j.LocalizedLogger; 039import org.forgerock.opendj.ldap.ByteString; 040import org.forgerock.opendj.ldap.ModificationType; 041import org.forgerock.opendj.ldap.ResultCode; 042import org.forgerock.opendj.ldap.schema.MatchingRule; 043import org.forgerock.opendj.ldap.schema.Syntax; 044import org.forgerock.util.Reject; 045import org.forgerock.util.Utils; 046import org.opends.server.api.AccessControlHandler; 047import org.opends.server.api.AuthenticationPolicy; 048import org.opends.server.api.Backend; 049import org.opends.server.api.ClientConnection; 050import org.opends.server.api.PasswordStorageScheme; 051import org.opends.server.api.SynchronizationProvider; 052import org.opends.server.api.plugin.PluginResult.PostOperation; 053import org.opends.server.controls.LDAPAssertionRequestControl; 054import org.opends.server.controls.LDAPPostReadRequestControl; 055import org.opends.server.controls.LDAPPreReadRequestControl; 056import org.opends.server.controls.PasswordPolicyErrorType; 057import org.opends.server.controls.PasswordPolicyResponseControl; 058import org.opends.server.core.AccessControlConfigManager; 059import org.opends.server.core.DirectoryServer; 060import org.opends.server.core.ModifyOperation; 061import org.opends.server.core.ModifyOperationWrapper; 062import org.opends.server.core.PasswordPolicy; 063import org.opends.server.core.PasswordPolicyState; 064import org.opends.server.core.PersistentSearch; 065import org.opends.server.schema.AuthPasswordSyntax; 066import org.opends.server.schema.UserPasswordSyntax; 067import org.opends.server.types.AcceptRejectWarn; 068import org.opends.server.types.AccountStatusNotification; 069import org.opends.server.types.AccountStatusNotificationType; 070import org.opends.server.types.Attribute; 071import org.opends.server.types.AttributeBuilder; 072import org.opends.server.types.AttributeType; 073import org.opends.server.types.AuthenticationInfo; 074import org.opends.server.types.CanceledOperationException; 075import org.opends.server.types.Control; 076import org.opends.server.types.DN; 077import org.opends.server.types.DirectoryException; 078import org.opends.server.types.Entry; 079import org.opends.server.types.LockManager.DNLock; 080import org.opends.server.types.Modification; 081import org.opends.server.types.ObjectClass; 082import org.opends.server.types.Privilege; 083import org.opends.server.types.RDN; 084import org.opends.server.types.SearchFilter; 085import org.opends.server.types.SynchronizationProviderResult; 086import org.opends.server.types.operation.PostOperationModifyOperation; 087import org.opends.server.types.operation.PostResponseModifyOperation; 088import org.opends.server.types.operation.PostSynchronizationModifyOperation; 089import org.opends.server.types.operation.PreOperationModifyOperation; 090 091import static org.opends.messages.CoreMessages.*; 092import static org.opends.server.config.ConfigConstants.*; 093import static org.opends.server.core.DirectoryServer.*; 094import static org.opends.server.types.AbstractOperation.*; 095import static org.opends.server.types.AccountStatusNotificationType.*; 096import static org.opends.server.util.ServerConstants.*; 097import static org.opends.server.util.StaticUtils.*; 098 099/** This class defines an operation used to modify an entry in a local backend of the Directory Server. */ 100public class LocalBackendModifyOperation 101 extends ModifyOperationWrapper 102 implements PreOperationModifyOperation, PostOperationModifyOperation, 103 PostResponseModifyOperation, 104 PostSynchronizationModifyOperation 105{ 106 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 107 108 /** The backend in which the target entry exists. */ 109 private Backend<?> backend; 110 /** The client connection associated with this operation. */ 111 private ClientConnection clientConnection; 112 private boolean preOperationPluginsExecuted; 113 114 /** Indicates whether this modify operation includes a password change. */ 115 private boolean passwordChanged; 116 /** Indicates whether the password change is a self-change. */ 117 private boolean selfChange; 118 /** Indicates whether the request included the user's current password. */ 119 private boolean currentPasswordProvided; 120 /** Indicates whether the user's account has been enabled or disabled by this modify operation. */ 121 private boolean enabledStateChanged; 122 /** Indicates whether the user's account is currently enabled. */ 123 private boolean isEnabled; 124 /** Indicates whether the user's account was locked before this change. */ 125 private boolean wasLocked; 126 127 /** Indicates whether the request included the LDAP no-op control. */ 128 private boolean noOp; 129 /** Indicates whether the request included the Permissive Modify control. */ 130 private boolean permissiveModify; 131 /** Indicates whether the request included the password policy request control. */ 132 private boolean pwPolicyControlRequested; 133 /** The post-read request control, if present. */ 134 private LDAPPostReadRequestControl postReadRequest; 135 /** The pre-read request control, if present. */ 136 private LDAPPreReadRequestControl preReadRequest; 137 138 /** The DN of the entry to modify. */ 139 private DN entryDN; 140 /** The current entry, before any changes are applied. */ 141 private Entry currentEntry; 142 /** The modified entry that will be stored in the backend. */ 143 private Entry modifiedEntry; 144 /** The set of modifications contained in this request. */ 145 private List<Modification> modifications; 146 147 /** The number of passwords contained in the modify operation. */ 148 private int numPasswords; 149 150 /** The set of clear-text current passwords (if any were provided). */ 151 private List<ByteString> currentPasswords; 152 /** The set of clear-text new passwords (if any were provided). */ 153 private List<ByteString> newPasswords; 154 155 /** The password policy error type for this operation. */ 156 private PasswordPolicyErrorType pwpErrorType; 157 /** The password policy state for this modify operation. */ 158 private PasswordPolicyState pwPolicyState; 159 160 161 /** 162 * Creates a new operation that may be used to modify an entry in a 163 * local backend of the Directory Server. 164 * 165 * @param modify The operation to enhance. 166 */ 167 public LocalBackendModifyOperation(ModifyOperation modify) 168 { 169 super(modify); 170 LocalBackendWorkflowElement.attachLocalOperation (modify, this); 171 } 172 173 /** 174 * Returns whether authentication for this user is managed locally 175 * or via Pass-Through Authentication. 176 */ 177 private boolean isAuthnManagedLocally() 178 { 179 return pwPolicyState != null; 180 } 181 182 /** 183 * Retrieves the current entry before any modifications are applied. This 184 * will not be available to pre-parse plugins. 185 * 186 * @return The current entry, or {@code null} if it is not yet available. 187 */ 188 @Override 189 public final Entry getCurrentEntry() 190 { 191 return currentEntry; 192 } 193 194 195 196 /** 197 * Retrieves the set of clear-text current passwords for the user, if 198 * available. This will only be available if the modify operation contains 199 * one or more delete elements that target the password attribute and provide 200 * the values to delete in the clear. It will not be available to pre-parse 201 * plugins. 202 * 203 * @return The set of clear-text current password values as provided in the 204 * modify request, or {@code null} if there were none or this 205 * information is not yet available. 206 */ 207 @Override 208 public final List<ByteString> getCurrentPasswords() 209 { 210 return currentPasswords; 211 } 212 213 214 215 /** 216 * Retrieves the modified entry that is to be written to the backend. This 217 * will be available to pre-operation plugins, and if such a plugin does make 218 * a change to this entry, then it is also necessary to add that change to 219 * the set of modifications to ensure that the update will be consistent. 220 * 221 * @return The modified entry that is to be written to the backend, or 222 * {@code null} if it is not yet available. 223 */ 224 @Override 225 public final Entry getModifiedEntry() 226 { 227 return modifiedEntry; 228 } 229 230 231 232 /** 233 * Retrieves the set of clear-text new passwords for the user, if available. 234 * This will only be available if the modify operation contains one or more 235 * add or replace elements that target the password attribute and provide the 236 * values in the clear. It will not be available to pre-parse plugins. 237 * 238 * @return The set of clear-text new passwords as provided in the modify 239 * request, or {@code null} if there were none or this 240 * information is not yet available. 241 */ 242 @Override 243 public final List<ByteString> getNewPasswords() 244 { 245 return newPasswords; 246 } 247 248 249 250 /** 251 * Adds the provided modification to the set of modifications to this modify operation. 252 * In addition, the modification is applied to the modified entry. 253 * <p> 254 * This may only be called by pre-operation plugins. 255 * 256 * @param modification The modification to add to the set of changes for 257 * this modify operation. 258 * @throws DirectoryException If an unexpected problem occurs while applying 259 * the modification to the entry. 260 */ 261 @Override 262 public void addModification(Modification modification) 263 throws DirectoryException 264 { 265 modifiedEntry.applyModification(modification, permissiveModify); 266 super.addModification(modification); 267 } 268 269 270 271 /** 272 * Process this modify operation against a local backend. 273 * 274 * @param wfe 275 * The local backend work-flow element. 276 * @throws CanceledOperationException 277 * if this operation should be cancelled 278 */ 279 void processLocalModify(final LocalBackendWorkflowElement wfe) throws CanceledOperationException 280 { 281 this.backend = wfe.getBackend(); 282 this.clientConnection = getClientConnection(); 283 284 checkIfCanceled(false); 285 try 286 { 287 processModify(); 288 289 if (pwPolicyControlRequested) 290 { 291 addResponseControl(new PasswordPolicyResponseControl(null, 0, pwpErrorType)); 292 } 293 294 invokePostModifyPlugins(); 295 } 296 finally 297 { 298 LocalBackendWorkflowElement.filterNonDisclosableMatchedDN(this); 299 } 300 301 302 // Register a post-response call-back which will notify persistent 303 // searches and change listeners. 304 if (getResultCode() == ResultCode.SUCCESS) 305 { 306 registerPostResponseCallback(new Runnable() 307 { 308 @Override 309 public void run() 310 { 311 for (PersistentSearch psearch : backend.getPersistentSearches()) 312 { 313 psearch.processModify(modifiedEntry, currentEntry); 314 } 315 } 316 }); 317 } 318 } 319 320 private boolean invokePreModifyPlugins() throws CanceledOperationException 321 { 322 if (!isSynchronizationOperation()) 323 { 324 preOperationPluginsExecuted = true; 325 if (!processOperationResult(this, getPluginConfigManager().invokePreOperationModifyPlugins(this))) 326 { 327 return false; 328 } 329 } 330 return true; 331 } 332 333 private void invokePostModifyPlugins() 334 { 335 if (isSynchronizationOperation()) 336 { 337 if (getResultCode() == ResultCode.SUCCESS) 338 { 339 getPluginConfigManager().invokePostSynchronizationModifyPlugins(this); 340 } 341 } 342 else if (preOperationPluginsExecuted) 343 { 344 PostOperation result = getPluginConfigManager().invokePostOperationModifyPlugins(this); 345 if (!processOperationResult(this, result)) 346 { 347 return; 348 } 349 } 350 } 351 352 private void processModify() throws CanceledOperationException 353 { 354 entryDN = getEntryDN(); 355 if (entryDN == null) 356 { 357 return; 358 } 359 360 // Process the modifications to convert them from their raw form to the 361 // form required for the rest of the modify processing. 362 modifications = getModifications(); 363 if (modifications == null) 364 { 365 return; 366 } 367 368 if (modifications.isEmpty()) 369 { 370 setResultCode(ResultCode.CONSTRAINT_VIOLATION); 371 appendErrorMessage(ERR_MODIFY_NO_MODIFICATIONS.get(entryDN)); 372 return; 373 } 374 375 checkIfCanceled(false); 376 377 // Acquire a write lock on the target entry. 378 final DNLock entryLock = DirectoryServer.getLockManager().tryWriteLockEntry(entryDN); 379 try 380 { 381 if (entryLock == null) 382 { 383 setResultCode(ResultCode.BUSY); 384 appendErrorMessage(ERR_MODIFY_CANNOT_LOCK_ENTRY.get(entryDN)); 385 return; 386 } 387 388 checkIfCanceled(false); 389 390 currentEntry = backend.getEntry(entryDN); 391 if (currentEntry == null) 392 { 393 setResultCode(ResultCode.NO_SUCH_OBJECT); 394 appendErrorMessage(ERR_MODIFY_NO_SUCH_ENTRY.get(entryDN)); 395 setMatchedDN(findMatchedDN(entryDN)); 396 return; 397 } 398 399 processRequestControls(); 400 401 // Get the password policy state object for the entry that can be used 402 // to perform any appropriate password policy processing. Also, see 403 // if the entry is being updated by the end user or an administrator. 404 final DN authzDN = getAuthorizationDN(); 405 selfChange = entryDN.equals(authzDN); 406 407 // Should the authorizing account change its password? 408 if (mustChangePassword(selfChange, getAuthorizationEntry())) 409 { 410 pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET; 411 setResultCode(ResultCode.CONSTRAINT_VIOLATION); 412 appendErrorMessage(ERR_MODIFY_MUST_CHANGE_PASSWORD.get(authzDN != null ? authzDN : "anonymous")); 413 return; 414 } 415 416 // FIXME -- Need a way to enable debug mode. 417 pwPolicyState = createPasswordPolicyState(currentEntry); 418 419 // Create a duplicate of the entry and apply the changes to it. 420 modifiedEntry = currentEntry.duplicate(false); 421 422 if (!noOp && !handleConflictResolution()) 423 { 424 return; 425 } 426 427 processNonPasswordModifications(); 428 429 // Check to see if the client has permission to perform the modify. 430 // The access control check is not made any earlier because the handler 431 // needs access to the modified entry. 432 433 // FIXME: for now assume that this will check all permissions pertinent to the operation. 434 // This includes proxy authorization and any other controls specified. 435 436 // FIXME: earlier checks to see if the entry already exists may have 437 // already exposed sensitive information to the client. 438 if (!operationIsAllowed()) 439 { 440 return; 441 } 442 443 if (isAuthnManagedLocally()) 444 { 445 processPasswordPolicyModifications(); 446 performAdditionalPasswordChangedProcessing(); 447 448 if (currentUserMustChangePassword()) 449 { 450 // The user did not attempt to change their password. 451 pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET; 452 setResultCode(ResultCode.CONSTRAINT_VIOLATION); 453 appendErrorMessage(ERR_MODIFY_MUST_CHANGE_PASSWORD.get(authzDN != null ? authzDN : "anonymous")); 454 return; 455 } 456 } 457 458 if (mustCheckSchema()) 459 { 460 // make sure that the new entry is valid per the server schema. 461 LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder(); 462 if (!modifiedEntry.conformsToSchema(null, false, false, false, invalidReason)) 463 { 464 setResultCode(ResultCode.OBJECTCLASS_VIOLATION); 465 appendErrorMessage(ERR_MODIFY_VIOLATES_SCHEMA.get(entryDN, invalidReason)); 466 return; 467 } 468 } 469 470 checkIfCanceled(false); 471 472 if (!invokePreModifyPlugins()) 473 { 474 return; 475 } 476 477 // Actually perform the modify operation. This should also include 478 // taking care of any synchronization that might be needed. 479 if (backend == null) 480 { 481 setResultCode(ResultCode.NO_SUCH_OBJECT); 482 appendErrorMessage(ERR_MODIFY_NO_BACKEND_FOR_ENTRY.get(entryDN)); 483 return; 484 } 485 486 LocalBackendWorkflowElement.checkIfBackendIsWritable(backend, this, 487 entryDN, ERR_MODIFY_SERVER_READONLY, ERR_MODIFY_BACKEND_READONLY); 488 489 if (noOp) 490 { 491 appendErrorMessage(INFO_MODIFY_NOOP.get()); 492 setResultCode(ResultCode.NO_OPERATION); 493 } 494 else 495 { 496 if (!processPreOperation()) 497 { 498 return; 499 } 500 501 backend.replaceEntry(currentEntry, modifiedEntry, this); 502 503 if (isAuthnManagedLocally()) 504 { 505 generatePwpAccountStatusNotifications(); 506 } 507 } 508 509 // Handle any processing that may be needed for the pre-read and/or post-read controls. 510 LocalBackendWorkflowElement.addPreReadResponse(this, preReadRequest, currentEntry); 511 LocalBackendWorkflowElement.addPostReadResponse(this, postReadRequest, modifiedEntry); 512 513 if (!noOp) 514 { 515 setResultCode(ResultCode.SUCCESS); 516 } 517 } 518 catch (DirectoryException de) 519 { 520 logger.traceException(de); 521 522 setResponseData(de); 523 } 524 finally 525 { 526 if (entryLock != null) 527 { 528 entryLock.unlock(); 529 } 530 processSynchPostOperationPlugins(); 531 } 532 } 533 534 private boolean operationIsAllowed() 535 { 536 try 537 { 538 if (!getAccessControlHandler().isAllowed(this)) 539 { 540 setResultCodeAndMessageNoInfoDisclosure(modifiedEntry, 541 ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 542 ERR_MODIFY_AUTHZ_INSUFFICIENT_ACCESS_RIGHTS.get(entryDN)); 543 return false; 544 } 545 return true; 546 } 547 catch (DirectoryException e) 548 { 549 setResultCode(e.getResultCode()); 550 appendErrorMessage(e.getMessageObject()); 551 return false; 552 } 553 } 554 555 private boolean currentUserMustChangePassword() 556 { 557 return !isInternalOperation() && selfChange && !passwordChanged && pwPolicyState.mustChangePassword(); 558 } 559 560 private boolean mustChangePassword(boolean selfChange, Entry authzEntry) throws DirectoryException 561 { 562 return !isInternalOperation() && !selfChange && authzEntry != null && mustChangePassword(authzEntry); 563 } 564 565 private boolean mustChangePassword(Entry authzEntry) throws DirectoryException 566 { 567 PasswordPolicyState authzState = createPasswordPolicyState(authzEntry); 568 return authzState != null && authzState.mustChangePassword(); 569 } 570 571 private PasswordPolicyState createPasswordPolicyState(Entry entry) throws DirectoryException 572 { 573 AuthenticationPolicy policy = AuthenticationPolicy.forUser(entry, true); 574 if (policy.isPasswordPolicy()) 575 { 576 return (PasswordPolicyState) policy.createAuthenticationPolicyState(entry); 577 } 578 return null; 579 } 580 581 private AccessControlHandler<?> getAccessControlHandler() 582 { 583 return AccessControlConfigManager.getInstance().getAccessControlHandler(); 584 } 585 586 private DirectoryException newDirectoryException(Entry entry, 587 ResultCode resultCode, LocalizableMessage message) throws DirectoryException 588 { 589 return LocalBackendWorkflowElement.newDirectoryException(this, entry, 590 entryDN, resultCode, message, ResultCode.NO_SUCH_OBJECT, 591 ERR_MODIFY_NO_SUCH_ENTRY.get(entryDN)); 592 } 593 594 private void setResultCodeAndMessageNoInfoDisclosure(Entry entry, 595 ResultCode realResultCode, LocalizableMessage realMessage) throws DirectoryException 596 { 597 LocalBackendWorkflowElement.setResultCodeAndMessageNoInfoDisclosure(this, 598 entry, entryDN, realResultCode, realMessage, ResultCode.NO_SUCH_OBJECT, 599 ERR_MODIFY_NO_SUCH_ENTRY.get(entryDN)); 600 } 601 602 private DN findMatchedDN(DN entryDN) 603 { 604 try 605 { 606 DN matchedDN = entryDN.getParentDNInSuffix(); 607 while (matchedDN != null) 608 { 609 if (DirectoryServer.entryExists(matchedDN)) 610 { 611 return matchedDN; 612 } 613 614 matchedDN = matchedDN.getParentDNInSuffix(); 615 } 616 } 617 catch (Exception e) 618 { 619 logger.traceException(e); 620 } 621 return null; 622 } 623 624 /** 625 * Processes any controls contained in the modify request. 626 * 627 * @throws DirectoryException If a problem is encountered with any of the 628 * controls. 629 */ 630 private void processRequestControls() throws DirectoryException 631 { 632 LocalBackendWorkflowElement.evaluateProxyAuthControls(this); 633 LocalBackendWorkflowElement.removeAllDisallowedControls(entryDN, this); 634 635 List<Control> requestControls = getRequestControls(); 636 if (requestControls != null && !requestControls.isEmpty()) 637 { 638 for (ListIterator<Control> iter = requestControls.listIterator(); iter.hasNext();) 639 { 640 final Control c = iter.next(); 641 final String oid = c.getOID(); 642 643 if (OID_LDAP_ASSERTION.equals(oid)) 644 { 645 LDAPAssertionRequestControl assertControl = 646 getRequestControl(LDAPAssertionRequestControl.DECODER); 647 648 SearchFilter filter; 649 try 650 { 651 filter = assertControl.getSearchFilter(); 652 } 653 catch (DirectoryException de) 654 { 655 logger.traceException(de); 656 657 throw newDirectoryException(currentEntry, de.getResultCode(), 658 ERR_MODIFY_CANNOT_PROCESS_ASSERTION_FILTER.get( 659 entryDN, de.getMessageObject())); 660 } 661 662 // Check if the current user has permission to make this determination. 663 if (!getAccessControlHandler().isAllowed(this, currentEntry, filter)) 664 { 665 throw new DirectoryException( 666 ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 667 ERR_CONTROL_INSUFFICIENT_ACCESS_RIGHTS.get(oid)); 668 } 669 670 try 671 { 672 if (!filter.matchesEntry(currentEntry)) 673 { 674 throw newDirectoryException(currentEntry, 675 ResultCode.ASSERTION_FAILED, 676 ERR_MODIFY_ASSERTION_FAILED.get(entryDN)); 677 } 678 } 679 catch (DirectoryException de) 680 { 681 if (de.getResultCode() == ResultCode.ASSERTION_FAILED) 682 { 683 throw de; 684 } 685 686 logger.traceException(de); 687 688 throw newDirectoryException(currentEntry, de.getResultCode(), 689 ERR_MODIFY_CANNOT_PROCESS_ASSERTION_FILTER.get( 690 entryDN, de.getMessageObject())); 691 } 692 } 693 else if (OID_LDAP_NOOP_OPENLDAP_ASSIGNED.equals(oid)) 694 { 695 noOp = true; 696 } 697 else if (OID_PERMISSIVE_MODIFY_CONTROL.equals(oid)) 698 { 699 permissiveModify = true; 700 } 701 else if (OID_LDAP_READENTRY_PREREAD.equals(oid)) 702 { 703 preReadRequest = getRequestControl(LDAPPreReadRequestControl.DECODER); 704 } 705 else if (OID_LDAP_READENTRY_POSTREAD.equals(oid)) 706 { 707 if (c instanceof LDAPPostReadRequestControl) 708 { 709 postReadRequest = (LDAPPostReadRequestControl) c; 710 } 711 else 712 { 713 postReadRequest = getRequestControl(LDAPPostReadRequestControl.DECODER); 714 iter.set(postReadRequest); 715 } 716 } 717 else if (LocalBackendWorkflowElement.isProxyAuthzControl(oid)) 718 { 719 continue; 720 } 721 else if (OID_PASSWORD_POLICY_CONTROL.equals(oid)) 722 { 723 pwPolicyControlRequested = true; 724 } 725 // NYI -- Add support for additional controls. 726 else if (c.isCritical() 727 && (backend == null || !backend.supportsControl(oid))) 728 { 729 throw newDirectoryException(currentEntry, 730 ResultCode.UNAVAILABLE_CRITICAL_EXTENSION, 731 ERR_MODIFY_UNSUPPORTED_CRITICAL_CONTROL.get(entryDN, oid)); 732 } 733 } 734 } 735 } 736 737 private void processNonPasswordModifications() throws DirectoryException 738 { 739 for (Modification m : modifications) 740 { 741 Attribute a = m.getAttribute(); 742 AttributeType t = a.getAttributeType(); 743 744 745 // If the attribute type is marked "NO-USER-MODIFICATION" then fail unless 746 // this is an internal operation or is related to synchronization in some way. 747 final boolean isInternalOrSynchro = isInternalOrSynchro(m); 748 if (t.isNoUserModification() && !isInternalOrSynchro) 749 { 750 throw newDirectoryException(currentEntry, 751 ResultCode.CONSTRAINT_VIOLATION, 752 ERR_MODIFY_ATTR_IS_NO_USER_MOD.get(entryDN, a.getName())); 753 } 754 755 // If the attribute type is marked "OBSOLETE" and the modification is 756 // setting new values, then fail unless this is an internal operation or 757 // is related to synchronization in some way. 758 if (t.isObsolete() 759 && !a.isEmpty() 760 && m.getModificationType() != ModificationType.DELETE 761 && !isInternalOrSynchro) 762 { 763 throw newDirectoryException(currentEntry, 764 ResultCode.CONSTRAINT_VIOLATION, 765 ERR_MODIFY_ATTR_IS_OBSOLETE.get(entryDN, a.getName())); 766 } 767 768 769 // See if the attribute is one which controls the privileges available for a user. 770 // If it is, then the client must have the PRIVILEGE_CHANGE privilege. 771 if (t.hasName(OP_ATTR_PRIVILEGE_NAME) 772 && !clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE, this)) 773 { 774 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 775 ERR_MODIFY_CHANGE_PRIVILEGE_INSUFFICIENT_PRIVILEGES.get()); 776 } 777 778 // If the modification is not updating the password attribute, 779 // then perform any schema processing. 780 if (!isPassword(t)) 781 { 782 processModification(m); 783 } 784 } 785 } 786 787 private boolean isInternalOrSynchro(Modification m) 788 { 789 return isInternalOperation() || m.isInternal() || isSynchronizationOperation(); 790 } 791 792 private boolean isPassword(AttributeType t) 793 { 794 return pwPolicyState != null 795 && t.equals(pwPolicyState.getAuthenticationPolicy().getPasswordAttribute()); 796 } 797 798 /** Processes the modifications related to password policy for this modify operation. */ 799 private void processPasswordPolicyModifications() throws DirectoryException 800 { 801 // Declare variables used for password policy state processing. 802 currentPasswordProvided = false; 803 isEnabled = true; 804 enabledStateChanged = false; 805 806 final PasswordPolicy authPolicy = pwPolicyState.getAuthenticationPolicy(); 807 if (currentEntry.hasAttribute(authPolicy.getPasswordAttribute())) 808 { 809 // It may actually have more than one, but we can't tell the difference if 810 // the values are encoded, and its enough for our purposes just to know 811 // that there is at least one. 812 numPasswords = 1; 813 } 814 else 815 { 816 numPasswords = 0; 817 } 818 819 passwordChanged = !isInternalOperation() && !isSynchronizationOperation() && isModifyingPassword(); 820 821 822 for (Modification m : modifications) 823 { 824 AttributeType t = m.getAttribute().getAttributeType(); 825 826 // If the modification is updating the password attribute, then perform 827 // any necessary password policy processing. This processing should be 828 // skipped for synchronization operations. 829 if (isPassword(t)) 830 { 831 if (!isSynchronizationOperation()) 832 { 833 // If the attribute contains any options and new values are going to 834 // be added, then reject it. Passwords will not be allowed to have options. 835 if (!isInternalOperation()) 836 { 837 validatePasswordModification(m, authPolicy); 838 } 839 preProcessPasswordModification(m); 840 } 841 842 processModification(m); 843 } 844 else if (!isInternalOrSynchro(m) 845 && t.equals(getAttributeTypeOrDefault(OP_ATTR_ACCOUNT_DISABLED))) 846 { 847 enabledStateChanged = true; 848 isEnabled = !pwPolicyState.isDisabled(); 849 } 850 } 851 } 852 853 /** Adds the appropriate state changes for the provided modification. */ 854 private void preProcessPasswordModification(Modification m) throws DirectoryException 855 { 856 switch (m.getModificationType().asEnum()) 857 { 858 case ADD: 859 case REPLACE: 860 preProcessPasswordAddOrReplace(m); 861 break; 862 863 case DELETE: 864 preProcessPasswordDelete(m); 865 break; 866 867 // case INCREMENT does not make any sense for passwords 868 default: 869 Attribute a = m.getAttribute(); 870 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 871 ERR_MODIFY_INVALID_MOD_TYPE_FOR_PASSWORD.get(m.getModificationType(), a.getName())); 872 } 873 } 874 875 private boolean isModifyingPassword() throws DirectoryException 876 { 877 for (Modification m : modifications) 878 { 879 if (isPassword(m.getAttribute().getAttributeType())) 880 { 881 if (!selfChange && !clientConnection.hasPrivilege(Privilege.PASSWORD_RESET, this)) 882 { 883 pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; 884 throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, 885 ERR_MODIFY_PWRESET_INSUFFICIENT_PRIVILEGES.get()); 886 } 887 return true; 888 } 889 } 890 return false; 891 } 892 893 private void validatePasswordModification(Modification m, PasswordPolicy authPolicy) throws DirectoryException 894 { 895 Attribute a = m.getAttribute(); 896 if (a.hasOptions()) 897 { 898 switch (m.getModificationType().asEnum()) 899 { 900 case REPLACE: 901 if (!a.isEmpty()) 902 { 903 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 904 ERR_MODIFY_PASSWORDS_CANNOT_HAVE_OPTIONS.get()); 905 } 906 // Allow delete operations to clean up after import. 907 break; 908 case ADD: 909 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 910 ERR_MODIFY_PASSWORDS_CANNOT_HAVE_OPTIONS.get()); 911 default: 912 // Allow delete operations to clean up after import. 913 break; 914 } 915 } 916 917 // If it's a self change, then see if that's allowed. 918 if (selfChange && !authPolicy.isAllowUserPasswordChanges()) 919 { 920 pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; 921 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 922 ERR_MODIFY_NO_USER_PW_CHANGES.get()); 923 } 924 925 926 // If we require secure password changes, then makes sure it's a 927 // secure communication channel. 928 if (authPolicy.isRequireSecurePasswordChanges() 929 && !clientConnection.isSecure()) 930 { 931 pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; 932 throw new DirectoryException(ResultCode.CONFIDENTIALITY_REQUIRED, 933 ERR_MODIFY_REQUIRE_SECURE_CHANGES.get()); 934 } 935 936 937 // If it's a self change and it's not been long enough since the 938 // previous change, then reject it. 939 if (selfChange && pwPolicyState.isWithinMinimumAge()) 940 { 941 pwpErrorType = PasswordPolicyErrorType.PASSWORD_TOO_YOUNG; 942 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 943 ERR_MODIFY_WITHIN_MINIMUM_AGE.get()); 944 } 945 } 946 947 /** 948 * Process the provided modification and updates the entry appropriately. 949 * 950 * @param m 951 * The modification to perform 952 * @throws DirectoryException 953 * If a problem occurs that should cause the modify operation to fail. 954 */ 955 private void processModification(Modification m) throws DirectoryException 956 { 957 Attribute attr = m.getAttribute(); 958 switch (m.getModificationType().asEnum()) 959 { 960 case ADD: 961 processAddModification(attr); 962 break; 963 964 case DELETE: 965 processDeleteModification(attr); 966 break; 967 968 case REPLACE: 969 processReplaceModification(attr); 970 break; 971 972 case INCREMENT: 973 processIncrementModification(attr); 974 break; 975 } 976 } 977 978 private void preProcessPasswordAddOrReplace(Modification m) throws DirectoryException 979 { 980 Attribute pwAttr = m.getAttribute(); 981 int passwordsToAdd = pwAttr.size(); 982 983 if (m.getModificationType() == ModificationType.ADD) 984 { 985 numPasswords += passwordsToAdd; 986 } 987 else 988 { 989 numPasswords = passwordsToAdd; 990 } 991 992 // If there were multiple password values, then make sure that's OK. 993 final PasswordPolicy authPolicy = pwPolicyState.getAuthenticationPolicy(); 994 if (!isInternalOperation() 995 && !authPolicy.isAllowMultiplePasswordValues() 996 && passwordsToAdd > 1) 997 { 998 pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; 999 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 1000 ERR_MODIFY_MULTIPLE_VALUES_NOT_ALLOWED.get()); 1001 } 1002 1003 // Iterate through the password values and see if any of them are 1004 // pre-encoded. If so, then check to see if we'll allow it. 1005 // Otherwise, store the clear-text values for later validation 1006 // and update the attribute with the encoded values. 1007 AttributeBuilder builder = new AttributeBuilder(pwAttr, true); 1008 for (ByteString v : pwAttr) 1009 { 1010 if (pwPolicyState.passwordIsPreEncoded(v)) 1011 { 1012 if (!isInternalOperation() 1013 && !authPolicy.isAllowPreEncodedPasswords()) 1014 { 1015 pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY; 1016 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 1017 ERR_MODIFY_NO_PREENCODED_PASSWORDS.get()); 1018 } 1019 1020 builder.add(v); 1021 } 1022 else 1023 { 1024 if (m.getModificationType() == ModificationType.ADD 1025 // Make sure that the password value does not already exist. 1026 && pwPolicyState.passwordMatches(v)) 1027 { 1028 pwpErrorType = PasswordPolicyErrorType.PASSWORD_IN_HISTORY; 1029 throw new DirectoryException(ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, 1030 ERR_MODIFY_PASSWORD_EXISTS.get()); 1031 } 1032 1033 if (newPasswords == null) 1034 { 1035 newPasswords = new LinkedList<>(); 1036 } 1037 newPasswords.add(v); 1038 1039 builder.addAll(pwPolicyState.encodePassword(v)); 1040 } 1041 } 1042 1043 m.setAttribute(builder.toAttribute()); 1044 } 1045 1046 private void preProcessPasswordDelete(Modification m) throws DirectoryException 1047 { 1048 // Iterate through the password values and see if any of them are pre-encoded. 1049 // We will never allow pre-encoded passwords for user password changes, 1050 // but we will allow them for administrators. 1051 // For each clear-text value, verify that at least one value in the entry matches 1052 // and replace the clear-text value with the appropriate encoded forms. 1053 Attribute pwAttr = m.getAttribute(); 1054 if (pwAttr.isEmpty()) 1055 { 1056 // Removing all current password values. 1057 numPasswords = 0; 1058 } 1059 1060 AttributeBuilder builder = new AttributeBuilder(pwAttr, true); 1061 for (ByteString v : pwAttr) 1062 { 1063 if (pwPolicyState.passwordIsPreEncoded(v)) 1064 { 1065 if (!isInternalOperation() && selfChange) 1066 { 1067 pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY; 1068 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 1069 ERR_MODIFY_NO_PREENCODED_PASSWORDS.get()); 1070 } 1071 1072 // We still need to check if the pre-encoded password matches 1073 // an existing value, to decrease the number of passwords. 1074 List<Attribute> attrList = currentEntry.getAttribute(pwAttr.getAttributeType()); 1075 if (attrList == null || attrList.isEmpty()) 1076 { 1077 throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, ERR_MODIFY_NO_EXISTING_VALUES.get()); 1078 } 1079 1080 if (addIfAttributeValueExistsPreEncodedPassword(builder, attrList, v)) 1081 { 1082 numPasswords--; 1083 } 1084 } 1085 else 1086 { 1087 List<Attribute> attrList = currentEntry.getAttribute(pwAttr.getAttributeType()); 1088 if (attrList == null || attrList.isEmpty()) 1089 { 1090 throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, 1091 ERR_MODIFY_NO_EXISTING_VALUES.get()); 1092 } 1093 1094 if (addIfAttributeValueExistsNoPreEncodedPassword(builder, attrList, v)) 1095 { 1096 if (currentPasswords == null) 1097 { 1098 currentPasswords = new LinkedList<>(); 1099 } 1100 currentPasswords.add(v); 1101 numPasswords--; 1102 } 1103 else 1104 { 1105 throw new DirectoryException(ResultCode.NO_SUCH_ATTRIBUTE, 1106 ERR_MODIFY_INVALID_PASSWORD.get()); 1107 } 1108 1109 currentPasswordProvided = true; 1110 } 1111 } 1112 1113 m.setAttribute(builder.toAttribute()); 1114 } 1115 1116 private boolean addIfAttributeValueExistsPreEncodedPassword(AttributeBuilder builder, List<Attribute> attrList, 1117 ByteString val) 1118 { 1119 for (Attribute attr : attrList) 1120 { 1121 for (ByteString av : attr) 1122 { 1123 if (av.equals(val)) 1124 { 1125 builder.add(val); 1126 return true; 1127 } 1128 } 1129 } 1130 return false; 1131 } 1132 1133 private boolean addIfAttributeValueExistsNoPreEncodedPassword(AttributeBuilder builder, List<Attribute> attrList, 1134 ByteString val) throws DirectoryException 1135 { 1136 boolean found = false; 1137 for (Attribute attr : attrList) 1138 { 1139 for (ByteString av : attr) 1140 { 1141 if (pwPolicyState.passwordIsPreEncoded(av)) 1142 { 1143 if (passwordMatches(val, av)) 1144 { 1145 builder.add(av); 1146 found = true; 1147 } 1148 } 1149 else if (av.equals(val)) 1150 { 1151 builder.add(val); 1152 found = true; 1153 } 1154 } 1155 } 1156 return found; 1157 } 1158 1159 private boolean passwordMatches(ByteString val, ByteString av) throws DirectoryException 1160 { 1161 if (pwPolicyState.getAuthenticationPolicy().isAuthPasswordSyntax()) 1162 { 1163 String[] components = AuthPasswordSyntax.decodeAuthPassword(av.toString()); 1164 PasswordStorageScheme<?> scheme = DirectoryServer.getAuthPasswordStorageScheme(components[0]); 1165 return scheme != null && scheme.authPasswordMatches(val, components[1], components[2]); 1166 } else { 1167 String[] components = UserPasswordSyntax.decodeUserPassword(av.toString()); 1168 PasswordStorageScheme<?> scheme = DirectoryServer.getPasswordStorageScheme(toLowerCase(components[0])); 1169 return scheme != null && scheme.passwordMatches(val, ByteString.valueOfUtf8(components[1])); 1170 } 1171 } 1172 1173 /** 1174 * Process an add modification and updates the entry appropriately. 1175 * 1176 * @param attr 1177 * The attribute being added. 1178 * @throws DirectoryException 1179 * If a problem occurs that should cause the modify operation to fail. 1180 */ 1181 private void processAddModification(Attribute attr) throws DirectoryException 1182 { 1183 // Make sure that one or more values have been provided for the attribute. 1184 if (attr.isEmpty()) 1185 { 1186 throw newDirectoryException(currentEntry, ResultCode.PROTOCOL_ERROR, 1187 ERR_MODIFY_ADD_NO_VALUES.get(entryDN, attr.getName())); 1188 } 1189 1190 if (mustCheckSchema()) 1191 { 1192 // make sure that all the new values are valid according to the associated syntax. 1193 checkSchema(attr, ERR_MODIFY_ADD_INVALID_SYNTAX, ERR_MODIFY_ADD_INVALID_SYNTAX_NO_VALUE); 1194 } 1195 1196 // If the attribute to be added is the object class attribute 1197 // then make sure that all the object classes are known and not obsoleted. 1198 if (attr.getAttributeType().isObjectClass()) 1199 { 1200 validateObjectClasses(attr); 1201 } 1202 1203 // Add the provided attribute or merge an existing attribute with 1204 // the values of the new attribute. If there are any duplicates, then fail. 1205 List<ByteString> duplicateValues = new LinkedList<>(); 1206 modifiedEntry.addAttribute(attr, duplicateValues); 1207 if (!duplicateValues.isEmpty() && !permissiveModify) 1208 { 1209 String duplicateValuesStr = Utils.joinAsString(", ", duplicateValues); 1210 1211 throw newDirectoryException(currentEntry, 1212 ResultCode.ATTRIBUTE_OR_VALUE_EXISTS, 1213 ERR_MODIFY_ADD_DUPLICATE_VALUE.get(entryDN, attr.getName(), duplicateValuesStr)); 1214 } 1215 } 1216 1217 private boolean mustCheckSchema() 1218 { 1219 return !isSynchronizationOperation() && DirectoryServer.checkSchema(); 1220 } 1221 1222 /** 1223 * Verifies that all the new values are valid according to the associated syntax. 1224 * 1225 * @throws DirectoryException 1226 * If any of the new values violate the server schema configuration and server is 1227 * configured to reject violations. 1228 */ 1229 private void checkSchema(Attribute attr, 1230 Arg4<Object, Object, Object, Object> invalidSyntaxErrorMsg, 1231 Arg3<Object, Object, Object> invalidSyntaxNoValueErrorMsg) throws DirectoryException 1232 { 1233 AcceptRejectWarn syntaxPolicy = DirectoryServer.getSyntaxEnforcementPolicy(); 1234 Syntax syntax = attr.getAttributeType().getSyntax(); 1235 1236 LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder(); 1237 for (ByteString v : attr) 1238 { 1239 if (!syntax.valueIsAcceptable(v, invalidReason)) 1240 { 1241 LocalizableMessage msg = isHumanReadable(syntax) 1242 ? invalidSyntaxErrorMsg.get(entryDN, attr.getName(), v, invalidReason) 1243 : invalidSyntaxNoValueErrorMsg.get(entryDN, attr.getName(), invalidReason); 1244 1245 switch (syntaxPolicy) 1246 { 1247 case REJECT: 1248 throw newDirectoryException(currentEntry, ResultCode.INVALID_ATTRIBUTE_SYNTAX, msg); 1249 1250 case WARN: 1251 // FIXME remove next line of code. According to Matt, since this is 1252 // just a warning, the code should not set the resultCode 1253 setResultCode(ResultCode.INVALID_ATTRIBUTE_SYNTAX); 1254 logger.error(msg); 1255 invalidReason = new LocalizableMessageBuilder(); 1256 break; 1257 } 1258 } 1259 } 1260 } 1261 1262 private boolean isHumanReadable(Syntax syntax) 1263 { 1264 return syntax.isHumanReadable() && !syntax.isBEREncodingRequired(); 1265 } 1266 1267 /** 1268 * Ensures that the provided object class attribute contains known 1269 * non-obsolete object classes. 1270 * 1271 * @param attr 1272 * The object class attribute to validate. 1273 * @throws DirectoryException 1274 * If the attribute contained unknown or obsolete object 1275 * classes. 1276 */ 1277 private void validateObjectClasses(Attribute attr) throws DirectoryException 1278 { 1279 final AttributeType attrType = attr.getAttributeType(); 1280 Reject.ifFalse(attrType.isObjectClass()); 1281 final MatchingRule eqRule = attrType.getEqualityMatchingRule(); 1282 1283 for (ByteString v : attr) 1284 { 1285 String name = v.toString(); 1286 1287 String lowerName; 1288 try 1289 { 1290 lowerName = eqRule.normalizeAttributeValue(v).toString(); 1291 } 1292 catch (Exception e) 1293 { 1294 logger.traceException(e); 1295 1296 lowerName = toLowerCase(name); 1297 } 1298 1299 ObjectClass oc = DirectoryServer.getObjectClass(lowerName); 1300 if (oc == null) 1301 { 1302 throw newDirectoryException(currentEntry, 1303 ResultCode.OBJECTCLASS_VIOLATION, 1304 ERR_ENTRY_ADD_UNKNOWN_OC.get(name, entryDN)); 1305 } 1306 else if (oc.isObsolete()) 1307 { 1308 throw newDirectoryException(currentEntry, 1309 ResultCode.CONSTRAINT_VIOLATION, 1310 ERR_ENTRY_ADD_OBSOLETE_OC.get(name, entryDN)); 1311 } 1312 } 1313 } 1314 1315 1316 1317 /** 1318 * Process a delete modification and updates the entry appropriately. 1319 * 1320 * @param attr 1321 * The attribute being deleted. 1322 * @throws DirectoryException 1323 * If a problem occurs that should cause the modify operation to fail. 1324 */ 1325 private void processDeleteModification(Attribute attr) throws DirectoryException 1326 { 1327 // Remove the specified attribute values or the entire attribute from the value. 1328 // If there are any specified values that were not present, then fail. 1329 // If the RDN attribute value would be removed, then fail. 1330 List<ByteString> missingValues = new LinkedList<>(); 1331 boolean attrExists = modifiedEntry.removeAttribute(attr, missingValues); 1332 1333 if (attrExists) 1334 { 1335 if (missingValues.isEmpty()) 1336 { 1337 AttributeType t = attr.getAttributeType(); 1338 1339 RDN rdn = modifiedEntry.getName().rdn(); 1340 if (rdn != null 1341 && rdn.hasAttributeType(t) 1342 && !modifiedEntry.hasValue(t, attr.getOptions(), rdn.getAttributeValue(t))) 1343 { 1344 throw newDirectoryException(currentEntry, 1345 ResultCode.NOT_ALLOWED_ON_RDN, 1346 ERR_MODIFY_DELETE_RDN_ATTR.get(entryDN, attr.getName())); 1347 } 1348 } 1349 else if (!permissiveModify) 1350 { 1351 String missingValuesStr = Utils.joinAsString(", ", missingValues); 1352 1353 throw newDirectoryException(currentEntry, ResultCode.NO_SUCH_ATTRIBUTE, 1354 ERR_MODIFY_DELETE_MISSING_VALUES.get(entryDN, attr.getName(), missingValuesStr)); 1355 } 1356 } 1357 else if (!permissiveModify) 1358 { 1359 throw newDirectoryException(currentEntry, ResultCode.NO_SUCH_ATTRIBUTE, 1360 ERR_MODIFY_DELETE_NO_SUCH_ATTR.get(entryDN, attr.getName())); 1361 } 1362 } 1363 1364 1365 1366 /** 1367 * Process a replace modification and updates the entry appropriately. 1368 * 1369 * @param attr 1370 * The attribute being replaced. 1371 * @throws DirectoryException 1372 * If a problem occurs that should cause the modify operation to fail. 1373 */ 1374 private void processReplaceModification(Attribute attr) throws DirectoryException 1375 { 1376 if (mustCheckSchema()) 1377 { 1378 // make sure that all the new values are valid according to the associated syntax. 1379 checkSchema(attr, ERR_MODIFY_REPLACE_INVALID_SYNTAX, ERR_MODIFY_REPLACE_INVALID_SYNTAX_NO_VALUE); 1380 } 1381 1382 // If the attribute to be replaced is the object class attribute 1383 // then make sure that all the object classes are known and not obsoleted. 1384 if (attr.getAttributeType().isObjectClass()) 1385 { 1386 validateObjectClasses(attr); 1387 } 1388 1389 // Replace the provided attribute. 1390 modifiedEntry.replaceAttribute(attr); 1391 1392 // Make sure that the RDN attribute value(s) has not been removed. 1393 AttributeType t = attr.getAttributeType(); 1394 RDN rdn = modifiedEntry.getName().rdn(); 1395 if (rdn != null 1396 && rdn.hasAttributeType(t) 1397 && !modifiedEntry.hasValue(t, attr.getOptions(), rdn.getAttributeValue(t))) 1398 { 1399 throw newDirectoryException(modifiedEntry, ResultCode.NOT_ALLOWED_ON_RDN, 1400 ERR_MODIFY_DELETE_RDN_ATTR.get(entryDN, attr.getName())); 1401 } 1402 } 1403 1404 /** 1405 * Process an increment modification and updates the entry appropriately. 1406 * 1407 * @param attr 1408 * The attribute being incremented. 1409 * @throws DirectoryException 1410 * If a problem occurs that should cause the modify operation to fail. 1411 */ 1412 private void processIncrementModification(Attribute attr) throws DirectoryException 1413 { 1414 // The specified attribute type must not be an RDN attribute. 1415 AttributeType t = attr.getAttributeType(); 1416 RDN rdn = modifiedEntry.getName().rdn(); 1417 if (rdn != null && rdn.hasAttributeType(t)) 1418 { 1419 throw newDirectoryException(modifiedEntry, ResultCode.NOT_ALLOWED_ON_RDN, 1420 ERR_MODIFY_INCREMENT_RDN.get(entryDN, attr.getName())); 1421 } 1422 1423 // The provided attribute must have a single value, and it must be an integer 1424 if (attr.isEmpty()) 1425 { 1426 throw newDirectoryException(modifiedEntry, ResultCode.PROTOCOL_ERROR, 1427 ERR_MODIFY_INCREMENT_REQUIRES_VALUE.get(entryDN, attr.getName())); 1428 } 1429 else if (attr.size() > 1) 1430 { 1431 throw newDirectoryException(modifiedEntry, ResultCode.PROTOCOL_ERROR, 1432 ERR_MODIFY_INCREMENT_REQUIRES_SINGLE_VALUE.get(entryDN, attr.getName())); 1433 } 1434 1435 MatchingRule eqRule = attr.getAttributeType().getEqualityMatchingRule(); 1436 ByteString v = attr.iterator().next(); 1437 1438 long incrementValue; 1439 try 1440 { 1441 String nv = eqRule.normalizeAttributeValue(v).toString(); 1442 incrementValue = Long.parseLong(nv); 1443 } 1444 catch (Exception e) 1445 { 1446 logger.traceException(e); 1447 1448 throw new DirectoryException(ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1449 ERR_MODIFY_INCREMENT_PROVIDED_VALUE_NOT_INTEGER.get(entryDN, attr.getName(), v), e); 1450 } 1451 1452 // Get the attribute that is to be incremented. 1453 Attribute a = modifiedEntry.getExactAttribute(t, attr.getOptions()); 1454 if (a == null) 1455 { 1456 throw newDirectoryException(modifiedEntry, 1457 ResultCode.CONSTRAINT_VIOLATION, 1458 ERR_MODIFY_INCREMENT_REQUIRES_EXISTING_VALUE.get(entryDN, attr.getName())); 1459 } 1460 1461 // Increment each attribute value by the specified amount. 1462 AttributeBuilder builder = new AttributeBuilder(a, true); 1463 for (ByteString existingValue : a) 1464 { 1465 long currentValue; 1466 try 1467 { 1468 currentValue = Long.parseLong(existingValue.toString()); 1469 } 1470 catch (Exception e) 1471 { 1472 logger.traceException(e); 1473 1474 throw new DirectoryException( 1475 ResultCode.INVALID_ATTRIBUTE_SYNTAX, 1476 ERR_MODIFY_INCREMENT_REQUIRES_INTEGER_VALUE.get(entryDN, a.getName(), existingValue), 1477 e); 1478 } 1479 1480 long newValue = currentValue + incrementValue; 1481 builder.add(String.valueOf(newValue)); 1482 } 1483 1484 // Replace the existing attribute with the incremented version. 1485 modifiedEntry.replaceAttribute(builder.toAttribute()); 1486 } 1487 1488 /** 1489 * Performs additional preliminary processing that is required for a password change. 1490 * 1491 * @throws DirectoryException 1492 * If a problem occurs that should cause the modify operation to fail. 1493 */ 1494 private void performAdditionalPasswordChangedProcessing() throws DirectoryException 1495 { 1496 if (!passwordChanged) 1497 { 1498 // Nothing to do. 1499 return; 1500 } 1501 1502 // If it was a self change, then see if the current password was provided 1503 // and handle accordingly. 1504 final PasswordPolicy authPolicy = pwPolicyState.getAuthenticationPolicy(); 1505 if (selfChange 1506 && authPolicy.isPasswordChangeRequiresCurrentPassword() 1507 && !currentPasswordProvided) 1508 { 1509 pwpErrorType = PasswordPolicyErrorType.MUST_SUPPLY_OLD_PASSWORD; 1510 throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, 1511 ERR_MODIFY_PW_CHANGE_REQUIRES_CURRENT_PW.get()); 1512 } 1513 1514 1515 // If this change would result in multiple password values, then see if that's OK. 1516 if (numPasswords > 1 && !authPolicy.isAllowMultiplePasswordValues()) 1517 { 1518 pwpErrorType = PasswordPolicyErrorType.PASSWORD_MOD_NOT_ALLOWED; 1519 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 1520 ERR_MODIFY_MULTIPLE_PASSWORDS_NOT_ALLOWED.get()); 1521 } 1522 1523 1524 // If any of the password values should be validated, then do so now. 1525 if (newPasswords != null 1526 && (selfChange || !authPolicy.isSkipValidationForAdministrators())) 1527 { 1528 HashSet<ByteString> clearPasswords = new HashSet<>(pwPolicyState.getClearPasswords()); 1529 if (currentPasswords != null) 1530 { 1531 clearPasswords.addAll(currentPasswords); 1532 } 1533 1534 for (ByteString v : newPasswords) 1535 { 1536 LocalizableMessageBuilder invalidReason = new LocalizableMessageBuilder(); 1537 if (! pwPolicyState.passwordIsAcceptable(this, modifiedEntry, 1538 v, clearPasswords, invalidReason)) 1539 { 1540 pwpErrorType = PasswordPolicyErrorType.INSUFFICIENT_PASSWORD_QUALITY; 1541 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 1542 ERR_MODIFY_PW_VALIDATION_FAILED.get(invalidReason)); 1543 } 1544 } 1545 } 1546 1547 // If we should check the password history, then do so now. 1548 if (newPasswords != null && pwPolicyState.maintainHistory()) 1549 { 1550 for (ByteString v : newPasswords) 1551 { 1552 if (pwPolicyState.isPasswordInHistory(v) 1553 && (selfChange || !authPolicy.isSkipValidationForAdministrators())) 1554 { 1555 pwpErrorType = PasswordPolicyErrorType.PASSWORD_IN_HISTORY; 1556 throw new DirectoryException(ResultCode.CONSTRAINT_VIOLATION, 1557 ERR_MODIFY_PW_IN_HISTORY.get()); 1558 } 1559 } 1560 1561 pwPolicyState.updatePasswordHistory(); 1562 } 1563 1564 1565 wasLocked = pwPolicyState.isLocked(); 1566 1567 // Update the password policy state attributes in the user's entry. If the 1568 // modification fails, then these changes won't be applied. 1569 pwPolicyState.setPasswordChangedTime(); 1570 pwPolicyState.clearFailureLockout(); 1571 pwPolicyState.clearGraceLoginTimes(); 1572 pwPolicyState.clearWarnedTime(); 1573 1574 if (authPolicy.isForceChangeOnAdd() || authPolicy.isForceChangeOnReset()) 1575 { 1576 if (selfChange) 1577 { 1578 pwPolicyState.setMustChangePassword(false); 1579 } 1580 else 1581 { 1582 if (pwpErrorType == null && authPolicy.isForceChangeOnReset()) 1583 { 1584 pwpErrorType = PasswordPolicyErrorType.CHANGE_AFTER_RESET; 1585 } 1586 1587 pwPolicyState.setMustChangePassword(authPolicy.isForceChangeOnReset()); 1588 } 1589 } 1590 1591 if (authPolicy.getRequireChangeByTime() > 0) 1592 { 1593 pwPolicyState.setRequiredChangeTime(); 1594 } 1595 1596 modifications.addAll(pwPolicyState.getModifications()); 1597 modifiedEntry.applyModifications(pwPolicyState.getModifications()); 1598 } 1599 1600 /** Generate any password policy account status notifications as a result of modify processing. */ 1601 private void generatePwpAccountStatusNotifications() 1602 { 1603 if (passwordChanged) 1604 { 1605 if (selfChange) 1606 { 1607 AuthenticationInfo authInfo = clientConnection.getAuthenticationInfo(); 1608 if (authInfo.getAuthenticationDN().equals(modifiedEntry.getName())) 1609 { 1610 clientConnection.setMustChangePassword(false); 1611 } 1612 1613 generateAccountStatusNotificationForPwds(PASSWORD_CHANGED, INFO_MODIFY_PASSWORD_CHANGED.get()); 1614 } 1615 else 1616 { 1617 generateAccountStatusNotificationForPwds(PASSWORD_RESET, INFO_MODIFY_PASSWORD_RESET.get()); 1618 } 1619 } 1620 1621 if (enabledStateChanged) 1622 { 1623 if (isEnabled) 1624 { 1625 generateAccountStatusNotificationNoPwds(ACCOUNT_ENABLED, INFO_MODIFY_ACCOUNT_ENABLED.get()); 1626 } 1627 else 1628 { 1629 generateAccountStatusNotificationNoPwds(ACCOUNT_DISABLED, INFO_MODIFY_ACCOUNT_DISABLED.get()); 1630 } 1631 } 1632 1633 if (wasLocked) 1634 { 1635 generateAccountStatusNotificationNoPwds(ACCOUNT_UNLOCKED, INFO_MODIFY_ACCOUNT_UNLOCKED.get()); 1636 } 1637 } 1638 1639 private void generateAccountStatusNotificationNoPwds( 1640 AccountStatusNotificationType notificationType, LocalizableMessage message) 1641 { 1642 pwPolicyState.generateAccountStatusNotification(notificationType, modifiedEntry, message, 1643 AccountStatusNotification.createProperties(pwPolicyState, false, -1, null, null)); 1644 } 1645 1646 private void generateAccountStatusNotificationForPwds( 1647 AccountStatusNotificationType notificationType, LocalizableMessage message) 1648 { 1649 pwPolicyState.generateAccountStatusNotification(notificationType, modifiedEntry, message, 1650 AccountStatusNotification.createProperties(pwPolicyState, false, -1, currentPasswords, newPasswords)); 1651 } 1652 1653 /** 1654 * Handle conflict resolution. 1655 * 1656 * @return {@code true} if processing should continue for the operation, or {@code false} if not. 1657 */ 1658 private boolean handleConflictResolution() { 1659 for (SynchronizationProvider<?> provider : getSynchronizationProviders()) { 1660 try { 1661 SynchronizationProviderResult result = 1662 provider.handleConflictResolution(this); 1663 if (! result.continueProcessing()) { 1664 setResultCodeAndMessageNoInfoDisclosure(modifiedEntry, 1665 result.getResultCode(), result.getErrorMessage()); 1666 setMatchedDN(result.getMatchedDN()); 1667 setReferralURLs(result.getReferralURLs()); 1668 return false; 1669 } 1670 } catch (DirectoryException de) { 1671 logger.traceException(de); 1672 logger.error(ERR_MODIFY_SYNCH_CONFLICT_RESOLUTION_FAILED, 1673 getConnectionID(), getOperationID(), getExceptionMessage(de)); 1674 setResponseData(de); 1675 return false; 1676 } 1677 } 1678 return true; 1679 } 1680 1681 /** 1682 * Process pre operation. 1683 * @return {@code true} if processing should continue for the operation, or 1684 * {@code false} if not. 1685 */ 1686 private boolean processPreOperation() { 1687 for (SynchronizationProvider<?> provider : getSynchronizationProviders()) { 1688 try { 1689 if (!processOperationResult(this, provider.doPreOperation(this))) { 1690 return false; 1691 } 1692 } catch (DirectoryException de) { 1693 logger.traceException(de); 1694 logger.error(ERR_MODIFY_SYNCH_PREOP_FAILED, getConnectionID(), 1695 getOperationID(), getExceptionMessage(de)); 1696 setResponseData(de); 1697 return false; 1698 } 1699 } 1700 return true; 1701 } 1702 1703 /** Invoke post operation synchronization providers. */ 1704 private void processSynchPostOperationPlugins() { 1705 for (SynchronizationProvider<?> provider : getSynchronizationProviders()) { 1706 try { 1707 provider.doPostOperation(this); 1708 } catch (DirectoryException de) { 1709 logger.traceException(de); 1710 logger.error(ERR_MODIFY_SYNCH_POSTOP_FAILED, getConnectionID(), 1711 getOperationID(), getExceptionMessage(de)); 1712 setResponseData(de); 1713 return; 1714 } 1715 } 1716 } 1717}