/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.security.authorization.cache.internal;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Deque;
import java.util.HashSet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import javax.inject.Inject;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.xwiki.cache.Cache;
import org.xwiki.cache.CacheManager;
import org.xwiki.cache.DisposableCacheValue;
import org.xwiki.cache.config.CacheConfiguration;
import org.xwiki.cache.eviction.LRUEvictionConfiguration;
import org.xwiki.component.annotation.Component;
import org.xwiki.component.phase.Initializable;
import org.xwiki.component.phase.InitializationException;
import org.xwiki.model.reference.EntityReferenceSerializer;
import org.xwiki.security.GroupSecurityReference;
import org.xwiki.security.SecurityReference;
import org.xwiki.security.UserSecurityReference;
import org.xwiki.security.authorization.SecurityAccessEntry;
import org.xwiki.security.authorization.SecurityEntry;
import org.xwiki.security.authorization.SecurityRuleEntry;
import org.xwiki.security.authorization.cache.ConflictingInsertionException;
import org.xwiki.security.authorization.cache.ParentEntryEvictedException;
import org.xwiki.security.authorization.cache.SecurityShadowEntry;
/**
* Default implementation of the security cache.
*
* @version $Id: ff6c8323eba5128a38405fd4c8e638c6a2c2689d $
* @since 4.0M2
*/
@Component
@Singleton
public class DefaultSecurityCache implements SecurityCache, Initializable
{
/** Default capacity for security cache. */
private static final int DEFAULT_CAPACITY = 10000;
/** Separator used for composing key for the cache. */
private static final String KEY_CACHE_SEPARATOR = "@@";
/** Logger. **/
@Inject
private Logger logger;
/** Fair read-write lock used for fair scheduling of cache access. */
private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock(true);
/** Fair read lock. */
private final Lock readLock = readWriteLock.readLock();
/** Fair write lock. */
private final Lock writeLock = readWriteLock.writeLock();
/** The keys in the cache are generated from instances of {@link org.xwiki.model.reference.EntityReference}. */
@Inject
private EntityReferenceSerializer<String> keySerializer;
/** Cache manager to create the cache. */
@Inject
private CacheManager cacheManager;
/** The cache instance. */
private Cache<SecurityCacheEntry> cache;
/** The new entry being added */
private SecurityCacheEntry newEntry;
/**
* @return a new configured security cache
* @throws InitializationException if a CacheException arise during creation
*/
private Cache<SecurityCacheEntry> newCache() throws InitializationException
{
CacheConfiguration cacheConfig = new CacheConfiguration();
cacheConfig.setConfigurationId("platform.security.authorization.cache");
LRUEvictionConfiguration lru = new LRUEvictionConfiguration();
lru.setMaxEntries(DEFAULT_CAPACITY);
cacheConfig.put(LRUEvictionConfiguration.CONFIGURATIONID, lru);
try {
return cacheManager.createNewCache(cacheConfig);
} catch (Exception e) {
throw new InitializationException(
String.format("Unable to create the security cache with a capacity of [%d] entries",
lru.getMaxEntries()), e);
}
}
@Override
public void initialize() throws InitializationException
{
cache = newCache();
}
/**
* Cache entry.
*/
private class SecurityCacheEntry implements DisposableCacheValue
{
/**
* The cached security entry.
*/
private SecurityEntry entry;
/**
* Parents of this cached entry.
*/
private Collection<SecurityCacheEntry> parents;
/**
* Children of this cached entry.
*/
private Collection<SecurityCacheEntry> children;
/**
* True if this entry has been removed.
*/
private boolean disposed;
/**
* Create a new cache entry for a security rule, linking it to its parent.
* @param entry the security rule entry to cache.
* @throws ParentEntryEvictedException if the parent required is no more available in the cache.
*/
SecurityCacheEntry(SecurityRuleEntry entry) throws ParentEntryEvictedException
{
this.entry = entry;
SecurityReference parentReference = entry.getReference().getParentSecurityReference();
if (parentReference != null) {
SecurityCacheEntry parent = DefaultSecurityCache.this.getEntry(parentReference);
if (parent == null) {
throw new ParentEntryEvictedException();
}
this.parents = Arrays.asList(parent);
parent.addChild(this);
logNewEntry();
} else {
this.parents = null;
logNewEntry();
}
}
/**
* Create a new cache entry for a security shadow, linking it to its parent.
* @param entry the security rule entry to cache.
* @throws ParentEntryEvictedException if the parent required is no more available in the cache.
*/
SecurityCacheEntry(SecurityShadowEntry entry) throws ParentEntryEvictedException
{
this.entry = entry;
SecurityCacheEntry parent1 = DefaultSecurityCache.this.getEntry(entry.getReference());
SecurityCacheEntry parent2 = DefaultSecurityCache.this.getEntry(entry.getWikiReference());
if (parent1 == null || parent2 == null) {
throw new ParentEntryEvictedException();
}
this.parents = Arrays.asList(parent1, parent2);
parent1.addChild(this);
parent2.addChild(this);
logNewEntry();
}
/**
* Create a new cache entry for a user access, linking it to the related entity and user.
* @param entry the security access entry to cache.
* @throws ParentEntryEvictedException if the parents required are no more available in the cache.
*/
SecurityCacheEntry(SecurityAccessEntry entry) throws ParentEntryEvictedException
{
this(entry, null);
}
/**
* Create a new cache entry for a user access, linking it to the related entity and user, or shadow user.
* @param entry the security access entry to cache.
* @param wiki if not null, the wiki context of the shadow user.
* @throws ParentEntryEvictedException if the parents required are no more available in the cache.
*/
SecurityCacheEntry(SecurityAccessEntry entry, SecurityReference wiki) throws ParentEntryEvictedException
{
this.entry = entry;
boolean isSelf = entry.getReference().equals(entry.getUserReference());
SecurityCacheEntry parent1 = DefaultSecurityCache.this.getEntry(entry.getReference());
SecurityCacheEntry parent2 = (isSelf) ? parent1
: (wiki != null) ? DefaultSecurityCache.this.getShadowEntry(entry.getUserReference(), wiki)
: DefaultSecurityCache.this.getEntry(entry.getUserReference());
if (parent1 == null || parent2 == null) {
throw new ParentEntryEvictedException();
}
this.parents = (isSelf) ? Arrays.asList(parent1) : Arrays.asList(parent1, parent2);
parent1.addChild(this);
if (!isSelf) {
parent2.addChild(this);
}
logNewEntry();
}
/**
* Create a new cache entry for a user rule entry, linking it to its parent and to all provided groups.
* @param entry the security rule entry to cache.
* @param groups the list of groups to link this entry to.
* @throws ParentEntryEvictedException if the parents required are no more available in the cache.
*/
SecurityCacheEntry(SecurityRuleEntry entry, Collection<GroupSecurityReference> groups)
throws ParentEntryEvictedException
{
this(entry, groups, entry.getReference().getParentSecurityReference());
}
/**
* Create a new cache entry for a user rule entry, linking it to its parent and to all provided groups.
* @param entry the security rule entry to cache.
* @param groups the list of groups to link this entry to.
* @throws ParentEntryEvictedException if the parents required are no more available in the cache.
*/
SecurityCacheEntry(SecurityShadowEntry entry, Collection<GroupSecurityReference> groups)
throws ParentEntryEvictedException
{
this(entry, groups, entry.getReference());
}
/**
* Create a new cache entry for a user rule entry, linking it to its parent and to all provided groups.
* @param entry the security rule entry to cache.
* @param groups the list of groups to link this entry to.
* @param parentReference the reference to the parent to link to.
* @throws ParentEntryEvictedException if the parents required are no more available in the cache.
*/
private SecurityCacheEntry(SecurityEntry entry, Collection<GroupSecurityReference> groups,
SecurityReference parentReference)
throws ParentEntryEvictedException
{
this.entry = entry;
int parentSize = groups.size() + ((parentReference == null) ? 0 : 1);
if (parentSize > 0) {
this.parents = new ArrayList<SecurityCacheEntry>(parentSize);
if (parentReference != null) {
SecurityCacheEntry parent = DefaultSecurityCache.this.getEntry(parentReference);
if (parent == null) {
throw new ParentEntryEvictedException();
}
this.parents.add(parent);
parent.addChild(this);
}
addParentGroups(groups, parentReference);
logNewEntry();
} else {
this.parents = null;
logNewEntry();
}
}
/**
* Add provided groups as parent of this entry, excluding the main parent reference.
*
* @param groups the list of groups to add.
* @param parentReference the main parent reference to exclude.
* @throws ParentEntryEvictedException if the parents required are no more available in the cache.
*/
private void addParentGroups(Collection<GroupSecurityReference> groups,
SecurityReference parentReference) throws ParentEntryEvictedException
{
for (GroupSecurityReference group : groups) {
if (group.equals(parentReference)) {
continue;
}
SecurityCacheEntry parent = (entry instanceof SecurityShadowEntry && group.isGlobal())
? DefaultSecurityCache.this.getShadowEntry(group,
((SecurityShadowEntry) entry).getWikiReference())
: DefaultSecurityCache.this.getEntry(group);
if (parent == null) {
throw new ParentEntryEvictedException();
}
this.parents.add(parent);
parent.addChild(this);
}
}
/**
* Update an existing cached security rule entry with parents groups if it does not have any already.
*
* @param groups the groups to be added to this entry, if null or empty nothing will be done.
* @throws ParentEntryEvictedException if one of the groups has been evicted from the cache.
*/
boolean updateParentGroups(Collection<GroupSecurityReference> groups)
throws ParentEntryEvictedException
{
if (isUser() || !(entry instanceof SecurityRuleEntry)) {
return false;
}
if (groups != null && !groups.isEmpty()) {
if (this.parents == null) {
this.parents = new ArrayList<SecurityCacheEntry>(groups.size());
addParentGroups(groups, null);
} else {
SecurityCacheEntry parent = this.parents.iterator().next();
this.parents = new ArrayList<SecurityCacheEntry>(groups.size() + 1);
this.parents.add(parent);
addParentGroups(groups, parent.entry.getReference());
}
}
return true;
}
/**
* Log the new entry creation.
*/
private void logNewEntry()
{
if (logger.isDebugEnabled()) {
if (parents == null || parents.size() == 0) {
logger.debug("New orphan entry [{}].", getKey());
return;
}
StringBuilder sb = new StringBuilder("New entry [");
sb.append(getKey()).append("] as child of ");
boolean first = true;
for (SecurityCacheEntry parent : parents) {
if (!first) {
sb.append(", ");
} else {
first = false;
}
sb.append('[').append(parent.getKey()).append(']');
}
sb.append(".");
logger.debug(sb.toString());
}
}
/**
* @return the original security entry cached in this cache entry.
*/
SecurityEntry getEntry()
{
return this.entry;
}
/**
* @return the serialized key of this entry.
*/
String getKey()
{
return DefaultSecurityCache.this.getEntryKey(entry);
}
/**
* Dispose this entry from the cache, removing all children relation in its parents, and removing
* all its children recursively. This method is not thread safe in regards to the cache, proper
* locking should be done externally.
*/
@Override
public void dispose()
{
if (!disposed) {
disposed = true;
disconnectFromParents();
disposeChildren();
}
}
protected void disconnectFromParents()
{
if (parents != null) {
for (SecurityCacheEntry parent : parents) {
if (!parent.disposed) {
parent.removeChild(this);
}
}
}
}
private void disposeChildren()
{
if (children != null) {
for (SecurityCacheEntry child : children) {
if (!child.disposed) {
if (logger.isDebugEnabled()) {
logger.debug("Cascaded removal of entry [{}] from cache.", child.getKey());
}
// XWIKI-13746: Prevent an addition in progress to bite his own entry in a bad way.
if (child == newEntry) {
child.dispose();
} else {
try {
DefaultSecurityCache.this.cache.remove(child.getKey());
} catch (Throwable e) {
logger.error("Security cache failure during eviction of entry [{}]", child.getKey(), e);
}
}
}
}
}
}
/**
* Add a children to this cache entry.
* @param entry the children entry to add.
*/
private void addChild(SecurityCacheEntry entry)
{
if (this.children == null) {
this.children = new ArrayList<SecurityCacheEntry>();
}
this.children.add(entry);
}
/**
* Remove a children from this cache entry.
* @param entry the children entry to remove.
*/
private void removeChild(SecurityCacheEntry entry)
{
if (this.children != null) {
this.children.remove(entry);
if (logger.isDebugEnabled()) {
logger.debug("Remove child [{}] from [{}].", entry.getKey(), getKey());
}
}
}
/**
* @return true if the entity is a user.
*/
public boolean isUser()
{
return entry.getReference() instanceof UserSecurityReference && !(entry instanceof SecurityAccessEntry);
}
}
/**
* @param reference the reference to build the key.
* @return a unique key for this reference.
*/
private String getEntryKey(SecurityReference reference)
{
return keySerializer.serialize(reference);
}
/**
* @param userReference the user reference to build the key.
* @param reference the entity reference to build the key.
* @return a unique key for the combination of this user and entity.
*/
private String getEntryKey(UserSecurityReference userReference, SecurityReference reference)
{
return keySerializer.serialize(userReference)
+ KEY_CACHE_SEPARATOR + keySerializer.serialize(reference);
}
/**
* @param userReference the user reference to build the key.
* @param root the entity reference of the sub-wiki.
* @return a unique key for the combination of this user and entity.
*/
private String getShadowEntryKey(SecurityReference userReference, SecurityReference root)
{
return keySerializer.serialize(root) + KEY_CACHE_SEPARATOR + keySerializer.serialize(userReference);
}
/**
* @param entry the security entry for which a key is requested. It could be either a {@link SecurityRuleEntry}
* or a {@link SecurityAccessEntry}.
* @return a unique key for this security entry.
*/
private String getEntryKey(SecurityEntry entry)
{
if (entry instanceof SecurityAccessEntry) {
return getEntryKey((SecurityAccessEntry) entry);
} else if (entry instanceof SecurityRuleEntry) {
return getEntryKey((SecurityRuleEntry) entry);
} else {
return getEntryKey((SecurityShadowEntry) entry);
}
}
/**
* @param entry the security entry for which a key is requested. It could be either a {@link SecurityRuleEntry}
* or a {@link SecurityAccessEntry}.
* @return a unique key for this security entry.
*/
private String getEntryKey(SecurityShadowEntry entry)
{
return getShadowEntryKey(entry.getReference(), entry.getWikiReference());
}
/**
* @param entry the security rule entry for which the key is requested.
* @return a unique key for this security rule entry.
*/
private String getEntryKey(SecurityRuleEntry entry)
{
return getEntryKey(entry.getReference());
}
/**
* @param entry the security access entry for which the key is requested.
* @return a unique key for this security access entry.
*/
private String getEntryKey(SecurityAccessEntry entry)
{
return getEntryKey(entry.getUserReference(), entry.getReference());
}
/**
* @param reference the reference requested.
* @return a security cache entry corresponding to given reference, null if none is available in the cache.
*/
private SecurityCacheEntry getEntry(SecurityReference reference)
{
readLock.lock();
try {
return cache.get(getEntryKey(reference));
} finally {
readLock.unlock();
}
}
/**
* @param userReference the user reference requested.
* @param reference the reference requested.
* @return a security cache entry corresponding to the given user and reference, null if none is available
* in the cache.
*/
private SecurityCacheEntry getEntry(UserSecurityReference userReference, SecurityReference reference)
{
readLock.lock();
try {
return cache.get(getEntryKey(userReference, reference));
} finally {
readLock.unlock();
}
}
/**
* @param userReference the user reference requested.
* @param wiki the wiki context of the shadow reference to retrieve.
* @return a security cache entry corresponding to the given user and reference, null if none is available
* in the cache.
*/
private SecurityCacheEntry getShadowEntry(SecurityReference userReference, SecurityReference wiki)
{
readLock.lock();
try {
return cache.get(getShadowEntryKey(userReference, wiki));
} finally {
readLock.unlock();
}
}
/**
* @param key the key of the cache slot to check.
* @param entry the entry to compare to.
* @return true, if the given entry has been inserted by another thread, false if the slot is available.
* @throws ConflictingInsertionException if another thread use this slot with a different entry.
*/
private boolean isAlreadyInserted(String key, SecurityEntry entry) throws ConflictingInsertionException
{
try {
return isAlreadyInserted(key, entry, null);
} catch (ParentEntryEvictedException e) {
// Impossible to reach
return true;
}
}
private boolean isAlreadyInserted(String key, SecurityEntry entry, Collection<GroupSecurityReference> groups)
throws ConflictingInsertionException, ParentEntryEvictedException
{
SecurityCacheEntry oldEntry = cache.get(key);
if (oldEntry != null) {
if (!oldEntry.getEntry().equals(entry)) {
// Another thread have inserted an entry which is different from this entry!
throw new ConflictingInsertionException();
}
// If the user/group has been completed
if (oldEntry.updateParentGroups(groups)) {
// Upgrade it to a user/group entry
oldEntry.entry = entry;
}
return true;
}
// The slot is available
return false;
}
/**
* Add a new entry in the cache and prevent cache container deadlock (in cooperation with the entry
* dispose method) in case adding the entry cause this same entry to be evicted.
* @param key the key of the entry to be added.
* @param entry the entry to add.
* @throws ConflictingInsertionException when the entry have been disposed while being added, the full load should
* be retried.
*/
private void addEntry(String key, SecurityCacheEntry entry) throws ConflictingInsertionException
{
try {
newEntry = entry;
cache.set(key, newEntry);
if (entry.disposed) {
// XWIKI-13746: The added entry have been disposed while being added, meaning that the eviction
// triggered by adding the entry has hit the entry itself, so remove it and fail.
cache.remove(key);
throw new ConflictingInsertionException();
}
} finally {
newEntry = null;
}
}
@Override
public void add(SecurityRuleEntry entry)
throws ParentEntryEvictedException, ConflictingInsertionException
{
add(entry, null);
}
@Override
public void add(SecurityRuleEntry entry, Collection<GroupSecurityReference> groups)
throws ConflictingInsertionException, ParentEntryEvictedException
{
add((SecurityEntry) entry, groups);
}
@Override
public void add(SecurityShadowEntry entry, Collection<GroupSecurityReference> groups)
throws ConflictingInsertionException, ParentEntryEvictedException
{
add((SecurityEntry) entry, groups);
}
/**
* Add either a rule or shadow user/group entry into the cache.
* @param entry the rule or shadow entry
* @param groups Local groups references that this user/group is a member.
* @exception ParentEntryEvictedException when the parent entry of
* this entry was evicted before this insertion. Since all
* entries, except wiki-entries, must have a parent cached, the
* {@link org.xwiki.security.authorization.cache.SecurityCacheLoader} must restart its load attempt.
* @throws ConflictingInsertionException when another thread have
* inserted this entry, but with a different content.
*/
private void add(SecurityEntry entry, Collection<GroupSecurityReference> groups)
throws ConflictingInsertionException, ParentEntryEvictedException
{
String key = getEntryKey(entry);
writeLock.lock();
try {
if (isAlreadyInserted(key, entry, groups)) {
return;
}
addEntry(key, newSecurityCacheEntry(entry, groups));
logger.debug("Added rule/shadow entry [{}] into the cache.", key);
} finally {
writeLock.unlock();
}
}
/**
* Construct a security cache entry for the given arguments.
* @param entry the rule or shadow entry
* @param groups Local groups references that this user/group is a member.
* @return the created security cache entry
* @exception ParentEntryEvictedException when the parent entry of
* this entry was evicted before this insertion. Since all
* entries, except wiki-entries, must have a parent cached, the
* {@link org.xwiki.security.authorization.cache.SecurityCacheLoader} must restart its load attempt.
* @throws ConflictingInsertionException when another thread have
* inserted this entry, but with a different content.
*/
private SecurityCacheEntry newSecurityCacheEntry(SecurityEntry entry, Collection<GroupSecurityReference> groups)
throws ConflictingInsertionException, ParentEntryEvictedException
{
if (entry instanceof SecurityRuleEntry) {
return (groups == null)
? new SecurityCacheEntry((SecurityRuleEntry) entry)
: new SecurityCacheEntry((SecurityRuleEntry) entry, groups);
} else {
return (groups == null)
? new SecurityCacheEntry((SecurityShadowEntry) entry)
: new SecurityCacheEntry((SecurityShadowEntry) entry, groups);
}
}
@Override
public void add(SecurityAccessEntry entry)
throws ParentEntryEvictedException, ConflictingInsertionException
{
internalAdd(entry, null);
}
@Override
public void add(SecurityAccessEntry entry, SecurityReference wiki)
throws ParentEntryEvictedException, ConflictingInsertionException
{
internalAdd(entry, wiki);
}
/**
* Add an entry to this cache.
* @param entry The access entry to add.
* @param wiki The sub-wiki context of this entry. Null for a global entry.
* @throws ParentEntryEvictedException when the parent entry of
* this entry was evicted before this insertion. Since all
* entries, except wiki-entries, must have a parent cached, the
* {@link org.xwiki.security.authorization.cache.SecurityCacheLoader} must restart its load attempt.
* @throws ConflictingInsertionException when another thread have
* inserted this entry, but with a different content.
*/
private void internalAdd(SecurityAccessEntry entry, SecurityReference wiki)
throws ParentEntryEvictedException, ConflictingInsertionException
{
String key = getEntryKey(entry);
writeLock.lock();
try {
if (isAlreadyInserted(key, entry)) {
return;
}
addEntry(key, new SecurityCacheEntry(entry, wiki));
logger.debug("Added access entry [{}] into the cache.", key);
} finally {
newEntry = null;
writeLock.unlock();
}
}
/**
* Retrieve an entry from the cache directly the internal cache. Used during unit test only.
* @param entryKey the key to be retrieved.
* @return the entry stored in the internal cache or Null if no entry was found.
*/
SecurityEntry get(String entryKey) {
SecurityCacheEntry entry = cache.get(entryKey);
return (entry != null) ? entry.getEntry() : null;
}
@Override
public SecurityAccessEntry get(UserSecurityReference user, SecurityReference entity)
{
SecurityCacheEntry entry = getEntry(user, entity);
if (entry == null) {
if (logger.isDebugEnabled()) {
logger.debug("Miss read access entry for [{}].", getEntryKey(user, entity));
}
return null;
}
if (logger.isDebugEnabled()) {
logger.debug("Success read access entry for [{}].", getEntryKey(user, entity));
}
return (SecurityAccessEntry) entry.getEntry();
}
@Override
public SecurityRuleEntry get(SecurityReference entity)
{
SecurityCacheEntry entry = getEntry(entity);
if (entry == null) {
if (logger.isDebugEnabled()) {
logger.debug("Miss read rule entry for [{}].", getEntryKey(entity));
}
return null;
}
if (logger.isDebugEnabled()) {
logger.debug("Success read rule entry for [{}].", getEntryKey(entity));
}
return (SecurityRuleEntry) entry.getEntry();
}
@Override
public void remove(UserSecurityReference user, SecurityReference entity)
{
writeLock.lock();
try {
SecurityCacheEntry entry = getEntry(user, entity);
if (entry != null) {
if (logger.isDebugEnabled()) {
logger.debug("Remove outdated access entry for [{}].", getEntryKey(user, entity));
}
this.cache.remove(entry.getKey());
}
} finally {
writeLock.unlock();
}
}
@Override
public void remove(SecurityReference entity)
{
writeLock.lock();
try {
SecurityCacheEntry entry = getEntry(entity);
if (entry != null) {
if (logger.isDebugEnabled()) {
logger.debug("Remove outdated rule entry for [{}].", getEntryKey(entity));
}
this.cache.remove(entry.getKey());
}
} finally {
writeLock.unlock();
}
}
@Override
public Collection<GroupSecurityReference> getImmediateGroupsFor(UserSecurityReference user)
{
Collection<GroupSecurityReference> groups = new HashSet<>();
SecurityCacheEntry userEntry = getEntry(user);
// If the user is not in the cache, or if it is, but not as a user, but as a regular document
if (userEntry == null || !userEntry.isUser()) {
// In that case, the ancestors are not fully loaded
return null;
}
for (SecurityCacheEntry parent : userEntry.parents) {
// Add the parent group (if we have not already seen it)
SecurityReference parentRef = parent.getEntry().getReference();
if (parentRef instanceof GroupSecurityReference) {
groups.add((GroupSecurityReference) parentRef);
}
}
return groups;
}
@Override
public Collection<GroupSecurityReference> getGroupsFor(UserSecurityReference user, SecurityReference entityWiki)
{
Collection<GroupSecurityReference> groups = new HashSet<>();
SecurityCacheEntry userEntry = (entityWiki != null) ? getShadowEntry(user, entityWiki) : getEntry(user);
// If the user is not in the cache, or if it is, but not as a user, but as a regular document
if (userEntry == null || !userEntry.isUser()) {
// In that case, the ancestors are not fully loaded
return null;
}
// We are going to get the parents of the security cache entry recursively, that is why we use a stack
// (instead of using the execution stack which would be more costly).
Deque<SecurityCacheEntry> entriesToExplore = new ArrayDeque<>();
// Special case if the user is a shadow.
if (entityWiki != null) {
// We start with the parents of the original entry, and the parent of this shadow (excluding the original)
addParentsWhenEntryIsShadow(userEntry, user, groups, entriesToExplore);
} else {
// We start with the current user
entriesToExplore.add(userEntry);
}
// Let's go
while (!entriesToExplore.isEmpty()) {
SecurityCacheEntry entry = entriesToExplore.pop();
// We add the parents of the current entry
addParentsToTheListOfEntriesToExplore(entry.parents, groups, entriesToExplore);
// If the entry has a shadow (in the concerned subwiki), we also add the parents of the shadow
if (entityWiki != null) {
GroupSecurityReference entryRef = (GroupSecurityReference) entry.getEntry().getReference();
if (entryRef.isGlobal()) {
SecurityCacheEntry shadow = getShadowEntry(entryRef, entityWiki);
if (shadow != null) {
addParentsToTheListOfEntriesToExplore(shadow.parents, groups, entriesToExplore, entry);
}
}
}
}
return groups;
}
private void addParentsWhenEntryIsShadow(SecurityCacheEntry shadow, UserSecurityReference user,
Collection<GroupSecurityReference> groups,
Deque<SecurityCacheEntry> entriesToExplore)
{
SecurityCacheEntry originalEntry = getEntry(user);
// We add the parents of the original (but not the original, otherwise we could have the same group twice)
addParentsToTheListOfEntriesToExplore(originalEntry.parents, groups, entriesToExplore);
// And we add the parent groups of the shadow
addParentsToTheListOfEntriesToExplore(shadow.parents, groups, entriesToExplore, originalEntry);
}
/**
* Add the parents of an entry to the list of entries to explore.
*
* @param parents the parents of the entry
* @param groups the collection where we store the found groups
* @param entriesToExplore the collection holding the entries we still have to explore
*/
private void addParentsToTheListOfEntriesToExplore(Collection<SecurityCacheEntry> parents,
Collection<GroupSecurityReference> groups,
Deque<SecurityCacheEntry> entriesToExplore)
{
addParentsToTheListOfEntriesToExplore(parents, groups, entriesToExplore, null);
}
/**
* Add the parents of an entry to the list of entries to explore.
*
* @param parents the parents of the entry
* @param groups the collection where we store the found groups
* @param entriesToExplore the collection holding the entries we still have to explore
* @param originalEntry the original entry of the current entry (if the current entry is a shadow), null otherwise
*/
private void addParentsToTheListOfEntriesToExplore(Collection<SecurityCacheEntry> parents,
Collection<GroupSecurityReference> groups,
Deque<SecurityCacheEntry> entriesToExplore, SecurityCacheEntry originalEntry)
{
if (parents == null) {
return;
}
for (SecurityCacheEntry parent : parents) {
// skip this parent if the entry is a shadow and the parent is the original entry
// (ie: don't explore the original entry)
if (originalEntry != null && parent == originalEntry) {
continue;
}
// Add the parent group (if we have not already seen it)
SecurityReference parentRef = parent.getEntry().getReference();
if (parentRef instanceof GroupSecurityReference && groups.add((GroupSecurityReference) parentRef)) {
entriesToExplore.add(parent);
}
}
}
}