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            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 (AccessController accessController : _accessControllerEP.getSupportingExtensions(obj))
472            {
473                try
474                {
475                    if (userIdentity == __ANONYMOUS_USER_IDENTITY)
476                    {
477                        accessResults.add(rightId == null ? accessController.getReadAccessPermissionForAnonymous(obj) : accessController.getPermissionForAnonymous(rightId, obj));
478                    }
479                    else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
480                    {
481                        accessResults.add(rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(obj) : accessController.getPermissionForAnyConnectedUser(rightId, obj));
482                    }
483                    else
484                    {
485                        accessResults.add(rightId == null ? accessController.getReadAccessPermission(userIdentity, groups, obj) : accessController.getPermission(userIdentity, groups, rightId, obj));
486                    }
487                }
488                catch (Exception e)
489                {
490                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", accessController.getId(), obj, e);
491                }
492            }
493        }
494        
495        return accessResults;
496    }
497    
498    /* --------------- */
499    /* HAS READ ACCESS */
500    /* --------------- */
501    
502    /**
503     * Returns true if the current user has READ access on the given object
504     * @param object The object to check the right. Can be null to search on any object.
505     * @return true if the given user has READ access on the given object
506     */
507    public boolean currentUserHasReadAccess(Object object)
508    {
509        return hasReadAccess(_currentUserProvider.getUser(), object);
510    }
511
512    /**
513     * Returns true if the given user has READ access on the given object
514     * @param userIdentity The user identity. Cannot be null.
515     * @param object The object to check the right. Can be null to search on any object.
516     * @return true if the given user has READ access on the given object
517     */
518    public boolean hasReadAccess(UserIdentity userIdentity, Object object)
519    {
520        UserIdentity objectUserIdentity = userIdentity == null ? __ANONYMOUS_USER_IDENTITY : userIdentity;
521
522        return _hasRightOrRead(objectUserIdentity, null, object) == RightResult.RIGHT_ALLOW;
523    }
524    
525    /**
526     * Returns true if the object is not restricted, i.e. an anonymous user has READ access (is allowed) on the object
527     * @param object The object to check. Cannot be null
528     * @return true if the object is restricted, i.e. an anonymous user has READ access (is allowed) on the object
529     */
530    public boolean hasAnonymousReadAccess(Object object)
531    {
532        return _hasRightOrRead(__ANONYMOUS_USER_IDENTITY, null, object) == RightResult.RIGHT_ALLOW;
533    }
534    
535    /**
536     * Returns true if any connected user has READ access allowed on the object
537     * @param object The object to check. Cannot be null
538     * @return true if any connected user has READ access allowed on the object
539     */
540    public boolean hasAnyConnectedUserReadAccess(Object object)
541    {
542        return _hasRightOrRead(__ANY_CONNECTED_USER_IDENTITY, null, object) == RightResult.RIGHT_ALLOW;
543    }
544    
545    /* ------------- */
546    /* ALLOWED USERS */
547    /* ------------- */
548
549    /**
550     * Get the list of users that have a particular right in a particular context.
551     * @param rightId The name of the right to check. Cannot be null.
552     * @param object The object to check the right. Cannot be null.
553     * @return The list of users allowed with that right as a Set of String (user identities).
554     * @throws RightsException if an error occurs.
555     */
556    public AllowedUsers getAllowedUsers(String rightId, Object object)
557    {
558        if (StringUtils.isBlank(rightId))
559        {
560            throw new RightsException("The rightId cannot be null");
561        }
562
563        return _getAllowedUsers(rightId, object);
564    }
565    
566    /**
567     * Get the users with a READ access on given object
568     * @param object The object
569     * @return The representation of allowed users
570     */
571    public AllowedUsers getReadAccessAllowedUsers(Object object)
572    {
573        return _getAllowedUsers(null, object);
574    }
575    
576    private AllowedUsers _getAllowedUsers(String rightId, Object object)
577    {
578        Optional.ofNullable(object).orElseThrow(() ->
579        {
580            return new RightsException("The object cannot be null");
581        });
582        
583        // Get the objects to check
584        Set<Object> objects = _getConvertedObjects(object, new HashSet<>());
585        
586        // For each object, retrieve the allowed and denied users/groups
587        Boolean isAnyConnectedAllowed = null; // unknown
588        Set<UserIdentity> allAllowedUsers = new HashSet<>();
589        Set<UserIdentity> allDeniedUsers = new HashSet<>();
590        Set<GroupIdentity> allAllowedGroups = new HashSet<>();
591        Set<GroupIdentity> allDeniedGroups = new HashSet<>();
592        
593        for (Object obj : objects)
594        {
595            for (AccessController accessController : _accessControllerEP.getSupportingExtensions(obj))
596            {
597                try
598                {
599                    if ((rightId == null ? accessController.getReadAccessPermissionForAnonymous(obj) : accessController.getPermissionForAnonymous(rightId, obj)) == AccessResult.ANONYMOUS_ALLOWED)
600                    {
601                        // Any anonymous user is allowed
602                        return new AllowedUsers(true, false, null, null, null, null, _userManager, _groupManager, null);
603                    }
604                    
605                    AccessResult permissionForAnyConnectedUser = rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(obj) : accessController.getPermissionForAnyConnectedUser(rightId, obj);
606                    if (permissionForAnyConnectedUser == AccessResult.ANY_CONNECTED_DENIED)
607                    {
608                        // For having any connected user allowed, you need to not have the denied access for one object
609                        isAnyConnectedAllowed = Boolean.FALSE;
610                    }
611                    else if (isAnyConnectedAllowed == null && permissionForAnyConnectedUser == AccessResult.ANY_CONNECTED_ALLOWED)
612                    {
613                        isAnyConnectedAllowed = Boolean.TRUE;
614                    }
615                    
616                    Map<UserIdentity, AccessResult> permissionsByUser = rightId == null ? accessController.getReadAccessPermissionByUser(obj) : accessController.getPermissionByUser(rightId, obj);
617                    
618                    Set<UserIdentity> allowedUsersOnObj = permissionsByUser.entrySet().stream()
619                            .filter(entry -> AccessResult.USER_ALLOWED.equals(entry.getValue()))
620                            .map(Entry::getKey)
621                            .collect(Collectors.toSet());
622                    allAllowedUsers.addAll(allowedUsersOnObj);
623                    
624                    Set<UserIdentity> deniedUsersOnObj = permissionsByUser.entrySet().stream()
625                            .filter(entry -> AccessResult.USER_DENIED.equals(entry.getValue()))
626                            .map(Entry::getKey)
627                            .collect(Collectors.toSet());
628                    allDeniedUsers.addAll(deniedUsersOnObj);
629                    
630                    
631                    Map<GroupIdentity, AccessResult> permissionsByGroup = rightId == null ? accessController.getReadAccessPermissionByGroup(obj) : accessController.getPermissionByGroup(rightId, obj);
632                    
633                    Set<GroupIdentity> allowedGroupsOnObj = permissionsByGroup.entrySet().stream()
634                            .filter(entry -> AccessResult.GROUP_ALLOWED.equals(entry.getValue()))
635                            .map(Entry::getKey)
636                            .collect(Collectors.toSet());
637                    allAllowedGroups.addAll(allowedGroupsOnObj);
638                    
639                    Set<GroupIdentity> deniedGroupsOnObj = permissionsByGroup.entrySet().stream()
640                            .filter(entry -> AccessResult.GROUP_DENIED.equals(entry.getValue()))
641                            .map(Entry::getKey)
642                            .collect(Collectors.toSet());
643                    allDeniedGroups.addAll(deniedGroupsOnObj);
644                }
645                catch (Exception e)
646                {
647                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", accessController.getId(), obj, e);
648                }
649            }
650        }
651        
652        Request request = ContextHelper.getRequest(_context);
653        @SuppressWarnings("unchecked")
654        List<String> populationContexts = (List<String>) request.getAttribute(PopulationContextHelper.POPULATION_CONTEXTS_REQUEST_ATTR);
655        
656        // Then, return the AllowedUsers object
657        return new AllowedUsers(false, isAnyConnectedAllowed != null && isAnyConnectedAllowed.booleanValue(), allAllowedUsers, allDeniedUsers, allAllowedGroups, allDeniedGroups, _userManager, _groupManager, populationContexts != null ? new HashSet<>(populationContexts) : new HashSet<>());
658    }
659    
660    /* --------------- */
661    /* GET USER RIGHTS */
662    /* --------------- */
663    
664    /**
665     * Get the list of rights a user is allowed, on a particular object.
666     * @param userIdentity the user identity. Cannot be null.
667     * @param object The object to check the right. Cannot be null.
668     * @return The list of rights as a Set of String (id).
669     * @throws RightsException if an error occurs.
670     */
671    public Set<String> getUserRights(UserIdentity userIdentity, Object object) throws RightsException
672    {
673        if (userIdentity == null)
674        {
675            throw new RightsException("The userIdentity cannot be null");
676        }
677        else if (object == null)
678        {
679            throw new RightsException("The object cannot be null");
680        }
681        
682        // Get the objects to check
683        Set<Object> objects = _getConvertedObjects(object, new HashSet<>());
684        
685        // Retrieve groups the user belongs to
686        Set<GroupIdentity> groups = _groupManager.getUserGroups(userIdentity);
687        
688        // Gets the access by rights
689        Map<String, AccessResult> accessResultsByRight = _getAccessResultByRight(userIdentity, groups, objects);
690        
691        // Keep only positive rights
692        Set<String> allowedRights = accessResultsByRight.entrySet().stream().filter(entry -> entry.getValue().toRightResult() == RightResult.RIGHT_ALLOW).map(entry -> entry.getKey()).collect(Collectors.toSet());
693        return allowedRights;
694    }
695    
696    /**
697     * clear all caches related to RightManager and AccesControllers
698     */
699    public void clearCaches()
700    {
701        Request request = ContextHelper.getRequest(_context);
702
703        Enumeration<String> attrNames = request.getAttributeNames();
704        while (attrNames.hasMoreElements())
705        {
706            String attrName = attrNames.nextElement();
707            if (attrName != null && attrName.startsWith(AbstractCacheManager.ROLE + "$" + RightManager.ROLE + "$"))
708            {
709                Cache cache = (Cache) request.getAttribute(attrName);
710                if (cache != null)
711                {
712                    cache.invalidateAll();
713                }
714            }
715        }
716    }
717    
718    private Map<String, AccessResult> _getAccessResultByRight(UserIdentity userIdentity, Set<GroupIdentity> groups, Set<Object> objects)
719    {
720        Map<String, AccessResult> result = new HashMap<>();
721        
722        for (Object obj : objects)
723        {
724            for (AccessController accessController : _accessControllerEP.getSupportingExtensions(obj))
725            {
726                try
727                {
728                    // Update the result map
729                    Map<String, AccessResult> permissionsByRight = accessController.getPermissionByRight(userIdentity, groups, obj);
730                    for (String rightId : permissionsByRight.keySet())
731                    {
732                        result.put(rightId, AccessResult.merge(result.get(rightId), permissionsByRight.get(rightId)));
733                    }
734                }
735                catch (Exception e)
736                {
737                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", accessController.getId(), obj, e);
738                }
739            }
740        }
741        
742        return result;
743    }
744
745    /* ------- */
746    /* PRIVATE */
747    /* ------- */
748    
749    private Set<Object> _getConvertedObjects(Object object, Set<Object> alreadyHandledObjects)
750    {
751        Set<Object> finalObjects = new HashSet<>();
752        
753        if (!alreadyHandledObjects.contains(object))
754        {
755            alreadyHandledObjects.add(object);
756            
757            Set<Object> objects = _rightContextConvertorEP.getExtensionsIds().stream()
758                    .map(_rightContextConvertorEP::getExtension)
759                    .flatMap(convertor -> convertor.convert(object).stream())
760                    .collect(Collectors.toSet());
761            
762            finalObjects.addAll(objects);
763            finalObjects.add(object);
764    
765            for (Object convertedObject : objects)
766            {
767                finalObjects.addAll(_getConvertedObjects(convertedObject, alreadyHandledObjects));
768            }
769        }
770        
771        return finalObjects;
772    }
773    
774    private Set<GroupIdentity> _getGroups(UserIdentity userIdentity)
775    {
776        if (userIdentity == __ANONYMOUS_USER_IDENTITY || userIdentity == __ANY_CONNECTED_USER_IDENTITY)
777        {
778            return Collections.EMPTY_SET;
779        }
780        else
781        {
782            Set<GroupIdentity> userGroups = _groupManager.getUserGroups(userIdentity);
783            return userGroups;
784        }
785    }
786
787    
788    
789    private RightResult _hasRightResultInFirstCache(UserIdentity userIdentity, String rightId, Object object)
790    {
791
792        Cache<Cache1Key, RightResult> mapCache = _cacheManager.get(CACHE_1);
793        Cache1Key key = Cache1Key.of(userIdentity, rightId, object);
794        if (mapCache.hasKey(key))
795        {
796            RightResult cacheResult = mapCache.get(key);
797            getLogger().debug("Find entry in cache for [{}, {}, {}] => {}", userIdentity, rightId, object, cacheResult);
798            return cacheResult;
799        }
800        getLogger().debug("Did not find entry in cache for [{}, {}, {}]", userIdentity, rightId, object);
801        return null;
802    }
803    
804    private void _putInFirstCache(UserIdentity userIdentity, String rightId, Object object, RightResult rightResult)
805    {
806        Cache<Cache1Key, RightResult> mapCache = _cacheManager.get(CACHE_1);
807        if (mapCache != null)
808        {
809            mapCache.put(Cache1Key.of(userIdentity, rightId, object), rightResult);
810        }
811    }
812    
813    private RightResult _hasRightResultInSecondCache(Set<Object> workspacesContexts, UserIdentity userIdentity, String rightId)
814    {
815        Cache<Cache2Key, RightResult> mapCache = _cacheManager.get(CACHE_2);
816        Cache2Key key = Cache2Key.of(userIdentity, rightId, workspacesContexts);
817        if (mapCache != null)
818        {
819            if (mapCache.hasKey(key))
820            {
821                RightResult cacheResult = mapCache.get(key);
822                getLogger().debug("Find entry in cache2 for [{}, {}, {}] => {}", workspacesContexts, userIdentity, rightId, cacheResult);
823                return cacheResult;
824            }
825        }
826        
827        getLogger().debug("Did not find entry in cache2 for [{}, {}, {}]", workspacesContexts, userIdentity, rightId);
828        return null;
829    }
830    
831    private void _putInSecondCache(Set<Object> workspacesContexts, UserIdentity userIdentity, String rightId, RightResult rightResult)
832    {
833        Cache<Cache2Key, RightResult> mapCache = _cacheManager.get(CACHE_2);
834
835        if (mapCache != null)
836        {
837            mapCache.put(Cache2Key.of(userIdentity, rightId, workspacesContexts), rightResult);
838        }
839    }
840    
841    /**
842     * Explain why a given user has a right on a context.
843     * 
844     * This method will return the explanations provided by every supporting access controller.
845     * Merging the access result of every explanation will return the same result as the
846     * {@link #_hasRight(UserIdentity, String, Object)} method.
847     * 
848     * @implNote this method implementation should mirror the {@link #hasRight(UserIdentity, String, Object)} method
849     * without using cache.
850     * 
851     * @param userIdentity the user identity or null for anonymous
852     * @param rightId the right to explain or null for read right.
853     * @param object the object to check right on. Can't be null.
854     * @return the list of explanation.
855     */
856    public List<AccessExplanation> explain(UserIdentity userIdentity, String rightId, Object object)
857    {
858        UserIdentity objectUserIdentity = userIdentity == null ? __ANONYMOUS_USER_IDENTITY : userIdentity;
859        
860        if (object == null)
861        {
862            throw new RightsException("The object cannot be null");
863        }
864        
865        // Retrieve groups the user belongs to
866        Set<GroupIdentity> groups = _getGroups(objectUserIdentity);
867        
868        // Get the objects to check
869        Set<Object> objects = _getConvertedObjects(object, new HashSet<>());
870        
871        return _explainAccessResults(objectUserIdentity, groups, rightId, objects);
872    }
873    
874    /**
875     * Iterate on the objects and access controller to retrieve the explanations of all the supported controllers
876     * @return the explanations
877     */
878    private List<AccessExplanation> _explainAccessResults(UserIdentity userIdentity, Set<GroupIdentity> groups, String rightId, Set<Object> objects)
879    {
880        List<AccessExplanation> accessExplanations = new ArrayList<>();
881        
882        for (Object object : objects)
883        {
884            for (AccessController accessController : _accessControllerEP.getSupportingExtensions(object))
885            {
886                try
887                {
888                    AccessExplanation explanation = _getAccessControllerExplanation(accessController, userIdentity, groups, rightId, object);
889                    
890                    accessExplanations.add(explanation);
891                }
892                catch (Exception e)
893                {
894                    getLogger().error("An error occured with controller '{}' for object {}. Thus, this controller will be ignored.", accessController.getId(), object, e);
895                }
896            }
897        }
898        
899        return accessExplanations;
900    }
901    
902    /**
903     * Get all the permissions that concern a given user.
904     * 
905     * The permissions are organized by object context and are paired with a list of explanation
906     * returned by the {@link AccessController} that granted the permission.
907     * 
908     * @param userIdentity the user identity
909     * @return all the permissions of a user.
910     */
911    public Map<ExplanationObject, Map<Permission, List<AccessExplanation>>> getAllPermissions(UserIdentity userIdentity)
912    {
913        // Retrieve groups the user belongs to
914        Set<GroupIdentity> groups = _groupManager.getUserGroups(userIdentity);
915        
916        Map<ExplanationObject, Map<Permission, List<AccessExplanation>>> permissionsForUser = new HashMap<>();
917        
918        for (String controllerId : _accessControllerEP.getExtensionsIds())
919        {
920            AccessController accessController = _accessControllerEP.getExtension(controllerId);
921            
922            try
923            {
924                Map<ExplanationObject, Map<Permission, AccessExplanation>> controllerPermissionsForUser = accessController.explainAllPermissions(userIdentity, groups);
925                for (ExplanationObject context : controllerPermissionsForUser.keySet())
926                {
927                    Map<Permission, List<AccessExplanation>> contextExplanations = permissionsForUser.computeIfAbsent(context, o -> new HashMap<>());
928                    Map<Permission, AccessExplanation> contextPermissions = controllerPermissionsForUser.get(context);
929                    for (Permission permission : contextPermissions.keySet())
930                    {
931                        List<AccessExplanation> rightExplanation = contextExplanations.computeIfAbsent(permission, str -> new ArrayList<>());
932                        rightExplanation.add(contextPermissions.get(permission));
933                    }
934                }
935            }
936            catch (Exception e)
937            {
938                getLogger().error("An error occured while retrieving the permission for the controller '" + controllerId + "'. The controller will be ignored.", e);
939            }
940        }
941        
942        return permissionsForUser;
943    }
944
945    private AccessExplanation _getAccessControllerExplanation(AccessController accessController, UserIdentity userIdentity, Set<GroupIdentity> groups, String rightId, Object object)
946    {
947        AccessExplanation explanation;
948        try
949        {
950            if (userIdentity == __ANONYMOUS_USER_IDENTITY)
951            {
952                explanation = rightId == null ? accessController.explainReadAccessPermissionForAnonymous(object) : accessController.explainPermissionForAnonymous(rightId, object);
953            }
954            else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
955            {
956                explanation = rightId == null ? accessController.explainReadAccessPermissionForAnyConnectedUser(object) : accessController.explainPermissionForAnyConnectedUser(rightId, object);
957            }
958            else
959            {
960                explanation = rightId == null ? accessController.explainReadAccessPermission(userIdentity, groups, object) : accessController.explainPermission(userIdentity, groups, rightId, object);
961            }
962        }
963        catch (Exception e)
964        {
965            // Try to fallback to the get permission method in case of failure
966            // An explanation without all the supported access controller
967            // may be false. By including a less detailed explanation, we make sure the
968            // general explanation stay correct
969            AccessResult result;
970            if (userIdentity == __ANONYMOUS_USER_IDENTITY)
971            {
972                result = rightId == null ? accessController.getReadAccessPermissionForAnonymous(object) : accessController.getPermissionForAnonymous(rightId, object);
973            }
974            else if (userIdentity == __ANY_CONNECTED_USER_IDENTITY)
975            {
976                result = rightId == null ? accessController.getReadAccessPermissionForAnyConnectedUser(object) : accessController.getPermissionForAnyConnectedUser(rightId, object);
977            }
978            else
979            {
980                result = rightId == null ? accessController.getReadAccessPermission(userIdentity, groups, object) : accessController.getPermission(userIdentity, groups, rightId, object);
981            }
982            
983            explanation = AccessController.getDefaultAccessExplanation(accessController.getId(), result);
984            getLogger().warn("An error occured while explaining access with controller '{}' for object {}. A generic explanation was returned.", accessController.getId(), object, e);
985        }
986        return explanation;
987    }
988    
989    /**
990     * Get the detail of all the permission granted on a given context
991     * The permissions are returned organized in 4 different groups:
992     * permission granted to anonymous, any connected, groups, users.
993     * 
994     * @param object the object to check permissions
995     * @return the permissions on context
996     */
997    public ContextPermissions explainAllPermissions(Object object)
998    {
999        Map<Permission, List<AccessExplanation>> permissionsForAnonymous = new HashMap<>();
1000        Map<Permission, List<AccessExplanation>> permissionsForAnyConnected = new HashMap<>();
1001        Map<UserIdentity, Map<Permission, List<AccessExplanation>>> permissionsByUser = new HashMap<>();
1002        Map<GroupIdentity, Map<Permission, List<AccessExplanation>>> permissionsByGroup = new HashMap<>();
1003
1004        // Get the objects to check
1005        Set<Object> objects = _getConvertedObjects(object, new HashSet<>());
1006        
1007        for (Object objectToCheck : objects)
1008        {
1009            for (String controllerId : _accessControllerEP.getExtensionsIds())
1010            {
1011                AccessController accessController = _accessControllerEP.getExtension(controllerId);
1012                try
1013                {
1014                    if (accessController.supports(objectToCheck))
1015                    {
1016                        Map<Permission, AccessExplanation> controllerPermissionsForAnonymous = accessController.explainAllPermissionsForAnonymous(objectToCheck);
1017                        for (Permission permission : controllerPermissionsForAnonymous.keySet())
1018                        {
1019                            List<AccessExplanation> rightExplanation = permissionsForAnonymous.computeIfAbsent(permission, str -> new ArrayList<>());
1020                            rightExplanation.add(controllerPermissionsForAnonymous.get(permission));
1021                        }
1022                        
1023                        Map<Permission, AccessExplanation> controllerPermissionsForAnyConnected = accessController.explainAllPermissionsForAnyConnected(objectToCheck);
1024                        for (Permission permission : controllerPermissionsForAnyConnected.keySet())
1025                        {
1026                            List<AccessExplanation> rightExplanation = permissionsForAnyConnected.computeIfAbsent(permission, str -> new ArrayList<>());
1027                            rightExplanation.add(controllerPermissionsForAnyConnected.get(permission));
1028                        }
1029                        
1030                        Map<UserIdentity, Map<Permission, AccessExplanation>> controllerPermissionsByUser = accessController.explainAllPermissionsByUser(objectToCheck);
1031                        for (UserIdentity user: controllerPermissionsByUser.keySet())
1032                        {
1033                            Map<Permission, List<AccessExplanation>> permissionsForUser = permissionsByUser.computeIfAbsent(user, u -> new HashMap<>());
1034                            Map<Permission, AccessExplanation> controllerPermissionsForUser = controllerPermissionsByUser.get(user);
1035                            for (Permission permission : controllerPermissionsForUser.keySet())
1036                            {
1037                                List<AccessExplanation> rightExplanation = permissionsForUser.computeIfAbsent(permission, str -> new ArrayList<>());
1038                                rightExplanation.add(controllerPermissionsForUser.get(permission));
1039                            }
1040                        }
1041                        
1042                        Map<GroupIdentity, Map<Permission, AccessExplanation>> controllerPermissionsByGroup = accessController.explainAllPermissionsByGroup(objectToCheck);
1043                        for (GroupIdentity group: controllerPermissionsByGroup.keySet())
1044                        {
1045                            Map<Permission, List<AccessExplanation>> permissionsForUser = permissionsByGroup.computeIfAbsent(group, u -> new HashMap<>());
1046                            Map<Permission, AccessExplanation> controllerPermissionsForGroup = controllerPermissionsByGroup.get(group);
1047                            for (Permission permission : controllerPermissionsForGroup.keySet())
1048                            {
1049                                List<AccessExplanation> rightExplanation = permissionsForUser.computeIfAbsent(permission, str -> new ArrayList<>());
1050                                rightExplanation.add(controllerPermissionsForGroup.get(permission));
1051                            }
1052                        }
1053                    }
1054                }
1055                catch (Exception e)
1056                {
1057                    getLogger().error("An error occured with controller '{}' while explaining permission for '{}'. Thus, this controller will be ignored.", controllerId, objectToCheck.toString(), e);
1058                }
1059            }
1060        }
1061        
1062        return new ContextPermissions(permissionsForAnonymous, permissionsForAnyConnected, permissionsByUser, permissionsByGroup);
1063    }
1064    
1065    /**
1066     * Record to describe all the permissions granted on a context
1067     * @param permissionsForAnonymous the permissions for anonymous user
1068     * @param permissionsForAnyConnected the permissions for any connected user
1069     * @param permissionsByUser the permissions for specific users by user identity
1070     * @param permissionsByGroup the permissions for specific groups by group identity
1071     */
1072    public record ContextPermissions(
1073            Map<Permission, List<AccessExplanation>> permissionsForAnonymous,
1074            Map<Permission, List<AccessExplanation>> permissionsForAnyConnected,
1075            Map<UserIdentity, Map<Permission, List<AccessExplanation>>> permissionsByUser,
1076            Map<GroupIdentity, Map<Permission, List<AccessExplanation>>> permissionsByGroup) { }
1077    
1078    static class Cache1Key extends AbstractCacheKey
1079    {
1080        Cache1Key(UserIdentity userIdentity, String rightId, Object object)
1081        {
1082            super(userIdentity, rightId, object);
1083        }
1084
1085        static Cache1Key of(UserIdentity userIdentity, String rightId, Object object)
1086        {
1087            return new Cache1Key(userIdentity, StringUtils.defaultString(rightId), object);
1088        }
1089        
1090    }
1091    static class Cache2Key extends AbstractCacheKey
1092    {
1093        Cache2Key(UserIdentity userIdentity, String rightId, Set<Object> workspacesContexts)
1094        {
1095            super(userIdentity, rightId, workspacesContexts);
1096        }
1097
1098        static Cache2Key of(UserIdentity userIdentity, String rightId, Set<Object> workspacesContexts)
1099        {
1100            return new Cache2Key(userIdentity, StringUtils.defaultString(rightId), workspacesContexts);
1101        }
1102        
1103    }
1104}