001/*
002 *  Copyright 2016 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.core.right;
017
018import java.io.InputStream;
019import java.util.ArrayList;
020import java.util.Collections;
021import java.util.Enumeration;
022import java.util.HashMap;
023import java.util.HashSet;
024import java.util.List;
025import java.util.Map;
026import java.util.Map.Entry;
027import java.util.Optional;
028import java.util.Set;
029import java.util.stream.Collectors;
030
031import org.apache.avalon.framework.activity.Initializable;
032import org.apache.avalon.framework.component.Component;
033import org.apache.avalon.framework.configuration.Configurable;
034import org.apache.avalon.framework.configuration.Configuration;
035import org.apache.avalon.framework.configuration.ConfigurationException;
036import org.apache.avalon.framework.configuration.DefaultConfigurationBuilder;
037import org.apache.avalon.framework.context.Context;
038import org.apache.avalon.framework.context.ContextException;
039import org.apache.avalon.framework.context.Contextualizable;
040import org.apache.avalon.framework.service.ServiceException;
041import org.apache.avalon.framework.service.ServiceManager;
042import org.apache.avalon.framework.service.Serviceable;
043import org.apache.avalon.framework.thread.ThreadSafe;
044import org.apache.cocoon.components.ContextHelper;
045import org.apache.cocoon.environment.Request;
046import org.apache.commons.lang3.StringUtils;
047import org.apache.excalibur.source.Source;
048import org.apache.excalibur.source.SourceResolver;
049
050import org.ametys.core.cache.AbstractCacheManager;
051import org.ametys.core.cache.Cache;
052import org.ametys.core.group.GroupDirectoryDAO;
053import org.ametys.core.group.GroupIdentity;
054import org.ametys.core.group.GroupManager;
055import org.ametys.core.right.AccessController.AccessResult;
056import org.ametys.core.right.AccessController.Permission;
057import org.ametys.core.right.AccessController.ExplanationObject;
058import org.ametys.core.user.CurrentUserProvider;
059import org.ametys.core.user.UserIdentity;
060import org.ametys.core.user.UserManager;
061import org.ametys.core.user.population.PopulationContextHelper;
062import org.ametys.core.user.population.UserPopulationDAO;
063import org.ametys.plugins.core.impl.cache.AbstractCacheKey;
064import org.ametys.runtime.i18n.I18nizableText;
065import org.ametys.runtime.plugin.component.AbstractLogEnabled;
066
067/**
068 * Abstraction for testing a right associated with a resource and a user from a single source.
069 */
070public class RightManager extends AbstractLogEnabled implements Serviceable, Configurable, ThreadSafe, Component, Contextualizable, Initializable
071{
072    /** For avalon service manager */
073    public static final String ROLE = RightManager.class.getName();
074    /** The id of the READER profile */
075    public static final String READER_PROFILE_ID = "READER";
076    
077    /** The instance of ObjectUserIdentity for anonymous */
078    protected static final UserIdentity __ANONYMOUS_USER_IDENTITY = new UserIdentity(null, null);
079    /** The instance of ObjectUserIdentity for any connected user */
080    protected static final UserIdentity __ANY_CONNECTED_USER_IDENTITY = new UserIdentity("", "");
081
082    private static final String CACHE_1 = RightManager.class.getName() + "$Cache-1";
083    private static final String CACHE_2 = RightManager.class.getName() + "$Cache-2";
084    
085    /** Avalon ServiceManager */
086    protected ServiceManager _manager;
087    /** Avalon SourceResolver */
088    protected SourceResolver _resolver;
089    /** The rights' list container */
090    protected RightsExtensionPoint _rightsEP;
091    /** The extension point for the Right Context Convertors */
092    protected RightContextConvertorExtensionPoint _rightContextConvertorEP;
093    /** The extension point for Access Controllers */
094    protected AccessControllerExtensionPoint _accessControllerEP;
095    /** The user manager */
096    protected UserManager _userManager;
097    /** The group manager */
098    protected GroupManager _groupManager;
099    /** The DAO for user populations */
100    protected UserPopulationDAO _userPopulationDAO;
101    /** The DAO for group directories */
102    protected GroupDirectoryDAO _groupDirectoryDAO;
103    /** The current user provider */
104    protected CurrentUserProvider _currentUserProvider;
105    /** The rights DAO */
106    protected RightProfilesDAO _profilesDAO;
107    /** Cache Manager */
108    protected AbstractCacheManager _cacheManager;
109    
110    private Context _context;
111    private List<String> _webinfRights = new ArrayList<>();
112    
113    /**
114     * Enumeration of all possible values returned by hasRight(user, right, context)
115     */
116    public enum RightResult
117    {
118        /**
119         * Indicates that a given user has the required right.
120         */
121        RIGHT_ALLOW,
122        
123        /**
124         * Indicates that a given user does NOT have the required right.
125         */
126        RIGHT_DENY,
127        
128        /**
129         * Indicates that the system knows nothing about the fact that a given user has a right or not.
130         */
131        RIGHT_UNKNOWN;
132    }
133    
134    @Override
135    public void contextualize(Context context) throws ContextException
136    {
137        _context = context;
138    }
139    
140    @Override
141    public void service(ServiceManager manager) throws ServiceException
142    {
143        _manager = manager;
144        _userManager = (UserManager) manager.lookup(UserManager.ROLE);
145        _groupManager = (GroupManager) manager.lookup(GroupManager.ROLE);
146        _userPopulationDAO = (UserPopulationDAO) manager.lookup(UserPopulationDAO.ROLE);
147        _groupDirectoryDAO = (GroupDirectoryDAO) manager.lookup(GroupDirectoryDAO.ROLE);
148        _rightsEP = (RightsExtensionPoint) manager.lookup(RightsExtensionPoint.ROLE);
149        _rightContextConvertorEP = (RightContextConvertorExtensionPoint) manager.lookup(RightContextConvertorExtensionPoint.ROLE);
150        _accessControllerEP = (AccessControllerExtensionPoint) manager.lookup(AccessControllerExtensionPoint.ROLE);
151        _resolver = (SourceResolver) _manager.lookup(SourceResolver.ROLE);
152        _currentUserProvider = (CurrentUserProvider) _manager.lookup(CurrentUserProvider.ROLE);
153        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
154    }
155
156    public void initialize() throws Exception
157    {
158        _cacheManager.createRequestCache(CACHE_1,
159                new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHT_MANAGER_CACHE_1_LABEL"),
160                new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHT_MANAGER_CACHE_1_DESCRIPTION"),
161                true);
162        _cacheManager.createRequestCache(CACHE_2,
163                new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHT_MANAGER_CACHE_2_LABEL"),
164                new I18nizableText("plugin.core", "PLUGINS_CORE_RIGHT_MANAGER_CACHE_2_DESCRIPTION"),
165                true);
166    }
167    
168    /**
169     * Returns the DAO for profiles
170     * @return The DAO
171     */
172    protected RightProfilesDAO _getProfileDAO ()
173    {
174        try
175        {
176            if (_profilesDAO == null)
177            {
178                _profilesDAO = (RightProfilesDAO) _manager.lookup(RightProfilesDAO.ROLE);
179            }
180            return _profilesDAO;
181        }
182        catch (ServiceException e)
183        {
184            throw new RuntimeException("Failed to retrieve the DAO for profiles", e);
185        }
186    }
187    
188    @Override
189    public void configure(Configuration configuration) throws ConfigurationException
190    {
191        Configuration rightsConfiguration = configuration.getChild("rights");
192
193        String externalFile = rightsConfiguration.getAttribute("config", null);
194        if (externalFile != null)
195        {
196            Source source = null;
197            try
198            {
199                source = _resolver.resolveURI("context://" + externalFile);
200
201                if (source.exists())
202                {
203                    Configuration externalConfiguration;
204                    try (InputStream is = source.getInputStream();)
205                    {
206                        externalConfiguration = new DefaultConfigurationBuilder().build(is);
207                    }
208
209                    configureRights(externalConfiguration);
210                }
211                else if (getLogger().isInfoEnabled())
212                {
213                    getLogger().info("The optional external rights file '" + externalFile + "' is missing.");
214                }
215            }
216            catch (Exception e)
217            {
218                String message = "An error occured while retriving external file '" + externalFile + "'";
219                getLogger().error(message, e);
220                throw new ConfigurationException(message, configuration, e);
221            }
222            finally
223            {
224                if (source != null)
225                {
226                    _resolver.release(source);
227                }
228            }
229        }
230        else
231        {
232            configureRights(rightsConfiguration);
233        }
234    }
235    
236    private void configureRights(Configuration configuration) throws ConfigurationException
237    {
238        Configuration[] rights = configuration.getChildren("right");
239        for (Configuration rightConf : rights)
240        {
241            String id = rightConf.getAttribute("id", "");
242
243            Configuration labelConf = rightConf.getChild("label");
244            String label = labelConf.getValue("");
245            I18nizableText i18nLabel = _getI18nTextFromConf(labelConf, label);
246
247            Configuration descConf = rightConf.getChild("description");
248            String description = descConf.getValue("");
249            I18nizableText i18nDescription = _getI18nTextFromConf(descConf, description);
250
251            String category = rightConf.getChild("category").getValue("");
252            I18nizableText i18nCategory = new I18nizableText("application", category);
253
254            if (id.length() == 0 || label.length() == 0 || description.length() == 0 || category.length() == 0)
255            {
256                String message = "Error in " + RightManager.class.getName() + " configuration: attribute 'id' and elements 'label', 'description' and 'category' are mandatory.";
257                getLogger().error(message);
258                throw new ConfigurationException(message, configuration);
259            }
260            _rightsEP.addRight(id, i18nLabel, i18nDescription, i18nCategory);
261            _webinfRights.add(id);
262        }
263    }
264    
265    /**
266     * Getter for external rights ids
267     * @return a list of all the rights' ids from rights.xml file
268     */
269    public List<String> getExternalRightIds()
270    {
271        return _webinfRights;
272    }
273    
274    private I18nizableText _getI18nTextFromConf(Configuration config, String label)
275    {
276        return I18nizableText.isI18n(config)
277                ? new I18nizableText("application", label)
278                : new I18nizableText(label);
279    }
280    
281    /**
282     * Add a right to the right EP
283     * @param id the id for the new right
284     * @param label the label for the new right
285     * @param description the description for the new right
286     * @param category the category for the new right
287     */
288    public void  addExternalRight(String id, String label, String description, String category)
289    {
290        _rightsEP.addRight(id, new I18nizableText(label), new I18nizableText(description), new I18nizableText("application", category));
291        _webinfRights.add(id);
292    }
293    
294    /**
295     * Remove the right from the rightEP
296     * @param id the right to delete's id
297     */
298    public void removeExternalRight(String id)
299    {
300        _rightsEP.removeRight(id);
301        _webinfRights.remove(id);
302    }
303    
304    /* --------- */
305    /* HAS RIGHT */
306    /* --------- */
307    
308    /**
309     * Checks a permission for the current logged user, on a given object (or context).<br>
310     * If null, it checks if there is at least one object with this permission
311     * @param rightId The name of the right to check. Cannot be null.
312     * @param object The object to check the right. Can be null to search on any object.
313     * @return {@link RightResult#RIGHT_ALLOW}, {@link RightResult#RIGHT_DENY} or {@link RightResult#RIGHT_UNKNOWN}
314     * @throws RightsException if an error occurs.
315     */
316    public RightResult currentUserHasRight(String rightId, Object object) throws RightsException
317    {
318        return hasRight(_currentUserProvider.getUser(), rightId, object);
319    }
320    
321    /**
322     * Checks a permission for a user, on a given object (or context).<br>
323     * If null, it checks if there is at least one object with this permission
324     * @param userIdentity The user identity. Can be null for anonymous
325     * @param rightId The name of the right to check. Cannot be null.
326     * @param object The object to check the right. Can be null to search on any object.
327     * @return {@link RightResult#RIGHT_ALLOW}, {@link RightResult#RIGHT_DENY} or {@link RightResult#RIGHT_UNKNOWN}
328     * @throws RightsException if an error occurs.
329     */
330    public RightResult hasRight(UserIdentity userIdentity, String rightId, Object object) throws RightsException
331    {
332        UserIdentity objectUserIdentity = userIdentity == null ? __ANONYMOUS_USER_IDENTITY : userIdentity;
333        
334        return _hasRight(objectUserIdentity, rightId, object);
335    }
336    
337    /**
338     * Gets the right result for anonymous with given right on given object context
339     * @param rightId The id of the right
340     * @param object The object to check
341     * @return the right result for anonymous with given profile on given object context
342     */
343    public RightResult hasAnonymousRight(String rightId, Object object)
344    {
345        return _hasRight(__ANONYMOUS_USER_IDENTITY, rightId, object);
346    }
347
348    /**
349     * Gets the right result for any connected user with given profile on given object context
350     * @param rightId The right id to test
351     * @param object The object to check
352     * @return the right result for any connected user with given profile on given object context
353     */
354    public RightResult hasAnyConnectedUserRight(String rightId, Object object)
355    {
356        return _hasRight(__ANY_CONNECTED_USER_IDENTITY, rightId, object);
357    }
358
359    private RightResult _hasRight(UserIdentity userIdentity, String rightId, Object object)
360    {
361        getLogger().debug("Try to determine if user {} has the right '{}' on the object context {}", userIdentity, rightId, object);
362
363        if (StringUtils.isBlank(rightId))
364        {
365            throw new RightsException("The rightId cannot be null");
366        }
367
368        return _hasRightOrRead(userIdentity, rightId, object);
369    }
370    
371    private RightResult _hasRightOrRead(UserIdentity userIdentity, String rightId, Object object)
372    {
373        if (object == null)
374        {
375            return _hasRightOrRead(userIdentity, rightId);
376        }
377        
378        // Try to retrieve in first cache (the one which manages non-null contexts)
379        RightResult cacheResult = _hasRightResultInFirstCache(userIdentity, rightId, object);
380        if (cacheResult != null)
381        {
382            return cacheResult;
383        }
384        
385        // Retrieve groups the user belongs to
386        Set<GroupIdentity> groups = _getGroups(userIdentity);
387        
388        // Get the objects to check
389        Set<Object> objects = _getConvertedObjects(object, new HashSet<>());
390        
391        // Retrieve the set of AccessResult
392        Set<AccessResult> accessResults = _getAccessResults(userIdentity, groups, rightId, objects);
393        
394        // Compute access
395        AccessResult access = AccessResult.merge(accessResults);
396        
397        RightResult rightResult = access.toRightResult();
398        _putInFirstCache(userIdentity, rightId, object, rightResult);
399        
400        return rightResult;
401    }
402    
403    /**
404     * Has the user/anonymous/anyconnected the non null right on any content of the current workspace?
405     * @param userIdentity The user connecter or the value for anonymous or any connected user
406     * @param rightId The right id to test. Can be null to test read access
407     * @return The computed right result
408     */
409    private RightResult _hasRightOrRead(UserIdentity userIdentity, String rightId)
410    {
411        // Resolve contexts
412        Set<Object> workspacesContexts = _getConvertedObjects("/${WorkspaceName}", new HashSet<>());
413        
414        // Try to retrieve in second cache (the one which manages null context)
415        RightResult cacheResult = _hasRightResultInSecondCache(workspacesContexts, userIdentity, rightId);
416        if (cacheResult != null)
417        {
418            return cacheResult;
419        }
420        
421        // Retrieve groups the user belongs to
422        Set<GroupIdentity> groups = _getGroups(userIdentity);
423
424        RightResult rightResult = RightResult.RIGHT_UNKNOWN;
425        for (String controllerId : _accessControllerEP.getExtensionsIds())
426        {
427            AccessController accessController = _accessControllerEP.getExtension(controllerId);
428            try
429            {
430                if (userIdentity == __ANONYMOUS_USER_IDENTITY)
431                {
432                    if (rightId == null ? accessController.hasAnonymousAnyReadAccessPermissionOnWorkspace(workspacesContexts) : accessController.hasAnonymousAnyPermissionOnWorkspace(workspacesContexts, rightId))
433                    {
434                        rightResult = RightResult.RIGHT_ALLOW;
435                        break;
436                    }
437                }
438                else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
439                {
440                    if (rightId == null ? accessController.hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(workspacesContexts) : accessController.hasAnyConnectedUserAnyPermissionOnWorkspace(workspacesContexts, rightId))
441                    {
442                        rightResult = RightResult.RIGHT_ALLOW;
443                        break;
444                    }
445                }
446                else
447                {
448                    if (rightId == null ? accessController.hasUserAnyReadAccessPermissionOnWorkspace(workspacesContexts, userIdentity, groups) : accessController.hasUserAnyPermissionOnWorkspace(workspacesContexts, userIdentity, groups, rightId))
449                    {
450                        rightResult = RightResult.RIGHT_ALLOW;
451                        break;
452                    }
453                }
454            }
455            catch (Exception e)
456            {
457                getLogger().error("An error occured with controller '{}'. Thus, this controller will be ignored.", controllerId, e);
458            }
459        }
460        
461        getLogger().debug("Right result found for [{}, {}, {}] => {}", workspacesContexts, userIdentity, rightId, rightResult);
462        _putInSecondCache(workspacesContexts, userIdentity, rightId, rightResult);
463        return rightResult;
464    }
465    
466    private Set<AccessResult> _getAccessResults(UserIdentity userIdentity, Set<GroupIdentity> groups, String rightId, Set<Object> objects)
467    {
468        Set<AccessResult> accessResults = new HashSet<>();
469        for (Object obj : objects)
470        {
471            for (String controllerId : _accessControllerEP.getExtensionsIds())
472            {
473                AccessController accessController = _accessControllerEP.getExtension(controllerId);
474                try
475                {
476                    if (accessController.isSupported(obj))
477                    {
478                        if (userIdentity == __ANONYMOUS_USER_IDENTITY)
479                        {
480                            accessResults.add(rightId == null ? accessController.getReadAccessPermissionForAnonymous(obj) : accessController.getPermissionForAnonymous(rightId, obj));
481                        }
482                        else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
483                        {
484                            accessResults.add(rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(obj) : accessController.getPermissionForAnyConnectedUser(rightId, obj));
485                        }
486                        else
487                        {
488                            accessResults.add(rightId == null ? accessController.getReadAccessPermission(userIdentity, groups, obj) : accessController.getPermission(userIdentity, groups, rightId, obj));
489                        }
490                    }
491                    else
492                    {
493                        accessResults.add(AccessResult.UNKNOWN);
494                    }
495                }
496                catch (Exception e)
497                {
498                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", controllerId, obj, e);
499                }
500            }
501        }
502        
503        return accessResults;
504    }
505    
506    /* --------------- */
507    /* HAS READ ACCESS */
508    /* --------------- */
509    
510    /**
511     * Returns true if the current user has READ access on the given object
512     * @param object The object to check the right. Can be null to search on any object.
513     * @return true if the given user has READ access on the given object
514     */
515    public boolean currentUserHasReadAccess(Object object)
516    {
517        return hasReadAccess(_currentUserProvider.getUser(), object);
518    }
519
520    /**
521     * Returns true if the given user has READ access on the given object
522     * @param userIdentity The user identity. Cannot be null.
523     * @param object The object to check the right. Can be null to search on any object.
524     * @return true if the given user has READ access on the given object
525     */
526    public boolean hasReadAccess(UserIdentity userIdentity, Object object)
527    {
528        UserIdentity objectUserIdentity = userIdentity == null ? __ANONYMOUS_USER_IDENTITY : userIdentity;
529
530        return _hasRightOrRead(objectUserIdentity, null, object) == RightResult.RIGHT_ALLOW;
531    }
532    
533    /**
534     * Returns true if the object is not restricted, i.e. an anonymous user has READ access (is allowed) on the object
535     * @param object The object to check. Cannot be null
536     * @return true if the object is restricted, i.e. an anonymous user has READ access (is allowed) on the object
537     */
538    public boolean hasAnonymousReadAccess(Object object)
539    {
540        return _hasRightOrRead(__ANONYMOUS_USER_IDENTITY, null, object) == RightResult.RIGHT_ALLOW;
541    }
542    
543    /**
544     * Returns true if any connected user has READ access allowed on the object
545     * @param object The object to check. Cannot be null
546     * @return true if any connected user has READ access allowed on the object
547     */
548    public boolean hasAnyConnectedUserReadAccess(Object object)
549    {
550        return _hasRightOrRead(__ANY_CONNECTED_USER_IDENTITY, null, object) == RightResult.RIGHT_ALLOW;
551    }
552    
553    /* ------------- */
554    /* ALLOWED USERS */
555    /* ------------- */
556
557    /**
558     * Get the list of users that have a particular right in a particular context.
559     * @param rightId The name of the right to check. Cannot be null.
560     * @param object The object to check the right. Cannot be null.
561     * @return The list of users allowed with that right as a Set of String (user identities).
562     * @throws RightsException if an error occurs.
563     */
564    public AllowedUsers getAllowedUsers(String rightId, Object object)
565    {
566        if (StringUtils.isBlank(rightId))
567        {
568            throw new RightsException("The rightId cannot be null");
569        }
570
571        return _getAllowedUsers(rightId, object);
572    }
573    
574    /**
575     * Get the users with a READ access on given object
576     * @param object The object
577     * @return The representation of allowed users
578     */
579    public AllowedUsers getReadAccessAllowedUsers(Object object)
580    {
581        return _getAllowedUsers(null, object);
582    }
583    
584    private AllowedUsers _getAllowedUsers(String rightId, Object object)
585    {
586        Optional.ofNullable(object).orElseThrow(() ->
587        {
588            return new RightsException("The object cannot be null");
589        });
590        
591        // Get the objects to check
592        Set<Object> objects = _getConvertedObjects(object, new HashSet<>());
593        
594        // For each object, retrieve the allowed and denied users/groups
595        Boolean isAnyConnectedAllowed = null; // unknown
596        Set<UserIdentity> allAllowedUsers = new HashSet<>();
597        Set<UserIdentity> allDeniedUsers = new HashSet<>();
598        Set<GroupIdentity> allAllowedGroups = new HashSet<>();
599        Set<GroupIdentity> allDeniedGroups = new HashSet<>();
600        
601        for (Object obj : objects)
602        {
603            for (String controllerId : _accessControllerEP.getExtensionsIds())
604            {
605                AccessController accessController = _accessControllerEP.getExtension(controllerId);
606                try
607                {
608                    if (accessController.isSupported(obj))
609                    {
610                        if ((rightId == null ? accessController.getReadAccessPermissionForAnonymous(obj) : accessController.getPermissionForAnonymous(rightId, obj)) == AccessResult.ANONYMOUS_ALLOWED)
611                        {
612                            // Any anonymous user is allowed
613                            return new AllowedUsers(true, false, null, null, null, null, _userManager, _groupManager, null);
614                        }
615                        
616                        AccessResult permissionForAnyConnectedUser = rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(obj) : accessController.getPermissionForAnyConnectedUser(rightId, obj);
617                        if (permissionForAnyConnectedUser == AccessResult.ANY_CONNECTED_DENIED)
618                        {
619                            // For having any connected user allowed, you need to not have the denied access for one object
620                            isAnyConnectedAllowed = Boolean.FALSE;
621                        }
622                        else if (isAnyConnectedAllowed == null && permissionForAnyConnectedUser == AccessResult.ANY_CONNECTED_ALLOWED)
623                        {
624                            isAnyConnectedAllowed = Boolean.TRUE;
625                        }
626                        
627                        Map<UserIdentity, AccessResult> permissionsByUser = rightId == null ? accessController.getReadAccessPermissionByUser(obj) : accessController.getPermissionByUser(rightId, obj);
628                        
629                        Set<UserIdentity> allowedUsersOnObj = permissionsByUser.entrySet().stream()
630                                .filter(entry -> AccessResult.USER_ALLOWED.equals(entry.getValue()))
631                                .map(Entry::getKey)
632                                .collect(Collectors.toSet());
633                        allAllowedUsers.addAll(allowedUsersOnObj);
634                        
635                        Set<UserIdentity> deniedUsersOnObj = permissionsByUser.entrySet().stream()
636                                .filter(entry -> AccessResult.USER_DENIED.equals(entry.getValue()))
637                                .map(Entry::getKey)
638                                .collect(Collectors.toSet());
639                        allDeniedUsers.addAll(deniedUsersOnObj);
640                        
641                        
642                        Map<GroupIdentity, AccessResult> permissionsByGroup = rightId == null ? accessController.getReadAccessPermissionByGroup(obj) : accessController.getPermissionByGroup(rightId, obj);
643                        
644                        Set<GroupIdentity> allowedGroupsOnObj = permissionsByGroup.entrySet().stream()
645                                .filter(entry -> AccessResult.GROUP_ALLOWED.equals(entry.getValue()))
646                                .map(Entry::getKey)
647                                .collect(Collectors.toSet());
648                        allAllowedGroups.addAll(allowedGroupsOnObj);
649                        
650                        Set<GroupIdentity> deniedGroupsOnObj = permissionsByGroup.entrySet().stream()
651                                .filter(entry -> AccessResult.GROUP_DENIED.equals(entry.getValue()))
652                                .map(Entry::getKey)
653                                .collect(Collectors.toSet());
654                        allDeniedGroups.addAll(deniedGroupsOnObj);
655                    }
656                }
657                catch (Exception e)
658                {
659                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", controllerId, obj, e);
660                }
661            }
662        }
663        
664        Request request = ContextHelper.getRequest(_context);
665        @SuppressWarnings("unchecked")
666        List<String> populationContexts = (List<String>) request.getAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR);
667        
668        // Then, return the AllowedUsers object
669        return new AllowedUsers(false, isAnyConnectedAllowed != null && isAnyConnectedAllowed.booleanValue(), allAllowedUsers, allDeniedUsers, allAllowedGroups, allDeniedGroups, _userManager, _groupManager, populationContexts != null ? new HashSet<>(populationContexts) : new HashSet<>());
670    }
671    
672    /* --------------- */
673    /* GET USER RIGHTS */
674    /* --------------- */
675    
676    /**
677     * Get the list of rights a user is allowed, on a particular object.
678     * @param userIdentity the user identity. Cannot be null.
679     * @param object The object to check the right. Cannot be null.
680     * @return The list of rights as a Set of String (id).
681     * @throws RightsException if an error occurs.
682     */
683    public Set<String> getUserRights(UserIdentity userIdentity, Object object) throws RightsException
684    {
685        if (userIdentity == null)
686        {
687            throw new RightsException("The userIdentity cannot be null");
688        }
689        else if (object == null)
690        {
691            throw new RightsException("The object cannot be null");
692        }
693        
694        // Get the objects to check
695        Set<Object> objects = _getConvertedObjects(object, new HashSet<>());
696        
697        // Retrieve groups the user belongs to
698        Set<GroupIdentity> groups = _groupManager.getUserGroups(userIdentity);
699        
700        // Gets the access by rights
701        Map<String, AccessResult> accessResultsByRight = _getAccessResultByRight(userIdentity, groups, objects);
702        
703        // Keep only positive rights
704        Set<String> allowedRights = accessResultsByRight.entrySet().stream().filter(entry -> entry.getValue().toRightResult() == RightResult.RIGHT_ALLOW).map(entry -> entry.getKey()).collect(Collectors.toSet());
705        return allowedRights;
706    }
707    
708    /**
709     * clear all caches related to RightManager and AccesControllers
710     */
711    public void clearCaches()
712    {
713        Request request = ContextHelper.getRequest(_context);
714
715        Enumeration<String> attrNames = request.getAttributeNames();
716        while (attrNames.hasMoreElements())
717        {
718            String attrName = attrNames.nextElement();
719            if (attrName != null && attrName.startsWith(AbstractCacheManager.ROLE + "$" + RightManager.ROLE + "$"))
720            {
721                Cache cache = (Cache) request.getAttribute(attrName);
722                if (cache != null)
723                {
724                    cache.invalidateAll();
725                }
726            }
727        }
728    }
729    
730    private Map<String, AccessResult> _getAccessResultByRight(UserIdentity userIdentity, Set<GroupIdentity> groups, Set<Object> objects)
731    {
732        Map<String, AccessResult> result = new HashMap<>();
733        
734        for (Object obj : objects)
735        {
736            for (String controllerId : _accessControllerEP.getExtensionsIds())
737            {
738                AccessController accessController = _accessControllerEP.getExtension(controllerId);
739                try
740                {
741                    if (accessController.isSupported(obj))
742                    {
743                        // Update the result map
744                        Map<String, AccessResult> permissionsByRight = accessController.getPermissionByRight(userIdentity, groups, obj);
745                        for (String rightId : permissionsByRight.keySet())
746                        {
747                            result.put(rightId, AccessResult.merge(result.get(rightId), permissionsByRight.get(rightId)));
748                        }
749                    }
750                }
751                catch (Exception e)
752                {
753                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", controllerId, obj, e);
754                }
755            }
756        }
757        
758        return result;
759    }
760
761    /* ------- */
762    /* PRIVATE */
763    /* ------- */
764    
765    private Set<Object> _getConvertedObjects(Object object, Set<Object> alreadyHandledObjects)
766    {
767        Set<Object> finalObjects = new HashSet<>();
768        
769        if (!alreadyHandledObjects.contains(object))
770        {
771            alreadyHandledObjects.add(object);
772            
773            Set<Object> objects = _rightContextConvertorEP.getExtensionsIds().stream()
774                    .map(_rightContextConvertorEP::getExtension)
775                    .flatMap(convertor -> convertor.convert(object).stream())
776                    .collect(Collectors.toSet());
777            
778            finalObjects.addAll(objects);
779            finalObjects.add(object);
780    
781            for (Object convertedObject : objects)
782            {
783                finalObjects.addAll(_getConvertedObjects(convertedObject, alreadyHandledObjects));
784            }
785        }
786        
787        return finalObjects;
788    }
789    
790    private Set<GroupIdentity> _getGroups(UserIdentity userIdentity)
791    {
792        if (userIdentity == __ANONYMOUS_USER_IDENTITY || userIdentity == __ANY_CONNECTED_USER_IDENTITY)
793        {
794            return Collections.EMPTY_SET;
795        }
796        else
797        {
798            Set<GroupIdentity> userGroups = _groupManager.getUserGroups(userIdentity);
799            return userGroups;
800        }
801    }
802
803    
804    
805    private RightResult _hasRightResultInFirstCache(UserIdentity userIdentity, String rightId, Object object)
806    {
807
808        Cache<Cache1Key, RightResult> mapCache = _cacheManager.get(CACHE_1);
809        Cache1Key key = Cache1Key.of(userIdentity, rightId, object);
810        if (mapCache.hasKey(key))
811        {
812            RightResult cacheResult = mapCache.get(key);
813            getLogger().debug("Find entry in cache for [{}, {}, {}] => {}", userIdentity, rightId, object, cacheResult);
814            return cacheResult;
815        }
816        getLogger().debug("Did not find entry in cache for [{}, {}, {}]", userIdentity, rightId, object);
817        return null;
818    }
819    
820    private void _putInFirstCache(UserIdentity userIdentity, String rightId, Object object, RightResult rightResult)
821    {
822        Cache<Cache1Key, RightResult> mapCache = _cacheManager.get(CACHE_1);
823        if (mapCache != null)
824        {
825            mapCache.put(Cache1Key.of(userIdentity, rightId, object), rightResult);
826        }
827    }
828    
829    private RightResult _hasRightResultInSecondCache(Set<Object> workspacesContexts, UserIdentity userIdentity, String rightId)
830    {
831        Cache<Cache2Key, RightResult> mapCache = _cacheManager.get(CACHE_2);
832        Cache2Key key = Cache2Key.of(userIdentity, rightId, workspacesContexts);
833        if (mapCache != null)
834        {
835            if (mapCache.hasKey(key))
836            {
837                RightResult cacheResult = mapCache.get(key);
838                getLogger().debug("Find entry in cache2 for [{}, {}, {}] => {}", workspacesContexts, userIdentity, rightId, cacheResult);
839                return cacheResult;
840            }
841        }
842        
843        getLogger().debug("Did not find entry in cache2 for [{}, {}, {}]", workspacesContexts, userIdentity, rightId);
844        return null;
845    }
846    
847    private void _putInSecondCache(Set<Object> workspacesContexts, UserIdentity userIdentity, String rightId, RightResult rightResult)
848    {
849        Cache<Cache2Key, RightResult> mapCache = _cacheManager.get(CACHE_2);
850
851        if (mapCache != null)
852        {
853            mapCache.put(Cache2Key.of(userIdentity, rightId, workspacesContexts), rightResult);
854        }
855    }
856    
857    /**
858     * Explain why a given user has a right on a context.
859     * 
860     * This method will return the explanations provided by every supporting access controller.
861     * Merging the access result of every explanation will return the same result as the
862     * {@link #_hasRight(UserIdentity, String, Object)} method.
863     * 
864     * @implNote this method implementation should mirror the {@link #hasRight(UserIdentity, String, Object)} method
865     * without using cache.
866     * 
867     * @param userIdentity the user identity or null for anonymous
868     * @param rightId the right to explain or null for read right.
869     * @param object the object to check right on. Can't be null.
870     * @return the list of explanation.
871     */
872    public List<AccessExplanation> explain(UserIdentity userIdentity, String rightId, Object object)
873    {
874        UserIdentity objectUserIdentity = userIdentity == null ? __ANONYMOUS_USER_IDENTITY : userIdentity;
875        
876        if (object == null)
877        {
878            throw new RightsException("The object cannot be null");
879        }
880        
881        // Retrieve groups the user belongs to
882        Set<GroupIdentity> groups = _getGroups(objectUserIdentity);
883        
884        // Get the objects to check
885        Set<Object> objects = _getConvertedObjects(object, new HashSet<>());
886        
887        return _explainAccessResults(objectUserIdentity, groups, rightId, objects);
888    }
889    
890    /**
891     * Iterate on the objects and access controller to retrieve the explanations of all the supported controllers
892     * @return the explanations
893     */
894    private List<AccessExplanation> _explainAccessResults(UserIdentity userIdentity, Set<GroupIdentity> groups, String rightId, Set<Object> objects)
895    {
896        List<AccessExplanation> accessExplanations = new ArrayList<>();
897        
898        for (Object object : objects)
899        {
900            for (String controllerId : _accessControllerEP.getExtensionsIds())
901            {
902                AccessController accessController = _accessControllerEP.getExtension(controllerId);
903                if (accessController.isSupported(object))
904                {
905                    AccessExplanation explanation = _getAccessControllerExplanation(accessController, userIdentity, groups, rightId, object);
906                    
907                    accessExplanations.add(explanation);
908                }
909            }
910        }
911        
912        return accessExplanations;
913    }
914    
915    /**
916     * Get all the permissions that concern a given user.
917     * 
918     * The permissions are organized by object context and are paired with a list of explanation
919     * returned by the {@link AccessController} that granted the permission.
920     * 
921     * @param userIdentity the user identity
922     * @return all the permissions of a user.
923     */
924    public Map<ExplanationObject, Map<Permission, List<AccessExplanation>>> getAllPermissions(UserIdentity userIdentity)
925    {
926        // Retrieve groups the user belongs to
927        Set<GroupIdentity> groups = _groupManager.getUserGroups(userIdentity);
928        
929        Map<ExplanationObject, Map<Permission, List<AccessExplanation>>> permissionsForUser = new HashMap<>();
930        
931        for (String controllerId : _accessControllerEP.getExtensionsIds())
932        {
933            AccessController accessController = _accessControllerEP.getExtension(controllerId);
934            
935            Map<ExplanationObject, Map<Permission, AccessExplanation>> controllerPermissionsForUser = accessController.explainAllPermissions(userIdentity, groups);
936            for (ExplanationObject context : controllerPermissionsForUser.keySet())
937            {
938                Map<Permission, List<AccessExplanation>> contextExplanations = permissionsForUser.computeIfAbsent(context, o -> new HashMap<Permission, List<AccessExplanation>>());
939                Map<Permission, AccessExplanation> contextPermissions = controllerPermissionsForUser.get(context);
940                for (Permission permission : contextPermissions.keySet())
941                {
942                    List<AccessExplanation> rightExplanation = contextExplanations.computeIfAbsent(permission, str -> new ArrayList<>());
943                    rightExplanation.add(contextPermissions.get(permission));
944                }
945            }
946        }
947        
948        return permissionsForUser;
949    }
950
951    private AccessExplanation _getAccessControllerExplanation(AccessController accessController, UserIdentity userIdentity, Set<GroupIdentity> groups, String rightId, Object object)
952    {
953        AccessExplanation explanation;
954        try
955        {
956            if (userIdentity == __ANONYMOUS_USER_IDENTITY)
957            {
958                explanation = rightId == null ? accessController.explainReadAccessPermissionForAnonymous(object) : accessController.explainPermissionForAnonymous(rightId, object);
959            }
960            else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
961            {
962                explanation = rightId == null ? accessController.explainReadAccessPermissionForAnyConnectedUser(object) : accessController.explainPermissionForAnyConnectedUser(rightId, object);
963            }
964            else
965            {
966                explanation = rightId == null ? accessController.explainReadAccessPermission(userIdentity, groups, object) : accessController.explainPermission(userIdentity, groups, rightId, object);
967            }
968        }
969        catch (Exception e)
970        {
971            // Try to fallback to the get permission method in case of failure
972            // An explanation without all the supported access controller
973            // may be false. By including a less detailed explanation, we make sure the
974            // general explanation stay correct
975            AccessResult result;
976            if (userIdentity == __ANONYMOUS_USER_IDENTITY)
977            {
978                result = rightId == null ? accessController.getReadAccessPermissionForAnonymous(object) : accessController.getPermissionForAnonymous(rightId, object);
979            }
980            else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
981            {
982                result = rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(object) : accessController.getPermissionForAnyConnectedUser(rightId, object);
983            }
984            else
985            {
986                result = rightId == null ? accessController.getReadAccessPermission(userIdentity, groups, object) : accessController.getPermission(userIdentity, groups, rightId, object);
987            }
988            
989            explanation = AccessController.getDefaultAccessExplanation(accessController.getId(), result);
990            getLogger().warn("An error occured while explaining access with controller '{}' for object {}. A generic explanation was returned.", accessController.getId(), object, e);
991        }
992        return explanation;
993    }
994    
995    static class Cache1Key extends AbstractCacheKey
996    {
997        Cache1Key(UserIdentity userIdentity, String rightId, Object object)
998        {
999            super(userIdentity, rightId, object);
1000        }
1001
1002        static Cache1Key of(UserIdentity userIdentity, String rightId, Object object)
1003        {
1004            return new Cache1Key(userIdentity, StringUtils.defaultString(rightId), object);
1005        }
1006        
1007    }
1008    static class Cache2Key extends AbstractCacheKey
1009    {
1010        Cache2Key(UserIdentity userIdentity, String rightId, Set<Object> workspacesContexts)
1011        {
1012            super(userIdentity, rightId, workspacesContexts);
1013        }
1014
1015        static Cache2Key of(UserIdentity userIdentity, String rightId, Set<Object> workspacesContexts)
1016        {
1017            return new Cache2Key(userIdentity, StringUtils.defaultString(rightId), workspacesContexts);
1018        }
1019        
1020    }
1021}