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 Sun Microsystems, Inc.
015 * Portions Copyright 2014-2016 ForgeRock AS.
016 */
017package org.opends.server.plugins;
018
019import static org.opends.messages.PluginMessages.*;
020
021import java.util.List;
022import java.util.Set;
023
024import org.forgerock.i18n.LocalizableMessage;
025import org.forgerock.i18n.LocalizedIllegalArgumentException;
026import org.forgerock.opendj.config.server.ConfigChangeResult;
027import org.forgerock.opendj.config.server.ConfigException;
028import org.forgerock.opendj.ldap.AVA;
029import org.forgerock.opendj.ldap.ByteSequence;
030import org.forgerock.opendj.ldap.ByteString;
031import org.forgerock.opendj.ldap.DN;
032import org.forgerock.opendj.ldap.RDN;
033import org.forgerock.opendj.ldap.ResultCode;
034import org.forgerock.opendj.ldap.schema.AttributeType;
035import org.forgerock.opendj.config.server.ConfigurationChangeListener;
036import org.forgerock.opendj.server.config.meta.PluginCfgDefn;
037import org.forgerock.opendj.server.config.server.SevenBitCleanPluginCfg;
038import org.forgerock.opendj.server.config.server.PluginCfg;
039import org.opends.server.api.plugin.DirectoryServerPlugin;
040import org.opends.server.api.plugin.PluginResult;
041import org.opends.server.api.plugin.PluginType;
042import org.opends.server.core.DirectoryServer;
043import org.opends.server.types.Attribute;
044import org.opends.server.types.Entry;
045import org.opends.server.types.LDAPException;
046import org.opends.server.types.LDIFImportConfig;
047import org.opends.server.types.RawAttribute;
048import org.opends.server.types.RawModification;
049import org.opends.server.types.operation.PreParseAddOperation;
050import org.opends.server.types.operation.PreParseModifyDNOperation;
051import org.opends.server.types.operation.PreParseModifyOperation;
052
053/**
054 * This class implements a Directory Server plugin that can be used to ensure
055 * that the values for a specified set of attributes (optionally, below a
056 * specified set of base DNs) are 7-bit clean (i.e., contain only ASCII
057 * characters).
058 */
059public final class SevenBitCleanPlugin
060       extends DirectoryServerPlugin<SevenBitCleanPluginCfg>
061       implements ConfigurationChangeListener<SevenBitCleanPluginCfg>
062{
063  /** The bitmask that will be used to make the comparisons. */
064  private static final byte MASK = 0x7F;
065
066  /** The current configuration for this plugin. */
067  private SevenBitCleanPluginCfg currentConfig;
068
069  /**
070   * Creates a new instance of this Directory Server plugin.  Every plugin must
071   * implement a default constructor (it is the only one that will be used to
072   * create plugins defined in the configuration), and every plugin constructor
073   * must call {@code super()} as its first element.
074   */
075  public SevenBitCleanPlugin()
076  {
077    super();
078  }
079
080  @Override
081  public final void initializePlugin(Set<PluginType> pluginTypes,
082                                     SevenBitCleanPluginCfg configuration)
083         throws ConfigException
084  {
085    currentConfig = configuration;
086    configuration.addSevenBitCleanChangeListener(this);
087
088    // Make sure that the plugin has been enabled for the appropriate types.
089    for (PluginType t : pluginTypes)
090    {
091      switch (t)
092      {
093        case LDIF_IMPORT:
094        case PRE_PARSE_ADD:
095        case PRE_PARSE_MODIFY:
096        case PRE_PARSE_MODIFY_DN:
097          // These are acceptable.
098          break;
099
100        default:
101          throw new ConfigException(ERR_PLUGIN_7BIT_INVALID_PLUGIN_TYPE.get(t));
102      }
103    }
104  }
105
106  @Override
107  public final void finalizePlugin()
108  {
109    currentConfig.removeSevenBitCleanChangeListener(this);
110  }
111
112  @Override
113  public final PluginResult.ImportLDIF
114               doLDIFImport(LDIFImportConfig importConfig, Entry entry)
115  {
116    // Get the current configuration for this plugin.
117    SevenBitCleanPluginCfg config = currentConfig;
118
119    // Make sure that the entry is within the scope of this plugin.  While
120    // processing an LDIF import, we don't have access to the set of public
121    // naming contexts defined in the server, so if no explicit set of base DNs
122    // is defined, then assume that the entry is in scope.
123    if (!isDescendantOfAny(entry.getName(), config.getBaseDN()))
124    {
125      // The entry is out of scope, so we won't process it.
126      return PluginResult.ImportLDIF.continueEntryProcessing();
127    }
128
129    // Make sure all configured attributes have clean values.
130    for (AttributeType t : config.getAttributeType())
131    {
132      for (Attribute a : entry.getAttribute(t))
133      {
134        for (ByteString v : a)
135        {
136          if (!is7BitClean(v))
137          {
138            LocalizableMessage rejectMessage =
139                 ERR_PLUGIN_7BIT_IMPORT_ATTR_NOT_CLEAN.get(a.getAttributeDescription());
140            return PluginResult.ImportLDIF.stopEntryProcessing(rejectMessage);
141          }
142        }
143      }
144    }
145
146    // If we've gotten here, then everything is acceptable.
147    return PluginResult.ImportLDIF.continueEntryProcessing();
148  }
149
150  @Override
151  public final PluginResult.PreParse
152               doPreParse(PreParseAddOperation addOperation)
153  {
154    // Get the current configuration for this plugin.
155    SevenBitCleanPluginCfg config = currentConfig;
156
157    // If the entry is within the scope of this plugin, then make sure all
158    // configured attributes have clean values.
159    DN entryDN;
160    try
161    {
162      entryDN = DN.valueOf(addOperation.getRawEntryDN());
163    }
164    catch (LocalizedIllegalArgumentException e)
165    {
166      return PluginResult.PreParse.stopProcessing(ResultCode.INVALID_DN_SYNTAX,
167          ERR_PLUGIN_7BIT_CANNOT_DECODE_DN.get(e.getMessageObject()));
168    }
169
170    if (isInScope(config, entryDN))
171    {
172      for (RawAttribute rawAttr : addOperation.getRawAttributes())
173      {
174        Attribute a;
175        try
176        {
177          a = rawAttr.toAttribute();
178        }
179        catch (LDAPException le)
180        {
181          return PluginResult.PreParse.stopProcessing(
182              ResultCode.valueOf(le.getResultCode()),
183              ERR_PLUGIN_7BIT_CANNOT_DECODE_ATTR.get(
184                  rawAttr.getAttributeType(), le.getErrorMessage()));
185        }
186
187        if (!config.getAttributeType().contains(a.getAttributeDescription().getAttributeType()))
188        {
189          continue;
190        }
191
192        for (ByteString v : a)
193        {
194          if (!is7BitClean(v))
195          {
196            return PluginResult.PreParse.stopProcessing(
197                ResultCode.CONSTRAINT_VIOLATION,
198                ERR_PLUGIN_7BIT_MODIFYDN_ATTR_NOT_CLEAN.get(
199                    rawAttr.getAttributeType()));
200          }
201        }
202      }
203    }
204
205    // If we've gotten here, then everything is acceptable.
206    return PluginResult.PreParse.continueOperationProcessing();
207  }
208
209  @Override
210  public final PluginResult.PreParse
211                    doPreParse(PreParseModifyOperation modifyOperation)
212  {
213    // Get the current configuration for this plugin.
214    SevenBitCleanPluginCfg config = currentConfig;
215
216    // If the target entry is within the scope of this plugin, then make sure
217    // all values that will be added during the modification will be acceptable.
218    DN entryDN;
219    try
220    {
221      entryDN = DN.valueOf(modifyOperation.getRawEntryDN());
222    }
223    catch (LocalizedIllegalArgumentException e)
224    {
225      return PluginResult.PreParse.stopProcessing(ResultCode.INVALID_DN_SYNTAX,
226          ERR_PLUGIN_7BIT_CANNOT_DECODE_DN.get(e.getMessageObject()));
227    }
228
229    if (isInScope(config, entryDN))
230    {
231      for (RawModification m : modifyOperation.getRawModifications())
232      {
233        switch (m.getModificationType().asEnum())
234        {
235          case ADD:
236          case REPLACE:
237            // These are modification types that we will process.
238            break;
239          default:
240            // This is not a modification type that we will process.
241            continue;
242        }
243
244        RawAttribute rawAttr = m.getAttribute();
245        Attribute a;
246        try
247        {
248          a = rawAttr.toAttribute();
249        }
250        catch (LDAPException le)
251        {
252          return PluginResult.PreParse.stopProcessing(
253              ResultCode.valueOf(le.getResultCode()),
254              ERR_PLUGIN_7BIT_CANNOT_DECODE_ATTR.get(
255                  rawAttr.getAttributeType(), le.getErrorMessage()));
256        }
257
258        if (!config.getAttributeType().contains(a.getAttributeDescription().getAttributeType()))
259        {
260          continue;
261        }
262
263        for (ByteString v : a)
264        {
265          if (!is7BitClean(v))
266          {
267            return PluginResult.PreParse.stopProcessing(
268                ResultCode.CONSTRAINT_VIOLATION,
269                ERR_PLUGIN_7BIT_MODIFYDN_ATTR_NOT_CLEAN.get(
270                    rawAttr.getAttributeType()));
271          }
272        }
273      }
274    }
275
276    // If we've gotten here, then everything is acceptable.
277    return PluginResult.PreParse.continueOperationProcessing();
278  }
279
280  @Override
281  public final PluginResult.PreParse
282                    doPreParse(PreParseModifyDNOperation modifyDNOperation)
283  {
284    // Get the current configuration for this plugin.
285    SevenBitCleanPluginCfg config = currentConfig;
286
287    // If the target entry is within the scope of this plugin, then make sure
288    // all values that will be added during the modification will be acceptable.
289    DN entryDN;
290    try
291    {
292      entryDN = DN.valueOf(modifyDNOperation.getRawEntryDN());
293    }
294    catch (LocalizedIllegalArgumentException e)
295    {
296      return PluginResult.PreParse.stopProcessing(ResultCode.INVALID_DN_SYNTAX,
297          ERR_PLUGIN_7BIT_CANNOT_DECODE_DN.get(e.getMessageObject()));
298    }
299
300    if (isInScope(config, entryDN))
301    {
302      ByteString rawNewRDN = modifyDNOperation.getRawNewRDN();
303
304      RDN newRDN;
305      try
306      {
307        newRDN = RDN.valueOf(rawNewRDN.toString());
308      }
309      catch (LocalizedIllegalArgumentException e)
310      {
311        return PluginResult.PreParse.stopProcessing(ResultCode.INVALID_DN_SYNTAX,
312            ERR_PLUGIN_7BIT_CANNOT_DECODE_NEW_RDN.get(e.getMessageObject()));
313      }
314
315      for (AVA ava : newRDN)
316      {
317        if (!config.getAttributeType().contains(ava.getAttributeType()))
318        {
319          continue;
320        }
321
322        if (!is7BitClean(ava.getAttributeValue()))
323        {
324          return PluginResult.PreParse.stopProcessing(
325              ResultCode.CONSTRAINT_VIOLATION,
326              ERR_PLUGIN_7BIT_MODIFYDN_ATTR_NOT_CLEAN.get(ava.getAttributeName()));
327        }
328      }
329    }
330
331    // If we've gotten here, then everything is acceptable.
332    return PluginResult.PreParse.continueOperationProcessing();
333  }
334
335  /**
336   * Indicates whether the provided DN is within the scope of this plugin.
337   *
338   * @param  config  The configuration to use when making the determination.
339   * @param  dn      The DN for which to make the determination.
340   *
341   * @return  {@code true} if the provided DN is within the scope of this
342   *          plugin, or {@code false} if  not.
343   */
344  private final boolean isInScope(SevenBitCleanPluginCfg config, DN dn)
345  {
346    Set<DN> baseDNs = config.getBaseDN();
347    if (baseDNs == null || baseDNs.isEmpty())
348    {
349      baseDNs = DirectoryServer.getPublicNamingContexts().keySet();
350    }
351    return isDescendantOfAny(dn, baseDNs);
352  }
353
354  private boolean isDescendantOfAny(DN dn, Set<DN> baseDNs)
355  {
356    if (baseDNs != null)
357    {
358      for (DN baseDN: baseDNs)
359      {
360        if (dn.isSubordinateOrEqualTo(baseDN))
361        {
362          return true;
363        }
364      }
365    }
366    return false;
367  }
368
369  /**
370   * Indicates whether the provided value is 7-bit clean.
371   *
372   * @param  value  The value for which to make the determination.
373   *
374   * @return {@code true} if the provided value is 7-bit clean, or {@code false}
375   *         if it is not.
376   */
377  private final boolean is7BitClean(ByteSequence value)
378  {
379    for (int i = 0; i < value.length(); i++)
380    {
381      byte b = value.byteAt(i);
382      if ((b & MASK) != b)
383      {
384        return false;
385      }
386    }
387    return true;
388  }
389
390  @Override
391  public boolean isConfigurationAcceptable(PluginCfg configuration,
392                                           List<LocalizableMessage> unacceptableReasons)
393  {
394    SevenBitCleanPluginCfg cfg = (SevenBitCleanPluginCfg) configuration;
395    return isConfigurationChangeAcceptable(cfg, unacceptableReasons);
396  }
397
398  @Override
399  public boolean isConfigurationChangeAcceptable(
400                      SevenBitCleanPluginCfg configuration,
401                      List<LocalizableMessage> unacceptableReasons)
402  {
403    boolean configAcceptable = true;
404
405    // Ensure that the set of plugin types is acceptable.
406    for (PluginCfgDefn.PluginType pluginType : configuration.getPluginType())
407    {
408      switch (pluginType)
409      {
410        case LDIFIMPORT:
411        case PREPARSEADD:
412        case PREPARSEMODIFY:
413        case PREPARSEMODIFYDN:
414          // These are acceptable.
415          break;
416
417        default:
418          unacceptableReasons.add(ERR_PLUGIN_7BIT_INVALID_PLUGIN_TYPE.get(pluginType));
419          configAcceptable = false;
420      }
421    }
422
423    return configAcceptable;
424  }
425
426  @Override
427  public ConfigChangeResult applyConfigurationChange(
428                                 SevenBitCleanPluginCfg configuration)
429  {
430    currentConfig = configuration;
431    return new ConfigChangeResult();
432  }
433}