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