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.ExplanationObject;
057import org.ametys.core.right.AccessController.Permission;
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            Right right = _rightsEP.addRight("application", rightConf);
242            _webinfRights.add(right.getId());
243        }
244    }
245    
246    /**
247     * Getter for external rights ids
248     * @return a list of all the rights' ids from rights.xml file
249     */
250    public List<String> getExternalRightIds()
251    {
252        return _webinfRights;
253    }
254    
255    /**
256     * Add a right to the right EP
257     * @param id the id for the new right
258     * @param label the label for the new right
259     * @param description the description for the new right
260     * @param category the category for the new right
261     */
262    public void  addExternalRight(String id, String label, String description, String category)
263    {
264        _rightsEP.addRight(id, new I18nizableText(label), new I18nizableText(description), new I18nizableText("application", category));
265        _webinfRights.add(id);
266    }
267    
268    /**
269     * Remove the right from the rightEP
270     * @param id the right to delete's id
271     */
272    public void removeExternalRight(String id)
273    {
274        _rightsEP.removeRight(id);
275        _webinfRights.remove(id);
276    }
277    
278    /* --------- */
279    /* HAS RIGHT */
280    /* --------- */
281    
282    /**
283     * Checks a permission for the current logged user, on a given object (or context).<br>
284     * If null, it checks if there is at least one object with this permission
285     * @param rightId The name of the right to check. Cannot be null.
286     * @param object The object to check the right. Can be null to search on any object.
287     * @return {@link RightResult#RIGHT_ALLOW}, {@link RightResult#RIGHT_DENY} or {@link RightResult#RIGHT_UNKNOWN}
288     * @throws RightsException if an error occurs.
289     */
290    public RightResult currentUserHasRight(String rightId, Object object) throws RightsException
291    {
292        return hasRight(_currentUserProvider.getUser(), rightId, object);
293    }
294    
295    /**
296     * Checks a permission for a user, on a given object (or context).<br>
297     * If null, it checks if there is at least one object with this permission
298     * @param userIdentity The user identity. Can be null for anonymous
299     * @param rightId The name of the right to check. Cannot be null.
300     * @param object The object to check the right. Can be null to search on any object.
301     * @return {@link RightResult#RIGHT_ALLOW}, {@link RightResult#RIGHT_DENY} or {@link RightResult#RIGHT_UNKNOWN}
302     * @throws RightsException if an error occurs.
303     */
304    public RightResult hasRight(UserIdentity userIdentity, String rightId, Object object) throws RightsException
305    {
306        UserIdentity objectUserIdentity = userIdentity == null ? __ANONYMOUS_USER_IDENTITY : userIdentity;
307        
308        return _hasRight(objectUserIdentity, rightId, object);
309    }
310    
311    /**
312     * Gets the right result for anonymous with given right on given object context
313     * @param rightId The id of the right
314     * @param object The object to check
315     * @return the right result for anonymous with given profile on given object context
316     */
317    public RightResult hasAnonymousRight(String rightId, Object object)
318    {
319        return _hasRight(__ANONYMOUS_USER_IDENTITY, rightId, object);
320    }
321
322    /**
323     * Gets the right result for any connected user with given profile on given object context
324     * @param rightId The right id to test
325     * @param object The object to check
326     * @return the right result for any connected user with given profile on given object context
327     */
328    public RightResult hasAnyConnectedUserRight(String rightId, Object object)
329    {
330        return _hasRight(__ANY_CONNECTED_USER_IDENTITY, rightId, object);
331    }
332
333    private RightResult _hasRight(UserIdentity userIdentity, String rightId, Object object)
334    {
335        getLogger().debug("Try to determine if user {} has the right '{}' on the object context {}", userIdentity, rightId, object);
336
337        if (StringUtils.isBlank(rightId))
338        {
339            throw new RightsException("The rightId cannot be null");
340        }
341
342        return _hasRightOrRead(userIdentity, rightId, object);
343    }
344    
345    private RightResult _hasRightOrRead(UserIdentity userIdentity, String rightId, Object object)
346    {
347        if (object == null)
348        {
349            return _hasRightOrRead(userIdentity, rightId);
350        }
351        
352        // Try to retrieve in first cache (the one which manages non-null contexts)
353        RightResult cacheResult = _hasRightResultInFirstCache(userIdentity, rightId, object);
354        if (cacheResult != null)
355        {
356            return cacheResult;
357        }
358        
359        // Retrieve groups the user belongs to
360        Set<GroupIdentity> groups = _getGroups(userIdentity);
361        
362        // Get the objects to check
363        Set<Object> objects = _rightContextConvertorEP.getConvertedObjects(object);
364        
365        // Retrieve the set of AccessResult
366        Set<AccessResult> accessResults = _getAccessResults(userIdentity, groups, rightId, objects);
367        
368        // Compute access
369        AccessResult access = AccessResult.merge(accessResults);
370        
371        RightResult rightResult = access.toRightResult();
372        _putInFirstCache(userIdentity, rightId, object, rightResult);
373        
374        return rightResult;
375    }
376    
377    /**
378     * Has the user/anonymous/anyconnected the non null right on any content of the current workspace?
379     * @param userIdentity The user connecter or the value for anonymous or any connected user
380     * @param rightId The right id to test. Can be null to test read access
381     * @return The computed right result
382     */
383    private RightResult _hasRightOrRead(UserIdentity userIdentity, String rightId)
384    {
385        // Resolve contexts
386        Set<Object> workspacesContexts = _rightContextConvertorEP.getConvertedObjects("/${WorkspaceName}");
387        
388        // Try to retrieve in second cache (the one which manages null context)
389        RightResult cacheResult = _hasRightResultInSecondCache(workspacesContexts, userIdentity, rightId);
390        if (cacheResult != null)
391        {
392            return cacheResult;
393        }
394        
395        // Retrieve groups the user belongs to
396        Set<GroupIdentity> groups = _getGroups(userIdentity);
397
398        RightResult rightResult = RightResult.RIGHT_UNKNOWN;
399        for (String controllerId : _accessControllerEP.getExtensionsIds())
400        {
401            AccessController accessController = _accessControllerEP.getExtension(controllerId);
402            try
403            {
404                if (userIdentity == __ANONYMOUS_USER_IDENTITY)
405                {
406                    if (rightId == null ? accessController.hasAnonymousAnyReadAccessPermissionOnWorkspace(workspacesContexts) : accessController.hasAnonymousAnyPermissionOnWorkspace(workspacesContexts, rightId))
407                    {
408                        rightResult = RightResult.RIGHT_ALLOW;
409                        break;
410                    }
411                }
412                else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
413                {
414                    if (rightId == null ? accessController.hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(workspacesContexts) : accessController.hasAnyConnectedUserAnyPermissionOnWorkspace(workspacesContexts, rightId))
415                    {
416                        rightResult = RightResult.RIGHT_ALLOW;
417                        break;
418                    }
419                }
420                else
421                {
422                    if (rightId == null ? accessController.hasUserAnyReadAccessPermissionOnWorkspace(workspacesContexts, userIdentity, groups) : accessController.hasUserAnyPermissionOnWorkspace(workspacesContexts, userIdentity, groups, rightId))
423                    {
424                        rightResult = RightResult.RIGHT_ALLOW;
425                        break;
426                    }
427                }
428            }
429            catch (Exception e)
430            {
431                getLogger().error("An error occured with controller '{}'. Thus, this controller will be ignored.", controllerId, e);
432            }
433        }
434        
435        getLogger().debug("Right result found for [{}, {}, {}] => {}", workspacesContexts, userIdentity, rightId, rightResult);
436        _putInSecondCache(workspacesContexts, userIdentity, rightId, rightResult);
437        return rightResult;
438    }
439    
440    private Set<AccessResult> _getAccessResults(UserIdentity userIdentity, Set<GroupIdentity> groups, String rightId, Set<Object> objects)
441    {
442        Set<AccessResult> accessResults = new HashSet<>();
443        for (Object obj : objects)
444        {
445            for (AccessController accessController : _accessControllerEP.getSupportingExtensions(obj))
446            {
447                try
448                {
449                    if (userIdentity == __ANONYMOUS_USER_IDENTITY)
450                    {
451                        accessResults.add(rightId == null ? accessController.getReadAccessPermissionForAnonymous(obj) : accessController.getPermissionForAnonymous(rightId, obj));
452                    }
453                    else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
454                    {
455                        accessResults.add(rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(obj) : accessController.getPermissionForAnyConnectedUser(rightId, obj));
456                    }
457                    else
458                    {
459                        accessResults.add(rightId == null ? accessController.getReadAccessPermission(userIdentity, groups, obj) : accessController.getPermission(userIdentity, groups, rightId, obj));
460                    }
461                }
462                catch (Exception e)
463                {
464                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", accessController.getId(), obj, e);
465                }
466            }
467        }
468        
469        return accessResults;
470    }
471    
472    /* --------------- */
473    /* HAS READ ACCESS */
474    /* --------------- */
475    
476    /**
477     * Returns true if the current user has READ access on the given object
478     * @param object The object to check the right. Can be null to search on any object.
479     * @return true if the given user has READ access on the given object
480     */
481    public boolean currentUserHasReadAccess(Object object)
482    {
483        return hasReadAccess(_currentUserProvider.getUser(), object);
484    }
485
486    /**
487     * Returns true if the given user has READ access on the given object
488     * @param userIdentity The user identity. Cannot be null.
489     * @param object The object to check the right. Can be null to search on any object.
490     * @return true if the given user has READ access on the given object
491     */
492    public boolean hasReadAccess(UserIdentity userIdentity, Object object)
493    {
494        UserIdentity objectUserIdentity = userIdentity == null ? __ANONYMOUS_USER_IDENTITY : userIdentity;
495
496        return _hasRightOrRead(objectUserIdentity, null, object) == RightResult.RIGHT_ALLOW;
497    }
498    
499    /**
500     * Returns true if the object is not restricted, i.e. an anonymous user has READ access (is allowed) on the object
501     * @param object The object to check. Cannot be null
502     * @return true if the object is restricted, i.e. an anonymous user has READ access (is allowed) on the object
503     */
504    public boolean hasAnonymousReadAccess(Object object)
505    {
506        return _hasRightOrRead(__ANONYMOUS_USER_IDENTITY, null, object) == RightResult.RIGHT_ALLOW;
507    }
508    
509    /**
510     * Returns true if any connected user has READ access allowed on the object
511     * @param object The object to check. Cannot be null
512     * @return true if any connected user has READ access allowed on the object
513     */
514    public boolean hasAnyConnectedUserReadAccess(Object object)
515    {
516        return _hasRightOrRead(__ANY_CONNECTED_USER_IDENTITY, null, object) == RightResult.RIGHT_ALLOW;
517    }
518    
519    /* ------------- */
520    /* ALLOWED USERS */
521    /* ------------- */
522
523    /**
524     * Get the list of users that have a particular right in a particular context.
525     * @param rightId The name of the right to check. Cannot be null.
526     * @param object The object to check the right. Cannot be null.
527     * @return The list of users allowed with that right as a Set of String (user identities).
528     * @throws RightsException if an error occurs.
529     */
530    public AllowedUsers getAllowedUsers(String rightId, Object object)
531    {
532        if (StringUtils.isBlank(rightId))
533        {
534            throw new RightsException("The rightId cannot be null");
535        }
536
537        return _getAllowedUsers(rightId, object);
538    }
539    
540    /**
541     * Get the users with a READ access on given object
542     * @param object The object
543     * @return The representation of allowed users
544     */
545    public AllowedUsers getReadAccessAllowedUsers(Object object)
546    {
547        return _getAllowedUsers(null, object);
548    }
549    
550    private AllowedUsers _getAllowedUsers(String rightId, Object object)
551    {
552        Optional.ofNullable(object).orElseThrow(() ->
553        {
554            return new RightsException("The object cannot be null");
555        });
556        
557        // Get the objects to check
558        Set<Object> objects = _rightContextConvertorEP.getConvertedObjects(object);
559        
560        // For each object, retrieve the allowed and denied users/groups
561        Boolean isAnyConnectedAllowed = null; // unknown
562        Set<UserIdentity> allAllowedUsers = new HashSet<>();
563        Set<UserIdentity> allDeniedUsers = new HashSet<>();
564        Set<GroupIdentity> allAllowedGroups = new HashSet<>();
565        Set<GroupIdentity> allDeniedGroups = new HashSet<>();
566        
567        for (Object obj : objects)
568        {
569            for (AccessController accessController : _accessControllerEP.getSupportingExtensions(obj))
570            {
571                try
572                {
573                    if ((rightId == null ? accessController.getReadAccessPermissionForAnonymous(obj) : accessController.getPermissionForAnonymous(rightId, obj)) == AccessResult.ANONYMOUS_ALLOWED)
574                    {
575                        // Any anonymous user is allowed
576                        return new AllowedUsers(true, false, null, null, null, null, _userManager, _groupManager, null);
577                    }
578                    
579                    AccessResult permissionForAnyConnectedUser = rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(obj) : accessController.getPermissionForAnyConnectedUser(rightId, obj);
580                    if (permissionForAnyConnectedUser == AccessResult.ANY_CONNECTED_DENIED)
581                    {
582                        // For having any connected user allowed, you need to not have the denied access for one object
583                        isAnyConnectedAllowed = Boolean.FALSE;
584                    }
585                    else if (isAnyConnectedAllowed == null && permissionForAnyConnectedUser == AccessResult.ANY_CONNECTED_ALLOWED)
586                    {
587                        isAnyConnectedAllowed = Boolean.TRUE;
588                    }
589                    
590                    Map<UserIdentity, AccessResult> permissionsByUser = rightId == null ? accessController.getReadAccessPermissionByUser(obj) : accessController.getPermissionByUser(rightId, obj);
591                    
592                    Set<UserIdentity> allowedUsersOnObj = permissionsByUser.entrySet().stream()
593                            .filter(entry -> AccessResult.USER_ALLOWED.equals(entry.getValue()))
594                            .map(Entry::getKey)
595                            .collect(Collectors.toSet());
596                    allAllowedUsers.addAll(allowedUsersOnObj);
597                    
598                    Set<UserIdentity> deniedUsersOnObj = permissionsByUser.entrySet().stream()
599                            .filter(entry -> AccessResult.USER_DENIED.equals(entry.getValue()))
600                            .map(Entry::getKey)
601                            .collect(Collectors.toSet());
602                    allDeniedUsers.addAll(deniedUsersOnObj);
603                    
604                    
605                    Map<GroupIdentity, AccessResult> permissionsByGroup = rightId == null ? accessController.getReadAccessPermissionByGroup(obj) : accessController.getPermissionByGroup(rightId, obj);
606                    
607                    Set<GroupIdentity> allowedGroupsOnObj = permissionsByGroup.entrySet().stream()
608                            .filter(entry -> AccessResult.GROUP_ALLOWED.equals(entry.getValue()))
609                            .map(Entry::getKey)
610                            .collect(Collectors.toSet());
611                    allAllowedGroups.addAll(allowedGroupsOnObj);
612                    
613                    Set<GroupIdentity> deniedGroupsOnObj = permissionsByGroup.entrySet().stream()
614                            .filter(entry -> AccessResult.GROUP_DENIED.equals(entry.getValue()))
615                            .map(Entry::getKey)
616                            .collect(Collectors.toSet());
617                    allDeniedGroups.addAll(deniedGroupsOnObj);
618                }
619                catch (Exception e)
620                {
621                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", accessController.getId(), obj, e);
622                }
623            }
624        }
625        
626        Request request = ContextHelper.getRequest(_context);
627        @SuppressWarnings("unchecked")
628        List<String> populationContexts = (List<String>) request.getAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR);
629        
630        // Then, return the AllowedUsers object
631        return new AllowedUsers(false, isAnyConnectedAllowed != null && isAnyConnectedAllowed.booleanValue(), allAllowedUsers, allDeniedUsers, allAllowedGroups, allDeniedGroups, _userManager, _groupManager, populationContexts != null ? new HashSet<>(populationContexts) : new HashSet<>());
632    }
633    
634    /* --------------- */
635    /* GET USER RIGHTS */
636    /* --------------- */
637    
638    /**
639     * Get the list of rights a user is allowed, on a particular object.
640     * @param userIdentity the user identity. Cannot be null.
641     * @param object The object to check the right. Cannot be null.
642     * @return The list of rights as a Set of String (id).
643     * @throws RightsException if an error occurs.
644     */
645    public Set<String> getUserRights(UserIdentity userIdentity, Object object) throws RightsException
646    {
647        if (userIdentity == null)
648        {
649            throw new RightsException("The userIdentity cannot be null");
650        }
651        else if (object == null)
652        {
653            throw new RightsException("The object cannot be null");
654        }
655        
656        // Get the objects to check
657        Set<Object> objects = _rightContextConvertorEP.getConvertedObjects(object);
658        
659        // Retrieve groups the user belongs to
660        Set<GroupIdentity> groups = _groupManager.getUserGroups(userIdentity);
661        
662        // Gets the access by rights
663        Map<String, AccessResult> accessResultsByRight = _getAccessResultByRight(userIdentity, groups, objects);
664        
665        // Keep only positive rights
666        Set<String> allowedRights = accessResultsByRight.entrySet().stream().filter(entry -> entry.getValue().toRightResult() == RightResult.RIGHT_ALLOW).map(entry -> entry.getKey()).collect(Collectors.toSet());
667        return allowedRights;
668    }
669    
670    /**
671     * clear all caches related to RightManager and AccesControllers
672     */
673    public void clearCaches()
674    {
675        Request request = ContextHelper.getRequest(_context);
676
677        Enumeration<String> attrNames = request.getAttributeNames();
678        while (attrNames.hasMoreElements())
679        {
680            String attrName = attrNames.nextElement();
681            if (attrName != null && attrName.startsWith(AbstractCacheManager.ROLE + "$" + RightManager.ROLE + "$"))
682            {
683                Cache cache = (Cache) request.getAttribute(attrName);
684                if (cache != null)
685                {
686                    cache.invalidateAll();
687                }
688            }
689        }
690    }
691    
692    private Map<String, AccessResult> _getAccessResultByRight(UserIdentity userIdentity, Set<GroupIdentity> groups, Set<Object> objects)
693    {
694        Map<String, AccessResult> result = new HashMap<>();
695        
696        for (Object obj : objects)
697        {
698            for (AccessController accessController : _accessControllerEP.getSupportingExtensions(obj))
699            {
700                try
701                {
702                    // Update the result map
703                    Map<String, AccessResult> permissionsByRight = accessController.getPermissionByRight(userIdentity, groups, obj);
704                    for (String rightId : permissionsByRight.keySet())
705                    {
706                        result.put(rightId, AccessResult.merge(result.get(rightId), permissionsByRight.get(rightId)));
707                    }
708                }
709                catch (Exception e)
710                {
711                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", accessController.getId(), obj, e);
712                }
713            }
714        }
715        
716        return result;
717    }
718
719    /* ------- */
720    /* PRIVATE */
721    /* ------- */
722    private Set<GroupIdentity> _getGroups(UserIdentity userIdentity)
723    {
724        if (userIdentity == __ANONYMOUS_USER_IDENTITY || userIdentity == __ANY_CONNECTED_USER_IDENTITY)
725        {
726            return Collections.EMPTY_SET;
727        }
728        else
729        {
730            Set<GroupIdentity> userGroups = _groupManager.getUserGroups(userIdentity);
731            return userGroups;
732        }
733    }
734
735    
736    
737    private RightResult _hasRightResultInFirstCache(UserIdentity userIdentity, String rightId, Object object)
738    {
739
740        Cache<Cache1Key, RightResult> mapCache = _cacheManager.get(CACHE_1);
741        Cache1Key key = Cache1Key.of(userIdentity, rightId, object);
742        if (mapCache.hasKey(key))
743        {
744            RightResult cacheResult = mapCache.get(key);
745            getLogger().debug("Find entry in cache for [{}, {}, {}] => {}", userIdentity, rightId, object, cacheResult);
746            return cacheResult;
747        }
748        getLogger().debug("Did not find entry in cache for [{}, {}, {}]", userIdentity, rightId, object);
749        return null;
750    }
751    
752    private void _putInFirstCache(UserIdentity userIdentity, String rightId, Object object, RightResult rightResult)
753    {
754        Cache<Cache1Key, RightResult> mapCache = _cacheManager.get(CACHE_1);
755        if (mapCache != null)
756        {
757            mapCache.put(Cache1Key.of(userIdentity, rightId, object), rightResult);
758        }
759    }
760    
761    private RightResult _hasRightResultInSecondCache(Set<Object> workspacesContexts, UserIdentity userIdentity, String rightId)
762    {
763        Cache<Cache2Key, RightResult> mapCache = _cacheManager.get(CACHE_2);
764        Cache2Key key = Cache2Key.of(userIdentity, rightId, workspacesContexts);
765        if (mapCache != null)
766        {
767            if (mapCache.hasKey(key))
768            {
769                RightResult cacheResult = mapCache.get(key);
770                getLogger().debug("Find entry in cache2 for [{}, {}, {}] => {}", workspacesContexts, userIdentity, rightId, cacheResult);
771                return cacheResult;
772            }
773        }
774        
775        getLogger().debug("Did not find entry in cache2 for [{}, {}, {}]", workspacesContexts, userIdentity, rightId);
776        return null;
777    }
778    
779    private void _putInSecondCache(Set<Object> workspacesContexts, UserIdentity userIdentity, String rightId, RightResult rightResult)
780    {
781        Cache<Cache2Key, RightResult> mapCache = _cacheManager.get(CACHE_2);
782
783        if (mapCache != null)
784        {
785            mapCache.put(Cache2Key.of(userIdentity, rightId, workspacesContexts), rightResult);
786        }
787    }
788    
789    /**
790     * Explain why a given user has a right on a context.
791     * 
792     * This method will return the explanations provided by every supporting access controller.
793     * Merging the access result of every explanation will return the same result as the
794     * {@link #_hasRight(UserIdentity, String, Object)} method.
795     * 
796     * @implNote this method implementation should mirror the {@link #hasRight(UserIdentity, String, Object)} method
797     * without using cache.
798     * 
799     * @param userIdentity the user identity or null for anonymous
800     * @param rightId the right to explain or null for read right.
801     * @param object the object to check right on. Can't be null.
802     * @return the list of explanation.
803     */
804    public List<AccessExplanation> explain(UserIdentity userIdentity, String rightId, Object object)
805    {
806        UserIdentity objectUserIdentity = userIdentity == null ? __ANONYMOUS_USER_IDENTITY : userIdentity;
807        
808        if (object == null)
809        {
810            throw new RightsException("The object cannot be null");
811        }
812        
813        // Retrieve groups the user belongs to
814        Set<GroupIdentity> groups = _getGroups(objectUserIdentity);
815        
816        // Get the objects to check
817        Set<Object> objects = _rightContextConvertorEP.getConvertedObjects(object);
818        
819        return _explainAccessResults(objectUserIdentity, groups, rightId, objects);
820    }
821    
822    /**
823     * Iterate on the objects and access controller to retrieve the explanations of all the supported controllers
824     * @return the explanations
825     */
826    private List<AccessExplanation> _explainAccessResults(UserIdentity userIdentity, Set<GroupIdentity> groups, String rightId, Set<Object> objects)
827    {
828        List<AccessExplanation> accessExplanations = new ArrayList<>();
829        
830        for (Object object : objects)
831        {
832            for (AccessController accessController : _accessControllerEP.getSupportingExtensions(object))
833            {
834                try
835                {
836                    AccessExplanation explanation = _getAccessControllerExplanation(accessController, userIdentity, groups, rightId, object);
837                    
838                    accessExplanations.add(explanation);
839                }
840                catch (Exception e)
841                {
842                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", accessController.getId(), object, e);
843                }
844            }
845        }
846        
847        return accessExplanations;
848    }
849    
850    /**
851     * Get all the permissions that concern a given user.
852     * 
853     * The permissions are organized by object context and are paired with a list of explanation
854     * returned by the {@link AccessController} that granted the permission.
855     * 
856     * @param userIdentity the user identity
857     * @return all the permissions of a user.
858     */
859    public Map<ExplanationObject, Map<Permission, List<AccessExplanation>>> getAllPermissions(UserIdentity userIdentity)
860    {
861        // Resolve contexts
862        Set<Object> workspacesContexts = _rightContextConvertorEP.getConvertedObjects("/${WorkspaceName}");
863        
864        // Retrieve groups the user belongs to
865        Set<GroupIdentity> groups = _groupManager.getUserGroups(userIdentity);
866        
867        Map<ExplanationObject, Map<Permission, List<AccessExplanation>>> permissionsForUser = new HashMap<>();
868        
869        for (String controllerId : _accessControllerEP.getExtensionsIds())
870        {
871            AccessController accessController = _accessControllerEP.getExtension(controllerId);
872            
873            try
874            {
875                Map<ExplanationObject, Map<Permission, AccessExplanation>> controllerPermissionsForUser = accessController.explainAllPermissions(userIdentity, groups, workspacesContexts);
876                for (ExplanationObject context : controllerPermissionsForUser.keySet())
877                {
878                    Map<Permission, List<AccessExplanation>> contextExplanations = permissionsForUser.computeIfAbsent(context, o -> new HashMap<>());
879                    Map<Permission, AccessExplanation> contextPermissions = controllerPermissionsForUser.get(context);
880                    for (Permission permission : contextPermissions.keySet())
881                    {
882                        List<AccessExplanation> rightExplanation = contextExplanations.computeIfAbsent(permission, str -> new ArrayList<>());
883                        rightExplanation.add(contextPermissions.get(permission));
884                    }
885                }
886            }
887            catch (Exception e)
888            {
889                getLogger().error("An error occured while retrieving the permission for the controller '" + controllerId + "'. The controller will be ignored.", e);
890            }
891        }
892        
893        return permissionsForUser;
894    }
895
896    private AccessExplanation _getAccessControllerExplanation(AccessController accessController, UserIdentity userIdentity, Set<GroupIdentity> groups, String rightId, Object object)
897    {
898        AccessExplanation explanation;
899        try
900        {
901            if (userIdentity == __ANONYMOUS_USER_IDENTITY)
902            {
903                explanation = rightId == null ? accessController.explainReadAccessPermissionForAnonymous(object) : accessController.explainPermissionForAnonymous(rightId, object);
904            }
905            else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
906            {
907                explanation = rightId == null ? accessController.explainReadAccessPermissionForAnyConnectedUser(object) : accessController.explainPermissionForAnyConnectedUser(rightId, object);
908            }
909            else
910            {
911                explanation = rightId == null ? accessController.explainReadAccessPermission(userIdentity, groups, object) : accessController.explainPermission(userIdentity, groups, rightId, object);
912            }
913        }
914        catch (Exception e)
915        {
916            // Try to fallback to the get permission method in case of failure
917            // An explanation without all the supported access controller
918            // may be false. By including a less detailed explanation, we make sure the
919            // general explanation stay correct
920            AccessResult result;
921            if (userIdentity == __ANONYMOUS_USER_IDENTITY)
922            {
923                result = rightId == null ? accessController.getReadAccessPermissionForAnonymous(object) : accessController.getPermissionForAnonymous(rightId, object);
924            }
925            else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
926            {
927                result = rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(object) : accessController.getPermissionForAnyConnectedUser(rightId, object);
928            }
929            else
930            {
931                result = rightId == null ? accessController.getReadAccessPermission(userIdentity, groups, object) : accessController.getPermission(userIdentity, groups, rightId, object);
932            }
933            
934            explanation = AccessController.getDefaultAccessExplanation(accessController.getId(), result);
935            getLogger().warn("An error occured while explaining access with controller '{}' for object {}. A generic explanation was returned.", accessController.getId(), object, e);
936        }
937        return explanation;
938    }
939    
940    /**
941     * Get the detail of all the permission granted on a given context
942     * The permissions are returned organized in 4 different groups:
943     * permission granted to anonymous, any connected, groups, users.
944     * 
945     * @param object the object to check permissions
946     * @return the permissions on context
947     */
948    public ContextPermissions explainAllPermissions(Object object)
949    {
950        Map<Permission, List<AccessExplanation>> permissionsForAnonymous = new HashMap<>();
951        Map<Permission, List<AccessExplanation>> permissionsForAnyConnected = new HashMap<>();
952        Map<UserIdentity, Map<Permission, List<AccessExplanation>>> permissionsByUser = new HashMap<>();
953        Map<GroupIdentity, Map<Permission, List<AccessExplanation>>> permissionsByGroup = new HashMap<>();
954
955        // Get the objects to check
956        Set<Object> objects = _rightContextConvertorEP.getConvertedObjects(object);
957        
958        for (Object objectToCheck : objects)
959        {
960            for (String controllerId : _accessControllerEP.getExtensionsIds())
961            {
962                AccessController accessController = _accessControllerEP.getExtension(controllerId);
963                try
964                {
965                    if (accessController.supports(objectToCheck))
966                    {
967                        Map<Permission, AccessExplanation> controllerPermissionsForAnonymous = accessController.explainAllPermissionsForAnonymous(objectToCheck);
968                        for (Permission permission : controllerPermissionsForAnonymous.keySet())
969                        {
970                            List<AccessExplanation> rightExplanation = permissionsForAnonymous.computeIfAbsent(permission, str -> new ArrayList<>());
971                            rightExplanation.add(controllerPermissionsForAnonymous.get(permission));
972                        }
973                        
974                        Map<Permission, AccessExplanation> controllerPermissionsForAnyConnected = accessController.explainAllPermissionsForAnyConnected(objectToCheck);
975                        for (Permission permission : controllerPermissionsForAnyConnected.keySet())
976                        {
977                            List<AccessExplanation> rightExplanation = permissionsForAnyConnected.computeIfAbsent(permission, str -> new ArrayList<>());
978                            rightExplanation.add(controllerPermissionsForAnyConnected.get(permission));
979                        }
980                        
981                        Map<UserIdentity, Map<Permission, AccessExplanation>> controllerPermissionsByUser = accessController.explainAllPermissionsByUser(objectToCheck);
982                        for (UserIdentity user: controllerPermissionsByUser.keySet())
983                        {
984                            Map<Permission, List<AccessExplanation>> permissionsForUser = permissionsByUser.computeIfAbsent(user, u -> new HashMap<>());
985                            Map<Permission, AccessExplanation> controllerPermissionsForUser = controllerPermissionsByUser.get(user);
986                            for (Permission permission : controllerPermissionsForUser.keySet())
987                            {
988                                List<AccessExplanation> rightExplanation = permissionsForUser.computeIfAbsent(permission, str -> new ArrayList<>());
989                                rightExplanation.add(controllerPermissionsForUser.get(permission));
990                            }
991                        }
992                        
993                        Map<GroupIdentity, Map<Permission, AccessExplanation>> controllerPermissionsByGroup = accessController.explainAllPermissionsByGroup(objectToCheck);
994                        for (GroupIdentity group: controllerPermissionsByGroup.keySet())
995                        {
996                            Map<Permission, List<AccessExplanation>> permissionsForUser = permissionsByGroup.computeIfAbsent(group, u -> new HashMap<>());
997                            Map<Permission, AccessExplanation> controllerPermissionsForGroup = controllerPermissionsByGroup.get(group);
998                            for (Permission permission : controllerPermissionsForGroup.keySet())
999                            {
1000                                List<AccessExplanation> rightExplanation = permissionsForUser.computeIfAbsent(permission, str -> new ArrayList<>());
1001                                rightExplanation.add(controllerPermissionsForGroup.get(permission));
1002                            }
1003                        }
1004                    }
1005                }
1006                catch (Exception e)
1007                {
1008                    getLogger().error("An error occured with controller '{}' while explaining permission for '{}'. Thus, this controller will be ignored.", controllerId, objectToCheck.toString(), e);
1009                }
1010            }
1011        }
1012        
1013        return new ContextPermissions(permissionsForAnonymous, permissionsForAnyConnected, permissionsByUser, permissionsByGroup);
1014    }
1015    
1016    /**
1017     * Record to describe all the permissions granted on a context
1018     * @param permissionsForAnonymous the permissions for anonymous user
1019     * @param permissionsForAnyConnected the permissions for any connected user
1020     * @param permissionsByUser the permissions for specific users by user identity
1021     * @param permissionsByGroup the permissions for specific groups by group identity
1022     */
1023    public record ContextPermissions(
1024            Map<Permission, List<AccessExplanation>> permissionsForAnonymous,
1025            Map<Permission, List<AccessExplanation>> permissionsForAnyConnected,
1026            Map<UserIdentity, Map<Permission, List<AccessExplanation>>> permissionsByUser,
1027            Map<GroupIdentity, Map<Permission, List<AccessExplanation>>> permissionsByGroup) { }
1028    
1029    static class Cache1Key extends AbstractCacheKey
1030    {
1031        Cache1Key(UserIdentity userIdentity, String rightId, Object object)
1032        {
1033            super(userIdentity, rightId, object);
1034        }
1035
1036        static Cache1Key of(UserIdentity userIdentity, String rightId, Object object)
1037        {
1038            return new Cache1Key(userIdentity, StringUtils.defaultString(rightId), object);
1039        }
1040        
1041    }
1042    static class Cache2Key extends AbstractCacheKey
1043    {
1044        Cache2Key(UserIdentity userIdentity, String rightId, Set<Object> workspacesContexts)
1045        {
1046            super(userIdentity, rightId, workspacesContexts);
1047        }
1048
1049        static Cache2Key of(UserIdentity userIdentity, String rightId, Set<Object> workspacesContexts)
1050        {
1051            return new Cache2Key(userIdentity, StringUtils.defaultString(rightId), workspacesContexts);
1052        }
1053        
1054    }
1055}