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}