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-2010 Sun Microsystems, Inc. 015 * Portions Copyright 2011-2016 ForgeRock AS. 016 */ 017package org.opends.server.authorization.dseecompat; 018 019import java.util.*; 020 021import org.forgerock.i18n.LocalizableMessage; 022import org.forgerock.i18n.slf4j.LocalizedLogger; 023import org.forgerock.opendj.ldap.DN; 024import org.forgerock.opendj.ldap.ResultCode; 025import org.forgerock.opendj.ldap.SearchScope; 026import org.opends.server.api.AlertGenerator; 027import org.opends.server.api.Backend; 028import org.opends.server.api.BackendInitializationListener; 029import org.opends.server.api.plugin.InternalDirectoryServerPlugin; 030import org.opends.server.api.plugin.PluginResult; 031import org.opends.server.api.plugin.PluginResult.PostOperation; 032import org.opends.server.api.plugin.PluginType; 033import org.opends.server.core.DirectoryServer; 034import org.opends.server.protocols.internal.InternalClientConnection; 035import org.opends.server.protocols.internal.InternalSearchOperation; 036import org.opends.server.protocols.internal.SearchRequest; 037import org.opends.server.protocols.ldap.LDAPControl; 038import org.forgerock.opendj.ldap.schema.AttributeType; 039import org.opends.server.types.*; 040import org.opends.server.types.operation.*; 041import org.opends.server.workflowelement.localbackend.LocalBackendSearchOperation; 042 043import static org.opends.messages.AccessControlMessages.*; 044import static org.opends.server.protocols.internal.InternalClientConnection.*; 045import static org.opends.server.protocols.internal.Requests.*; 046import static org.opends.server.util.ServerConstants.*; 047 048/** 049 * The AciListenerManager updates an ACI list after each modification 050 * operation. Also, updates ACI list when backends are initialized and 051 * finalized. 052 */ 053public class AciListenerManager implements 054 BackendInitializationListener, AlertGenerator 055{ 056 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 057 058 /** 059 * The fully-qualified name of this class. 060 */ 061 private static final String CLASS_NAME = 062 "org.opends.server.authorization.dseecompat.AciListenerManager"; 063 064 065 066 /** 067 * Internal plugin used for updating the cache before a response is 068 * sent to the client. 069 */ 070 private final class AciChangeListenerPlugin extends 071 InternalDirectoryServerPlugin 072 { 073 private AciChangeListenerPlugin() 074 { 075 super(configurationDN, EnumSet.of( 076 PluginType.POST_SYNCHRONIZATION_ADD, 077 PluginType.POST_SYNCHRONIZATION_DELETE, 078 PluginType.POST_SYNCHRONIZATION_MODIFY, 079 PluginType.POST_SYNCHRONIZATION_MODIFY_DN, 080 PluginType.POST_OPERATION_ADD, 081 PluginType.POST_OPERATION_DELETE, 082 PluginType.POST_OPERATION_MODIFY, 083 PluginType.POST_OPERATION_MODIFY_DN), true); 084 } 085 086 087 088 /** {@inheritDoc} */ 089 @Override 090 public void doPostSynchronization( 091 PostSynchronizationAddOperation addOperation) 092 { 093 Entry entry = addOperation.getEntryToAdd(); 094 if (entry != null) 095 { 096 doPostAdd(entry); 097 } 098 } 099 100 101 102 /** {@inheritDoc} */ 103 @Override 104 public void doPostSynchronization( 105 PostSynchronizationDeleteOperation deleteOperation) 106 { 107 Entry entry = deleteOperation.getEntryToDelete(); 108 if (entry != null) 109 { 110 doPostDelete(entry); 111 } 112 } 113 114 115 116 /** {@inheritDoc} */ 117 @Override 118 public void doPostSynchronization( 119 PostSynchronizationModifyDNOperation modifyDNOperation) 120 { 121 Entry entry = modifyDNOperation.getUpdatedEntry(); 122 if (entry != null) 123 { 124 doPostModifyDN(entry.getName(), entry.getName()); 125 } 126 } 127 128 129 130 /** {@inheritDoc} */ 131 @Override 132 public void doPostSynchronization( 133 PostSynchronizationModifyOperation modifyOperation) 134 { 135 Entry entry = modifyOperation.getCurrentEntry(); 136 Entry modEntry = modifyOperation.getModifiedEntry(); 137 if (entry != null && modEntry != null) 138 { 139 doPostModify(modifyOperation.getModifications(), entry, modEntry); 140 } 141 } 142 143 144 145 /** {@inheritDoc} */ 146 @Override 147 public PostOperation doPostOperation( 148 PostOperationAddOperation addOperation) 149 { 150 // Only do something if the operation is successful, meaning there 151 // has been a change. 152 if (addOperation.getResultCode() == ResultCode.SUCCESS) 153 { 154 doPostAdd(addOperation.getEntryToAdd()); 155 } 156 157 // If we've gotten here, then everything is acceptable. 158 return PluginResult.PostOperation.continueOperationProcessing(); 159 } 160 161 162 163 /** {@inheritDoc} */ 164 @Override 165 public PostOperation doPostOperation( 166 PostOperationDeleteOperation deleteOperation) 167 { 168 // Only do something if the operation is successful, meaning there 169 // has been a change. 170 if (deleteOperation.getResultCode() == ResultCode.SUCCESS) 171 { 172 doPostDelete(deleteOperation.getEntryToDelete()); 173 } 174 175 // If we've gotten here, then everything is acceptable. 176 return PluginResult.PostOperation.continueOperationProcessing(); 177 } 178 179 180 181 /** {@inheritDoc} */ 182 @Override 183 public PostOperation doPostOperation( 184 PostOperationModifyDNOperation modifyDNOperation) 185 { 186 // Only do something if the operation is successful, meaning there 187 // has been a change. 188 if (modifyDNOperation.getResultCode() == ResultCode.SUCCESS) 189 { 190 doPostModifyDN(modifyDNOperation.getOriginalEntry().getName(), 191 modifyDNOperation.getUpdatedEntry().getName()); 192 } 193 194 // If we've gotten here, then everything is acceptable. 195 return PluginResult.PostOperation.continueOperationProcessing(); 196 } 197 198 199 200 /** {@inheritDoc} */ 201 @Override 202 public PostOperation doPostOperation( 203 PostOperationModifyOperation modifyOperation) 204 { 205 // Only do something if the operation is successful, meaning there 206 // has been a change. 207 if (modifyOperation.getResultCode() == ResultCode.SUCCESS) 208 { 209 doPostModify(modifyOperation.getModifications(), modifyOperation 210 .getCurrentEntry(), modifyOperation.getModifiedEntry()); 211 } 212 213 // If we've gotten here, then everything is acceptable. 214 return PluginResult.PostOperation.continueOperationProcessing(); 215 } 216 217 218 219 private void doPostAdd(Entry addedEntry) 220 { 221 // This entry might have both global and aci attribute types. 222 boolean hasAci = addedEntry.hasOperationalAttribute(AciHandler.aciType); 223 boolean hasGlobalAci = addedEntry.hasAttribute(AciHandler.globalAciType); 224 if (hasAci || hasGlobalAci) 225 { 226 // Ignore this list, the ACI syntax has already passed and it 227 // should be empty. 228 List<LocalizableMessage> failedACIMsgs = new LinkedList<>(); 229 230 aciList.addAci(addedEntry, hasAci, hasGlobalAci, failedACIMsgs); 231 } 232 } 233 234 235 236 private void doPostDelete(Entry deletedEntry) 237 { 238 // This entry might have both global and aci attribute types. 239 boolean hasAci = deletedEntry.hasOperationalAttribute( 240 AciHandler.aciType); 241 boolean hasGlobalAci = deletedEntry.hasAttribute( 242 AciHandler.globalAciType); 243 aciList.removeAci(deletedEntry, hasAci, hasGlobalAci); 244 } 245 246 247 248 private void doPostModifyDN(DN fromDN, DN toDN) 249 { 250 aciList.renameAci(fromDN, toDN); 251 } 252 253 254 255 private void doPostModify(List<Modification> mods, Entry oldEntry, 256 Entry newEntry) 257 { 258 // A change to the ACI list is expensive so let's first make sure 259 // that the modification included changes to the ACI. We'll check 260 // for both "aci" attribute types and global "ds-cfg-global-aci" 261 // attribute types. 262 boolean hasAci = false, hasGlobalAci = false; 263 for (Modification mod : mods) 264 { 265 AttributeType attributeType = mod.getAttribute().getAttributeDescription().getAttributeType(); 266 if (attributeType.equals(AciHandler.aciType)) 267 { 268 hasAci = true; 269 } 270 else if (attributeType.equals(AciHandler.globalAciType)) 271 { 272 hasGlobalAci = true; 273 } 274 275 if (hasAci && hasGlobalAci) 276 { 277 break; 278 } 279 } 280 281 if (hasAci || hasGlobalAci) 282 { 283 aciList.modAciOldNewEntry(oldEntry, newEntry, hasAci, 284 hasGlobalAci); 285 } 286 } 287 288 } 289 290 291 292 /** The configuration DN. */ 293 private DN configurationDN; 294 295 /** True if the server is in lockdown mode. */ 296 private boolean inLockDownMode; 297 298 /** The AciList caches the ACIs. */ 299 private AciList aciList; 300 301 /** Search filter used in context search for "aci" attribute types. */ 302 private static SearchFilter aciFilter; 303 304 /** 305 * Internal plugin used for updating the cache before a response is 306 * sent to the client. 307 */ 308 private final AciChangeListenerPlugin plugin; 309 310 /** The aci attribute type is operational so we need to specify it to be returned. */ 311 private static LinkedHashSet<String> attrs = new LinkedHashSet<>(); 312 static 313 { 314 // Set up the filter used to search private and public contexts. 315 try 316 { 317 aciFilter = SearchFilter.createFilterFromString("(aci=*)"); 318 } 319 catch (DirectoryException ex) 320 { 321 // TODO should never happen, error message? 322 } 323 attrs.add("aci"); 324 } 325 326 327 328 /** 329 * Save the list created by the AciHandler routine. Registers as an 330 * Alert Generator that can send alerts when the server is being put 331 * in lockdown mode. Registers as backend initialization listener that 332 * is used to manage the ACI list cache when backends are 333 * initialized/finalized. Registers as a change notification listener 334 * that is used to manage the ACI list cache after ACI modifications 335 * have been performed. 336 * 337 * @param aciList 338 * The list object created and loaded by the handler. 339 * @param cfgDN 340 * The DN of the access control configuration entry. 341 */ 342 public AciListenerManager(AciList aciList, DN cfgDN) 343 { 344 this.aciList = aciList; 345 this.configurationDN = cfgDN; 346 this.plugin = new AciChangeListenerPlugin(); 347 348 // Process ACI from already registered backends. 349 Map<String, Backend> backendMap = DirectoryServer.getBackends(); 350 if (backendMap != null) { 351 for (Backend backend : backendMap.values()) { 352 performBackendPreInitializationProcessing(backend); 353 } 354 } 355 356 DirectoryServer.registerInternalPlugin(plugin); 357 DirectoryServer.registerBackendInitializationListener(this); 358 DirectoryServer.registerAlertGenerator(this); 359 } 360 361 362 363 /** 364 * Deregister from the change notification listener, the backend 365 * initialization listener and the alert generator. 366 */ 367 public void finalizeListenerManager() 368 { 369 DirectoryServer.deregisterInternalPlugin(plugin); 370 DirectoryServer.deregisterBackendInitializationListener(this); 371 DirectoryServer.deregisterAlertGenerator(this); 372 } 373 374 375 376 /** 377 * {@inheritDoc} In this case, the server will search the backend to 378 * find all aci attribute type values that it may contain and add them 379 * to the ACI list. 380 */ 381 @Override 382 public void performBackendPreInitializationProcessing(Backend<?> backend) 383 { 384 // Check to make sure that the backend has a presence index defined 385 // for the ACI attribute. If it does not, then log a warning message 386 // because this processing could be very expensive. 387 AttributeType aciType = DirectoryServer.getAttributeType("aci"); 388 if (backend.getEntryCount() > 0 389 && !backend.isIndexed(aciType, IndexType.PRESENCE)) 390 { 391 logger.warn(WARN_ACI_ATTRIBUTE_NOT_INDEXED, backend.getBackendID(), "aci"); 392 } 393 394 LinkedList<LocalizableMessage> failedACIMsgs = new LinkedList<>(); 395 396 InternalClientConnection conn = getRootConnection(); 397 // Add manageDsaIT control so any ACIs in referral entries will be 398 // picked up. 399 LDAPControl c1 = new LDAPControl(OID_MANAGE_DSAIT_CONTROL, true); 400 // Add group membership control to let a backend look for it and 401 // decide if it would abort searches. 402 LDAPControl c2 = new LDAPControl(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE, false); 403 404 for (DN baseDN : backend.getBaseDNs()) 405 { 406 try 407 { 408 if (!backend.entryExists(baseDN)) 409 { 410 continue; 411 } 412 } 413 catch (Exception e) 414 { 415 logger.traceException(e); 416 continue; 417 } 418 SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, aciFilter) 419 .addControl(c1) 420 .addControl(c2) 421 .addAttribute(attrs); 422 InternalSearchOperation internalSearch = 423 new InternalSearchOperation(conn, nextOperationID(), nextMessageID(), request); 424 LocalBackendSearchOperation localInternalSearch = 425 new LocalBackendSearchOperation(internalSearch); 426 try 427 { 428 backend.search(localInternalSearch); 429 } 430 catch (Exception e) 431 { 432 logger.traceException(e); 433 continue; 434 } 435 if (!internalSearch.getSearchEntries().isEmpty()) 436 { 437 int validAcis = aciList.addAci(internalSearch.getSearchEntries(), failedACIMsgs); 438 if (!failedACIMsgs.isEmpty()) 439 { 440 logMsgsSetLockDownMode(failedACIMsgs); 441 } 442 logger.debug(INFO_ACI_ADD_LIST_ACIS, validAcis, baseDN); 443 } 444 } 445 } 446 447 448 449 /** 450 * {@inheritDoc} In this case, the server will remove all aci 451 * attribute type values associated with entries in the provided 452 * backend. 453 */ 454 @Override 455 public void performBackendPostFinalizationProcessing(Backend<?> backend) 456 { 457 aciList.removeAci(backend); 458 } 459 460 @Override 461 public void performBackendPostInitializationProcessing(Backend<?> backend) { 462 // Nothing to do. 463 } 464 465 @Override 466 public void performBackendPreFinalizationProcessing(Backend<?> backend) { 467 // nothing to do. 468 } 469 470 471 /** 472 * Retrieves the fully-qualified name of the Java class for this alert 473 * generator implementation. 474 * 475 * @return The fully-qualified name of the Java class for this alert 476 * generator implementation. 477 */ 478 @Override 479 public String getClassName() 480 { 481 return CLASS_NAME; 482 } 483 484 485 486 /** 487 * Retrieves the DN of the configuration entry used to configure the 488 * handler. 489 * 490 * @return The DN of the configuration entry containing the Access 491 * Control configuration information. 492 */ 493 @Override 494 public DN getComponentEntryDN() 495 { 496 return this.configurationDN; 497 } 498 499 500 501 /** 502 * Retrieves information about the set of alerts that this generator 503 * may produce. The map returned should be between the notification 504 * type for a particular notification and the human-readable 505 * description for that notification. This alert generator must not 506 * generate any alerts with types that are not contained in this list. 507 * 508 * @return Information about the set of alerts that this generator may 509 * produce. 510 */ 511 @Override 512 public LinkedHashMap<String, String> getAlerts() 513 { 514 LinkedHashMap<String, String> alerts = new LinkedHashMap<>(); 515 alerts.put(ALERT_TYPE_ACCESS_CONTROL_PARSE_FAILED, 516 ALERT_DESCRIPTION_ACCESS_CONTROL_PARSE_FAILED); 517 return alerts; 518 } 519 520 /** 521 * Log the exception messages from the failed ACI decode and then put 522 * the server in lockdown mode -- if needed. 523 * 524 * @param failedACIMsgs 525 * List of exception messages from failed ACI decodes. 526 */ 527 public void logMsgsSetLockDownMode(LinkedList<LocalizableMessage> failedACIMsgs) 528 { 529 for (LocalizableMessage msg : failedACIMsgs) 530 { 531 logger.warn(WARN_ACI_SERVER_DECODE_FAILED, msg); 532 } 533 if (!inLockDownMode) 534 { 535 setLockDownMode(); 536 } 537 } 538 539 540 541 /** 542 * Send an WARN_ACI_ENTER_LOCKDOWN_MODE alert notification and put the 543 * server in lockdown mode. 544 */ 545 private void setLockDownMode() 546 { 547 if (!inLockDownMode) 548 { 549 inLockDownMode = true; 550 // Send ALERT_TYPE_ACCESS_CONTROL_PARSE_FAILED alert that 551 // lockdown is about to be entered. 552 LocalizableMessage lockDownMsg = WARN_ACI_ENTER_LOCKDOWN_MODE.get(); 553 DirectoryServer.sendAlertNotification(this, 554 ALERT_TYPE_ACCESS_CONTROL_PARSE_FAILED, lockDownMsg); 555 // Enter lockdown mode. 556 DirectoryServer.setLockdownMode(true); 557 558 } 559 } 560}