/*
* 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.Collection;
import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.slf4j.Logger;
import org.xwiki.component.annotation.Component;
import org.xwiki.security.GroupSecurityReference;
import org.xwiki.security.SecurityReference;
import org.xwiki.security.UserSecurityReference;
import org.xwiki.security.authorization.AuthorizationException;
import org.xwiki.security.authorization.AuthorizationSettler;
import org.xwiki.security.authorization.Right;
import org.xwiki.security.authorization.SecurityAccessEntry;
import org.xwiki.security.authorization.SecurityEntryReader;
import org.xwiki.security.authorization.SecurityRule;
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.SecurityCacheLoader;
import org.xwiki.security.authorization.cache.SecurityCacheRulesInvalidator;
import org.xwiki.security.authorization.internal.AbstractSecurityRuleEntry;
import org.xwiki.security.internal.UserBridge;
/**
* Default implementation of the security cache loader.
*
* It depends on a {@link org.xwiki.security.authorization.SecurityEntryReader}
* for reading rules missing from the cache, and on a
* {@link org.xwiki.security.authorization.AuthorizationSettler} for resolving
* access from rules.
*
* @version $Id: afbfb6385b5c0411684df1b3bd09edbe39486ce1 $
* @since 4.0M2
*/
@Component
@Singleton
public class DefaultSecurityCacheLoader implements SecurityCacheLoader
{
/** Maximum number of attempts at loading an entry. */
private static final int MAX_RETRIES = 5;
/** Logger. **/
@Inject
private Logger logger;
/** The right cache. */
@Inject
private SecurityCache securityCache;
/** Event listener responsible for invalidating cache entries. */
@Inject
private SecurityCacheRulesInvalidator rulesInvalidator;
/** Factory object for producing SecurityRule instances from the corresponding xwiki rights objects. */
@Inject
private SecurityEntryReader securityEntryReader;
/** User bridge to retrieve the list of group a user is related to. */
@Inject
private UserBridge userBridge;
/** Provide the configured authorization settler. */
@Inject
private Provider<AuthorizationSettler> authorizationSettlerProvider;
/**
* Implementation of the SecurityRuleEntry.
*/
private final class EmptySecurityRuleEntry extends AbstractSecurityRuleEntry
{
/** Reference of the related entity. */
private final SecurityReference reference;
/**
* @param reference reference of the related entity
*/
private EmptySecurityRuleEntry(SecurityReference reference)
{
this.reference = reference;
}
/**
* @return the reference of the related entity
*/
@Override
public SecurityReference getReference()
{
return reference;
}
/**
* @return an empty list of rules
*/
@Override
public Collection<SecurityRule> getRules()
{
return Collections.emptyList();
}
@Override
public boolean isEmpty()
{
return true;
}
}
@Override
public SecurityAccessEntry load(UserSecurityReference user, SecurityReference entity)
throws AuthorizationException
{
int retries = 0;
Exception lastException;
while (true) {
rulesInvalidator.suspend();
try {
retries++;
return loadRequiredEntries(user, entity);
} catch (ParentEntryEvictedException e) {
lastException = e;
if (retries < MAX_RETRIES) {
this.logger.debug("The parent entry was evicted. Have tried {} times. Trying again...", retries);
continue;
}
} catch (ConflictingInsertionException e) {
lastException = e;
if (retries < MAX_RETRIES) {
this.logger.debug("There were conflicting insertions. Have tried {} times. Retrying...", retries);
continue;
}
} finally {
rulesInvalidator.resume();
}
String message = String.format("Failed to load the cache in %d attempts. Giving up.", retries);
this.logger.error(message);
throw new AuthorizationException(user.getOriginalDocumentReference(), entity.getOriginalReference(),
message, lastException);
}
}
/**
* Load entity entries, group entries, and user entries required to settle the access, settle it,
* add this decision into the cache and return the access.
*
* @param user The user to check access for.
* @param entity The entity to check access to.
* @return The resulting access for the user on the entity.
* @throws ParentEntryEvictedException If one of the parent entries are evicted before the load is completed.
* @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
* @throws org.xwiki.security.authorization.AuthorizationException On error.
*/
private SecurityAccessEntry loadRequiredEntries(UserSecurityReference user, SecurityReference entity)
throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
{
// No entity, return default rights for user in its wiki
if (entity == null) {
return authorizationSettlerProvider.get().settle(user,
loadUserEntry(user, user.getWikiReference(), null), null);
}
// Retrieve rules for the entity from the cache
Deque<SecurityRuleEntry> ruleEntries = getRules(entity);
// Evaluate, store and return the access right
return loadAccessEntries(user, entity, ruleEntries);
}
/**
* Load group entries, and user entries required, to settle the access, settle it,
* add this decision into the cache and return the access.
*
* @param user The user to check access for.
* @param entity The lowest entity providing security rules on the path of the entity to check access for.
* @param ruleEntries The rule entries associated with the above entity.
* @return The access for the user at the entity (equivalent to the one of the entity to check access for).
* @throws ParentEntryEvictedException If one of the parent entries are evicted before the load is completed.
* @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
* @throws org.xwiki.security.authorization.AuthorizationException On error.
*/
private SecurityAccessEntry loadAccessEntries(UserSecurityReference user, SecurityReference entity,
Deque<SecurityRuleEntry> ruleEntries)
throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
{
// userWiki is the wiki of the user
SecurityReference userWiki = user.getWikiReference();
// entityWiki is the wiki of the entity when the user is global and the entity is local
SecurityReference entityWiki = user.isGlobal() ? entity.getWikiReference() : null;
if (entityWiki != null && userWiki.equals(entityWiki)) {
entityWiki = null;
}
// Load user and related groups into the cache (global and shadowed locals) as needed
Collection<GroupSecurityReference> groups = loadUserEntry(user, userWiki, entityWiki);
// Settle the access
SecurityAccessEntry accessEntry = authorizationSettlerProvider.get().settle(user, groups, ruleEntries);
// Store the result into the cache
securityCache.add(accessEntry, entityWiki);
// Return the result
return accessEntry;
}
/**
* Load user/group entry in the cache as needed, load related group entries and return the list of all groups
* associated with the given user/group in both the user wiki and the given entity wiki. Groups containing
* (recursively) groups containing the user/group are also listed.
*
* @param user The user/group to load.
* @param userWiki The user wiki. Should correspond to the wiki of the user/group provided above.
* @param entityWiki Only for global user, the wiki of the entity currently evaluated if it differ from the user
* wiki, null otherwise. Local group information of the entity wiki will be evaluated for the user/group to load
* and a shadow user will be made available in that wiki to support access entries.
* @return A collection of groups associated to the requested user/group (both user wiki and entity wiki)
* @throws ParentEntryEvictedException if any of the parent entries of the group were evicted.
* @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
* @throws org.xwiki.security.authorization.AuthorizationException on error.
*/
private Collection<GroupSecurityReference> loadUserEntry(UserSecurityReference user, SecurityReference userWiki,
SecurityReference entityWiki)
throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
{
// First, we try to get the groups of the user from the cache
Collection<GroupSecurityReference> groups = securityCache.getGroupsFor(user, entityWiki);
if (groups != null) {
// Since we have then in the cache, it means that the entry is already loaded
return groups;
}
// Otherwise we have to load the entry
groups = new HashSet<GroupSecurityReference>();
// Public access could not appear in any group, no need to load it carefully, just optimized here
if (user.getOriginalReference() == null) {
if (securityCache.get(user) == null) {
// Main wiki entry should be loaded
getRules(user);
}
if (entityWiki != null) {
// Ensure there is a Public shadow in the subwiki of the checked entity
securityCache.add(new DefaultSecurityShadowEntry(user, entityWiki), null);
}
return groups;
}
// If the user/group is global and we are looking for rules inside a subwiki, we need to ensure that the
// global user/group is loaded first, and we should also looks at global groups that she is a member of,
// before looking at the group she is a member of in the local wiki.
if (entityWiki != null) {
// First we add the global groups containing that user/group
// Check availability of the information from the user/group entry in the cache
Collection<GroupSecurityReference> globalGroups = securityCache.getGroupsFor(user, null);
Collection<GroupSecurityReference> immediateGroups;
if (globalGroups == null) {
// No luck, the global user does not seems to be in the cache, so we need to load it
globalGroups = new HashSet<>();
immediateGroups = loadUserGroups(user, userWiki, globalGroups);
loadUserEntry(user, immediateGroups);
} else {
immediateGroups = securityCache.getImmediateGroupsFor(user);
}
groups.addAll(globalGroups);
// Now we also need to consider the local groups that contains the global groups found, since the user/group
// should be considered indirectly a member of these groups as well
for (GroupSecurityReference group : globalGroups) {
// Check availability of the information from the shadow entry of the global group in the entity wiki
Collection<GroupSecurityReference> localGroups = securityCache.getGroupsFor(group, entityWiki);
if (localGroups == null) {
// No luck, the shadow of the global group in the entity wiki does not seems to be in the cache,
// so we need to load it
localGroups = new HashSet<>();
securityCache.add(new DefaultSecurityShadowEntry(group, entityWiki),
loadUserGroups(group, entityWiki, localGroups));
}
groups.addAll(localGroups);
}
Collection<GroupSecurityReference> localGroups = new HashSet<>();
immediateGroups.addAll(loadUserGroups(user, entityWiki, localGroups));
// Store a shadow entry for a global user/group involved in a local wiki
securityCache.add(new DefaultSecurityShadowEntry(user, entityWiki), immediateGroups);
groups.addAll(localGroups);
} else {
// We load the rules concerning the groups of the user, could be either
// the global group of a global user or local group for a local user
// and we finally load that user.
Collection<GroupSecurityReference> localGroups = new HashSet<>();
loadUserEntry(user, loadUserGroups(user, userWiki, localGroups));
groups.addAll(localGroups);
}
// Returns all collected groups for access evaluation
return groups;
}
/**
* Get the list of the groups a given user/group is a member in a given wiki, loading these groups into the cache.
*
* @param user the user/group being queried
* @param wiki the wiki into which the query is applied
* @param allGroups For the initial call, this collection should normally be empty, and will receive all the
* group associated with the given user (either directly or indirectly).
* @return the immediate group containing the user/group.
* @throws ParentEntryEvictedException if any of the parent entries of the groups were evicted.
* @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
* @throws AuthorizationException on error.
*/
private Collection<GroupSecurityReference> loadUserGroups(UserSecurityReference user, SecurityReference wiki,
Collection<GroupSecurityReference> allGroups)
throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
{
return loadUserGroups(user, wiki, allGroups, new ArrayDeque<GroupSecurityReference>());
}
private Collection<GroupSecurityReference> loadUserGroups(UserSecurityReference user, SecurityReference wiki,
Collection<GroupSecurityReference> allGroups, Deque<GroupSecurityReference> branchGroups)
throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
{
// Retrieve the list of immediate group for the user/group in either the entity wiki or the user/group wiki
Collection<GroupSecurityReference> groups = userBridge.getAllGroupsFor(user, wiki.getOriginalWikiReference());
Collection<GroupSecurityReference> immediateGroup = new ArrayList<GroupSecurityReference>();
// Loads all immediate groups recursively, collecting indirect groups along the way
for (GroupSecurityReference group : groups) {
// Loads the group only if it has never been seen before in the current path to avoid infinite recursion
if (!branchGroups.contains(group)) {
// We check the cache for real nodes (not shadows) since group are coming from their own wiki
Collection<GroupSecurityReference> groupsOfGroup = securityCache.getGroupsFor(group, null);
// And we load the groups only if they are not in the cache
if (groupsOfGroup == null) {
// Add this group into the list of immediate groups for this entry
immediateGroup.add(group);
// Load dependencies recursively
branchGroups.push(group);
loadUserEntry(group, loadUserGroups(group, wiki, allGroups, branchGroups));
branchGroups.pop();
} else {
// Check for possible recursion in the cached groups and add this group only if it is safe
boolean recursionFound = false;
for (GroupSecurityReference existingGroup : groupsOfGroup) {
if (branchGroups.contains(existingGroup)) {
recursionFound = true;
break;
}
}
if (!recursionFound) {
// Add this group into the list of immediate groups for this entry
immediateGroup.add(group);
// Add all group found in the cache for the final result
allGroups.addAll(groupsOfGroup);
}
}
}
}
// Collect groups of this entry for the final result
allGroups.addAll(immediateGroup);
return immediateGroup;
}
/**
* Load rules for a user/group into the cache with relations to immediate groups. Groups should be already loaded,
* else a ParentEntryEvictedException will be thrown. The parent chain of the loaded user will be loaded as needed.
*
* @param user The user/group to load.
* @param groups The collection of groups associated with the user/group
* @throws ParentEntryEvictedException if any of the parent entries of the group were evicted.
* @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
* @throws org.xwiki.security.authorization.AuthorizationException on error.
*/
private void loadUserEntry(UserSecurityReference user, Collection<GroupSecurityReference> groups)
throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
{
// Make sure the parent of the user document is loaded.
Deque<SecurityReference> chain = user.getReversedSecurityReferenceChain();
chain.removeLast();
for (SecurityReference ref : chain) {
SecurityRuleEntry entry = securityCache.get(ref);
if (entry == null) {
entry = securityEntryReader.read(ref);
securityCache.add(entry);
}
}
SecurityRuleEntry entry = securityEntryReader.read(user);
securityCache.add(entry, groups);
}
/**
* Retrieve rules for all hierarchy levels of the provided reference.
* Rules may be read from the cache, or from the entities and fill the cache.
*
* @param entity The entity for which rules should be loaded and retrieve.
* @return A collection of security rule entry, once for each level of the hierarchy.
* @exception org.xwiki.security.authorization.AuthorizationException if an error occurs
* @exception ParentEntryEvictedException if any parent entry is
* evicted before the operation completes.
* @throws ConflictingInsertionException When different threads
* have inserted conflicting entries into the cache.
*/
private Deque<SecurityRuleEntry> getRules(SecurityReference entity)
throws AuthorizationException, ParentEntryEvictedException, ConflictingInsertionException
{
Deque<SecurityRuleEntry> rules = new LinkedList<SecurityRuleEntry>();
List<SecurityRuleEntry> emptyRuleEntryTail = new ArrayList<SecurityRuleEntry>();
for (SecurityReference ref : entity.getReversedSecurityReferenceChain()) {
SecurityRuleEntry entry = securityCache.get(ref);
if (entry == null) {
if (Right.getEnabledRights(ref.getType()).isEmpty()) {
// Do not call the reader on entity that will give useless rules
entry = new EmptySecurityRuleEntry(ref);
emptyRuleEntryTail.add(entry);
} else {
entry = securityEntryReader.read(ref);
if (!emptyRuleEntryTail.isEmpty()) {
// Add intermediate empty rules sets to the cache to hold this significant one
for (SecurityRuleEntry emptyRuleEntry : emptyRuleEntryTail) {
securityCache.add(emptyRuleEntry);
}
emptyRuleEntryTail.clear();
}
securityCache.add(entry);
}
}
rules.push(entry);
}
return rules;
}
}