001/* 002 * Copyright 2018-2024 Ping Identity Corporation 003 * All Rights Reserved. 004 */ 005/* 006 * Copyright 2018-2024 Ping Identity Corporation 007 * 008 * Licensed under the Apache License, Version 2.0 (the "License"); 009 * you may not use this file except in compliance with the License. 010 * You may obtain a copy of the License at 011 * 012 * http://www.apache.org/licenses/LICENSE-2.0 013 * 014 * Unless required by applicable law or agreed to in writing, software 015 * distributed under the License is distributed on an "AS IS" BASIS, 016 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 017 * See the License for the specific language governing permissions and 018 * limitations under the License. 019 */ 020/* 021 * Copyright (C) 2018-2024 Ping Identity Corporation 022 * 023 * This program is free software; you can redistribute it and/or modify 024 * it under the terms of the GNU General Public License (GPLv2 only) 025 * or the terms of the GNU Lesser General Public License (LGPLv2.1 only) 026 * as published by the Free Software Foundation. 027 * 028 * This program is distributed in the hope that it will be useful, 029 * but WITHOUT ANY WARRANTY; without even the implied warranty of 030 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 031 * GNU General Public License for more details. 032 * 033 * You should have received a copy of the GNU General Public License 034 * along with this program; if not, see <http://www.gnu.org/licenses>. 035 */ 036package com.unboundid.ldap.sdk.unboundidds.logs; 037 038 039 040import java.util.ArrayList; 041import java.util.Arrays; 042import java.util.Collections; 043import java.util.HashSet; 044import java.util.List; 045import java.util.Set; 046 047import com.unboundid.asn1.ASN1OctetString; 048import com.unboundid.ldap.sdk.Attribute; 049import com.unboundid.ldap.sdk.ChangeType; 050import com.unboundid.ldap.sdk.DN; 051import com.unboundid.ldap.sdk.Modification; 052import com.unboundid.ldap.sdk.ModificationType; 053import com.unboundid.ldap.sdk.RDN; 054import com.unboundid.ldif.LDIFChangeRecord; 055import com.unboundid.ldif.LDIFModifyChangeRecord; 056import com.unboundid.ldif.LDIFModifyDNChangeRecord; 057import com.unboundid.ldif.LDIFException; 058import com.unboundid.ldif.LDIFReader; 059import com.unboundid.util.Debug; 060import com.unboundid.util.NotNull; 061import com.unboundid.util.Nullable; 062import com.unboundid.util.ObjectPair; 063import com.unboundid.util.StaticUtils; 064import com.unboundid.util.ThreadSafety; 065import com.unboundid.util.ThreadSafetyLevel; 066 067import static com.unboundid.ldap.sdk.unboundidds.logs.LogMessages.*; 068 069 070 071/** 072 * This class provides a data structure that holds information about an audit 073 * log message that represents a modify DN operation. 074 * <BR> 075 * <BLOCKQUOTE> 076 * <B>NOTE:</B> This class, and other classes within the 077 * {@code com.unboundid.ldap.sdk.unboundidds} package structure, are only 078 * supported for use against Ping Identity, UnboundID, and 079 * Nokia/Alcatel-Lucent 8661 server products. These classes provide support 080 * for proprietary functionality or for external specifications that are not 081 * considered stable or mature enough to be guaranteed to work in an 082 * interoperable way with other types of LDAP servers. 083 * </BLOCKQUOTE> 084 */ 085@ThreadSafety(level= ThreadSafetyLevel.COMPLETELY_THREADSAFE) 086public final class ModifyDNAuditLogMessage 087 extends AuditLogMessage 088{ 089 /** 090 * Retrieves the serial version UID for this serializable class. 091 */ 092 private static final long serialVersionUID = 3954476664207635518L; 093 094 095 096 // An LDIF change record that encapsulates the change represented by this 097 // modify DN audit log message. 098 @NotNull private final LDIFModifyDNChangeRecord modifyDNChangeRecord; 099 100 // The attribute modifications associated with this modify DN operation. 101 @Nullable private final List<Modification> attributeModifications; 102 103 104 105 /** 106 * Creates a new modify DN audit log message from the provided set of lines. 107 * 108 * @param logMessageLines The lines that comprise the log message. It must 109 * not be {@code null} or empty, and it must not 110 * contain any blank lines, although it may contain 111 * comments. In fact, it must contain at least one 112 * comment line that appears before any non-comment 113 * lines (but possibly after other comment lines) 114 * that serves as the message header. 115 * 116 * @throws AuditLogException If a problem is encountered while processing 117 * the provided list of log message lines. 118 */ 119 public ModifyDNAuditLogMessage(@NotNull final String... logMessageLines) 120 throws AuditLogException 121 { 122 this(StaticUtils.toList(logMessageLines), logMessageLines); 123 } 124 125 126 127 /** 128 * Creates a new modify DN audit log message from the provided set of lines. 129 * 130 * @param logMessageLines The lines that comprise the log message. It must 131 * not be {@code null} or empty, and it must not 132 * contain any blank lines, although it may contain 133 * comments. In fact, it must contain at least one 134 * comment line that appears before any non-comment 135 * lines (but possibly after other comment lines) 136 * that serves as the message header. 137 * 138 * @throws AuditLogException If a problem is encountered while processing 139 * audit provided list of log message lines. 140 */ 141 public ModifyDNAuditLogMessage(@NotNull final List<String> logMessageLines) 142 throws AuditLogException 143 { 144 this(logMessageLines, StaticUtils.toArray(logMessageLines, String.class)); 145 } 146 147 148 149 /** 150 * Creates a new modify DN audit log message from the provided information. 151 * 152 * @param logMessageLineList The lines that comprise the log message as a 153 * list. 154 * @param logMessageLineArray The lines that comprise the log message as an 155 * array. 156 * 157 * @throws AuditLogException If a problem is encountered while processing 158 * the provided list of log message lines. 159 */ 160 private ModifyDNAuditLogMessage( 161 @NotNull final List<String> logMessageLineList, 162 @NotNull final String[] logMessageLineArray) 163 throws AuditLogException 164 { 165 super(logMessageLineList); 166 167 try 168 { 169 final LDIFChangeRecord changeRecord = 170 LDIFReader.decodeChangeRecord(logMessageLineArray); 171 if (! (changeRecord instanceof LDIFModifyDNChangeRecord)) 172 { 173 throw new AuditLogException(logMessageLineList, 174 ERR_MODIFY_DN_AUDIT_LOG_MESSAGE_CHANGE_TYPE_NOT_MODIFY_DN.get( 175 changeRecord.getChangeType().getName(), 176 ChangeType.MODIFY_DN.getName())); 177 } 178 179 modifyDNChangeRecord = (LDIFModifyDNChangeRecord) changeRecord; 180 } 181 catch (final LDIFException e) 182 { 183 Debug.debugException(e); 184 throw new AuditLogException(logMessageLineList, 185 ERR_MODIFY_DN_AUDIT_LOG_MESSAGE_LINES_NOT_CHANGE_RECORD.get( 186 StaticUtils.getExceptionMessage(e)), 187 e); 188 } 189 190 attributeModifications = 191 decodeAttributeModifications(logMessageLineList, modifyDNChangeRecord); 192 } 193 194 195 196 /** 197 * Creates a new modify DN audit log message from the provided set of lines. 198 * 199 * @param logMessageLines The lines that comprise the log message. It 200 * must not be {@code null} or empty, and it 201 * must not contain any blank lines, although it 202 * may contain comments. In fact, it must 203 * contain at least one comment line that 204 * appears before any non-comment lines (but 205 * possibly after other comment lines) that 206 * serves as the message header. 207 * @param modifyDNChangeRecord The LDIF modify DN change record that is 208 * described by the provided log message lines. 209 * 210 * @throws AuditLogException If a problem is encountered while processing 211 * the provided list of log message lines. 212 */ 213 ModifyDNAuditLogMessage(@NotNull final List<String> logMessageLines, 214 @NotNull final LDIFModifyDNChangeRecord modifyDNChangeRecord) 215 throws AuditLogException 216 { 217 super(logMessageLines); 218 219 this.modifyDNChangeRecord = modifyDNChangeRecord; 220 221 attributeModifications = 222 decodeAttributeModifications(logMessageLines, modifyDNChangeRecord); 223 } 224 225 226 227 /** 228 * Decodes the list of attribute modifications from the audit log message, if 229 * available. 230 * 231 * @param logMessageLines The lines that comprise the log message. It 232 * must not be {@code null} or empty, and it 233 * must not contain any blank lines, although it 234 * may contain comments. In fact, it must 235 * contain at least one comment line that 236 * appears before any non-comment lines (but 237 * possibly after other comment lines) that 238 * serves as the message header. 239 * @param modifyDNChangeRecord The LDIF modify DN change record that is 240 * described by the provided log message lines. 241 * 242 * @return The list of attribute modifications from the audit log message, or 243 * {@code null} if there were no modifications. 244 */ 245 @Nullable() 246 private static List<Modification> decodeAttributeModifications( 247 @NotNull final List<String> logMessageLines, 248 @NotNull final LDIFModifyDNChangeRecord modifyDNChangeRecord) 249 { 250 List<String> ldifLines = null; 251 for (final String line : logMessageLines) 252 { 253 final String uncommentedLine; 254 if (line.startsWith("# ")) 255 { 256 uncommentedLine = line.substring(2); 257 } 258 else 259 { 260 break; 261 } 262 263 if (ldifLines == null) 264 { 265 final String lowerLine = StaticUtils.toLowerCase(uncommentedLine); 266 if (lowerLine.startsWith("modifydn attribute modifications")) 267 { 268 ldifLines = new ArrayList<>(logMessageLines.size()); 269 } 270 } 271 else 272 { 273 if (ldifLines.isEmpty()) 274 { 275 ldifLines.add("dn: " + modifyDNChangeRecord.getDN()); 276 ldifLines.add("changetype: modify"); 277 } 278 279 ldifLines.add(uncommentedLine); 280 } 281 } 282 283 if (ldifLines == null) 284 { 285 return null; 286 } 287 else if (ldifLines.isEmpty()) 288 { 289 return Collections.emptyList(); 290 } 291 else 292 { 293 try 294 { 295 final String[] ldifLineArray = 296 ldifLines.toArray(StaticUtils.NO_STRINGS); 297 final LDIFModifyChangeRecord changeRecord = 298 (LDIFModifyChangeRecord) 299 LDIFReader.decodeChangeRecord(ldifLineArray); 300 return Collections.unmodifiableList( 301 Arrays.asList(changeRecord.getModifications())); 302 } 303 catch (final Exception e) 304 { 305 Debug.debugException(e); 306 return null; 307 } 308 } 309 } 310 311 312 313 /** 314 * {@inheritDoc} 315 */ 316 @Override() 317 @NotNull() 318 public String getDN() 319 { 320 return modifyDNChangeRecord.getDN(); 321 } 322 323 324 325 /** 326 * Retrieves the new RDN for the associated modify DN operation. 327 * 328 * @return The new RDN for the associated modify DN operation. 329 */ 330 @NotNull() 331 public String getNewRDN() 332 { 333 return modifyDNChangeRecord.getNewRDN(); 334 } 335 336 337 338 /** 339 * Indicates whether the old RDN attribute values were removed from the entry. 340 * 341 * @return {@code true} if the old RDN attribute values were removed from the 342 * entry, or {@code false} if not. 343 */ 344 public boolean deleteOldRDN() 345 { 346 return modifyDNChangeRecord.deleteOldRDN(); 347 } 348 349 350 351 /** 352 * Retrieves the new superior DN for the associated modify DN operation, if 353 * available. 354 * 355 * @return The new superior DN for the associated modify DN operation, or 356 * {@code null} if there was no new superior DN. 357 */ 358 @Nullable() 359 public String getNewSuperiorDN() 360 { 361 return modifyDNChangeRecord.getNewSuperiorDN(); 362 } 363 364 365 366 /** 367 * Retrieves the list of attribute modifications for the associated modify DN 368 * operation, if available. 369 * 370 * @return The list of attribute modifications for the associated modify DN 371 * operation, or {@code null} if it is not available. If it is 372 * known that there were no attribute modifications, then an empty 373 * list will be returned. 374 */ 375 @Nullable() 376 public List<Modification> getAttributeModifications() 377 { 378 return attributeModifications; 379 } 380 381 382 383 /** 384 * {@inheritDoc} 385 */ 386 @Override() 387 @NotNull() 388 public ChangeType getChangeType() 389 { 390 return ChangeType.MODIFY_DN; 391 } 392 393 394 395 /** 396 * {@inheritDoc} 397 */ 398 @Override() 399 @NotNull() 400 public LDIFModifyDNChangeRecord getChangeRecord() 401 { 402 return modifyDNChangeRecord; 403 } 404 405 406 407 /** 408 * {@inheritDoc} 409 */ 410 @Override() 411 public boolean isRevertible() 412 { 413 // We can't revert a change record if the original DN was that of the root 414 // DSE. 415 final DN parsedDN; 416 final RDN oldRDN; 417 try 418 { 419 parsedDN = modifyDNChangeRecord.getParsedDN(); 420 oldRDN = parsedDN.getRDN(); 421 if (oldRDN == null) 422 { 423 return false; 424 } 425 } 426 catch (final Exception e) 427 { 428 Debug.debugException(e); 429 return false; 430 } 431 432 433 // We can't create a revert change record if we can't construct the new DN 434 // for the entry. 435 final DN newDN; 436 final RDN newRDN; 437 try 438 { 439 newDN = modifyDNChangeRecord.getNewDN(); 440 newRDN = modifyDNChangeRecord.getParsedNewRDN(); 441 } 442 catch (final Exception e) 443 { 444 Debug.debugException(e); 445 return false; 446 } 447 448 449 // Modify DN change records will only be revertible if we have a set of 450 // attribute modifications. If we don't have a set of attribute 451 // modifications, we can't know what value to use for the deleteOldRDN flag. 452 if (attributeModifications == null) 453 { 454 return false; 455 } 456 457 458 // If the set of attribute modifications is empty, then deleteOldRDN must 459 // be false or the new RDN must equal the old RDN. 460 if (attributeModifications.isEmpty()) 461 { 462 if (modifyDNChangeRecord.deleteOldRDN() && (! newRDN.equals(oldRDN))) 463 { 464 return false; 465 } 466 } 467 468 469 // If any of the included modifications has a modification type that is 470 // anything other than add, delete, or increment, then it's not revertible. 471 // And if any of the delete modifications don't have values, then it's not 472 // revertible. 473 for (final Modification m : attributeModifications) 474 { 475 if (!ModifyAuditLogMessage.modificationIsRevertible(m)) 476 { 477 return false; 478 } 479 } 480 481 482 // If we've gotten here, then we can change 483 return true; 484 } 485 486 487 488 /** 489 * {@inheritDoc} 490 */ 491 @Override() 492 @NotNull() 493 public List<LDIFChangeRecord> getRevertChangeRecords() 494 throws AuditLogException 495 { 496 // We can't create a set of revertible changes if we don't have access to 497 // attribute modifications. 498 if (attributeModifications == null) 499 { 500 throw new AuditLogException(getLogMessageLines(), 501 ERR_MODIFY_DN_NOT_REVERTIBLE.get(modifyDNChangeRecord.getDN())); 502 } 503 504 505 // Get the DN of the entry after the modify DN operation was processed, 506 // along with parsed versions of the original DN, new RDN, and new superior 507 // DN. 508 final DN newDN; 509 final DN newSuperiorDN; 510 final DN originalDN; 511 final RDN newRDN; 512 try 513 { 514 newDN = modifyDNChangeRecord.getNewDN(); 515 originalDN = modifyDNChangeRecord.getParsedDN(); 516 newSuperiorDN = modifyDNChangeRecord.getParsedNewSuperiorDN(); 517 newRDN = modifyDNChangeRecord.getParsedNewRDN(); 518 } 519 catch (final Exception e) 520 { 521 Debug.debugException(e); 522 523 if (modifyDNChangeRecord.getNewSuperiorDN() == null) 524 { 525 throw new AuditLogException(getLogMessageLines(), 526 ERR_MODIFY_DN_CANNOT_GET_NEW_DN_WITHOUT_NEW_SUPERIOR.get( 527 modifyDNChangeRecord.getDN(), 528 modifyDNChangeRecord.getNewRDN()), 529 e); 530 } 531 else 532 { 533 throw new AuditLogException(getLogMessageLines(), 534 ERR_MODIFY_DN_CANNOT_GET_NEW_DN_WITH_NEW_SUPERIOR.get( 535 modifyDNChangeRecord.getDN(), 536 modifyDNChangeRecord.getNewRDN(), 537 modifyDNChangeRecord.getNewSuperiorDN()), 538 e); 539 } 540 } 541 542 543 // If the original DN is the null DN, then fail. 544 if (originalDN.isNullDN()) 545 { 546 throw new AuditLogException(getLogMessageLines(), 547 ERR_MODIFY_DN_CANNOT_REVERT_NULL_DN.get()); 548 } 549 550 551 // If the set of attribute modifications is empty, then deleteOldRDN must 552 // be false or the new RDN must equal the old RDN. 553 if (attributeModifications.isEmpty()) 554 { 555 if (modifyDNChangeRecord.deleteOldRDN() && 556 (! newRDN.equals(originalDN.getRDN()))) 557 { 558 throw new AuditLogException(getLogMessageLines(), 559 ERR_MODIFY_DN_CANNOT_REVERT_WITHOUT_NECESSARY_MODS.get( 560 modifyDNChangeRecord.getDN())); 561 } 562 } 563 564 565 // Construct the DN, new RDN, and new superior DN values for the change 566 // needed to revert the modify DN operation. 567 final String revertedDN = newDN.toString(); 568 final String revertedNewRDN = originalDN.getRDNString(); 569 570 final String revertedNewSuperiorDN; 571 if (newSuperiorDN == null) 572 { 573 revertedNewSuperiorDN = null; 574 } 575 else 576 { 577 revertedNewSuperiorDN = originalDN.getParentString(); 578 } 579 580 581 // If the set of attribute modifications is empty, then deleteOldRDN must 582 // have been false and the new RDN attribute value(s) must have already been 583 // in the entry. 584 if (attributeModifications.isEmpty()) 585 { 586 return Collections.<LDIFChangeRecord>singletonList( 587 new LDIFModifyDNChangeRecord(revertedDN, revertedNewRDN, false, 588 revertedNewSuperiorDN)); 589 } 590 591 592 // Iterate through the modifications to see which new RDN attributes were 593 // added to the entry. If they were all added, then we need to use a 594 // deleteOldRDN value of true. If none of them were added, then we need to 595 // use a deleteOldRDN value of false. If some of them were added but some 596 // were not, then we need to use a deleteOldRDN value o false and have a 597 // second modification to delete those values that were added. 598 // 599 // Also, collect any additional modifications that don't involve new RDN 600 // attribute values. 601 final int numNewRDNs = newRDN.getAttributeNames().length; 602 final Set<ObjectPair<String,byte[]>> addedNewRDNValues = 603 new HashSet<>(StaticUtils.computeMapCapacity(numNewRDNs)); 604 final RDN originalRDN = originalDN.getRDN(); 605 final List<Modification> additionalModifications = 606 new ArrayList<>(attributeModifications.size()); 607 final int numModifications = attributeModifications.size(); 608 for (int i=numModifications - 1; i >= 0; i--) 609 { 610 final Modification m = attributeModifications.get(i); 611 if (m.getModificationType() == ModificationType.ADD) 612 { 613 final Attribute a = m.getAttribute(); 614 final ArrayList<byte[]> retainedValues = new ArrayList<>(a.size()); 615 for (final ASN1OctetString value : a.getRawValues()) 616 { 617 final byte[] valueBytes = value.getValue(); 618 if (newRDN.hasAttributeValue(a.getName(), valueBytes)) 619 { 620 addedNewRDNValues.add(new ObjectPair<>(a.getName(), valueBytes)); 621 } 622 else 623 { 624 retainedValues.add(valueBytes); 625 } 626 } 627 628 if (retainedValues.size() == a.size()) 629 { 630 additionalModifications.add(new Modification( 631 ModificationType.DELETE, a.getName(), a.getRawValues())); 632 } 633 else if (! retainedValues.isEmpty()) 634 { 635 additionalModifications.add(new Modification( 636 ModificationType.DELETE, a.getName(), 637 StaticUtils.toArray(retainedValues, byte[].class))); 638 } 639 } 640 else if (m.getModificationType() == ModificationType.DELETE) 641 { 642 final Attribute a = m.getAttribute(); 643 final ArrayList<byte[]> retainedValues = new ArrayList<>(a.size()); 644 for (final ASN1OctetString value : a.getRawValues()) 645 { 646 final byte[] valueBytes = value.getValue(); 647 if (! originalRDN.hasAttributeValue(a.getName(), valueBytes)) 648 { 649 retainedValues.add(valueBytes); 650 } 651 } 652 653 if (retainedValues.size() == a.size()) 654 { 655 additionalModifications.add(new Modification( 656 ModificationType.ADD, a.getName(), a.getRawValues())); 657 } 658 else if (! retainedValues.isEmpty()) 659 { 660 additionalModifications.add(new Modification( 661 ModificationType.ADD, a.getName(), 662 StaticUtils.toArray(retainedValues, byte[].class))); 663 } 664 } 665 else 666 { 667 final Modification revertModification = 668 ModifyAuditLogMessage.getRevertModification(m); 669 if (revertModification == null) 670 { 671 throw new AuditLogException(getLogMessageLines(), 672 ERR_MODIFY_DN_MOD_NOT_REVERTIBLE.get( 673 modifyDNChangeRecord.getDN(), 674 m.getModificationType().getName(), m.getAttributeName())); 675 } 676 else 677 { 678 additionalModifications.add(revertModification); 679 } 680 } 681 } 682 683 final boolean revertedDeleteOldRDN; 684 if (addedNewRDNValues.size() == numNewRDNs) 685 { 686 revertedDeleteOldRDN = true; 687 } 688 else 689 { 690 revertedDeleteOldRDN = false; 691 if (! addedNewRDNValues.isEmpty()) 692 { 693 for (final ObjectPair<String,byte[]> p : addedNewRDNValues) 694 { 695 additionalModifications.add(0, 696 new Modification(ModificationType.DELETE, p.getFirst(), 697 p.getSecond())); 698 } 699 } 700 } 701 702 703 final List<LDIFChangeRecord> changeRecords = new ArrayList<>(2); 704 changeRecords.add(new LDIFModifyDNChangeRecord(revertedDN, revertedNewRDN, 705 revertedDeleteOldRDN, revertedNewSuperiorDN)); 706 if (! additionalModifications.isEmpty()) 707 { 708 changeRecords.add(new LDIFModifyChangeRecord(originalDN.toString(), 709 additionalModifications)); 710 } 711 712 return Collections.unmodifiableList(changeRecords); 713 } 714 715 716 717 /** 718 * {@inheritDoc} 719 */ 720 @Override() 721 public void toString(@NotNull final StringBuilder buffer) 722 { 723 buffer.append(getUncommentedHeaderLine()); 724 buffer.append("; changeType=modify-dn; dn=\""); 725 buffer.append(modifyDNChangeRecord.getDN()); 726 buffer.append("\", newRDN=\""); 727 buffer.append(modifyDNChangeRecord.getNewRDN()); 728 buffer.append("\", deleteOldRDN="); 729 buffer.append(modifyDNChangeRecord.deleteOldRDN()); 730 731 final String newSuperiorDN = modifyDNChangeRecord.getNewSuperiorDN(); 732 if (newSuperiorDN != null) 733 { 734 buffer.append(", newSuperiorDN=\""); 735 buffer.append(newSuperiorDN); 736 buffer.append('"'); 737 } 738 } 739}