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