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