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 2014-2016 ForgeRock AS.
015 */
016package org.opends.server.backends;
017
018import static org.opends.messages.ConfigMessages.*;
019import static org.opends.server.config.ConfigConstants.ATTR_DEFAULT_ROOT_PRIVILEGE_NAME;
020import static org.opends.server.config.ConfigConstants.CONFIG_ARCHIVE_DIR_NAME;
021import static org.opends.server.util.StaticUtils.getExceptionMessage;
022
023import java.io.File;
024import java.nio.file.Path;
025import java.util.ArrayList;
026import java.util.Collections;
027import java.util.HashSet;
028import java.util.List;
029import java.util.ListIterator;
030import java.util.Set;
031import java.util.SortedSet;
032import java.util.TreeSet;
033
034import org.forgerock.i18n.LocalizableMessage;
035import org.forgerock.i18n.slf4j.LocalizedLogger;
036import org.forgerock.opendj.adapter.server3x.Converters;
037import org.forgerock.opendj.config.server.ConfigException;
038import org.forgerock.opendj.config.server.ConfigurationChangeListener;
039import org.forgerock.opendj.ldap.ConditionResult;
040import org.forgerock.opendj.ldap.DN;
041import org.forgerock.opendj.ldap.ResultCode;
042import org.forgerock.opendj.ldap.schema.AttributeType;
043import org.forgerock.opendj.server.config.meta.BackendCfgDefn.WritabilityMode;
044import org.forgerock.opendj.server.config.server.BackendCfg;
045import org.opends.server.api.Backend;
046import org.opends.server.api.Backupable;
047import org.opends.server.api.ClientConnection;
048import org.opends.server.backends.ConfigurationBackend.ConfigurationBackendCfg;
049import org.opends.server.config.ConfigurationHandler;
050import org.opends.server.core.AddOperation;
051import org.opends.server.core.DeleteOperation;
052import org.opends.server.core.DirectoryServer;
053import org.opends.server.core.ModifyDNOperation;
054import org.opends.server.core.ModifyOperation;
055import org.opends.server.core.SearchOperation;
056import org.opends.server.core.ServerContext;
057import org.opends.server.types.BackupConfig;
058import org.opends.server.types.BackupDirectory;
059import org.opends.server.types.DirectoryException;
060import org.opends.server.types.Entry;
061import org.opends.server.types.IndexType;
062import org.opends.server.types.InitializationException;
063import org.opends.server.types.LDIFExportConfig;
064import org.opends.server.types.LDIFImportConfig;
065import org.opends.server.types.LDIFImportResult;
066import org.opends.server.types.Modification;
067import org.opends.server.types.Privilege;
068import org.opends.server.types.RestoreConfig;
069import org.opends.server.util.BackupManager;
070import org.opends.server.util.StaticUtils;
071
072/** Back-end responsible for management of configuration entries. */
073public class ConfigurationBackend extends Backend<ConfigurationBackendCfg> implements Backupable
074{
075  /**
076   * Dummy {@link BackendCfg} implementation for the {@link ConfigurationBackend}. No config is
077   * needed for this specific backend, but this class is required to behave like other backends
078   * during initialization.
079   */
080  public final class ConfigurationBackendCfg implements BackendCfg
081  {
082    private ConfigurationBackendCfg()
083    {
084      // let nobody instantiate it
085    }
086
087    @Override
088    public DN dn()
089    {
090      return getBaseDNs().iterator().next();
091    }
092
093    @Override
094    public Class<? extends BackendCfg> configurationClass()
095    {
096      return this.getClass();
097    }
098
099    @Override
100    public String getBackendId()
101    {
102      return CONFIG_BACKEND_ID;
103    }
104
105    @Override
106    public SortedSet<DN> getBaseDN()
107    {
108      return Collections.unmodifiableSortedSet(new TreeSet<DN>(getBaseDNs()));
109    }
110
111    @Override
112    public boolean isEnabled()
113    {
114      return true;
115    }
116
117    @Override
118    public String getJavaClass()
119    {
120      return ConfigurationBackend.class.getName();
121    }
122
123    @Override
124    public WritabilityMode getWritabilityMode()
125    {
126      return WritabilityMode.ENABLED;
127    }
128
129    @Override
130    public void addChangeListener(ConfigurationChangeListener<BackendCfg> listener)
131    {
132      // no-op
133    }
134
135    @Override
136    public void removeChangeListener(ConfigurationChangeListener<BackendCfg> listener)
137    {
138      // no-op
139    }
140  }
141
142  /**
143   * The backend ID for the configuration backend.
144   * <p>
145   * Try to avoid potential conflict with user backend identifiers.
146   */
147  public static final String CONFIG_BACKEND_ID = "__config.ldif__";
148
149  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
150
151  /** The set of supported control OIDs for this backend. */
152  private static final Set<String> SUPPORTED_CONTROLS = new HashSet<>(0);
153  /** The set of supported feature OIDs for this backend. */
154  private static final Set<String> SUPPORTED_FEATURES = new HashSet<>(0);
155
156  /** The privilege array containing both the CONFIG_READ and CONFIG_WRITE privileges. */
157  private static final Privilege[] CONFIG_READ_AND_WRITE =
158  {
159    Privilege.CONFIG_READ,
160    Privilege.CONFIG_WRITE
161  };
162
163  /** Handles the configuration entries and their storage in files. */
164  private final ConfigurationHandler configurationHandler;
165
166  /** The reference to the configuration root entry. */
167  private final Entry configRootEntry;
168
169  /** The set of base DNs for this config handler backend. */
170  private Set<DN> baseDNs;
171
172  /**
173   * The write lock used to ensure that only one thread can apply a
174   * configuration update at any given time.
175   */
176  private final Object configLock = new Object();
177
178  /**
179   * Creates and initializes a new instance of this backend.
180   *
181   * @param serverContext
182   *            The server context.
183   * @param configurationHandler
184   *            Contains the configuration entries.
185   * @throws InitializationException
186   *            If an errors occurs.
187   */
188  public ConfigurationBackend(ServerContext serverContext, ConfigurationHandler configurationHandler)
189      throws InitializationException
190  {
191    this.configurationHandler = configurationHandler;
192    this.configRootEntry = Converters.to(configurationHandler.getRootEntry());
193    baseDNs = Collections.singleton(configRootEntry.getName());
194
195    setBackendID(CONFIG_BACKEND_ID);
196  }
197
198  /**
199   * Returns a new {@link ConfigurationBackendCfg} for this {@link ConfigurationBackend}.
200   *
201   * @return a new {@link ConfigurationBackendCfg} for this {@link ConfigurationBackend}
202   */
203  public ConfigurationBackendCfg getBackendCfg()
204  {
205    return new ConfigurationBackendCfg();
206  }
207
208  @Override
209  public void closeBackend()
210  {
211    try
212    {
213      DirectoryServer.deregisterBaseDN(configRootEntry.getName());
214    }
215    catch (Exception e)
216    {
217      logger.traceException(e, "Error when deregistering base DN: " + configRootEntry.getName());
218    }
219  }
220
221  @Override
222  public void configureBackend(ConfigurationBackendCfg cfg, ServerContext serverContext) throws ConfigException
223  {
224    // No action is required.
225  }
226
227  @Override
228  public void openBackend() throws InitializationException
229  {
230    DN baseDN = configRootEntry.getName();
231    try
232    {
233      DirectoryServer.registerBaseDN(baseDN, this, true);
234    }
235    catch (DirectoryException e)
236    {
237      logger.traceException(e);
238      throw new InitializationException(
239          ERR_CONFIG_CANNOT_REGISTER_AS_PRIVATE_SUFFIX.get(baseDN, getExceptionMessage(e)), e);
240    }
241  }
242
243  @Override
244  public Set<DN> getBaseDNs()
245  {
246    return baseDNs;
247  }
248
249  @Override
250  public Entry getEntry(DN entryDN)
251  {
252    try
253    {
254      org.forgerock.opendj.ldap.Entry entry = configurationHandler.getEntry(entryDN);
255      if (entry != null)
256      {
257        Entry serverEntry = Converters.to(entry);
258        serverEntry.processVirtualAttributes();
259        return serverEntry;
260      }
261    }
262    catch (ConfigException e)
263    {
264      // should never happen
265    }
266    return null;
267  }
268
269  @Override
270  public long getEntryCount()
271  {
272    try
273    {
274      return getNumberOfEntriesInBaseDN(configRootEntry.getName());
275    }
276    catch (DirectoryException e)
277    {
278      logger.traceException(e, "Unable to count entries of configuration backend");
279      return -1;
280    }
281  }
282
283  @Override
284  public File getDirectory()
285  {
286    return configurationHandler.getConfigurationFile().getParentFile();
287  }
288
289  @Override
290  public long getNumberOfChildren(DN parentDN) throws DirectoryException
291  {
292    try {
293      return configurationHandler.numSubordinates(parentDN, false);
294    }
295    catch (ConfigException e)
296    {
297      throw new DirectoryException(ResultCode.UNDEFINED, e.getMessageObject());
298    }
299  }
300
301  @Override
302  public long getNumberOfEntriesInBaseDN(DN baseDN) throws DirectoryException
303  {
304    try
305    {
306      return configurationHandler.numSubordinates(baseDN, true) + 1;
307    }
308    catch (ConfigException e)
309    {
310      throw new DirectoryException(ResultCode.UNDEFINED, e.getMessageObject());
311    }
312  }
313
314  @Override
315  public Set<String> getSupportedControls()
316  {
317    return SUPPORTED_CONTROLS;
318  }
319
320  @Override
321  public Set<String> getSupportedFeatures()
322  {
323    return SUPPORTED_FEATURES;
324  }
325
326  @Override
327  public ConditionResult hasSubordinates(DN entryDN) throws DirectoryException
328  {
329    long ret = getNumberOfChildren(entryDN);
330    if(ret < 0)
331    {
332      return ConditionResult.UNDEFINED;
333    }
334    return ConditionResult.valueOf(ret != 0);
335  }
336
337  @Override
338  public boolean isIndexed(AttributeType attributeType, IndexType indexType)
339  {
340    // All searches in this backend will always be considered indexed.
341    return true;
342  }
343
344  @Override
345  public boolean entryExists(DN entryDN) throws DirectoryException
346  {
347    try
348    {
349      return configurationHandler.hasEntry(entryDN);
350    }
351    catch (ConfigException e)
352    {
353      throw new DirectoryException(ResultCode.UNDEFINED, e.getMessageObject(), e);
354    }
355  }
356
357  @Override
358  public boolean supports(BackendOperation backendOperation)
359  {
360    switch (backendOperation)
361    {
362    case BACKUP:
363    case RESTORE:
364    case LDIF_EXPORT:
365      return true;
366    default:
367      return false;
368    }
369  }
370
371  @Override
372  public void search(SearchOperation searchOperation) throws DirectoryException
373  {
374    // Make sure that the associated user has the CONFIG_READ privilege.
375    ClientConnection clientConnection = searchOperation.getClientConnection();
376    if (! clientConnection.hasPrivilege(Privilege.CONFIG_READ, searchOperation))
377    {
378      LocalizableMessage message = ERR_CONFIG_FILE_SEARCH_INSUFFICIENT_PRIVILEGES.get();
379      throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
380    }
381
382    configurationHandler.search(searchOperation);
383  }
384
385  @Override
386  public void addEntry(Entry entry, AddOperation addOperation)
387         throws DirectoryException
388  {
389    // Make sure that the associated user has
390    // both the CONFIG_READ and CONFIG_WRITE privileges.
391    if (addOperation != null)
392    {
393      ClientConnection clientConnection = addOperation.getClientConnection();
394      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, addOperation))
395      {
396        LocalizableMessage message = ERR_CONFIG_FILE_ADD_INSUFFICIENT_PRIVILEGES.get();
397        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
398      }
399    }
400
401    // Only one configuration update may be in progress at any given time.
402    synchronized (configLock)
403    {
404      configurationHandler.addEntry(Converters.from(copyWithoutVirtualAttributes(entry)));
405    }
406  }
407
408  private Entry copyWithoutVirtualAttributes(Entry entry) {
409    return entry.duplicate(false);
410  }
411
412  @Override
413  public void deleteEntry(DN entryDN, DeleteOperation deleteOperation)
414         throws DirectoryException
415  {
416    // Make sure that the associated user
417    // has both the CONFIG_READ and CONFIG_WRITE privileges.
418    if (deleteOperation != null)
419    {
420      ClientConnection clientConnection = deleteOperation.getClientConnection();
421      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, deleteOperation))
422      {
423        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_INSUFFICIENT_PRIVILEGES.get();
424        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
425      }
426    }
427
428    // Only one configuration update may be in progress at any given time.
429    synchronized (configLock)
430    {
431      if (configRootEntry.getName().equals(entryDN))
432      {
433        LocalizableMessage message = ERR_CONFIG_FILE_DELETE_NO_PARENT.get(entryDN);
434        throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
435      }
436      configurationHandler.deleteEntry(entryDN);
437    }
438  }
439
440  @Override
441  public void replaceEntry(Entry oldEntry, Entry newEntry, ModifyOperation modifyOperation) throws DirectoryException
442  {
443    // Make sure that the associated user has both the CONFIG_READ and CONFIG_WRITE privileges.
444    // Also, if the operation targets the set of root privileges
445    // then make sure the user has the PRIVILEGE_CHANGE privilege.
446    if (modifyOperation != null)
447    {
448      ClientConnection clientConnection = modifyOperation.getClientConnection();
449      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, modifyOperation))
450      {
451        LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_INSUFFICIENT_PRIVILEGES.get();
452        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
453      }
454
455      for (Modification m : modifyOperation.getModifications())
456      {
457        if (m.getAttribute().getAttributeDescription().getAttributeType().hasName(ATTR_DEFAULT_ROOT_PRIVILEGE_NAME))
458        {
459          if (!clientConnection.hasPrivilege(Privilege.PRIVILEGE_CHANGE, modifyOperation))
460          {
461            LocalizableMessage message = ERR_CONFIG_FILE_MODIFY_PRIVS_INSUFFICIENT_PRIVILEGES.get();
462            throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
463          }
464
465          break;
466        }
467      }
468    }
469
470    // Only one configuration update may be in progress at any given time.
471    synchronized (configLock)
472    {
473      configurationHandler.replaceEntry(
474          Converters.from(copyWithoutVirtualAttributes(oldEntry)),
475          Converters.from(copyWithoutVirtualAttributes(newEntry)));
476    }
477  }
478
479  @Override
480  public void renameEntry(DN currentDN, Entry entry, ModifyDNOperation modifyDNOperation) throws DirectoryException
481  {
482    // Make sure that the associated
483    // user has both the CONFIG_READ and CONFIG_WRITE privileges.
484    if (modifyDNOperation != null)
485    {
486      ClientConnection clientConnection = modifyDNOperation.getClientConnection();
487      if (!clientConnection.hasAllPrivileges(CONFIG_READ_AND_WRITE, modifyDNOperation))
488      {
489        LocalizableMessage message = ERR_CONFIG_FILE_MODDN_INSUFFICIENT_PRIVILEGES.get();
490        throw new DirectoryException(ResultCode.INSUFFICIENT_ACCESS_RIGHTS, message);
491      }
492    }
493
494    // Modify DN operations will not be allowed in the configuration, so this
495    // will always throw an exception.
496    LocalizableMessage message = ERR_CONFIG_FILE_MODDN_NOT_ALLOWED.get();
497    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, message);
498  }
499
500  @Override
501  public void exportLDIF(LDIFExportConfig exportConfig) throws DirectoryException
502  {
503    configurationHandler.writeLDIF(exportConfig);
504  }
505
506  @Override
507  public LDIFImportResult importLDIF(LDIFImportConfig importConfig, ServerContext serverContext)
508         throws DirectoryException
509  {
510    throw new DirectoryException(ResultCode.UNWILLING_TO_PERFORM, ERR_CONFIG_FILE_UNWILLING_TO_IMPORT.get());
511  }
512
513  @Override
514  public void createBackup(BackupConfig backupConfig) throws DirectoryException
515  {
516    new BackupManager(getBackendID()).createBackup(this, backupConfig);
517  }
518
519  @Override
520  public void removeBackup(BackupDirectory backupDirectory, String backupID) throws DirectoryException
521  {
522    new BackupManager(getBackendID()).removeBackup(backupDirectory, backupID);
523  }
524
525  @Override
526  public void restoreBackup(RestoreConfig restoreConfig) throws DirectoryException
527  {
528    new BackupManager(getBackendID()).restoreBackup(this, restoreConfig);
529  }
530
531  @Override
532  public ListIterator<Path> getFilesToBackup()
533  {
534    final List<Path> files = new ArrayList<>();
535
536    File configFile = configurationHandler.getConfigurationFile();
537    files.add(configFile.toPath());
538
539    // the files in archive directory
540    File archiveDirectory = new File(getDirectory(), CONFIG_ARCHIVE_DIR_NAME);
541    if (archiveDirectory.exists())
542    {
543      for (File archiveFile : archiveDirectory.listFiles())
544      {
545        files.add(archiveFile.toPath());
546      }
547    }
548
549    return files.listIterator();
550  }
551
552  @Override
553  public boolean isDirectRestore()
554  {
555    return true;
556  }
557
558  @Override
559  public Path beforeRestore() throws DirectoryException
560  {
561    // save current config files to a save directory
562    return BackupManager.saveCurrentFilesToDirectory(this, getBackendID());
563  }
564
565  @Override
566  public void afterRestore(Path restoreDirectory, Path saveDirectory) throws DirectoryException
567  {
568    // restore was successful, delete the save directory
569    StaticUtils.recursiveDelete(saveDirectory.toFile());
570  }
571}