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