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