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}