001/* 002 * CDDL HEADER START 003 * 004 * The contents of this file are subject to the terms of the 005 * Common Development and Distribution License, Version 1.0 only 006 * (the "License"). You may not use this file except in compliance 007 * with the License. 008 * 009 * You can obtain a copy of the license at legal-notices/CDDLv1_0.txt 010 * or http://forgerock.org/license/CDDLv1.0.html. 011 * See the License for the specific language governing permissions 012 * and limitations under the License. 013 * 014 * When distributing Covered Code, include this CDDL HEADER in each 015 * file and include the License file at legal-notices/CDDLv1_0.txt. 016 * If applicable, add the following below this CDDL HEADER, with the 017 * fields enclosed by brackets "[]" replaced with your own identifying 018 * information: 019 * Portions Copyright [yyyy] [name of copyright owner] 020 * 021 * CDDL HEADER END 022 * 023 * 024 * Copyright 2008-2010 Sun Microsystems, Inc. 025 * Portions Copyright 2011-2015 ForgeRock AS 026 */ 027package org.opends.server.authorization.dseecompat; 028 029import java.util.*; 030 031import org.forgerock.i18n.LocalizableMessage; 032import org.forgerock.i18n.slf4j.LocalizedLogger; 033import org.forgerock.opendj.ldap.ResultCode; 034import org.forgerock.opendj.ldap.SearchScope; 035import org.opends.server.api.AlertGenerator; 036import org.opends.server.api.Backend; 037import org.opends.server.api.BackendInitializationListener; 038import org.opends.server.api.plugin.InternalDirectoryServerPlugin; 039import org.opends.server.api.plugin.PluginResult; 040import org.opends.server.api.plugin.PluginResult.PostOperation; 041import org.opends.server.api.plugin.PluginType; 042import org.opends.server.core.DirectoryServer; 043import org.opends.server.protocols.internal.InternalClientConnection; 044import org.opends.server.protocols.internal.InternalSearchOperation; 045import org.opends.server.protocols.internal.SearchRequest; 046import org.opends.server.protocols.ldap.LDAPControl; 047import org.opends.server.types.*; 048import org.opends.server.types.operation.*; 049import org.opends.server.workflowelement.localbackend.LocalBackendSearchOperation; 050 051import static org.opends.messages.AccessControlMessages.*; 052import static org.opends.server.protocols.internal.InternalClientConnection.*; 053import static org.opends.server.protocols.internal.Requests.*; 054import static org.opends.server.util.ServerConstants.*; 055 056/** 057 * The AciListenerManager updates an ACI list after each modification 058 * operation. Also, updates ACI list when backends are initialized and 059 * finalized. 060 */ 061public class AciListenerManager implements 062 BackendInitializationListener, AlertGenerator 063{ 064 private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); 065 066 /** 067 * The fully-qualified name of this class. 068 */ 069 private static final String CLASS_NAME = 070 "org.opends.server.authorization.dseecompat.AciListenerManager"; 071 072 073 074 /** 075 * Internal plugin used for updating the cache before a response is 076 * sent to the client. 077 */ 078 private final class AciChangeListenerPlugin extends 079 InternalDirectoryServerPlugin 080 { 081 private AciChangeListenerPlugin() 082 { 083 super(configurationDN, EnumSet.of( 084 PluginType.POST_SYNCHRONIZATION_ADD, 085 PluginType.POST_SYNCHRONIZATION_DELETE, 086 PluginType.POST_SYNCHRONIZATION_MODIFY, 087 PluginType.POST_SYNCHRONIZATION_MODIFY_DN, 088 PluginType.POST_OPERATION_ADD, 089 PluginType.POST_OPERATION_DELETE, 090 PluginType.POST_OPERATION_MODIFY, 091 PluginType.POST_OPERATION_MODIFY_DN), true); 092 } 093 094 095 096 /** {@inheritDoc} */ 097 @Override 098 public void doPostSynchronization( 099 PostSynchronizationAddOperation addOperation) 100 { 101 Entry entry = addOperation.getEntryToAdd(); 102 if (entry != null) 103 { 104 doPostAdd(entry); 105 } 106 } 107 108 109 110 /** {@inheritDoc} */ 111 @Override 112 public void doPostSynchronization( 113 PostSynchronizationDeleteOperation deleteOperation) 114 { 115 Entry entry = deleteOperation.getEntryToDelete(); 116 if (entry != null) 117 { 118 doPostDelete(entry); 119 } 120 } 121 122 123 124 /** {@inheritDoc} */ 125 @Override 126 public void doPostSynchronization( 127 PostSynchronizationModifyDNOperation modifyDNOperation) 128 { 129 Entry entry = modifyDNOperation.getUpdatedEntry(); 130 if (entry != null) 131 { 132 doPostModifyDN(entry.getName(), entry.getName()); 133 } 134 } 135 136 137 138 /** {@inheritDoc} */ 139 @Override 140 public void doPostSynchronization( 141 PostSynchronizationModifyOperation modifyOperation) 142 { 143 Entry entry = modifyOperation.getCurrentEntry(); 144 Entry modEntry = modifyOperation.getModifiedEntry(); 145 if (entry != null && modEntry != null) 146 { 147 doPostModify(modifyOperation.getModifications(), entry, modEntry); 148 } 149 } 150 151 152 153 /** {@inheritDoc} */ 154 @Override 155 public PostOperation doPostOperation( 156 PostOperationAddOperation addOperation) 157 { 158 // Only do something if the operation is successful, meaning there 159 // has been a change. 160 if (addOperation.getResultCode() == ResultCode.SUCCESS) 161 { 162 doPostAdd(addOperation.getEntryToAdd()); 163 } 164 165 // If we've gotten here, then everything is acceptable. 166 return PluginResult.PostOperation.continueOperationProcessing(); 167 } 168 169 170 171 /** {@inheritDoc} */ 172 @Override 173 public PostOperation doPostOperation( 174 PostOperationDeleteOperation deleteOperation) 175 { 176 // Only do something if the operation is successful, meaning there 177 // has been a change. 178 if (deleteOperation.getResultCode() == ResultCode.SUCCESS) 179 { 180 doPostDelete(deleteOperation.getEntryToDelete()); 181 } 182 183 // If we've gotten here, then everything is acceptable. 184 return PluginResult.PostOperation.continueOperationProcessing(); 185 } 186 187 188 189 /** {@inheritDoc} */ 190 @Override 191 public PostOperation doPostOperation( 192 PostOperationModifyDNOperation modifyDNOperation) 193 { 194 // Only do something if the operation is successful, meaning there 195 // has been a change. 196 if (modifyDNOperation.getResultCode() == ResultCode.SUCCESS) 197 { 198 doPostModifyDN(modifyDNOperation.getOriginalEntry().getName(), 199 modifyDNOperation.getUpdatedEntry().getName()); 200 } 201 202 // If we've gotten here, then everything is acceptable. 203 return PluginResult.PostOperation.continueOperationProcessing(); 204 } 205 206 207 208 /** {@inheritDoc} */ 209 @Override 210 public PostOperation doPostOperation( 211 PostOperationModifyOperation modifyOperation) 212 { 213 // Only do something if the operation is successful, meaning there 214 // has been a change. 215 if (modifyOperation.getResultCode() == ResultCode.SUCCESS) 216 { 217 doPostModify(modifyOperation.getModifications(), modifyOperation 218 .getCurrentEntry(), modifyOperation.getModifiedEntry()); 219 } 220 221 // If we've gotten here, then everything is acceptable. 222 return PluginResult.PostOperation.continueOperationProcessing(); 223 } 224 225 226 227 private void doPostAdd(Entry addedEntry) 228 { 229 // This entry might have both global and aci attribute types. 230 boolean hasAci = addedEntry.hasOperationalAttribute(AciHandler.aciType); 231 boolean hasGlobalAci = addedEntry.hasAttribute(AciHandler.globalAciType); 232 if (hasAci || hasGlobalAci) 233 { 234 // Ignore this list, the ACI syntax has already passed and it 235 // should be empty. 236 List<LocalizableMessage> failedACIMsgs = new LinkedList<>(); 237 238 aciList.addAci(addedEntry, hasAci, hasGlobalAci, failedACIMsgs); 239 } 240 } 241 242 243 244 private void doPostDelete(Entry deletedEntry) 245 { 246 // This entry might have both global and aci attribute types. 247 boolean hasAci = deletedEntry.hasOperationalAttribute( 248 AciHandler.aciType); 249 boolean hasGlobalAci = deletedEntry.hasAttribute( 250 AciHandler.globalAciType); 251 aciList.removeAci(deletedEntry, hasAci, hasGlobalAci); 252 } 253 254 255 256 private void doPostModifyDN(DN fromDN, DN toDN) 257 { 258 aciList.renameAci(fromDN, toDN); 259 } 260 261 262 263 private void doPostModify(List<Modification> mods, Entry oldEntry, 264 Entry newEntry) 265 { 266 // A change to the ACI list is expensive so let's first make sure 267 // that the modification included changes to the ACI. We'll check 268 // for both "aci" attribute types and global "ds-cfg-global-aci" 269 // attribute types. 270 boolean hasAci = false, hasGlobalAci = false; 271 for (Modification mod : mods) 272 { 273 AttributeType attributeType = mod.getAttribute() 274 .getAttributeType(); 275 if (attributeType.equals(AciHandler.aciType)) 276 { 277 hasAci = true; 278 } 279 else if (attributeType.equals(AciHandler.globalAciType)) 280 { 281 hasGlobalAci = true; 282 } 283 284 if (hasAci && hasGlobalAci) 285 { 286 break; 287 } 288 } 289 290 if (hasAci || hasGlobalAci) 291 { 292 aciList.modAciOldNewEntry(oldEntry, newEntry, hasAci, 293 hasGlobalAci); 294 } 295 } 296 297 } 298 299 300 301 /** The configuration DN. */ 302 private DN configurationDN; 303 304 /** True if the server is in lockdown mode. */ 305 private boolean inLockDownMode; 306 307 /** The AciList caches the ACIs. */ 308 private AciList aciList; 309 310 /** Search filter used in context search for "aci" attribute types. */ 311 private static SearchFilter aciFilter; 312 313 /** 314 * Internal plugin used for updating the cache before a response is 315 * sent to the client. 316 */ 317 private final AciChangeListenerPlugin plugin; 318 319 /** The aci attribute type is operational so we need to specify it to be returned. */ 320 private static LinkedHashSet<String> attrs = new LinkedHashSet<>(); 321 static 322 { 323 // Set up the filter used to search private and public contexts. 324 try 325 { 326 aciFilter = SearchFilter.createFilterFromString("(aci=*)"); 327 } 328 catch (DirectoryException ex) 329 { 330 // TODO should never happen, error message? 331 } 332 attrs.add("aci"); 333 } 334 335 336 337 /** 338 * Save the list created by the AciHandler routine. Registers as an 339 * Alert Generator that can send alerts when the server is being put 340 * in lockdown mode. Registers as backend initialization listener that 341 * is used to manage the ACI list cache when backends are 342 * initialized/finalized. Registers as a change notification listener 343 * that is used to manage the ACI list cache after ACI modifications 344 * have been performed. 345 * 346 * @param aciList 347 * The list object created and loaded by the handler. 348 * @param cfgDN 349 * The DN of the access control configuration entry. 350 */ 351 public AciListenerManager(AciList aciList, DN cfgDN) 352 { 353 this.aciList = aciList; 354 this.configurationDN = cfgDN; 355 this.plugin = new AciChangeListenerPlugin(); 356 357 // Process ACI from already registered backends. 358 Map<String, Backend> backendMap = DirectoryServer.getBackends(); 359 if (backendMap != null) { 360 for (Backend backend : backendMap.values()) { 361 performBackendInitializationProcessing(backend); 362 } 363 } 364 365 DirectoryServer.registerInternalPlugin(plugin); 366 DirectoryServer.registerBackendInitializationListener(this); 367 DirectoryServer.registerAlertGenerator(this); 368 } 369 370 371 372 /** 373 * Deregister from the change notification listener, the backend 374 * initialization listener and the alert generator. 375 */ 376 public void finalizeListenerManager() 377 { 378 DirectoryServer.deregisterInternalPlugin(plugin); 379 DirectoryServer.deregisterBackendInitializationListener(this); 380 DirectoryServer.deregisterAlertGenerator(this); 381 } 382 383 384 385 /** 386 * {@inheritDoc} In this case, the server will search the backend to 387 * find all aci attribute type values that it may contain and add them 388 * to the ACI list. 389 */ 390 @Override 391 public void performBackendInitializationProcessing(Backend<?> backend) 392 { 393 // Check to make sure that the backend has a presence index defined 394 // for the ACI attribute. If it does not, then log a warning message 395 // because this processing could be very expensive. 396 AttributeType aciType = DirectoryServer.getAttributeTypeOrDefault("aci"); 397 if (backend.getEntryCount() > 0 398 && !backend.isIndexed(aciType, IndexType.PRESENCE)) 399 { 400 logger.warn(WARN_ACI_ATTRIBUTE_NOT_INDEXED, backend.getBackendID(), "aci"); 401 } 402 403 LinkedList<LocalizableMessage> failedACIMsgs = new LinkedList<>(); 404 405 InternalClientConnection conn = getRootConnection(); 406 // Add manageDsaIT control so any ACIs in referral entries will be 407 // picked up. 408 LDAPControl c1 = new LDAPControl(OID_MANAGE_DSAIT_CONTROL, true); 409 // Add group membership control to let a backend look for it and 410 // decide if it would abort searches. 411 LDAPControl c2 = new LDAPControl(OID_INTERNAL_GROUP_MEMBERSHIP_UPDATE, false); 412 413 for (DN baseDN : backend.getBaseDNs()) 414 { 415 try 416 { 417 if (!backend.entryExists(baseDN)) 418 { 419 continue; 420 } 421 } 422 catch (Exception e) 423 { 424 logger.traceException(e); 425 continue; 426 } 427 SearchRequest request = newSearchRequest(baseDN, SearchScope.WHOLE_SUBTREE, aciFilter) 428 .addControl(c1) 429 .addControl(c2) 430 .addAttribute(attrs); 431 InternalSearchOperation internalSearch = 432 new InternalSearchOperation(conn, nextOperationID(), nextMessageID(), request); 433 LocalBackendSearchOperation localInternalSearch = 434 new LocalBackendSearchOperation(internalSearch); 435 try 436 { 437 backend.search(localInternalSearch); 438 } 439 catch (Exception e) 440 { 441 logger.traceException(e); 442 continue; 443 } 444 if (!internalSearch.getSearchEntries().isEmpty()) 445 { 446 int validAcis = aciList.addAci(internalSearch.getSearchEntries(), failedACIMsgs); 447 if (!failedACIMsgs.isEmpty()) 448 { 449 logMsgsSetLockDownMode(failedACIMsgs); 450 } 451 logger.debug(INFO_ACI_ADD_LIST_ACIS, validAcis, baseDN); 452 } 453 } 454 } 455 456 457 458 /** 459 * {@inheritDoc} In this case, the server will remove all aci 460 * attribute type values associated with entries in the provided 461 * backend. 462 */ 463 @Override 464 public void performBackendFinalizationProcessing(Backend<?> backend) 465 { 466 aciList.removeAci(backend); 467 } 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}