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}