1. Project Clover database Tue Dec 20 2016 21:24:09 CET
  2. Package org.xwiki.security.authorization.cache.internal

File DefaultSecurityCacheLoader.java

 

Coverage histogram

../../../../../../img/srcFileCovDistChart9.png
38% of files have more coverage

Code metrics

40
111
12
2
462
252
35
0.32
9.25
6
2.92

Classes

Class Line # Actions
DefaultSecurityCacheLoader 67 107 0% 31 22
0.8580645385.8%
DefaultSecurityCacheLoader.EmptySecurityRuleEntry 99 4 0% 4 8
0.00%
 

Contributing tests

This file is covered by 33 tests. .

Source view

1    /*
2    * See the NOTICE file distributed with this work for additional
3    * information regarding copyright ownership.
4    *
5    * This is free software; you can redistribute it and/or modify it
6    * under the terms of the GNU Lesser General Public License as
7    * published by the Free Software Foundation; either version 2.1 of
8    * the License, or (at your option) any later version.
9    *
10    * This software is distributed in the hope that it will be useful,
11    * but WITHOUT ANY WARRANTY; without even the implied warranty of
12    * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13    * Lesser General Public License for more details.
14    *
15    * You should have received a copy of the GNU Lesser General Public
16    * License along with this software; if not, write to the Free
17    * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
18    * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
19    */
20    package org.xwiki.security.authorization.cache.internal;
21   
22    import java.util.ArrayDeque;
23    import java.util.ArrayList;
24    import java.util.Collection;
25    import java.util.Collections;
26    import java.util.Deque;
27    import java.util.HashSet;
28    import java.util.LinkedList;
29    import java.util.List;
30   
31    import javax.inject.Inject;
32    import javax.inject.Provider;
33    import javax.inject.Singleton;
34   
35    import org.slf4j.Logger;
36    import org.xwiki.component.annotation.Component;
37    import org.xwiki.security.GroupSecurityReference;
38    import org.xwiki.security.SecurityReference;
39    import org.xwiki.security.UserSecurityReference;
40    import org.xwiki.security.authorization.AuthorizationException;
41    import org.xwiki.security.authorization.AuthorizationSettler;
42    import org.xwiki.security.authorization.Right;
43    import org.xwiki.security.authorization.SecurityAccessEntry;
44    import org.xwiki.security.authorization.SecurityEntryReader;
45    import org.xwiki.security.authorization.SecurityRule;
46    import org.xwiki.security.authorization.SecurityRuleEntry;
47    import org.xwiki.security.authorization.cache.ConflictingInsertionException;
48    import org.xwiki.security.authorization.cache.ParentEntryEvictedException;
49    import org.xwiki.security.authorization.cache.SecurityCacheLoader;
50    import org.xwiki.security.authorization.cache.SecurityCacheRulesInvalidator;
51    import org.xwiki.security.authorization.internal.AbstractSecurityRuleEntry;
52    import org.xwiki.security.internal.UserBridge;
53   
54    /**
55    * Default implementation of the security cache loader.
56    *
57    * It depends on a {@link org.xwiki.security.authorization.SecurityEntryReader}
58    * for reading rules missing from the cache, and on a
59    * {@link org.xwiki.security.authorization.AuthorizationSettler} for resolving
60    * access from rules.
61    *
62    * @version $Id: 3f56e549702288b66c7cf294891ba8b75d7266f3 $
63    * @since 4.0M2
64    */
65    @Component
66    @Singleton
 
67    public class DefaultSecurityCacheLoader implements SecurityCacheLoader
68    {
69    /** Maximum number of attempts at loading an entry. */
70    private static final int MAX_RETRIES = 5;
71   
72    /** Logger. **/
73    @Inject
74    private Logger logger;
75   
76    /** The right cache. */
77    @Inject
78    private SecurityCache securityCache;
79   
80    /** Event listener responsible for invalidating cache entries. */
81    @Inject
82    private SecurityCacheRulesInvalidator rulesInvalidator;
83   
84    /** Factory object for producing SecurityRule instances from the corresponding xwiki rights objects. */
85    @Inject
86    private SecurityEntryReader securityEntryReader;
87   
88    /** User bridge to retrieve the list of group a user is related to. */
89    @Inject
90    private UserBridge userBridge;
91   
92    /** Provide the configured authorization settler. */
93    @Inject
94    private Provider<AuthorizationSettler> authorizationSettlerProvider;
95   
96    /**
97    * Implementation of the SecurityRuleEntry.
98    */
 
99    private final class EmptySecurityRuleEntry extends AbstractSecurityRuleEntry
100    {
101    /** Reference of the related entity. */
102    private final SecurityReference reference;
103   
104    /**
105    * @param reference reference of the related entity
106    */
 
107  0 toggle private EmptySecurityRuleEntry(SecurityReference reference)
108    {
109  0 this.reference = reference;
110    }
111   
112    /**
113    * @return the reference of the related entity
114    */
 
115  0 toggle @Override
116    public SecurityReference getReference()
117    {
118  0 return reference;
119    }
120   
121    /**
122    * @return an empty list of rules
123    */
 
124  0 toggle @Override
125    public Collection<SecurityRule> getRules()
126    {
127  0 return Collections.emptyList();
128    }
129   
 
130  0 toggle @Override
131    public boolean isEmpty()
132    {
133  0 return true;
134    }
135    }
136   
 
137  2400 toggle @Override
138    public SecurityAccessEntry load(UserSecurityReference user, SecurityReference entity)
139    throws AuthorizationException
140    {
141  2400 int retries = 0;
142   
143  2400 while (true) {
144  2404 rulesInvalidator.suspend();
145   
146  2404 try {
147  2404 retries++;
148  2404 return loadRequiredEntries(user, entity);
149    } catch (ParentEntryEvictedException e) {
150  0 if (retries < MAX_RETRIES) {
151  0 this.logger.debug("The parent entry was evicted. Have tried {} times. Trying again...", retries);
152  0 continue;
153    }
154    } catch (ConflictingInsertionException e) {
155  5 if (retries < MAX_RETRIES) {
156  4 this.logger.debug("There were conflicting insertions. Have tried {} times. Retrying...", retries);
157  4 continue;
158    }
159    } finally {
160  2404 rulesInvalidator.resume();
161    }
162  1 String message = String.format("Failed to load the cache in %d attempts. Giving up.", retries);
163  1 this.logger.error(message);
164  1 throw new AuthorizationException(user.getOriginalDocumentReference(),
165    entity.getOriginalReference(), message);
166    }
167    }
168   
169    /**
170    * Load entity entries, group entries, and user entries required to settle the access, settle it,
171    * add this decision into the cache and return the access.
172    *
173    * @param user The user to check access for.
174    * @param entity The entity to check access to.
175    * @return The resulting access for the user on the entity.
176    * @throws ParentEntryEvictedException If one of the parent entries are evicted before the load is completed.
177    * @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
178    * @throws org.xwiki.security.authorization.AuthorizationException On error.
179    */
 
180  2404 toggle private SecurityAccessEntry loadRequiredEntries(UserSecurityReference user, SecurityReference entity)
181    throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
182    {
183    // No entity, return default rights for user in its wiki
184  2404 if (entity == null) {
185  0 return authorizationSettlerProvider.get().settle(user,
186    loadUserEntry(user, user.getWikiReference(), null), null);
187    }
188   
189    // Retrieve rules for the entity from the cache
190  2404 Deque<SecurityRuleEntry> ruleEntries = getRules(entity);
191   
192    // Evaluate, store and return the access right
193  2404 return loadAccessEntries(user, entity, ruleEntries);
194    }
195   
196    /**
197    * Load group entries, and user entries required, to settle the access, settle it,
198    * add this decision into the cache and return the access.
199    *
200    * @param user The user to check access for.
201    * @param entity The lowest entity providing security rules on the path of the entity to check access for.
202    * @param ruleEntries The rule entries associated with the above entity.
203    * @return The access for the user at the entity (equivalent to the one of the entity to check access for).
204    * @throws ParentEntryEvictedException If one of the parent entries are evicted before the load is completed.
205    * @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
206    * @throws org.xwiki.security.authorization.AuthorizationException On error.
207    */
 
208  2404 toggle private SecurityAccessEntry loadAccessEntries(UserSecurityReference user, SecurityReference entity,
209    Deque<SecurityRuleEntry> ruleEntries)
210    throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
211    {
212    // userWiki is the wiki of the user
213  2404 SecurityReference userWiki = user.getWikiReference();
214    // entityWiki is the wiki of the entity when the user is global and the entity is local
215  2404 SecurityReference entityWiki = user.isGlobal() ? entity.getWikiReference() : null;
216  2404 if (entityWiki != null && userWiki.equals(entityWiki)) {
217  2308 entityWiki = null;
218    }
219   
220    // Load user and related groups into the cache (global and shadowed locals) as needed
221  2404 Collection<GroupSecurityReference> groups = loadUserEntry(user, userWiki, entityWiki);
222   
223    // Settle the access
224  2404 SecurityAccessEntry accessEntry = authorizationSettlerProvider.get().settle(user, groups, ruleEntries);
225   
226    // Store the result into the cache
227  2404 securityCache.add(accessEntry, entityWiki);
228   
229    // Return the result
230  2399 return accessEntry;
231    }
232   
233    /**
234    * Load user/group entry in the cache as needed, load related group entries and return the list of all groups
235    * associated with the given user/group in both the user wiki and the given entity wiki. Groups containing
236    * (recursively) groups containing the user/group are also listed.
237    *
238    * @param user The user/group to load.
239    * @param userWiki The user wiki. Should correspond to the wiki of the user/group provided above.
240    * @param entityWiki Only for global user, the wiki of the entity currently evaluated if it differ from the user
241    * wiki, null otherwise. Local group information of the entity wiki will be evaluated for the user/group to load
242    * and a shadow user will be made available in that wiki to support access entries.
243    * @return A collection of groups associated to the requested user/group (both user wiki and entity wiki)
244    * @throws ParentEntryEvictedException if any of the parent entries of the group were evicted.
245    * @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
246    * @throws org.xwiki.security.authorization.AuthorizationException on error.
247    */
 
248  2404 toggle private Collection<GroupSecurityReference> loadUserEntry(UserSecurityReference user, SecurityReference userWiki,
249    SecurityReference entityWiki)
250    throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
251    {
252    // First, we try to get the groups of the user from the cache
253  2404 Collection<GroupSecurityReference> groups = securityCache.getGroupsFor(user, entityWiki);
254  2404 if (groups != null) {
255    // Since we have then in the cache, it means that the entry is already loaded
256  1166 return groups;
257    }
258   
259    // Otherwise we have to load the entry
260  1238 groups = new HashSet<GroupSecurityReference>();
261   
262    // Public access could not appear in any group, no need to load it carefully, just optimized here
263  1238 if (user.getOriginalReference() == null) {
264  1064 if (securityCache.get(user) == null) {
265    // Main wiki entry should be loaded
266  0 getRules(user);
267    }
268  1064 if (entityWiki != null) {
269    // Ensure there is a Public shadow in the subwiki of the checked entity
270  2 securityCache.add(new DefaultSecurityShadowEntry(user, entityWiki), null);
271    }
272  1064 return groups;
273    }
274   
275    // If the user/group is global and we are looking for rules inside a subwiki, we need to ensure that the
276    // global user/group is loaded first, and we should also looks at global groups that she is a member of,
277    // before looking at the group she is a member of in the local wiki.
278  174 if (entityWiki != null) {
279    // First we add the global groups containing that user/group
280    // Check availability of the information from the user/group entry in the cache
281  23 Collection<GroupSecurityReference> globalGroups = securityCache.getGroupsFor(user, null);
282  23 Collection<GroupSecurityReference> immediateGroups;
283  23 if (globalGroups == null) {
284    // No luck, the global user does not seems to be in the cache, so we need to load it
285  8 globalGroups = new HashSet<>();
286  8 immediateGroups = loadUserGroups(user, userWiki, globalGroups);
287  8 loadUserEntry(user, immediateGroups);
288    } else {
289  15 immediateGroups = securityCache.getImmediateGroupsFor(user);
290    }
291  23 groups.addAll(globalGroups);
292   
293    // Now we also need to consider the local groups that contains the global groups found, since the user/group
294    // should be considered indirectly a member of these groups as well
295  23 for (GroupSecurityReference group : globalGroups) {
296    // Check availability of the information from the shadow entry of the global group in the entity wiki
297  5 Collection<GroupSecurityReference> localGroups = securityCache.getGroupsFor(group, entityWiki);
298  5 if (localGroups == null) {
299    // No luck, the shadow of the global group in the entity wiki does not seems to be in the cache,
300    // so we need to load it
301  5 localGroups = new HashSet<>();
302  5 securityCache.add(new DefaultSecurityShadowEntry(group, entityWiki),
303    loadUserGroups(group, entityWiki, localGroups));
304    }
305  5 groups.addAll(localGroups);
306    }
307   
308  23 Collection<GroupSecurityReference> localGroups = new HashSet<>();
309  23 immediateGroups.addAll(loadUserGroups(user, entityWiki, localGroups));
310    // Store a shadow entry for a global user/group involved in a local wiki
311  23 securityCache.add(new DefaultSecurityShadowEntry(user, entityWiki), immediateGroups);
312  23 groups.addAll(localGroups);
313    } else {
314    // We load the rules concerning the groups of the user, could be either
315    // the global group of a global user or local group for a local user
316    // and we finally load that user.
317  151 Collection<GroupSecurityReference> localGroups = new HashSet<>();
318  151 loadUserEntry(user, loadUserGroups(user, userWiki, localGroups));
319  151 groups.addAll(localGroups);
320    }
321   
322    // Returns all collected groups for access evaluation
323  174 return groups;
324    }
325   
326    /**
327    * Get the list of the groups a given user/group is a member in a given wiki, loading these groups into the cache.
328    *
329    * @param user the user/group being queried
330    * @param wiki the wiki into which the query is applied
331    * @param allGroups For the initial call, this collection should normally be empty, and will receive all the
332    * group associated with the given user (either directly or indirectly).
333    * @return the immediate group containing the user/group.
334    * @throws ParentEntryEvictedException if any of the parent entries of the groups were evicted.
335    * @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
336    * @throws AuthorizationException on error.
337    */
 
338  187 toggle private Collection<GroupSecurityReference> loadUserGroups(UserSecurityReference user, SecurityReference wiki,
339    Collection<GroupSecurityReference> allGroups)
340    throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
341    {
342  187 return loadUserGroups(user, wiki, allGroups, new ArrayDeque<GroupSecurityReference>());
343    }
344   
 
345  237 toggle private Collection<GroupSecurityReference> loadUserGroups(UserSecurityReference user, SecurityReference wiki,
346    Collection<GroupSecurityReference> allGroups, Deque<GroupSecurityReference> branchGroups)
347    throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
348    {
349    // Retrieve the list of immediate group for the user/group in either the entity wiki or the user/group wiki
350  238 Collection<GroupSecurityReference> groups = userBridge.getAllGroupsFor(user, wiki.getOriginalWikiReference());
351   
352  238 Collection<GroupSecurityReference> immediateGroup = new ArrayList<GroupSecurityReference>();
353   
354    // Loads all immediate groups recursively, collecting indirect groups along the way
355  238 for (GroupSecurityReference group : groups) {
356    // Loads the group only if it has never been seen before in the current path to avoid infinite recursion
357  102 if (!branchGroups.contains(group)) {
358    // We check the cache for real nodes (not shadows) since group are coming from their own wiki
359  102 Collection<GroupSecurityReference> groupsOfGroup = securityCache.getGroupsFor(group, null);
360    // And we load the groups only if they are not in the cache
361  102 if (groupsOfGroup == null) {
362    // Add this group into the list of immediate groups for this entry
363  51 immediateGroup.add(group);
364   
365    // Load dependencies recursively
366  51 branchGroups.push(group);
367  51 loadUserEntry(group, loadUserGroups(group, wiki, allGroups, branchGroups));
368  51 branchGroups.pop();
369    } else {
370    // Check for possible recursion in the cached groups and add this group only if it is safe
371  51 boolean recursionFound = false;
372  51 for (GroupSecurityReference existingGroup : groupsOfGroup) {
373  3 if (branchGroups.contains(existingGroup)) {
374  0 recursionFound = true;
375  0 break;
376    }
377    }
378  51 if (!recursionFound) {
379    // Add this group into the list of immediate groups for this entry
380  51 immediateGroup.add(group);
381    // Add all group found in the cache for the final result
382  51 allGroups.addAll(groupsOfGroup);
383    }
384    }
385    }
386    }
387   
388    // Collect groups of this entry for the final result
389  238 allGroups.addAll(immediateGroup);
390   
391  238 return immediateGroup;
392    }
393   
394    /**
395    * Load rules for a user/group into the cache with relations to immediate groups. Groups should be already loaded,
396    * else a ParentEntryEvictedException will be thrown. The parent chain of the loaded user will be loaded as needed.
397    *
398    * @param user The user/group to load.
399    * @param groups The collection of groups associated with the user/group
400    * @throws ParentEntryEvictedException if any of the parent entries of the group were evicted.
401    * @throws ConflictingInsertionException When different threads have inserted conflicting entries into the cache.
402    * @throws org.xwiki.security.authorization.AuthorizationException on error.
403    */
 
404  210 toggle private void loadUserEntry(UserSecurityReference user, Collection<GroupSecurityReference> groups)
405    throws ParentEntryEvictedException, ConflictingInsertionException, AuthorizationException
406    {
407    // Make sure the parent of the user document is loaded.
408  210 Deque<SecurityReference> chain = user.getReversedSecurityReferenceChain();
409  210 chain.removeLast();
410  210 for (SecurityReference ref : chain) {
411  461 SecurityRuleEntry entry = securityCache.get(ref);
412  461 if (entry == null) {
413  52 entry = securityEntryReader.read(ref);
414  52 securityCache.add(entry);
415    }
416    }
417   
418  210 SecurityRuleEntry entry = securityEntryReader.read(user);
419  210 securityCache.add(entry, groups);
420    }
421   
422    /**
423    * Retrieve rules for all hierarchy levels of the provided reference.
424    * Rules may be read from the cache, or from the entities and fill the cache.
425    *
426    * @param entity The entity for which rules should be loaded and retrieve.
427    * @return A collection of security rule entry, once for each level of the hierarchy.
428    * @exception org.xwiki.security.authorization.AuthorizationException if an error occurs
429    * @exception ParentEntryEvictedException if any parent entry is
430    * evicted before the operation completes.
431    * @throws ConflictingInsertionException When different threads
432    * have inserted conflicting entries into the cache.
433    */
 
434  2404 toggle private Deque<SecurityRuleEntry> getRules(SecurityReference entity)
435    throws AuthorizationException, ParentEntryEvictedException, ConflictingInsertionException
436    {
437  2404 Deque<SecurityRuleEntry> rules = new LinkedList<SecurityRuleEntry>();
438  2404 List<SecurityRuleEntry> emptyRuleEntryTail = new ArrayList<SecurityRuleEntry>();
439  2404 for (SecurityReference ref : entity.getReversedSecurityReferenceChain()) {
440  7165 SecurityRuleEntry entry = securityCache.get(ref);
441  7165 if (entry == null) {
442  2093 if (Right.getEnabledRights(ref.getType()).isEmpty()) {
443    // Do not call the reader on entity that will give useless rules
444  0 entry = new EmptySecurityRuleEntry(ref);
445  0 emptyRuleEntryTail.add(entry);
446    } else {
447  2093 entry = securityEntryReader.read(ref);
448  2091 if (!emptyRuleEntryTail.isEmpty()) {
449    // Add intermediate empty rules sets to the cache to hold this significant one
450  0 for (SecurityRuleEntry emptyRuleEntry : emptyRuleEntryTail) {
451  0 securityCache.add(emptyRuleEntry);
452    }
453  0 emptyRuleEntryTail.clear();
454    }
455  2091 securityCache.add(entry);
456    }
457    }
458  7165 rules.push(entry);
459    }
460  2404 return rules;
461    }
462    }