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 final 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      return candidates;
125    }
126    finally
127    {
128      lock.readLock().unlock();
129    }
130  }
131
132  /**
133   * Add all the ACI from a set of entries to the ACI list. There is no need
134   * to check for global ACIs since they are processe by the AciHandler at
135   * startup using the addACi single entry method.
136   * @param entries The set of entries containing the "aci" attribute values.
137   * @param failedACIMsgs List that will hold error messages from ACI decode
138   *                      exceptions.
139   * @return The number of valid ACI attribute values added to the ACI list.
140   */
141  public int addAci(List<? extends Entry> entries,
142                                 LinkedList<LocalizableMessage> failedACIMsgs)
143  {
144    lock.writeLock().lock();
145    try
146    {
147      int validAcis = 0;
148      for (Entry entry : entries) {
149        DN dn=entry.getName();
150        List<Attribute> attributeList =
151             entry.getOperationalAttribute(AciHandler.aciType);
152        validAcis += addAciAttributeList(aciList, dn, configDN,
153                                         attributeList, failedACIMsgs);
154      }
155      return validAcis;
156    }
157    finally
158    {
159      lock.writeLock().unlock();
160    }
161  }
162
163  /**
164   * Add a set of ACIs to the ACI list. This is usually used a startup, when
165   * global ACIs are processed.
166   *
167   * @param dn The DN to add the ACIs under.
168   *
169   * @param acis A set of ACIs to add to the ACI list.
170   *
171   */
172  public void addAci(DN dn, SortedSet<Aci> acis) {
173    lock.writeLock().lock();
174    try
175    {
176      aciList.put(dn, new LinkedList<>(acis));
177    }
178    finally
179    {
180      lock.writeLock().unlock();
181    }
182  }
183
184  /**
185   * Add all of an entry's ACI (global or regular) attribute values to the
186   * ACI list.
187   * @param entry The entry containing the ACI attributes.
188   * @param hasAci True if the "aci" attribute type was seen in the entry.
189   * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was
190   * seen in the entry.
191   * @param failedACIMsgs List that will hold error messages from ACI decode
192   *                      exceptions.
193   * @return The number of valid ACI attribute values added to the ACI list.
194   */
195  public int addAci(Entry entry, boolean hasAci,
196                                 boolean hasGlobalAci,
197                                 List<LocalizableMessage> failedACIMsgs) {
198    lock.writeLock().lock();
199    try
200    {
201      int validAcis = 0;
202      //Process global "ds-cfg-global-aci" attribute type. The oldentry
203      //DN is checked to verify it is equal to the config DN. If not those
204      //attributes are skipped.
205      if(hasGlobalAci && entry.getName().equals(configDN)) {
206          List<Attribute> attributeList = entry.getAttribute(globalAciType);
207          validAcis = addAciAttributeList(aciList, DN.rootDN(), configDN,
208                                          attributeList, failedACIMsgs);
209      }
210
211      if(hasAci) {
212          List<Attribute> attributeList = entry.getAttribute(aciType);
213          validAcis += addAciAttributeList(aciList, entry.getName(), configDN,
214                                           attributeList, failedACIMsgs);
215      }
216      return validAcis;
217    }
218    finally
219    {
220      lock.writeLock().unlock();
221    }
222  }
223
224  /**
225   * Add an ACI's attribute type values to the ACI list. There is a chance that
226   * an ACI will throw an exception if it has an invalid syntax. If that
227   * happens a message will be logged and the ACI skipped.  A count is
228   * returned of the number of valid ACIs added.
229   * @param aciList The ACI list to which the ACI is to be added.
230   * @param dn The DN to use as the key in the ACI list.
231   * @param configDN The DN of the configuration entry used to configure the
232   *                 ACI handler. Used if a global ACI has an decode exception.
233   * @param attributeList List of attributes containing the ACI attribute
234   * values.
235   * @param failedACIMsgs List that will hold error messages from ACI decode
236   *                      exceptions.
237   * @return The number of valid attribute values added to the ACI list.
238   */
239  private static int addAciAttributeList(DITCacheMap<List<Aci>> aciList,
240                                         DN dn, DN configDN,
241                                         List<Attribute> attributeList,
242                                         List<LocalizableMessage> failedACIMsgs) {
243    if (attributeList.isEmpty()) {
244      return 0;
245    }
246
247    int validAcis=0;
248    List<Aci> acis = new ArrayList<>();
249    for (Attribute attribute : attributeList) {
250      for (ByteString value : attribute) {
251        try {
252          acis.add(Aci.decode(value, dn));
253          validAcis++;
254        } catch (AciException ex) {
255          DN msgDN=dn;
256          if(dn == DN.rootDN()) {
257            msgDN=configDN;
258          }
259          failedACIMsgs.add(WARN_ACI_ADD_LIST_FAILED_DECODE.get(value, msgDN, ex.getMessage()));
260        }
261      }
262    }
263    addAci(aciList, dn, acis);
264    return validAcis;
265  }
266
267  /**
268   * Remove all of the ACIs related to the old entry and then add all of the
269   * ACIs related to the new entry. This method locks/unlocks the list.
270   * In the case of global ACIs the DN of the entry is checked to make sure it
271   * is equal to the config DN. If not, the global ACI attribute type is
272   * silently skipped.
273   * @param oldEntry The old entry possibly containing old ACI attribute
274   * values.
275   * @param newEntry The new entry possibly containing new ACI attribute
276   * values.
277   * @param hasAci True if the "aci" attribute type was seen in the entry.
278   * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was
279   * seen in the entry.
280   */
281  public void modAciOldNewEntry(Entry oldEntry, Entry newEntry,
282                                             boolean hasAci,
283                                             boolean hasGlobalAci) {
284
285    lock.writeLock().lock();
286    try
287    {
288      List<LocalizableMessage> failedACIMsgs=new LinkedList<>();
289      //Process "aci" attribute types.
290      if(hasAci) {
291          aciList.remove(oldEntry.getName());
292          List<Attribute> attributeList =
293                  newEntry.getOperationalAttribute(aciType);
294          addAciAttributeList(aciList,newEntry.getName(), configDN,
295                              attributeList, failedACIMsgs);
296      }
297      //Process global "ds-cfg-global-aci" attribute type. The oldentry
298      //DN is checked to verify it is equal to the config DN. If not those
299      //attributes are skipped.
300      if(hasGlobalAci && oldEntry.getName().equals(configDN)) {
301          aciList.remove(DN.rootDN());
302          List<Attribute> attributeList = newEntry.getAttribute(globalAciType);
303          addAciAttributeList(aciList, DN.rootDN(), configDN,
304                              attributeList, failedACIMsgs);
305      }
306    }
307    finally
308    {
309      lock.writeLock().unlock();
310    }
311  }
312
313  /**
314   * Add ACI using the DN as a key. If the DN already
315   * has ACI(s) on the list, then the new ACI is added to the
316   * end of the array.
317   * @param aciList The set of ACIs to which ACI is to be added.
318   * @param dn The DN to use as the key.
319   * @param acis The ACI to be added.
320   */
321  private static void addAci(DITCacheMap<List<Aci>> aciList, DN dn,
322                             List<Aci> acis)
323  {
324    if(aciList.containsKey(dn)) {
325      List<Aci> tmpAci = aciList.get(dn);
326      tmpAci.addAll(acis);
327    } else {
328      aciList.put(dn, acis);
329    }
330  }
331
332  /**
333   * Remove global and regular ACIs from the list. It's possible that an entry
334   * could have both attribute types (aci and ds-cfg-global-aci). Global ACIs
335   * use the NULL DN for the key.  In the case of global ACIs the DN of the
336   * entry is checked to make sure it is equal to the config DN. If not, the
337   * global ACI attribute type is silently skipped.
338   * @param entry The entry containing the global ACIs.
339   * @param hasAci True if the "aci" attribute type was seen in the entry.
340   * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was
341   * seen in the entry.
342   * @return  True if the ACI set was deleted.
343   */
344  public boolean removeAci(Entry entry,  boolean hasAci,
345                                                      boolean hasGlobalAci) {
346    lock.writeLock().lock();
347    try
348    {
349      DN entryDN = entry.getName();
350      if (hasGlobalAci && entryDN.equals(configDN) &&
351          aciList.remove(DN.rootDN()) == null)
352      {
353        return false;
354      }
355      if (hasAci || !hasGlobalAci)
356      {
357        return aciList.removeSubtree(entryDN, null);
358      }
359      return true;
360    }
361    finally
362    {
363      lock.writeLock().unlock();
364    }
365  }
366
367  /**
368   * Remove all ACIs related to a backend.
369   * @param backend  The backend to check if each DN is handled by that
370   * backend.
371   */
372  public void removeAci(Backend<?> backend) {
373
374    lock.writeLock().lock();
375    try
376    {
377      Iterator<Map.Entry<DN,List<Aci>>> iterator =
378              aciList.entrySet().iterator();
379      while (iterator.hasNext())
380      {
381        Map.Entry<DN,List<Aci>> mapEntry = iterator.next();
382        if (backend.handlesEntry(mapEntry.getKey()))
383        {
384          iterator.remove();
385        }
386      }
387    }
388    finally
389    {
390      lock.writeLock().unlock();
391    }
392  }
393
394  /**
395   * Rename all ACIs under the specified old DN to the new DN. A simple
396   * interaction over the entire list is performed.
397   * @param oldDN The DN of the original entry that was moved.
398   * @param newDN The DN of the new entry.
399   */
400  public void renameAci(DN oldDN, DN newDN ) {
401
402    lock.writeLock().lock();
403    try
404    {
405      Map<DN,List<Aci>> tempAciList = new HashMap<>();
406      Iterator<Map.Entry<DN,List<Aci>>> iterator =
407              aciList.entrySet().iterator();
408      while (iterator.hasNext()) {
409        Map.Entry<DN,List<Aci>> hashEntry = iterator.next();
410        DN keyDn = hashEntry.getKey();
411        if (keyDn.isSubordinateOrEqualTo(oldDN)) {
412          DN relocateDN = keyDn.rename(oldDN, newDN);
413          List<Aci> acis = new LinkedList<>();
414          for(Aci aci : hashEntry.getValue()) {
415            try {
416               Aci newAci =
417                 Aci.decode(ByteString.valueOfUtf8(aci.toString()), relocateDN);
418               acis.add(newAci);
419            } catch (AciException ex) {
420              //This should never happen since only a copy of the
421              //ACI with a new DN is being made. Log a message if it does and
422              //keep going.
423              logger.warn(WARN_ACI_ADD_LIST_FAILED_DECODE, aci, relocateDN, ex.getMessage());
424            }
425          }
426          tempAciList.put(relocateDN, acis);
427          iterator.remove();
428        }
429      }
430      aciList.putAll(tempAciList);
431    }
432    finally
433    {
434      lock.writeLock().unlock();
435    }
436  }
437}