/* * The contents of this file are subject to the terms of the Common Development and * Distribution License (the License). You may not use this file except in compliance with the * License. * * You can obtain a copy of the License at legal/CDDLv1.0.txt. See the License for the * specific language governing permission and limitations under the License. * * When distributing Covered Software, include this CDDL Header Notice in each file and include * the License file at legal/CDDLv1.0.txt. If applicable, add the following below the CDDL * Header, with the fields enclosed by brackets [] replaced by your own identifying * information: "Portions Copyright [year] [name of copyright owner]". * * Copyright 2008-2010 Sun Microsystems, Inc. * Portions Copyright 2013-2016 ForgeRock AS. */ package org.opends.server.authorization.dseecompat; import static org.opends.messages.AccessControlMessages.*; import static org.opends.server.authorization.dseecompat.AciHandler.*; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.SortedSet; import java.util.concurrent.locks.ReentrantReadWriteLock; import org.forgerock.i18n.LocalizableMessage; import org.forgerock.i18n.slf4j.LocalizedLogger; import org.forgerock.opendj.ldap.ByteString; import org.forgerock.opendj.ldap.DN; import org.opends.server.api.Backend; import org.opends.server.api.DITCacheMap; import org.opends.server.types.Attribute; import org.opends.server.types.Entry; /** * The AciList class performs caching of the ACI attribute values * using the entry DN as the key. */ public class AciList { private static final LocalizedLogger logger = LocalizedLogger.getLoggerForThisClass(); /** * A map containing all the ACIs. * We use the copy-on-write technique to avoid locking when reading. */ private volatile DITCacheMap> aciList = new DITCacheMap<>(); /** * Lock to protect internal data structures. */ private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock(); /** The configuration DN used to compare against the global ACI entry DN. */ private final DN configDN; /** * Constructor to create an ACI list to cache ACI attribute types. * @param configDN The configuration entry DN. */ public AciList(DN configDN) { this.configDN=configDN; } /** * Using the base DN, return a list of ACIs that are candidates for * evaluation by walking up from the base DN towards the root of the * DIT gathering ACIs on parents. Global ACIs use the NULL DN as the key * and are included in the candidate set only if they have no * "target" keyword rules, or if the target keyword rule matches for * the specified base DN. * * @param baseDN The DN to check. * @return A list of candidate ACIs that might be applicable. */ public List getCandidateAcis(DN baseDN) { List candidates = new LinkedList<>(); if(baseDN == null) { return candidates; } lock.readLock().lock(); try { //Save the baseDN in case we need to evaluate a global ACI. DN entryDN=baseDN; while (baseDN != null) { List acis = aciList.get(baseDN); if (acis != null) { //Check if there are global ACIs. Global ACI has a NULL DN. if (baseDN.isRootDN()) { for (Aci aci : acis) { AciTargets targets = aci.getTargets(); //If there is a target, evaluate it to see if this ACI should //be included in the candidate set. if (targets != null && AciTargets.isTargetApplicable(aci, targets, entryDN)) { candidates.add(aci); //Add this ACI to the candidates. } } } else { candidates.addAll(acis); } } if(baseDN.isRootDN()) { break; } DN parentDN=baseDN.parent(); if(parentDN == null) { baseDN=DN.rootDN(); } else { baseDN=parentDN; } } return candidates; } finally { lock.readLock().unlock(); } } /** * Add all the ACI from a set of entries to the ACI list. There is no need * to check for global ACIs since they are processe by the AciHandler at * startup using the addACi single entry method. * @param entries The set of entries containing the "aci" attribute values. * @param failedACIMsgs List that will hold error messages from ACI decode * exceptions. * @return The number of valid ACI attribute values added to the ACI list. */ public int addAci(List entries, LinkedList failedACIMsgs) { lock.writeLock().lock(); try { int validAcis = 0; for (Entry entry : entries) { DN dn=entry.getName(); List attributeList = entry.getOperationalAttribute(AciHandler.aciType); validAcis += addAciAttributeList(aciList, dn, configDN, attributeList, failedACIMsgs); } return validAcis; } finally { lock.writeLock().unlock(); } } /** * Add a set of ACIs to the ACI list. This is usually used a startup, when * global ACIs are processed. * * @param dn The DN to add the ACIs under. * * @param acis A set of ACIs to add to the ACI list. * */ public void addAci(DN dn, SortedSet acis) { lock.writeLock().lock(); try { aciList.put(dn, new LinkedList<>(acis)); } finally { lock.writeLock().unlock(); } } /** * Add all of an entry's ACI (global or regular) attribute values to the * ACI list. * @param entry The entry containing the ACI attributes. * @param hasAci True if the "aci" attribute type was seen in the entry. * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was * seen in the entry. * @param failedACIMsgs List that will hold error messages from ACI decode * exceptions. * @return The number of valid ACI attribute values added to the ACI list. */ public int addAci(Entry entry, boolean hasAci, boolean hasGlobalAci, List failedACIMsgs) { lock.writeLock().lock(); try { int validAcis = 0; //Process global "ds-cfg-global-aci" attribute type. The oldentry //DN is checked to verify it is equal to the config DN. If not those //attributes are skipped. if(hasGlobalAci && entry.getName().equals(configDN)) { List attributeList = entry.getAttribute(globalAciType); validAcis = addAciAttributeList(aciList, DN.rootDN(), configDN, attributeList, failedACIMsgs); } if(hasAci) { List attributeList = entry.getAttribute(aciType); validAcis += addAciAttributeList(aciList, entry.getName(), configDN, attributeList, failedACIMsgs); } return validAcis; } finally { lock.writeLock().unlock(); } } /** * Add an ACI's attribute type values to the ACI list. There is a chance that * an ACI will throw an exception if it has an invalid syntax. If that * happens a message will be logged and the ACI skipped. A count is * returned of the number of valid ACIs added. * @param aciList The ACI list to which the ACI is to be added. * @param dn The DN to use as the key in the ACI list. * @param configDN The DN of the configuration entry used to configure the * ACI handler. Used if a global ACI has an decode exception. * @param attributeList List of attributes containing the ACI attribute * values. * @param failedACIMsgs List that will hold error messages from ACI decode * exceptions. * @return The number of valid attribute values added to the ACI list. */ private static int addAciAttributeList(DITCacheMap> aciList, DN dn, DN configDN, List attributeList, List failedACIMsgs) { if (attributeList.isEmpty()) { return 0; } int validAcis=0; List acis = new ArrayList<>(); for (Attribute attribute : attributeList) { for (ByteString value : attribute) { try { acis.add(Aci.decode(value, dn)); validAcis++; } catch (AciException ex) { DN msgDN=dn; if(dn == DN.rootDN()) { msgDN=configDN; } failedACIMsgs.add(WARN_ACI_ADD_LIST_FAILED_DECODE.get(value, msgDN, ex.getMessage())); } } } addAci(aciList, dn, acis); return validAcis; } /** * Remove all of the ACIs related to the old entry and then add all of the * ACIs related to the new entry. This method locks/unlocks the list. * In the case of global ACIs the DN of the entry is checked to make sure it * is equal to the config DN. If not, the global ACI attribute type is * silently skipped. * @param oldEntry The old entry possibly containing old ACI attribute * values. * @param newEntry The new entry possibly containing new ACI attribute * values. * @param hasAci True if the "aci" attribute type was seen in the entry. * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was * seen in the entry. */ public void modAciOldNewEntry(Entry oldEntry, Entry newEntry, boolean hasAci, boolean hasGlobalAci) { lock.writeLock().lock(); try { List failedACIMsgs=new LinkedList<>(); //Process "aci" attribute types. if(hasAci) { aciList.remove(oldEntry.getName()); List attributeList = newEntry.getOperationalAttribute(aciType); addAciAttributeList(aciList,newEntry.getName(), configDN, attributeList, failedACIMsgs); } //Process global "ds-cfg-global-aci" attribute type. The oldentry //DN is checked to verify it is equal to the config DN. If not those //attributes are skipped. if(hasGlobalAci && oldEntry.getName().equals(configDN)) { aciList.remove(DN.rootDN()); List attributeList = newEntry.getAttribute(globalAciType); addAciAttributeList(aciList, DN.rootDN(), configDN, attributeList, failedACIMsgs); } } finally { lock.writeLock().unlock(); } } /** * Add ACI using the DN as a key. If the DN already * has ACI(s) on the list, then the new ACI is added to the * end of the array. * @param aciList The set of ACIs to which ACI is to be added. * @param dn The DN to use as the key. * @param acis The ACI to be added. */ private static void addAci(DITCacheMap> aciList, DN dn, List acis) { if(aciList.containsKey(dn)) { List tmpAci = aciList.get(dn); tmpAci.addAll(acis); } else { aciList.put(dn, acis); } } /** * Remove global and regular ACIs from the list. It's possible that an entry * could have both attribute types (aci and ds-cfg-global-aci). Global ACIs * use the NULL DN for the key. In the case of global ACIs the DN of the * entry is checked to make sure it is equal to the config DN. If not, the * global ACI attribute type is silently skipped. * @param entry The entry containing the global ACIs. * @param hasAci True if the "aci" attribute type was seen in the entry. * @param hasGlobalAci True if the "ds-cfg-global-aci" attribute type was * seen in the entry. * @return True if the ACI set was deleted. */ public boolean removeAci(Entry entry, boolean hasAci, boolean hasGlobalAci) { lock.writeLock().lock(); try { DN entryDN = entry.getName(); if (hasGlobalAci && entryDN.equals(configDN) && aciList.remove(DN.rootDN()) == null) { return false; } if (hasAci || !hasGlobalAci) { return aciList.removeSubtree(entryDN, null); } return true; } finally { lock.writeLock().unlock(); } } /** * Remove all ACIs related to a backend. * @param backend The backend to check if each DN is handled by that * backend. */ public void removeAci(Backend backend) { lock.writeLock().lock(); try { Iterator>> iterator = aciList.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry> mapEntry = iterator.next(); if (backend.handlesEntry(mapEntry.getKey())) { iterator.remove(); } } } finally { lock.writeLock().unlock(); } } /** * Rename all ACIs under the specified old DN to the new DN. A simple * interaction over the entire list is performed. * @param oldDN The DN of the original entry that was moved. * @param newDN The DN of the new entry. */ public void renameAci(DN oldDN, DN newDN ) { lock.writeLock().lock(); try { Map> tempAciList = new HashMap<>(); Iterator>> iterator = aciList.entrySet().iterator(); while (iterator.hasNext()) { Map.Entry> hashEntry = iterator.next(); DN keyDn = hashEntry.getKey(); if (keyDn.isSubordinateOrEqualTo(oldDN)) { DN relocateDN = keyDn.rename(oldDN, newDN); List acis = new LinkedList<>(); for(Aci aci : hashEntry.getValue()) { try { Aci newAci = Aci.decode(ByteString.valueOfUtf8(aci.toString()), relocateDN); acis.add(newAci); } catch (AciException ex) { //This should never happen since only a copy of the //ACI with a new DN is being made. Log a message if it does and //keep going. logger.warn(WARN_ACI_ADD_LIST_FAILED_DECODE, aci, relocateDN, ex.getMessage()); } } tempAciList.put(relocateDN, acis); iterator.remove(); } } aciList.putAll(tempAciList); } finally { lock.writeLock().unlock(); } } }