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 2011 profiq s.r.o. 015 * Portions Copyright 2011-2016 ForgeRock AS. 016 */ 017package org.opends.server.plugins; 018 019import static org.opends.messages.PluginMessages.*; 020import static com.forgerock.opendj.util.StaticUtils.toLowerCase; 021 022import java.util.HashMap; 023import java.util.HashSet; 024import java.util.LinkedList; 025import java.util.List; 026import java.util.ListIterator; 027import java.util.Map; 028import java.util.Set; 029import java.util.concurrent.locks.ReentrantReadWriteLock; 030import java.util.concurrent.locks.ReentrantReadWriteLock.ReadLock; 031import java.util.concurrent.locks.ReentrantReadWriteLock.WriteLock; 032 033import org.forgerock.i18n.LocalizableMessage; 034import org.forgerock.i18n.slf4j.LocalizedLogger; 035import org.forgerock.opendj.config.server.ConfigChangeResult; 036import org.forgerock.opendj.config.server.ConfigException; 037import org.forgerock.opendj.ldap.ResultCode; 038import org.forgerock.opendj.config.server.ConfigurationChangeListener; 039import org.forgerock.opendj.server.config.server.AttributeCleanupPluginCfg; 040import org.forgerock.opendj.server.config.server.PluginCfg; 041import org.opends.server.api.plugin.DirectoryServerPlugin; 042import org.opends.server.api.plugin.PluginResult; 043import org.opends.server.api.plugin.PluginType; 044import org.opends.server.core.DirectoryServer; 045import org.opends.server.types.InitializationException; 046import org.opends.server.types.RawAttribute; 047import org.opends.server.types.RawModification; 048import org.opends.server.types.operation.PreParseAddOperation; 049import org.opends.server.types.operation.PreParseModifyOperation; 050 051/** 052 * The attribute cleanup plugin implementation class. The plugin removes and/or 053 * renames the configured parameters from the incoming ADD and MODIFY requests. 054 */ 055public class AttributeCleanupPlugin extends 056 DirectoryServerPlugin<AttributeCleanupPluginCfg> implements 057 ConfigurationChangeListener<AttributeCleanupPluginCfg> 058{ 059 060 /** Plugin configuration. */ 061 private AttributeCleanupPluginCfg config; 062 063 /** Debug tracer. */ 064 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 065 066 /** A table of attributes to be renamed. */ 067 private Map<String, String> attributesToRename; 068 069 /** The set of attributes to be removed. */ 070 private Set<String> attributesToRemove; 071 072 /** 073 * This lock prevents concurrent updates to the configuration while operations 074 * are being processed. 075 */ 076 private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); 077 private final ReadLock sharedLock = lock.readLock(); 078 private final WriteLock exclusiveLock = lock.writeLock(); 079 080 081 082 /** Default constructor. */ 083 public AttributeCleanupPlugin() 084 { 085 super(); 086 } 087 088 089 090 @Override 091 public ConfigChangeResult applyConfigurationChange( 092 final AttributeCleanupPluginCfg config) 093 { 094 exclusiveLock.lock(); 095 try 096 { 097 /* Apply the change, as at this point is has been validated. */ 098 this.config = config; 099 100 attributesToRename = new HashMap<>(); 101 for (final String mapping : config.getRenameInboundAttributes()) 102 { 103 final int colonPos = mapping.lastIndexOf(":"); 104 final String fromAttr = mapping.substring(0, colonPos).trim(); 105 final String toAttr = mapping.substring(colonPos + 1).trim(); 106 attributesToRename.put(toLowerCase(fromAttr), toLowerCase(toAttr)); 107 } 108 109 attributesToRemove = new HashSet<>(); 110 for (final String attr : config.getRemoveInboundAttributes()) 111 { 112 attributesToRemove.add(toLowerCase(attr.trim())); 113 } 114 115 /* Update was successful, no restart required. */ 116 return new ConfigChangeResult(); 117 } 118 finally 119 { 120 exclusiveLock.unlock(); 121 } 122 } 123 124 125 126 @Override 127 public PluginResult.PreParse doPreParse( 128 final PreParseAddOperation addOperation) 129 { 130 sharedLock.lock(); 131 try 132 { 133 /* First strip the listed attributes, then rename the ones that remain. */ 134 processInboundRemove(addOperation); 135 processInboundRename(addOperation); 136 137 return PluginResult.PreParse.continueOperationProcessing(); 138 } 139 finally 140 { 141 sharedLock.unlock(); 142 } 143 } 144 145 146 147 @Override 148 public PluginResult.PreParse doPreParse( 149 final PreParseModifyOperation modifyOperation) 150 { 151 sharedLock.lock(); 152 try 153 { 154 /* First strip the listed attributes, then rename the ones that remain. */ 155 processInboundRemove(modifyOperation); 156 processInboundRename(modifyOperation); 157 158 /* 159 * If the MODIFY request has been stripped of ALL modifications, stop the 160 * processing and return SUCCESS to the client. 161 */ 162 if (modifyOperation.getRawModifications().isEmpty()) 163 { 164 if (logger.isTraceEnabled()) 165 { 166 logger.trace("The AttributeCleanupPlugin has eliminated all " 167 + "modifications. The processing should be stopped."); 168 } 169 return PluginResult.PreParse.stopProcessing(ResultCode.SUCCESS, null); 170 } 171 172 return PluginResult.PreParse.continueOperationProcessing(); 173 } 174 finally 175 { 176 sharedLock.unlock(); 177 } 178 } 179 180 181 182 @Override 183 public void finalizePlugin() 184 { 185 /* 186 * It's not essential to take the lock here, but we will anyhow for 187 * consistency with other methods. 188 */ 189 exclusiveLock.lock(); 190 try 191 { 192 /* Deregister change listeners. */ 193 config.removeAttributeCleanupChangeListener(this); 194 } 195 finally 196 { 197 exclusiveLock.unlock(); 198 } 199 } 200 201 202 203 @Override 204 public void initializePlugin(final Set<PluginType> pluginTypes, 205 final AttributeCleanupPluginCfg configuration) throws ConfigException, 206 InitializationException 207 { 208 /* The plugin should be invoked only for pre-parse ADD and MODIFY operations. */ 209 for (final PluginType t : pluginTypes) 210 { 211 switch (t) 212 { 213 case PRE_PARSE_ADD: 214 break; 215 case PRE_PARSE_MODIFY: 216 break; 217 default: 218 throw new ConfigException(ERR_PLUGIN_ATTR_CLEANUP_INITIALIZE_PLUGIN.get(t)); 219 } 220 } 221 222 /* Verify the current configuration. */ 223 final List<LocalizableMessage> messages = new LinkedList<>(); 224 if (!isConfigurationChangeAcceptable(configuration, messages)) 225 { 226 throw new ConfigException(messages.get(0)); 227 } 228 229 /* Register change listeners. */ 230 configuration.addAttributeCleanupChangeListener(this); 231 232 /* Save the configuration. */ 233 applyConfigurationChange(configuration); 234 } 235 236 237 238 @Override 239 public boolean isConfigurationAcceptable(final PluginCfg configuration, 240 final List<LocalizableMessage> unacceptableReasons) 241 { 242 final AttributeCleanupPluginCfg cfg = 243 (AttributeCleanupPluginCfg) configuration; 244 return isConfigurationChangeAcceptable(cfg, unacceptableReasons); 245 } 246 247 248 249 @Override 250 public boolean isConfigurationChangeAcceptable( 251 final AttributeCleanupPluginCfg config, final List<LocalizableMessage> messages) 252 { 253 /* The admin framework will ensure that there are no duplicate attributes to be removed. */ 254 boolean isValid = true; 255 256 /* 257 * Verify that there are no duplicate mappings and that attributes are 258 * renamed to valid attribute types. 259 */ 260 final Set<String> fromAttrs = new HashSet<>(); 261 for (final String attr : config.getRenameInboundAttributes()) 262 { 263 /* 264 * The format is: from:to where each 'from' and 'to' are attribute 265 * descriptions. The admin framework ensures that the format is correct. 266 */ 267 final int colonPos = attr.lastIndexOf(":"); 268 final String fromAttr = attr.substring(0, colonPos).trim(); 269 final String toAttr = attr.substring(colonPos + 1).trim(); 270 271 /* 272 * Make sure that toAttr is defined within the server, being careful to 273 * ignore attribute options. 274 */ 275 final int semicolonPos = toAttr.indexOf(";"); 276 final String toAttrType = semicolonPos < 0 && semicolonPos < toAttr.length() - 1 277 ? toAttr 278 : toAttr.substring(semicolonPos + 1); 279 280 if (DirectoryServer.getSchema().getAttributeType(toAttrType).isPlaceHolder()) 281 { 282 messages.add(ERR_PLUGIN_ATTR_CLEANUP_ATTRIBUTE_MISSING.get(toAttr)); 283 isValid = false; 284 } 285 286 // Check for duplicates. 287 final String nfromAttr = toLowerCase(fromAttr); 288 if (!fromAttrs.add(nfromAttr)) 289 { 290 messages.add(ERR_PLUGIN_ATTR_CLEANUP_DUPLICATE_VALUE.get(fromAttr)); 291 isValid = false; 292 } 293 294 // Check that attribute does not map to itself. 295 if (nfromAttr.equals(toLowerCase(toAttr))) 296 { 297 messages.add(ERR_PLUGIN_ATTR_CLEANUP_EQUAL_VALUES.get(fromAttr, toAttr)); 298 isValid = false; 299 } 300 } 301 302 return isValid; 303 } 304 305 306 307 /** 308 * Remove the attributes listed in the configuration under 309 * ds-cfg-remove-inbound-attributes from the incoming ADD request. 310 * 311 * @param addOperation 312 * Current ADD operation. 313 */ 314 private void processInboundRemove(final PreParseAddOperation addOperation) 315 { 316 final List<RawAttribute> inAttrs = new LinkedList<>(addOperation.getRawAttributes()); 317 final ListIterator<RawAttribute> iterator = inAttrs.listIterator(); 318 while (iterator.hasNext()) 319 { 320 final RawAttribute rawAttr = iterator.next(); 321 final String attrName = toLowerCase(rawAttr.getAttributeType().trim()); 322 if (attributesToRemove.contains(attrName)) 323 { 324 if (logger.isTraceEnabled()) 325 { 326 logger.trace("AttributeCleanupPlugin removing '%s'", 327 rawAttr.getAttributeType()); 328 } 329 iterator.remove(); 330 } 331 } 332 addOperation.setRawAttributes(inAttrs); 333 } 334 335 336 337 /** 338 * Remove the attributes listed in the configuration under 339 * ds-cfg-remove-inbound-attributes from the incoming MODIFY request. 340 * 341 * @param modifyOperation 342 * Current MODIFY operation. 343 */ 344 private void processInboundRemove( 345 final PreParseModifyOperation modifyOperation) 346 { 347 final List<RawModification> rawMods = new LinkedList<>(modifyOperation.getRawModifications()); 348 final ListIterator<RawModification> iterator = rawMods.listIterator(); 349 while (iterator.hasNext()) 350 { 351 final RawModification rawMod = iterator.next(); 352 final RawAttribute rawAttr = rawMod.getAttribute(); 353 final String attrName = toLowerCase(rawAttr.getAttributeType().trim()); 354 if (attributesToRemove.contains(attrName)) 355 { 356 if (logger.isTraceEnabled()) 357 { 358 logger.trace("AttributeCleanupPlugin removing '%s'", 359 rawAttr.getAttributeType()); 360 } 361 iterator.remove(); 362 } 363 } 364 modifyOperation.setRawModifications(rawMods); 365 } 366 367 368 369 /** 370 * Map the incoming attributes to the local ones. 371 * 372 * @param addOperation 373 * Current ADD operation. 374 */ 375 private void processInboundRename(final PreParseAddOperation addOperation) 376 { 377 final List<RawAttribute> inAttrs = new LinkedList<>(addOperation.getRawAttributes()); 378 final ListIterator<RawAttribute> iterator = inAttrs.listIterator(); 379 while (iterator.hasNext()) 380 { 381 final RawAttribute rawAttr = iterator.next(); 382 final String fromName = toLowerCase(rawAttr.getAttributeType().trim()); 383 final String toName = attributesToRename.get(fromName); 384 if (toName != null) 385 { 386 if (logger.isTraceEnabled()) 387 { 388 logger.trace("AttributeCleanupPlugin renaming '%s' to '%s'", 389 rawAttr.getAttributeType(), toName); 390 } 391 rawAttr.setAttributeType(toName); 392 } 393 } 394 addOperation.setRawAttributes(inAttrs); 395 } 396 397 398 399 /** 400 * Rename the attributes in the incoming MODIFY request to names that exist in 401 * the local schema as defined in the configuration. 402 * 403 * @param modifyOperation 404 * Current MODIFY operation. 405 */ 406 private void processInboundRename( 407 final PreParseModifyOperation modifyOperation) 408 { 409 final List<RawModification> rawMods = new LinkedList<>(modifyOperation.getRawModifications()); 410 final ListIterator<RawModification> iterator = rawMods.listIterator(); 411 while (iterator.hasNext()) 412 { 413 final RawModification rawMod = iterator.next(); 414 final RawAttribute rawAttr = rawMod.getAttribute(); 415 final String fromName = toLowerCase(rawAttr.getAttributeType().trim()); 416 final String toName = attributesToRename.get(fromName); 417 if (toName != null) 418 { 419 if (logger.isTraceEnabled()) 420 { 421 logger.trace("AttributeCleanupPlugin renaming '%s' to '%s'", 422 rawAttr.getAttributeType(), toName); 423 } 424 rawAttr.setAttributeType(toName); 425 } 426 } 427 modifyOperation.setRawModifications(rawMods); 428 } 429}