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 2007-2009 Sun Microsystems, Inc.
015 * Portions Copyright 2011-2016 ForgeRock AS.
016 */
017package org.opends.server.core;
018
019import static org.forgerock.opendj.adapter.server3x.Converters.*;
020import static org.opends.messages.ConfigMessages.*;
021import static org.opends.server.util.StaticUtils.*;
022
023import java.util.ArrayList;
024import java.util.Collection;
025import java.util.LinkedHashMap;
026import java.util.LinkedHashSet;
027import java.util.List;
028import java.util.Map;
029import java.util.Map.Entry;
030import java.util.Set;
031import java.util.concurrent.ConcurrentHashMap;
032import java.util.concurrent.ConcurrentMap;
033
034import org.forgerock.i18n.LocalizableMessage;
035import org.forgerock.i18n.slf4j.LocalizedLogger;
036import org.forgerock.opendj.config.server.ConfigChangeResult;
037import org.forgerock.opendj.config.server.ConfigException;
038import org.forgerock.opendj.ldap.DN;
039import org.forgerock.opendj.ldap.ResultCode;
040import org.forgerock.util.Utils;
041import org.forgerock.opendj.config.ClassPropertyDefinition;
042import org.forgerock.opendj.config.server.ConfigurationAddListener;
043import org.forgerock.opendj.config.server.ConfigurationChangeListener;
044import org.forgerock.opendj.config.server.ConfigurationDeleteListener;
045import org.forgerock.opendj.server.config.meta.VirtualAttributeCfgDefn;
046import org.forgerock.opendj.server.config.server.RootCfg;
047import org.forgerock.opendj.server.config.server.VirtualAttributeCfg;
048import org.opends.server.api.VirtualAttributeProvider;
049import org.opends.server.types.DirectoryException;
050import org.opends.server.types.InitializationException;
051import org.opends.server.types.SearchFilter;
052import org.opends.server.types.VirtualAttributeRule;
053
054/**
055 * This class defines a utility that will be used to manage the set of
056 * virtual attribute providers defined in the Directory Server.  It will
057 * initialize the providers when the server starts, and then will manage any
058 * additions, removals, or modifications to any virtual attribute providers
059 * while the server is running.
060 */
061public class VirtualAttributeConfigManager
062       implements ConfigurationChangeListener<VirtualAttributeCfg>,
063                  ConfigurationAddListener<VirtualAttributeCfg>,
064                  ConfigurationDeleteListener<VirtualAttributeCfg>
065{
066  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
067
068  /** A mapping between the DNs of the config entries and the associated virtual attribute rules. */
069  private final ConcurrentMap<DN, VirtualAttributeRule> rules = new ConcurrentHashMap<>();
070
071  private final ServerContext serverContext;
072
073  /**
074   * Creates a new instance of this virtual attribute config manager.
075   *
076   * @param serverContext
077   *            The server context.
078   */
079  public VirtualAttributeConfigManager(ServerContext serverContext)
080  {
081    this.serverContext = serverContext;
082  }
083
084  /**
085   * Initializes all virtual attribute providers currently defined in the
086   * Directory Server configuration. This should only be called at Directory
087   * Server startup.
088   *
089   * @throws ConfigException
090   *           If a configuration problem causes the virtual attribute provider
091   *           initialization process to fail.
092   * @throws InitializationException
093   *           If a problem occurs while initializing the virtual attribute
094   *           providers that is not related to the server configuration.
095   */
096  public void initializeVirtualAttributes()
097         throws ConfigException, InitializationException
098  {
099    RootCfg rootConfiguration = serverContext.getRootConfig();
100    rootConfiguration.addVirtualAttributeAddListener(this);
101    rootConfiguration.addVirtualAttributeDeleteListener(this);
102
103    //Initialize the existing virtual attribute providers.
104    for (String providerName : rootConfiguration.listVirtualAttributes())
105    {
106      VirtualAttributeCfg cfg =
107           rootConfiguration.getVirtualAttribute(providerName);
108      cfg.addChangeListener(this);
109
110      if (cfg.isEnabled())
111      {
112        String className = cfg.getJavaClass();
113        try
114        {
115          VirtualAttributeProvider<? extends VirtualAttributeCfg> provider =
116               loadProvider(className, cfg, true);
117
118          Map<LocalizableMessage, DirectoryException> reasons =
119              new LinkedHashMap<>();
120          Set<SearchFilter> filters = buildFilters(cfg, reasons);
121          if (!reasons.isEmpty())
122          {
123            Entry<LocalizableMessage, DirectoryException> entry =
124                reasons.entrySet().iterator().next();
125            throw new ConfigException(entry.getKey(), entry.getValue());
126          }
127
128          if (cfg.getAttributeType().isSingleValue())
129          {
130            if (provider.isMultiValued())
131            {
132              LocalizableMessage message = ERR_CONFIG_VATTR_SV_TYPE_WITH_MV_PROVIDER.
133                  get(cfg.dn(), cfg.getAttributeType().getNameOrOID(), className);
134              throw new ConfigException(message);
135            }
136            else if (cfg.getConflictBehavior() ==
137                     VirtualAttributeCfgDefn.ConflictBehavior.
138                          MERGE_REAL_AND_VIRTUAL)
139            {
140              LocalizableMessage message = ERR_CONFIG_VATTR_SV_TYPE_WITH_MERGE_VALUES.
141                  get(cfg.dn(), cfg.getAttributeType().getNameOrOID());
142              throw new ConfigException(message);
143            }
144          }
145
146          VirtualAttributeRule rule = createRule(cfg, provider, filters);
147          rules.put(cfg.dn(), rule);
148        }
149        catch (InitializationException ie)
150        {
151          logger.error(ie.getMessageObject());
152          continue;
153        }
154      }
155    }
156  }
157
158  private VirtualAttributeRule createRule(VirtualAttributeCfg cfg,
159      VirtualAttributeProvider<? extends VirtualAttributeCfg> provider,
160      Set<SearchFilter> filters)
161  {
162    return new VirtualAttributeRule(cfg.getAttributeType(), provider,
163           cfg.getBaseDN(),
164           from(cfg.getScope()),
165           cfg.getGroupDN(),
166           filters,
167           cfg.getConflictBehavior());
168  }
169
170  @Override
171  public boolean isConfigurationAddAcceptable(
172                      VirtualAttributeCfg configuration,
173                      List<LocalizableMessage> unacceptableReasons)
174  {
175    if (configuration.isEnabled())
176    {
177      // Get the name of the class and make sure we can instantiate it as a
178      // virtual attribute provider.
179      String className = configuration.getJavaClass();
180      try
181      {
182        loadProvider(className, configuration, false);
183      }
184      catch (InitializationException ie)
185      {
186        unacceptableReasons.add(ie.getMessageObject());
187        return false;
188      }
189    }
190
191    // If there were any search filters provided, then make sure they are all
192    // valid.
193    return areFiltersAcceptable(configuration, unacceptableReasons);
194  }
195
196  private Set<SearchFilter> buildFilters(VirtualAttributeCfg cfg,
197      Map<LocalizableMessage, DirectoryException> unacceptableReasons)
198  {
199    Set<SearchFilter> filters = new LinkedHashSet<>();
200    for (String filterString : cfg.getFilter())
201    {
202      try
203      {
204        filters.add(SearchFilter.createFilterFromString(filterString));
205      }
206      catch (DirectoryException de)
207      {
208        logger.traceException(de);
209
210        LocalizableMessage message = ERR_CONFIG_VATTR_INVALID_SEARCH_FILTER.get(
211            filterString, cfg.dn(), de.getMessageObject());
212        unacceptableReasons.put(message, de);
213      }
214    }
215    return filters;
216  }
217
218  @Override
219  public ConfigChangeResult applyConfigurationAdd(
220                                 VirtualAttributeCfg configuration)
221  {
222    final ConfigChangeResult ccr = new ConfigChangeResult();
223
224    configuration.addChangeListener(this);
225
226    if (! configuration.isEnabled())
227    {
228      return ccr;
229    }
230
231    // Make sure that we can parse all of the search filters.
232    Map<LocalizableMessage, DirectoryException> reasons =
233        new LinkedHashMap<>();
234    Set<SearchFilter> filters = buildFilters(configuration, reasons);
235    if (!reasons.isEmpty())
236    {
237      ccr.getMessages().addAll(reasons.keySet());
238      ccr.setResultCodeIfSuccess(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
239    }
240
241    // Get the name of the class and make sure we can instantiate it as a
242    // certificate mapper.
243    VirtualAttributeProvider<? extends VirtualAttributeCfg> provider = null;
244    if (ccr.getResultCode() == ResultCode.SUCCESS)
245    {
246      String className = configuration.getJavaClass();
247      try
248      {
249        provider = loadProvider(className, configuration, true);
250      }
251      catch (InitializationException ie)
252      {
253        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
254        ccr.addMessage(ie.getMessageObject());
255      }
256    }
257
258    if (ccr.getResultCode() == ResultCode.SUCCESS)
259    {
260      VirtualAttributeRule rule = createRule(configuration, provider, filters);
261      rules.put(configuration.dn(), rule);
262    }
263
264    return ccr;
265  }
266
267  @Override
268  public boolean isConfigurationDeleteAcceptable(
269                      VirtualAttributeCfg configuration,
270                      List<LocalizableMessage> unacceptableReasons)
271  {
272    // We will always allow getting rid of a virtual attribute rule.
273    return true;
274  }
275
276  @Override
277  public ConfigChangeResult applyConfigurationDelete(
278                                 VirtualAttributeCfg configuration)
279  {
280    final ConfigChangeResult ccr = new ConfigChangeResult();
281
282    VirtualAttributeRule rule = rules.remove(configuration.dn());
283    if (rule != null)
284    {
285      rule.getProvider().finalizeVirtualAttributeProvider();
286    }
287
288    return ccr;
289  }
290
291  @Override
292  public boolean isConfigurationChangeAcceptable(
293                      VirtualAttributeCfg configuration,
294                      List<LocalizableMessage> unacceptableReasons)
295  {
296    if (configuration.isEnabled())
297    {
298      // Get the name of the class and make sure we can instantiate it as a
299      // virtual attribute provider.
300      String className = configuration.getJavaClass();
301      try
302      {
303        loadProvider(className, configuration, false);
304      }
305      catch (InitializationException ie)
306      {
307        unacceptableReasons.add(ie.getMessageObject());
308        return false;
309      }
310    }
311
312    // If there were any search filters provided, then make sure they are all
313    // valid.
314    return areFiltersAcceptable(configuration, unacceptableReasons);
315  }
316
317  private boolean areFiltersAcceptable(VirtualAttributeCfg cfg,
318      List<LocalizableMessage> unacceptableReasons)
319  {
320    Map<LocalizableMessage, DirectoryException> reasons =
321        new LinkedHashMap<>();
322    buildFilters(cfg, reasons);
323    if (!reasons.isEmpty())
324    {
325      unacceptableReasons.addAll(reasons.keySet());
326      return false;
327    }
328    return true;
329  }
330
331  @Override
332  public ConfigChangeResult applyConfigurationChange(
333                                 VirtualAttributeCfg configuration)
334  {
335    final ConfigChangeResult ccr = new ConfigChangeResult();
336
337    // Get the existing rule if it's already enabled.
338    VirtualAttributeRule existingRule = rules.get(configuration.dn());
339
340    // If the new configuration has the rule disabled, then disable it if it
341    // is enabled, or do nothing if it's already disabled.
342    if (! configuration.isEnabled())
343    {
344      if (existingRule != null)
345      {
346        rules.remove(configuration.dn());
347        existingRule.getProvider().finalizeVirtualAttributeProvider();
348      }
349
350      return ccr;
351    }
352
353    // Make sure that we can parse all of the search filters.
354    Map<LocalizableMessage, DirectoryException> reasons =
355        new LinkedHashMap<>();
356    Set<SearchFilter> filters = buildFilters(configuration, reasons);
357    if (!reasons.isEmpty())
358    {
359      ccr.getMessages().addAll(reasons.keySet());
360      ccr.setResultCodeIfSuccess(ResultCode.INVALID_ATTRIBUTE_SYNTAX);
361    }
362
363    // Get the name of the class and make sure we can instantiate it as a
364    // certificate mapper.
365    VirtualAttributeProvider<? extends VirtualAttributeCfg> provider = null;
366    if (ccr.getResultCode() == ResultCode.SUCCESS)
367    {
368      String className = configuration.getJavaClass();
369      try
370      {
371        provider = loadProvider(className, configuration, true);
372      }
373      catch (InitializationException ie)
374      {
375        ccr.setResultCode(DirectoryServer.getServerErrorResultCode());
376        ccr.addMessage(ie.getMessageObject());
377      }
378    }
379
380    if (ccr.getResultCode() == ResultCode.SUCCESS)
381    {
382      VirtualAttributeRule rule = createRule(configuration, provider, filters);
383      rules.put(configuration.dn(), rule);
384      if (existingRule != null)
385      {
386        existingRule.getProvider().finalizeVirtualAttributeProvider();
387      }
388    }
389
390    return ccr;
391  }
392
393  /**
394   * Loads the specified class, instantiates it as a certificate mapper, and
395   * optionally initializes that instance.
396   *
397   * @param  className      The fully-qualified name of the certificate mapper
398   *                        class to load, instantiate, and initialize.
399   * @param  cfg            The configuration to use to initialize the
400   *                        virtual attribute provider.  It must not be
401   *                        {@code null}.
402   * @param  initialize     Indicates whether the virtual attribute provider
403   *                        instance should be initialized.
404   *
405   * @return  The possibly initialized certificate mapper.
406   *
407   * @throws  InitializationException  If a problem occurred while attempting to
408   *                                   initialize the certificate mapper.
409   */
410  @SuppressWarnings({ "rawtypes", "unchecked" })
411  private VirtualAttributeProvider<? extends VirtualAttributeCfg>
412               loadProvider(String className, VirtualAttributeCfg cfg,
413                            boolean initialize)
414          throws InitializationException
415  {
416    try
417    {
418      VirtualAttributeCfgDefn definition =
419           VirtualAttributeCfgDefn.getInstance();
420      ClassPropertyDefinition propertyDefinition =
421           definition.getJavaClassPropertyDefinition();
422      Class<? extends VirtualAttributeProvider> providerClass =
423           propertyDefinition.loadClass(className,
424                                        VirtualAttributeProvider.class);
425      VirtualAttributeProvider provider = providerClass.newInstance();
426
427      if (initialize)
428      {
429        provider.initializeVirtualAttributeProvider(cfg);
430      }
431      else
432      {
433        List<LocalizableMessage> unacceptableReasons = new ArrayList<>();
434        if (!provider.isConfigurationAcceptable(cfg, unacceptableReasons))
435        {
436          String reasons = Utils.joinAsString(".  ", unacceptableReasons);
437          LocalizableMessage message = ERR_CONFIG_VATTR_CONFIG_NOT_ACCEPTABLE.get(cfg.dn(), reasons);
438          throw new InitializationException(message);
439        }
440      }
441
442      return provider;
443    }
444    catch (Exception e)
445    {
446      LocalizableMessage message = ERR_CONFIG_VATTR_INITIALIZATION_FAILED.
447          get(className, cfg.dn(), stackTraceToSingleLineString(e));
448      throw new InitializationException(message, e);
449    }
450  }
451
452  /**
453   * Retrieves the collection of registered virtual attribute rules.
454   *
455   * @return The collection of registered virtual attribute rules.
456   */
457  public Collection<VirtualAttributeRule> getVirtualAttributes()
458  {
459    return this.rules.values();
460  }
461
462  /**
463   * Registers the provided virtual attribute rule.
464   *
465   * @param rule
466   *          The virtual attribute rule to be registered.
467   */
468  public void register(VirtualAttributeRule rule)
469  {
470    rules.put(getDummyDN(rule), rule);
471  }
472
473  /**
474   * Deregisters the provided virtual attribute rule.
475   *
476   * @param rule
477   *          The virtual attribute rule to be deregistered.
478   */
479  public void deregister(VirtualAttributeRule rule)
480  {
481    rules.remove(getDummyDN(rule));
482  }
483
484  private DN getDummyDN(VirtualAttributeRule rule)
485  {
486    String name = rule.getAttributeType().getNameOrOID();
487    return DN.valueOf("cn=" + name + ",cn=Virtual Attributes,cn=config");
488  }
489}