001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2008-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS. 026 * Portions copyright 2011 profiq s.r.o. 027 */ 028package org.opends.server.plugins; 029 030import static org.opends.messages.PluginMessages.*; 031import static org.opends.server.protocols.internal.InternalClientConnection.*; 032import static org.opends.server.protocols.internal.Requests.*; 033import static org.opends.server.schema.SchemaConstants.*; 034import static org.opends.server.util.StaticUtils.*; 035 036import java.io.BufferedReader; 037import java.io.BufferedWriter; 038import java.io.File; 039import java.io.FileReader; 040import java.io.FileWriter; 041import java.io.IOException; 042import java.util.Collections; 043import java.util.HashSet; 044import java.util.LinkedHashMap; 045import java.util.LinkedHashSet; 046import java.util.LinkedList; 047import java.util.List; 048import java.util.Map; 049import java.util.Set; 050 051import org.forgerock.i18n.LocalizableMessage; 052import org.forgerock.i18n.slf4j.LocalizedLogger; 053import org.forgerock.opendj.config.server.ConfigChangeResult; 054import org.forgerock.opendj.config.server.ConfigException; 055import org.forgerock.opendj.ldap.ByteString; 056import org.forgerock.opendj.ldap.ModificationType; 057import org.forgerock.opendj.ldap.ResultCode; 058import org.forgerock.opendj.ldap.SearchScope; 059import org.opends.server.admin.server.ConfigurationChangeListener; 060import org.opends.server.admin.std.meta.PluginCfgDefn; 061import org.opends.server.admin.std.meta.ReferentialIntegrityPluginCfgDefn.CheckReferencesScopeCriteria; 062import org.opends.server.admin.std.server.PluginCfg; 063import org.opends.server.admin.std.server.ReferentialIntegrityPluginCfg; 064import org.opends.server.api.Backend; 065import org.opends.server.api.DirectoryThread; 066import org.opends.server.api.ServerShutdownListener; 067import org.opends.server.api.plugin.DirectoryServerPlugin; 068import org.opends.server.api.plugin.PluginResult; 069import org.opends.server.api.plugin.PluginType; 070import org.opends.server.core.DeleteOperation; 071import org.opends.server.core.DirectoryServer; 072import org.opends.server.core.ModifyOperation; 073import org.opends.server.protocols.internal.InternalClientConnection; 074import org.opends.server.protocols.internal.InternalSearchOperation; 075import org.opends.server.protocols.internal.SearchRequest; 076import org.opends.server.types.*; 077import org.opends.server.types.operation.PostOperationDeleteOperation; 078import org.opends.server.types.operation.PostOperationModifyDNOperation; 079import org.opends.server.types.operation.PreOperationAddOperation; 080import org.opends.server.types.operation.PreOperationModifyOperation; 081import org.opends.server.types.operation.SubordinateModifyDNOperation; 082 083/** 084 * This class implements a Directory Server post operation plugin that performs 085 * Referential Integrity processing on successful delete and modify DN 086 * operations. The plugin uses a set of configuration criteria to determine 087 * what attribute types to check referential integrity on, and, the set of 088 * base DNs to search for entries that might need referential integrity 089 * processing. If none of these base DNs are specified in the configuration, 090 * then the public naming contexts are used as the base DNs by default. 091 * <BR><BR> 092 * The plugin also has an option to process changes in background using 093 * a thread that wakes up periodically looking for change records in a log 094 * file. 095 */ 096public class ReferentialIntegrityPlugin 097 extends DirectoryServerPlugin<ReferentialIntegrityPluginCfg> 098 implements ConfigurationChangeListener<ReferentialIntegrityPluginCfg>, 099 ServerShutdownListener 100{ 101 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 102 103 104 105 /** Current plugin configuration. */ 106 private ReferentialIntegrityPluginCfg currentConfiguration; 107 108 /** List of attribute types that will be checked during referential integrity processing. */ 109 private LinkedHashSet<AttributeType> attributeTypes = new LinkedHashSet<>(); 110 /** List of base DNs that limit the scope of the referential integrity checking. */ 111 private Set<DN> baseDNs = new LinkedHashSet<>(); 112 113 /** 114 * The update interval the background thread uses. If it is 0, then 115 * the changes are processed in foreground. 116 */ 117 private long interval; 118 119 /** The flag used by the background thread to check if it should exit. */ 120 private boolean stopRequested; 121 122 /** The thread name. */ 123 private static final String name = 124 "Referential Integrity Background Update Thread"; 125 126 /** 127 * The name of the logfile that the update thread uses to process change 128 * records. Defaults to "logs/referint", but can be changed in the 129 * configuration. 130 */ 131 private String logFileName; 132 133 /** The File class that logfile corresponds to. */ 134 private File logFile; 135 136 /** The Thread class that the background thread corresponds to. */ 137 private Thread backGroundThread; 138 139 /** 140 * Used to save a map in the modifyDN operation attachment map that holds 141 * the old entry DNs and the new entry DNs related to a modify DN rename to 142 * new superior operation. 143 */ 144 public static final String MODIFYDN_DNS="modifyDNs"; 145 146 /** 147 * Used to save a set in the delete operation attachment map that 148 * holds the subordinate entry DNs related to a delete operation. 149 */ 150 public static final String DELETE_DNS="deleteDNs"; 151 152 /** 153 * The buffered reader that is used to read the log file by the background 154 * thread. 155 */ 156 private BufferedReader reader; 157 158 /** 159 * The buffered writer that is used to write update records in the log 160 * when the plugin is in background processing mode. 161 */ 162 private BufferedWriter writer; 163 164 /** 165 * Specifies the mapping between the attribute type (specified in the 166 * attributeTypes list) and the filter which the plugin should use 167 * to verify the integrity of the value of the given attribute. 168 */ 169 private LinkedHashMap<AttributeType, SearchFilter> attrFiltMap = new LinkedHashMap<>(); 170 171 172 /** {@inheritDoc} */ 173 @Override 174 public final void initializePlugin(Set<PluginType> pluginTypes, 175 ReferentialIntegrityPluginCfg pluginCfg) 176 throws ConfigException 177 { 178 pluginCfg.addReferentialIntegrityChangeListener(this); 179 LinkedList<LocalizableMessage> unacceptableReasons = new LinkedList<>(); 180 181 if (!isConfigurationAcceptable(pluginCfg, unacceptableReasons)) 182 { 183 throw new ConfigException(unacceptableReasons.getFirst()); 184 } 185 186 applyConfigurationChange(pluginCfg); 187 188 // Set up log file. Note: it is not allowed to change once the plugin is 189 // active. 190 setUpLogFile(pluginCfg.getLogFile()); 191 interval=pluginCfg.getUpdateInterval(); 192 193 //Set up background processing if interval > 0. 194 if(interval > 0) 195 { 196 setUpBackGroundProcessing(); 197 } 198 } 199 200 201 202 /** {@inheritDoc} */ 203 @Override 204 public ConfigChangeResult applyConfigurationChange( 205 ReferentialIntegrityPluginCfg newConfiguration) 206 { 207 final ConfigChangeResult ccr = new ConfigChangeResult(); 208 209 //Load base DNs from new configuration. 210 LinkedHashSet<DN> newConfiguredBaseDNs = new LinkedHashSet<>(newConfiguration.getBaseDN()); 211 //Load attribute types from new configuration. 212 LinkedHashSet<AttributeType> newAttributeTypes = 213 new LinkedHashSet<>(newConfiguration.getAttributeType()); 214 215 // Load the attribute-filter mapping 216 217 LinkedHashMap<AttributeType, SearchFilter> newAttrFiltMap = new LinkedHashMap<>(); 218 219 for (String attrFilt : newConfiguration.getCheckReferencesFilterCriteria()) 220 { 221 int sepInd = attrFilt.lastIndexOf(":"); 222 String attr = attrFilt.substring(0, sepInd); 223 String filtStr = attrFilt.substring(sepInd + 1); 224 225 AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(attr.toLowerCase()); 226 try 227 { 228 SearchFilter filter = SearchFilter.createFilterFromString(filtStr); 229 newAttrFiltMap.put(attrType, filter); 230 } 231 catch (DirectoryException de) 232 { 233 /* This should never happen because the filter has already 234 * been verified. 235 */ 236 logger.error(de.getMessageObject()); 237 } 238 } 239 240 //User is not allowed to change the logfile name, append a message that the 241 //server needs restarting for change to take effect. 242 // The first time the plugin is initialised the 'logFileName' is 243 // not initialised, so in order to verify if it is equal to the new 244 // log file name, we have to make sure the variable is not null. 245 String newLogFileName=newConfiguration.getLogFile(); 246 if(logFileName != null && !logFileName.equals(newLogFileName)) 247 { 248 ccr.setAdminActionRequired(true); 249 ccr.addMessage(INFO_PLUGIN_REFERENT_LOGFILE_CHANGE_REQUIRES_RESTART.get(logFileName, newLogFileName)); 250 } 251 252 //Switch to the new lists. 253 baseDNs = newConfiguredBaseDNs; 254 attributeTypes = newAttributeTypes; 255 attrFiltMap = newAttrFiltMap; 256 257 //If the plugin is enabled and the interval has changed, process that 258 //change. The change might start or stop the background processing thread. 259 long newInterval=newConfiguration.getUpdateInterval(); 260 if (newConfiguration.isEnabled() && newInterval != interval) 261 { 262 processIntervalChange(newInterval, ccr.getMessages()); 263 } 264 265 currentConfiguration = newConfiguration; 266 return ccr; 267 } 268 269 270 /** {@inheritDoc} */ 271 @Override 272 public boolean isConfigurationAcceptable(PluginCfg configuration, 273 List<LocalizableMessage> unacceptableReasons) 274 { 275 boolean isAcceptable = true; 276 ReferentialIntegrityPluginCfg pluginCfg = 277 (ReferentialIntegrityPluginCfg) configuration; 278 279 for (PluginCfgDefn.PluginType t : pluginCfg.getPluginType()) 280 { 281 switch (t) 282 { 283 case POSTOPERATIONDELETE: 284 case POSTOPERATIONMODIFYDN: 285 case SUBORDINATEMODIFYDN: 286 case SUBORDINATEDELETE: 287 case PREOPERATIONMODIFY: 288 case PREOPERATIONADD: 289 // These are acceptable. 290 break; 291 292 default: 293 isAcceptable = false; 294 unacceptableReasons.add(ERR_PLUGIN_REFERENT_INVALID_PLUGIN_TYPE.get(t)); 295 } 296 } 297 298 Set<DN> cfgBaseDNs = pluginCfg.getBaseDN(); 299 if (cfgBaseDNs == null || cfgBaseDNs.isEmpty()) 300 { 301 cfgBaseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 302 } 303 304 // Iterate through all of the defined attribute types and ensure that they 305 // have acceptable syntaxes and that they are indexed for equality below all 306 // base DNs. 307 Set<AttributeType> theAttributeTypes = pluginCfg.getAttributeType(); 308 for (AttributeType type : theAttributeTypes) 309 { 310 if (! isAttributeSyntaxValid(type)) 311 { 312 isAcceptable = false; 313 unacceptableReasons.add( 314 ERR_PLUGIN_REFERENT_INVALID_ATTRIBUTE_SYNTAX.get( 315 type.getNameOrOID(), 316 type.getSyntax().getName())); 317 } 318 319 for (DN baseDN : cfgBaseDNs) 320 { 321 Backend<?> b = DirectoryServer.getBackend(baseDN); 322 if (b != null && !b.isIndexed(type, IndexType.EQUALITY)) 323 { 324 isAcceptable = false; 325 unacceptableReasons.add(ERR_PLUGIN_REFERENT_ATTR_UNINDEXED.get( 326 pluginCfg.dn(), type.getNameOrOID(), b.getBackendID())); 327 } 328 } 329 } 330 331 /* Iterate through the attribute-filter mapping and verify that the 332 * map contains attributes listed in the attribute-type parameter 333 * and that the filter is valid. 334 */ 335 336 for (String attrFilt : pluginCfg.getCheckReferencesFilterCriteria()) 337 { 338 int sepInd = attrFilt.lastIndexOf(":"); 339 String attr = attrFilt.substring(0, sepInd).trim(); 340 String filtStr = attrFilt.substring(sepInd + 1).trim(); 341 342 /* TODO: strip the ;options part? */ 343 344 /* Get the attribute type for the given attribute. The attribute 345 * type has to be present in the attributeType list. 346 */ 347 348 AttributeType attrType = DirectoryServer.getAttributeTypeOrNull(attr.toLowerCase()); 349 if (attrType == null || !theAttributeTypes.contains(attrType)) 350 { 351 isAcceptable = false; 352 unacceptableReasons.add( 353 ERR_PLUGIN_REFERENT_ATTR_NOT_LISTED.get(attr)); 354 } 355 356 /* Verify the filter. 357 */ 358 359 try 360 { 361 SearchFilter.createFilterFromString(filtStr); 362 } 363 catch (DirectoryException de) 364 { 365 isAcceptable = false; 366 unacceptableReasons.add( 367 ERR_PLUGIN_REFERENT_BAD_FILTER.get(filtStr, de.getMessage())); 368 } 369 370 } 371 372 return isAcceptable; 373 } 374 375 376 /** {@inheritDoc} */ 377 @Override 378 public boolean isConfigurationChangeAcceptable( 379 ReferentialIntegrityPluginCfg configuration, 380 List<LocalizableMessage> unacceptableReasons) 381 { 382 return isConfigurationAcceptable(configuration, unacceptableReasons); 383 } 384 385 386 /** {@inheritDoc} */ 387 @SuppressWarnings("unchecked") 388 @Override 389 public PluginResult.PostOperation 390 doPostOperation(PostOperationModifyDNOperation 391 modifyDNOperation) 392 { 393 // If the operation itself failed, then we don't need to do anything because 394 // nothing changed. 395 if (modifyDNOperation.getResultCode() != ResultCode.SUCCESS) 396 { 397 return PluginResult.PostOperation.continueOperationProcessing(); 398 } 399 400 Map<DN,DN>modDNmap= 401 (Map<DN, DN>) modifyDNOperation.getAttachment(MODIFYDN_DNS); 402 if(modDNmap == null) 403 { 404 modDNmap = new LinkedHashMap<>(); 405 modifyDNOperation.setAttachment(MODIFYDN_DNS, modDNmap); 406 } 407 DN oldEntryDN=modifyDNOperation.getOriginalEntry().getName(); 408 DN newEntryDN=modifyDNOperation.getUpdatedEntry().getName(); 409 modDNmap.put(oldEntryDN, newEntryDN); 410 411 processModifyDN(modDNmap, interval != 0); 412 413 return PluginResult.PostOperation.continueOperationProcessing(); 414 } 415 416 417 418 /** {@inheritDoc} */ 419 @SuppressWarnings("unchecked") 420 @Override 421 public PluginResult.PostOperation doPostOperation( 422 PostOperationDeleteOperation deleteOperation) 423 { 424 // If the operation itself failed, then we don't need to do anything because 425 // nothing changed. 426 if (deleteOperation.getResultCode() != ResultCode.SUCCESS) 427 { 428 return PluginResult.PostOperation.continueOperationProcessing(); 429 } 430 431 Set<DN> deleteDNset = 432 (Set<DN>) deleteOperation.getAttachment(DELETE_DNS); 433 if(deleteDNset == null) 434 { 435 deleteDNset = new HashSet<>(); 436 deleteOperation.setAttachment(MODIFYDN_DNS, deleteDNset); 437 } 438 deleteDNset.add(deleteOperation.getEntryDN()); 439 440 processDelete(deleteDNset, interval != 0); 441 return PluginResult.PostOperation.continueOperationProcessing(); 442 } 443 444 /** {@inheritDoc} */ 445 @SuppressWarnings("unchecked") 446 @Override 447 public PluginResult.SubordinateModifyDN processSubordinateModifyDN( 448 SubordinateModifyDNOperation modifyDNOperation, Entry oldEntry, 449 Entry newEntry, List<Modification> modifications) 450 { 451 //This cast gives an unchecked cast warning, suppress it since the cast 452 //is ok. 453 Map<DN,DN>modDNmap= 454 (Map<DN, DN>) modifyDNOperation.getAttachment(MODIFYDN_DNS); 455 if(modDNmap == null) 456 { 457 // First time through, create the map and set it in the operation attachment. 458 modDNmap = new LinkedHashMap<>(); 459 modifyDNOperation.setAttachment(MODIFYDN_DNS, modDNmap); 460 } 461 modDNmap.put(oldEntry.getName(), newEntry.getName()); 462 return PluginResult.SubordinateModifyDN.continueOperationProcessing(); 463 } 464 465 /** {@inheritDoc} */ 466 @SuppressWarnings("unchecked") 467 @Override 468 public PluginResult.SubordinateDelete processSubordinateDelete( 469 DeleteOperation deleteOperation, Entry entry) 470 { 471 // This cast gives an unchecked cast warning, suppress it since the cast is ok. 472 Set<DN> deleteDNset = (Set<DN>) deleteOperation.getAttachment(DELETE_DNS); 473 if(deleteDNset == null) 474 { 475 // First time through, create the set and set it in the operation attachment. 476 deleteDNset = new HashSet<>(); 477 deleteOperation.setAttachment(DELETE_DNS, deleteDNset); 478 } 479 deleteDNset.add(entry.getName()); 480 return PluginResult.SubordinateDelete.continueOperationProcessing(); 481 } 482 483 /** 484 * Verify that the specified attribute has either a distinguished name syntax 485 * or "name and optional UID" syntax. 486 * 487 * @param attribute The attribute to check the syntax of. 488 * @return Returns <code>true</code> if the attribute has a valid syntax. 489 */ 490 private boolean isAttributeSyntaxValid(AttributeType attribute) 491 { 492 return attribute.getSyntax().getOID().equals(SYNTAX_DN_OID) || 493 attribute.getSyntax().getOID().equals(SYNTAX_NAME_AND_OPTIONAL_UID_OID); 494 } 495 496 /** 497 * Process the specified new interval value. This processing depends on what 498 * the current interval value is and new value will be. The values have been 499 * checked for equality at this point and are not equal. 500 * 501 * If the old interval is 0, then the server is in foreground mode and 502 * the background thread needs to be started using the new interval value. 503 * 504 * If the new interval value is 0, the the server is in background mode 505 * and the the background thread needs to be stopped. 506 * 507 * If the user just wants to change the interval value, the background thread 508 * needs to be interrupted so that it can use the new interval value. 509 * 510 * @param newInterval The new interval value to use. 511 * 512 * @param msgs An array list of messages that thread stop and start messages 513 * can be added to. 514 */ 515 private void processIntervalChange(long newInterval, List<LocalizableMessage> msgs) 516 { 517 if(interval == 0) { 518 DirectoryServer.registerShutdownListener(this); 519 interval=newInterval; 520 msgs.add(INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_STARTING.get(interval)); 521 setUpBackGroundProcessing(); 522 } else if(newInterval == 0) { 523 LocalizableMessage message= 524 INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_STOPPING.get(); 525 msgs.add(message); 526 processServerShutdown(message); 527 interval=newInterval; 528 } else { 529 interval=newInterval; 530 backGroundThread.interrupt(); 531 msgs.add(INFO_PLUGIN_REFERENT_BACKGROUND_PROCESSING_UPDATE_INTERVAL_CHANGED.get(interval, newInterval)); 532 } 533 } 534 535 /** 536 * Process a modify DN post operation using the specified map of old and new 537 * entry DNs. The boolean "log" is used to determine if the map 538 * is written to the log file for the background thread to pick up. If the 539 * map is to be processed in foreground, than each base DN or public 540 * naming context (if the base DN configuration is empty) is processed. 541 * 542 * @param modDNMap The map of old entry and new entry DNs from the modify 543 * DN operation. 544 * 545 * @param log Set to <code>true</code> if the map should be written to a log 546 * file so that the background thread can process the changes at 547 * a later time. 548 * 549 */ 550 private void processModifyDN(Map<DN, DN> modDNMap, boolean log) 551 { 552 if(modDNMap != null) 553 { 554 if(log) 555 { 556 writeLog(modDNMap); 557 } 558 else 559 { 560 for(DN baseDN : getBaseDNsToSearch()) 561 { 562 doBaseDN(baseDN, modDNMap); 563 } 564 } 565 } 566 } 567 568 /** 569 * Used by both the background thread and the delete post operation to 570 * process a delete operation on the specified entry DN. The 571 * boolean "log" is used to determine if the DN is written to the log file 572 * for the background thread to pick up. This value is set to false if the 573 * background thread is processing changes. If this method is being called 574 * by a delete post operation, then setting the "log" value to false will 575 * cause the DN to be processed in foreground 576 * 577 * If the DN is to be processed, than each base DN or public naming 578 * context (if the base DN configuration is empty) is is checked to see if 579 * entries under it contain references to the deleted entry DN that need 580 * to be removed. 581 * 582 * @param entryDN The DN of the deleted entry. 583 * 584 * @param log Set to <code>true</code> if the DN should be written to a log 585 * file so that the background thread can process the change at 586 * a later time. 587 * 588 */ 589 private void processDelete(Set<DN> deleteDNset, boolean log) 590 { 591 if(log) 592 { 593 writeLog(deleteDNset); 594 } 595 else 596 { 597 for(DN baseDN : getBaseDNsToSearch()) 598 { 599 doBaseDN(baseDN, deleteDNset); 600 } 601 } 602 } 603 604 /** 605 * Used by the background thread to process the specified old entry DN and 606 * new entry DN. Each base DN or public naming context (if the base DN 607 * configuration is empty) is checked to see if they contain entries with 608 * references to the old entry DN that need to be changed to the new entry DN. 609 * 610 * @param oldEntryDN The entry DN before the modify DN operation. 611 * 612 * @param newEntryDN The entry DN after the modify DN operation. 613 * 614 */ 615 private void processModifyDN(DN oldEntryDN, DN newEntryDN) 616 { 617 for(DN baseDN : getBaseDNsToSearch()) 618 { 619 searchBaseDN(baseDN, oldEntryDN, newEntryDN); 620 } 621 } 622 623 /** 624 * Return a set of DNs that are used to search for references under. If the 625 * base DN configuration set is empty, then the public naming contexts 626 * are used. 627 * 628 * @return A set of DNs to use in the reference searches. 629 * 630 */ 631 private Set<DN> getBaseDNsToSearch() 632 { 633 if (baseDNs.isEmpty()) 634 { 635 return DirectoryServer.getPublicNamingContexts().keySet(); 636 } 637 return baseDNs; 638 } 639 640 /** 641 * Search a base DN using a filter built from the configured attribute 642 * types and the specified old entry DN. For each entry that is found from 643 * the search, delete the old entry DN from the entry. If the new entry 644 * DN is not null, then add it to the entry. 645 * 646 * @param baseDN The DN to base the search at. 647 * 648 * @param oldEntryDN The old entry DN that needs to be deleted or replaced. 649 * 650 * @param newEntryDN The new entry DN that needs to be added. May be null 651 * if the original operation was a delete. 652 * 653 */ 654 private void searchBaseDN(DN baseDN, DN oldEntryDN, DN newEntryDN) 655 { 656 //Build an equality search with all of the configured attribute types 657 //and the old entry DN. 658 HashSet<SearchFilter> componentFilters=new HashSet<>(); 659 for(AttributeType attributeType : attributeTypes) 660 { 661 componentFilters.add(SearchFilter.createEqualityFilter(attributeType, 662 ByteString.valueOfUtf8(oldEntryDN.toString()))); 663 } 664 665 SearchFilter orFilter = SearchFilter.createORFilter(componentFilters); 666 final SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, orFilter); 667 InternalSearchOperation operation = getRootConnection().processSearch(request); 668 669 switch (operation.getResultCode().asEnum()) 670 { 671 case SUCCESS: 672 break; 673 674 case NO_SUCH_OBJECT: 675 logger.debug(INFO_PLUGIN_REFERENT_SEARCH_NO_SUCH_OBJECT, baseDN); 676 return; 677 678 default: 679 logger.error(ERR_PLUGIN_REFERENT_SEARCH_FAILED, operation.getErrorMessage()); 680 return; 681 } 682 683 for (SearchResultEntry entry : operation.getSearchEntries()) 684 { 685 deleteAddAttributesEntry(entry, oldEntryDN, newEntryDN); 686 } 687 } 688 689 /** 690 * This method is used in foreground processing of a modify DN operation. 691 * It uses the specified map to perform base DN searching for each map 692 * entry. The key is the old entry DN and the value is the 693 * new entry DN. 694 * 695 * @param baseDN The DN to base the search at. 696 * 697 * @param modifyDNmap The map containing the modify DN old and new entry DNs. 698 * 699 */ 700 private void doBaseDN(DN baseDN, Map<DN,DN> modifyDNmap) 701 { 702 for(Map.Entry<DN,DN> mapEntry: modifyDNmap.entrySet()) 703 { 704 searchBaseDN(baseDN, mapEntry.getKey(), mapEntry.getValue()); 705 } 706 } 707 708 /** 709 * This method is used in foreground processing of a delete operation. 710 * It uses the specified set to perform base DN searching for each 711 * element. 712 * 713 * @param baseDN The DN to base the search at. 714 * 715 * @param deleteDNset The set containing the delete DNs. 716 * 717 */ 718 private void doBaseDN(DN baseDN, Set<DN> deleteDNset) 719 { 720 for(DN deletedEntryDN : deleteDNset) 721 { 722 searchBaseDN(baseDN, deletedEntryDN, null); 723 } 724 } 725 726 /** 727 * For each attribute type, delete the specified old entry DN and 728 * optionally add the specified new entry DN if the DN is not null. 729 * The specified entry is used to see if it contains each attribute type so 730 * those types that the entry contains can be modified. An internal modify 731 * is performed to change the entry. 732 * 733 * @param e The entry that contains the old references. 734 * 735 * @param oldEntryDN The old entry DN to remove references to. 736 * 737 * @param newEntryDN The new entry DN to add a reference to, if it is not 738 * null. 739 * 740 */ 741 private void deleteAddAttributesEntry(Entry e, DN oldEntryDN, DN newEntryDN) 742 { 743 LinkedList<Modification> mods = new LinkedList<>(); 744 DN entryDN=e.getName(); 745 for(AttributeType type : attributeTypes) 746 { 747 if(e.hasAttribute(type)) 748 { 749 ByteString value = ByteString.valueOfUtf8(oldEntryDN.toString()); 750 if (e.hasValue(type, null, value)) 751 { 752 mods.add(new Modification(ModificationType.DELETE, Attributes 753 .create(type, value))); 754 755 // If the new entry DN exists, create an ADD modification for it. 756 if(newEntryDN != null) 757 { 758 mods.add(new Modification(ModificationType.ADD, Attributes 759 .create(type, newEntryDN.toString()))); 760 } 761 } 762 } 763 } 764 765 InternalClientConnection conn = 766 InternalClientConnection.getRootConnection(); 767 ModifyOperation modifyOperation = 768 conn.processModify(entryDN, mods); 769 if(modifyOperation.getResultCode() != ResultCode.SUCCESS) 770 { 771 logger.error(ERR_PLUGIN_REFERENT_MODIFY_FAILED, entryDN, modifyOperation.getErrorMessage()); 772 } 773 } 774 775 /** 776 * Sets up the log file that the plugin can write update recored to and 777 * the background thread can use to read update records from. The specifed 778 * log file name is the name to use for the file. If the file exists from 779 * a previous run, use it. 780 * 781 * @param logFileName The name of the file to use, may be absolute. 782 * 783 * @throws ConfigException If a new file cannot be created if needed. 784 * 785 */ 786 private void setUpLogFile(String logFileName) 787 throws ConfigException 788 { 789 this.logFileName=logFileName; 790 logFile=getFileForPath(logFileName); 791 792 try 793 { 794 if(!logFile.exists()) 795 { 796 logFile.createNewFile(); 797 } 798 } 799 catch (IOException io) 800 { 801 throw new ConfigException(ERR_PLUGIN_REFERENT_CREATE_LOGFILE.get( 802 io.getMessage()), io); 803 } 804 } 805 806 /** 807 * Sets up a buffered writer that the plugin can use to write update records 808 * with. 809 * 810 * @throws IOException If a new file writer cannot be created. 811 * 812 */ 813 private void setupWriter() throws IOException { 814 writer=new BufferedWriter(new FileWriter(logFile, true)); 815 } 816 817 818 /** 819 * Sets up a buffered reader that the background thread can use to read 820 * update records with. 821 * 822 * @throws IOException If a new file reader cannot be created. 823 * 824 */ 825 private void setupReader() throws IOException { 826 reader=new BufferedReader(new FileReader(logFile)); 827 } 828 829 /** 830 * Write the specified map of old entry and new entry DNs to the log 831 * file. Each entry of the map is a line in the file, the key is the old 832 * entry normalized DN and the value is the new entry normalized DN. 833 * The DNs are separated by the tab character. This map is related to a 834 * modify DN operation. 835 * 836 * @param modDNmap The map of old entry and new entry DNs. 837 * 838 */ 839 private void writeLog(Map<DN,DN> modDNmap) { 840 synchronized(logFile) 841 { 842 try 843 { 844 setupWriter(); 845 for(Map.Entry<DN,DN> mapEntry : modDNmap.entrySet()) 846 { 847 writer.write(mapEntry.getKey() + "\t" + mapEntry.getValue()); 848 writer.newLine(); 849 } 850 writer.flush(); 851 writer.close(); 852 } 853 catch (IOException io) 854 { 855 logger.error(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE, io.getMessage()); 856 } 857 } 858 } 859 860 /** 861 * Write the specified entry DNs to the log file. 862 * These entry DNs are related to a delete operation. 863 * 864 * @param deletedEntryDN The DN of the deleted entry. 865 * 866 */ 867 private void writeLog(Set<DN> deleteDNset) { 868 synchronized(logFile) 869 { 870 try 871 { 872 setupWriter(); 873 for (DN deletedEntryDN : deleteDNset) 874 { 875 writer.write(deletedEntryDN.toString()); 876 writer.newLine(); 877 } 878 writer.flush(); 879 writer.close(); 880 } 881 catch (IOException io) 882 { 883 logger.error(ERR_PLUGIN_REFERENT_CLOSE_LOGFILE, io.getMessage()); 884 } 885 } 886 } 887 888 /** 889 * Process all of the records in the log file. Each line of the file is read 890 * and parsed to determine if it was a delete operation (a single normalized 891 * DN) or a modify DN operation (two normalized DNs separated by a tab). The 892 * corresponding operation method is called to perform the referential 893 * integrity processing as though the operation was just processed. After 894 * all of the records in log file have been processed, the log file is 895 * cleared so that new records can be added. 896 * 897 */ 898 private void processLog() { 899 synchronized(logFile) { 900 try { 901 if(logFile.length() == 0) 902 { 903 return; 904 } 905 906 setupReader(); 907 String line; 908 while((line=reader.readLine()) != null) { 909 try { 910 String[] a=line.split("[\t]"); 911 DN origDn = DN.valueOf(a[0]); 912 //If there is only a single DN string than it must be a delete. 913 if(a.length == 1) { 914 processDelete(Collections.singleton(origDn), false); 915 } else { 916 DN movedDN=DN.valueOf(a[1]); 917 processModifyDN(origDn, movedDN); 918 } 919 } catch (DirectoryException ex) { 920 //This exception should rarely happen since the plugin wrote the DN 921 //strings originally. 922 logger.error(ERR_PLUGIN_REFERENT_CANNOT_DECODE_STRING_AS_DN, ex.getMessage()); 923 } 924 } 925 reader.close(); 926 logFile.delete(); 927 logFile.createNewFile(); 928 } catch (IOException io) { 929 logger.error(ERR_PLUGIN_REFERENT_REPLACE_LOGFILE, io.getMessage()); 930 } 931 } 932 } 933 934 /** 935 * Return the listener name. 936 * 937 * @return The name of the listener. 938 * 939 */ 940 @Override 941 public String getShutdownListenerName() { 942 return name; 943 } 944 945 946 /** {@inheritDoc} */ 947 @Override 948 public final void finalizePlugin() { 949 currentConfiguration.removeReferentialIntegrityChangeListener(this); 950 if(interval > 0) 951 { 952 processServerShutdown(null); 953 } 954 } 955 956 /** 957 * Process a server shutdown. If the background thread is running it needs 958 * to be interrupted so it can read the stop request variable and exit. 959 * 960 * @param reason The reason message for the shutdown. 961 * 962 */ 963 @Override 964 public void processServerShutdown(LocalizableMessage reason) 965 { 966 stopRequested = true; 967 968 // Wait for back ground thread to terminate 969 while (backGroundThread != null && backGroundThread.isAlive()) { 970 try { 971 // Interrupt if its sleeping 972 backGroundThread.interrupt(); 973 backGroundThread.join(); 974 } 975 catch (InterruptedException ex) { 976 //Expected. 977 } 978 } 979 DirectoryServer.deregisterShutdownListener(this); 980 backGroundThread=null; 981 } 982 983 984 /** 985 * Returns the interval time converted to milliseconds. 986 * 987 * @return The interval time for the background thread. 988 */ 989 private long getInterval() { 990 return interval * 1000; 991 } 992 993 /** 994 * Sets up background processing of referential integrity by creating a 995 * new background thread to process updates. 996 * 997 */ 998 private void setUpBackGroundProcessing() { 999 if(backGroundThread == null) { 1000 DirectoryServer.registerShutdownListener(this); 1001 stopRequested = false; 1002 backGroundThread = new BackGroundThread(); 1003 backGroundThread.start(); 1004 } 1005 } 1006 1007 1008 /** 1009 * Used by the background thread to determine if it should exit. 1010 * 1011 * @return Returns <code>true</code> if the background thread should exit. 1012 * 1013 */ 1014 private boolean isShuttingDown() { 1015 return stopRequested; 1016 } 1017 1018 /** 1019 * The background referential integrity processing thread. Wakes up after 1020 * sleeping for a configurable interval and checks the log file for update 1021 * records. 1022 * 1023 */ 1024 private class BackGroundThread extends DirectoryThread { 1025 1026 /** 1027 * Constructor for the background thread. 1028 */ 1029 public 1030 BackGroundThread() { 1031 super(name); 1032 } 1033 1034 /** 1035 * Run method for the background thread. 1036 */ 1037 @Override 1038 public void run() { 1039 while(!isShuttingDown()) { 1040 try { 1041 sleep(getInterval()); 1042 } catch(InterruptedException e) { 1043 continue; 1044 } catch(Exception e) { 1045 logger.traceException(e); 1046 } 1047 processLog(); 1048 } 1049 } 1050 } 1051 1052 /** {@inheritDoc} */ 1053 @Override 1054 public PluginResult.PreOperation doPreOperation( 1055 PreOperationModifyOperation modifyOperation) 1056 { 1057 /* Skip the integrity checks if the enforcing is not enabled 1058 */ 1059 1060 if (!currentConfiguration.isCheckReferences()) 1061 { 1062 return PluginResult.PreOperation.continueOperationProcessing(); 1063 } 1064 1065 final List<Modification> mods = modifyOperation.getModifications(); 1066 final Entry entry = modifyOperation.getModifiedEntry(); 1067 1068 /* Make sure the entry belongs to one of the configured naming 1069 * contexts. 1070 */ 1071 DN entryDN = entry.getName(); 1072 DN entryBaseDN = getEntryBaseDN(entryDN); 1073 if (entryBaseDN == null) 1074 { 1075 return PluginResult.PreOperation.continueOperationProcessing(); 1076 } 1077 1078 for (Modification mod : mods) 1079 { 1080 final ModificationType modType = mod.getModificationType(); 1081 1082 /* Process only ADD and REPLACE modification types. 1083 */ 1084 if (modType != ModificationType.ADD 1085 && modType != ModificationType.REPLACE) 1086 { 1087 break; 1088 } 1089 1090 AttributeType attrType = mod.getAttribute().getAttributeType(); 1091 Set<String> attrOptions = mod.getAttribute().getOptions(); 1092 Attribute modifiedAttribute = entry.getExactAttribute(attrType, 1093 attrOptions); 1094 if (modifiedAttribute != null) 1095 { 1096 PluginResult.PreOperation result = 1097 isIntegrityMaintained(modifiedAttribute, entryDN, entryBaseDN); 1098 if (result.getResultCode() != ResultCode.SUCCESS) 1099 { 1100 return result; 1101 } 1102 } 1103 } 1104 1105 /* At this point, everything is fine. 1106 */ 1107 return PluginResult.PreOperation.continueOperationProcessing(); 1108 } 1109 1110 /** {@inheritDoc} */ 1111 @Override 1112 public PluginResult.PreOperation doPreOperation( 1113 PreOperationAddOperation addOperation) 1114 { 1115 /* Skip the integrity checks if the enforcing is not enabled. 1116 */ 1117 1118 if (!currentConfiguration.isCheckReferences()) 1119 { 1120 return PluginResult.PreOperation.continueOperationProcessing(); 1121 } 1122 1123 final Entry entry = addOperation.getEntryToAdd(); 1124 1125 /* Make sure the entry belongs to one of the configured naming 1126 * contexts. 1127 */ 1128 DN entryDN = entry.getName(); 1129 DN entryBaseDN = getEntryBaseDN(entryDN); 1130 if (entryBaseDN == null) 1131 { 1132 return PluginResult.PreOperation.continueOperationProcessing(); 1133 } 1134 1135 for (AttributeType attrType : attributeTypes) 1136 { 1137 final List<Attribute> attrs = entry.getAttribute(attrType, false); 1138 1139 if (attrs != null) 1140 { 1141 PluginResult.PreOperation result = 1142 isIntegrityMaintained(attrs, entryDN, entryBaseDN); 1143 if (result.getResultCode() != ResultCode.SUCCESS) 1144 { 1145 return result; 1146 } 1147 } 1148 } 1149 1150 /* If we reahed this point, everything is fine. 1151 */ 1152 return PluginResult.PreOperation.continueOperationProcessing(); 1153 } 1154 1155 /** 1156 * Verifies that the integrity of values is maintained. 1157 * @param attrs Attribute list which refers to another entry in the 1158 * directory. 1159 * @param entryDN DN of the entry which contains the <CODE>attr</CODE> 1160 * attribute. 1161 * @return The SUCCESS if the integrity is maintained or 1162 * CONSTRAINT_VIOLATION oherwise 1163 */ 1164 private PluginResult.PreOperation 1165 isIntegrityMaintained(List<Attribute> attrs, DN entryDN, DN entryBaseDN) 1166 { 1167 for(Attribute attr : attrs) 1168 { 1169 PluginResult.PreOperation result = 1170 isIntegrityMaintained(attr, entryDN, entryBaseDN); 1171 if (result != PluginResult.PreOperation.continueOperationProcessing()) 1172 { 1173 return result; 1174 } 1175 } 1176 1177 return PluginResult.PreOperation.continueOperationProcessing(); 1178 } 1179 1180 /** 1181 * Verifies that the integrity of values is maintained. 1182 * @param attr Attribute which refers to another entry in the 1183 * directory. 1184 * @param entryDN DN of the entry which contains the <CODE>attr</CODE> 1185 * attribute. 1186 * @return The SUCCESS if the integrity is maintained or 1187 * CONSTRAINT_VIOLATION otherwise 1188 */ 1189 private PluginResult.PreOperation isIntegrityMaintained(Attribute attr, DN entryDN, DN entryBaseDN) 1190 { 1191 try 1192 { 1193 for (ByteString attrVal : attr) 1194 { 1195 DN valueEntryDN = DN.decode(attrVal); 1196 1197 final Entry valueEntry; 1198 if (currentConfiguration.getCheckReferencesScopeCriteria() == CheckReferencesScopeCriteria.NAMING_CONTEXT 1199 && valueEntryDN.matchesBaseAndScope(entryBaseDN, SearchScope.SUBORDINATES)) 1200 { 1201 return PluginResult.PreOperation.stopProcessing(ResultCode.CONSTRAINT_VIOLATION, 1202 ERR_PLUGIN_REFERENT_NAMINGCONTEXT_MISMATCH.get(valueEntryDN, attr.getName(), entryDN)); 1203 } 1204 valueEntry = DirectoryServer.getEntry(valueEntryDN); 1205 1206 // Verify that the value entry exists in the backend. 1207 if (valueEntry == null) 1208 { 1209 return PluginResult.PreOperation.stopProcessing(ResultCode.CONSTRAINT_VIOLATION, 1210 ERR_PLUGIN_REFERENT_ENTRY_MISSING.get(valueEntryDN, attr.getName(), entryDN)); 1211 } 1212 1213 // Verify that the value entry conforms to the filter. 1214 SearchFilter filter = attrFiltMap.get(attr.getAttributeType()); 1215 if (filter != null && !filter.matchesEntry(valueEntry)) 1216 { 1217 return PluginResult.PreOperation.stopProcessing(ResultCode.CONSTRAINT_VIOLATION, 1218 ERR_PLUGIN_REFERENT_FILTER_MISMATCH.get(valueEntry.getName(), attr.getName(), entryDN, filter)); 1219 } 1220 } 1221 } 1222 catch (Exception de) 1223 { 1224 return PluginResult.PreOperation.stopProcessing(ResultCode.OTHER, 1225 ERR_PLUGIN_REFERENT_EXCEPTION.get(de.getLocalizedMessage())); 1226 } 1227 1228 return PluginResult.PreOperation.continueOperationProcessing(); 1229 } 1230 1231 /** 1232 * Verifies if the entry with the specified DN belongs to the 1233 * configured naming contexts. 1234 * @param dn DN of the entry. 1235 * @return Returns <code>true</code> if the entry matches any of the 1236 * configured base DNs, and <code>false</code> if not. 1237 */ 1238 private DN getEntryBaseDN(DN dn) 1239 { 1240 /* Verify that the entry belongs to one of the configured naming 1241 * contexts. 1242 */ 1243 1244 DN namingContext = null; 1245 1246 if (baseDNs.isEmpty()) 1247 { 1248 baseDNs = DirectoryServer.getPublicNamingContexts().keySet(); 1249 } 1250 1251 for (DN baseDN : baseDNs) 1252 { 1253 if (dn.matchesBaseAndScope(baseDN, SearchScope.SUBORDINATES)) 1254 { 1255 namingContext = baseDN; 1256 break; 1257 } 1258 } 1259 1260 return namingContext; 1261 } 1262}