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 2013-2016 ForgeRock AS.
016 */
017package org.opends.server.authorization.dseecompat;
018
019import static org.opends.messages.AccessControlMessages.*;
020import static org.opends.server.authorization.dseecompat.AciHandler.*;
021
022import java.util.ArrayList;
023import java.util.HashMap;
024import java.util.Iterator;
025import java.util.LinkedList;
026import java.util.List;
027import java.util.Map;
028import java.util.SortedSet;
029import java.util.concurrent.locks.ReentrantReadWriteLock;
030
031import org.forgerock.i18n.LocalizableMessage;
032import org.forgerock.i18n.slf4j.LocalizedLogger;
033import org.forgerock.opendj.ldap.ByteString;
034import org.forgerock.opendj.ldap.DN;
035import org.opends.server.api.Backend;
036import org.opends.server.api.DITCacheMap;
037import org.opends.server.types.Attribute;
038import org.opends.server.types.Entry;
039
040/**
041 * The AciList class performs caching of the ACI attribute values
042 * using the entry DN as the key.
043 */
044public class AciList {
045
046  private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass();
047
048
049  /**
050   * A map containing all the ACIs.
051   * We use the copy-on-write technique to avoid locking when reading.
052   */
053  private volatile DITCacheMap<List<Aci>> aciList = new DITCacheMap<>();
054
055  /**
056   * Lock to protect internal data structures.
057   */
058  private final ReentrantReadWriteLock lock =
059          new ReentrantReadWriteLock();
060
061  /** The configuration DN used to compare against the global ACI entry DN. */
062  private DN configDN;
063
064  /**
065   * Constructor to create an ACI list to cache ACI attribute types.
066   * @param configDN The configuration entry DN.
067   */
068  public AciList(DN configDN) {
069     this.configDN=configDN;
070  }
071
072  /**
073   * Using the base DN, return a list of ACIs that are candidates for
074   * evaluation by walking up from the base DN towards the root of the
075   * DIT gathering ACIs on parents. Global ACIs use the NULL DN as the key
076   * and are included in the candidate set only if they have no
077   * "target" keyword rules, or if the target keyword rule matches for
078   * the specified base DN.
079   *
080   * @param baseDN  The DN to check.
081   * @return A list of candidate ACIs that might be applicable.
082   */
083  public List<Aci> getCandidateAcis(DN baseDN) {
084    List<Aci> candidates = new LinkedList<>();
085    if(baseDN == null)
086    {
087      return candidates;
088    }
089
090    lock.readLock().lock();
091    try
092    {
093      //Save the baseDN in case we need to evaluate a global ACI.
094      DN entryDN=baseDN;
095      while (baseDN != null) {
096        List<Aci> acis = aciList.get(baseDN);
097        if (acis != null) {
098          //Check if there are global ACIs. Global ACI has a NULL DN.
099          if (baseDN.isRootDN()) {
100            for (Aci aci : acis) {
101              AciTargets targets = aci.getTargets();
102              //If there is a target, evaluate it to see if this ACI should
103              //be included in the candidate set.
104              if (targets != null
105                  && AciTargets.isTargetApplicable(aci, targets, entryDN))
106              {
107                  candidates.add(aci);  //Add this ACI to the candidates.
108              }
109            }
110          } else {
111            candidates.addAll(acis);
112          }
113        }
114        if(baseDN.isRootDN()) {
115          break;
116        }
117        DN parentDN=baseDN.parent();
118        if(parentDN == null) {
119          baseDN=DN.rootDN();
120        } else {
121          baseDN=parentDN;
122        }
123      }
124    }
125    finally
126    {
127      lock.readLock().unlock();
128    }
129
130    return candidates;
131  }
132
133  /**
134   * Add all the ACI from a set of entries to the ACI list. There is no need
135   * to check for global ACIs since they are processe by the AciHandler at
136   * startup using the addACi single entry method.
137   * @param entries The set of entries containing the "aci" attribute values.
138   * @param failedACIMsgs List that will hold error messages from ACI decode
139   *                      exceptions.
140   * @return The number of valid ACI attribute values added to the ACI list.
141   */
142  public int addAci(List<? extends Entry> entries,
143                                 LinkedList<LocalizableMessage> failedACIMsgs)
144  {
145    int validAcis=0;
146
147    lock.writeLock().lock();
148    try
149    {
150      for (Entry entry : entries) {
151        DN dn=entry.getName();
152        List<Attribute> attributeList =
153             entry.getOperationalAttribute(AciHandler.aciType);
154        validAcis += addAciAttributeList(aciList, dn, configDN,
155                                         attributeList, failedACIMsgs);
156      }
157    }
158    finally
159    {
160      lock.writeLock().unlock();
161    }
162
163    return validAcis;
164  }
165
166  /**
167   * Add a set of ACIs to the ACI list. This is usually used a startup, when
168   * global ACIs are processed.
169   *
170   * @param dn The DN to add the ACIs under.
171   *
172   * @param acis A set of ACIs to add to the ACI list.
173   *
174   */
175  public void addAci(DN dn, SortedSet<Aci> acis) {
176    lock.writeLock().lock();
177    try
178    {
179      aciList.put(dn, new LinkedList<>(acis));
180    }
181    finally
182    {
183      lock.writeLock().unlock();
184    }
185  }
186
187  /**
188   * Add all of an entry's ACI (global or regular) attribute values to the
189   * ACI list.
190   * @param entry The entry containing the ACI attributes.
191   * @param hasAci True if the "aci" attribute type was seen in the entry.
192   * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was
193   * seen in the entry.
194   * @param failedACIMsgs List that will hold error messages from ACI decode
195   *                      exceptions.
196   * @return The number of valid ACI attribute values added to the ACI list.
197   */
198  public int addAci(Entry entry, boolean hasAci,
199                                 boolean hasGlobalAci,
200                                 List<LocalizableMessage> failedACIMsgs) {
201    int validAcis=0;
202
203    lock.writeLock().lock();
204    try
205    {
206      //Process global "ds-cfg-global-aci" attribute type. The oldentry
207      //DN is checked to verify it is equal to the config DN. If not those
208      //attributes are skipped.
209      if(hasGlobalAci && entry.getName().equals(configDN)) {
210          List<Attribute> attributeList = entry.getAttribute(globalAciType);
211          validAcis = addAciAttributeList(aciList, DN.rootDN(), configDN,
212                                          attributeList, failedACIMsgs);
213      }
214
215      if(hasAci) {
216          List<Attribute> attributeList = entry.getAttribute(aciType);
217          validAcis += addAciAttributeList(aciList, entry.getName(), configDN,
218                                           attributeList, failedACIMsgs);
219      }
220    }
221    finally
222    {
223      lock.writeLock().unlock();
224    }
225
226    return validAcis;
227  }
228
229  /**
230   * Add an ACI's attribute type values to the ACI list. There is a chance that
231   * an ACI will throw an exception if it has an invalid syntax. If that
232   * happens a message will be logged and the ACI skipped.  A count is
233   * returned of the number of valid ACIs added.
234   * @param aciList The ACI list to which the ACI is to be added.
235   * @param dn The DN to use as the key in the ACI list.
236   * @param configDN The DN of the configuration entry used to configure the
237   *                 ACI handler. Used if a global ACI has an decode exception.
238   * @param attributeList List of attributes containing the ACI attribute
239   * values.
240   * @param failedACIMsgs List that will hold error messages from ACI decode
241   *                      exceptions.
242   * @return The number of valid attribute values added to the ACI list.
243   */
244  private static int addAciAttributeList(DITCacheMap<List<Aci>> aciList,
245                                         DN dn, DN configDN,
246                                         List<Attribute> attributeList,
247                                         List<LocalizableMessage> failedACIMsgs) {
248    if (attributeList.isEmpty()) {
249      return 0;
250    }
251
252    int validAcis=0;
253    List<Aci> acis = new ArrayList<>();
254    for (Attribute attribute : attributeList) {
255      for (ByteString value : attribute) {
256        try {
257          acis.add(Aci.decode(value, dn));
258          validAcis++;
259        } catch (AciException ex) {
260          DN msgDN=dn;
261          if(dn == DN.rootDN()) {
262            msgDN=configDN;
263          }
264          failedACIMsgs.add(WARN_ACI_ADD_LIST_FAILED_DECODE.get(value, msgDN, ex.getMessage()));
265        }
266      }
267    }
268    addAci(aciList, dn, acis);
269    return validAcis;
270  }
271
272  /**
273   * Remove all of the ACIs related to the old entry and then add all of the
274   * ACIs related to the new entry. This method locks/unlocks the list.
275   * In the case of global ACIs the DN of the entry is checked to make sure it
276   * is equal to the config DN. If not, the global ACI attribute type is
277   * silently skipped.
278   * @param oldEntry The old entry possibly containing old ACI attribute
279   * values.
280   * @param newEntry The new entry possibly containing new ACI attribute
281   * values.
282   * @param hasAci True if the "aci" attribute type was seen in the entry.
283   * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was
284   * seen in the entry.
285   */
286  public void modAciOldNewEntry(Entry oldEntry, Entry newEntry,
287                                             boolean hasAci,
288                                             boolean hasGlobalAci) {
289
290    lock.writeLock().lock();
291    try
292    {
293      List<LocalizableMessage> failedACIMsgs=new LinkedList<>();
294      //Process "aci" attribute types.
295      if(hasAci) {
296          aciList.remove(oldEntry.getName());
297          List<Attribute> attributeList =
298                  newEntry.getOperationalAttribute(aciType);
299          addAciAttributeList(aciList,newEntry.getName(), configDN,
300                              attributeList, failedACIMsgs);
301      }
302      //Process global "ds-cfg-global-aci" attribute type. The oldentry
303      //DN is checked to verify it is equal to the config DN. If not those
304      //attributes are skipped.
305      if(hasGlobalAci && oldEntry.getName().equals(configDN)) {
306          aciList.remove(DN.rootDN());
307          List<Attribute> attributeList = newEntry.getAttribute(globalAciType);
308          addAciAttributeList(aciList, DN.rootDN(), configDN,
309                              attributeList, failedACIMsgs);
310      }
311    }
312    finally
313    {
314      lock.writeLock().unlock();
315    }
316  }
317
318  /**
319   * Add ACI using the DN as a key. If the DN already
320   * has ACI(s) on the list, then the new ACI is added to the
321   * end of the array.
322   * @param aciList The set of ACIs to which ACI is to be added.
323   * @param dn The DN to use as the key.
324   * @param acis The ACI to be added.
325   */
326  private static void addAci(DITCacheMap<List<Aci>> aciList, DN dn,
327                             List<Aci> acis)
328  {
329    if(aciList.containsKey(dn)) {
330      List<Aci> tmpAci = aciList.get(dn);
331      tmpAci.addAll(acis);
332    } else {
333      aciList.put(dn, acis);
334    }
335  }
336
337  /**
338   * Remove global and regular ACIs from the list. It's possible that an entry
339   * could have both attribute types (aci and ds-cfg-global-aci). Global ACIs
340   * use the NULL DN for the key.  In the case of global ACIs the DN of the
341   * entry is checked to make sure it is equal to the config DN. If not, the
342   * global ACI attribute type is silently skipped.
343   * @param entry The entry containing the global ACIs.
344   * @param hasAci True if the "aci" attribute type was seen in the entry.
345   * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was
346   * seen in the entry.
347   * @return  True if the ACI set was deleted.
348   */
349  public boolean removeAci(Entry entry,  boolean hasAci,
350                                                      boolean hasGlobalAci) {
351    DN entryDN = entry.getName();
352
353    lock.writeLock().lock();
354    try
355    {
356      if (hasGlobalAci && entryDN.equals(configDN) &&
357          aciList.remove(DN.rootDN()) == null)
358      {
359        return false;
360      }
361      if (hasAci || !hasGlobalAci)
362      {
363        return aciList.removeSubtree(entryDN, null);
364      }
365    }
366    finally
367    {
368      lock.writeLock().unlock();
369    }
370
371    return true;
372  }
373
374  /**
375   * Remove all ACIs related to a backend.
376   * @param backend  The backend to check if each DN is handled by that
377   * backend.
378   */
379  public void removeAci(Backend<?> backend) {
380
381    lock.writeLock().lock();
382    try
383    {
384      Iterator<Map.Entry<DN,List<Aci>>> iterator =
385              aciList.entrySet().iterator();
386      while (iterator.hasNext())
387      {
388        Map.Entry<DN,List<Aci>> mapEntry = iterator.next();
389        if (backend.handlesEntry(mapEntry.getKey()))
390        {
391          iterator.remove();
392        }
393      }
394    }
395    finally
396    {
397      lock.writeLock().unlock();
398    }
399  }
400
401  /**
402   * Rename all ACIs under the specified old DN to the new DN. A simple
403   * interaction over the entire list is performed.
404   * @param oldDN The DN of the original entry that was moved.
405   * @param newDN The DN of the new entry.
406   */
407  public void renameAci(DN oldDN, DN newDN ) {
408
409    lock.writeLock().lock();
410    try
411    {
412      Map<DN,List<Aci>> tempAciList = new HashMap<>();
413      Iterator<Map.Entry<DN,List<Aci>>> iterator =
414              aciList.entrySet().iterator();
415      while (iterator.hasNext()) {
416        Map.Entry<DN,List<Aci>> hashEntry = iterator.next();
417        DN keyDn = hashEntry.getKey();
418        if (keyDn.isSubordinateOrEqualTo(oldDN)) {
419          DN relocateDN = keyDn.rename(oldDN, newDN);
420          List<Aci> acis = new LinkedList<>();
421          for(Aci aci : hashEntry.getValue()) {
422            try {
423               Aci newAci =
424                 Aci.decode(ByteString.valueOfUtf8(aci.toString()), relocateDN);
425               acis.add(newAci);
426            } catch (AciException ex) {
427              //This should never happen since only a copy of the
428              //ACI with a new DN is being made. Log a message if it does and
429              //keep going.
430              logger.warn(WARN_ACI_ADD_LIST_FAILED_DECODE, aci, relocateDN, ex.getMessage());
431            }
432          }
433          tempAciList.put(relocateDN, acis);
434          iterator.remove();
435        }
436      }
437      aciList.putAll(tempAciList);
438    }
439    finally
440    {
441      lock.writeLock().unlock();
442    }
443  }
444}