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}