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}