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