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