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