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