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