001/*
002 *  Copyright 2020 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.plugins.repository.jcr;
017
018import java.util.Collections;
019import java.util.HashMap;
020import java.util.HashSet;
021import java.util.Map;
022import java.util.Set;
023import java.util.stream.Collectors;
024
025import javax.jcr.AccessDeniedException;
026import javax.jcr.ItemNotFoundException;
027import javax.jcr.Node;
028import javax.jcr.NodeIterator;
029import javax.jcr.PathNotFoundException;
030import javax.jcr.Repository;
031import javax.jcr.RepositoryException;
032import javax.jcr.Session;
033import javax.jcr.Value;
034import javax.jcr.lock.Lock;
035import javax.jcr.lock.LockManager;
036import javax.jcr.query.Query;
037
038import org.apache.avalon.framework.component.Component;
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.avalon.framework.service.Serviceable;
042import org.apache.jackrabbit.JcrConstants;
043import org.apache.jackrabbit.util.ISO9075;
044import org.apache.jackrabbit.util.Text;
045import org.slf4j.Logger;
046
047import org.ametys.core.group.GroupIdentity;
048import org.ametys.core.right.ProfileAssignmentStorage.AnonymousOrAnyConnectedKeys;
049import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup;
050import org.ametys.core.user.UserIdentity;
051import org.ametys.core.util.LambdaUtils;
052import org.ametys.plugins.repository.ACLAmetysObject;
053import org.ametys.plugins.repository.AmetysObject;
054import org.ametys.plugins.repository.AmetysObjectResolver;
055import org.ametys.plugins.repository.AmetysRepositoryException;
056import org.ametys.plugins.repository.ModifiableACLAmetysObject;
057import org.ametys.plugins.repository.ModifiableACLAmetysObjectProfileAssignmentStorage;
058import org.ametys.plugins.repository.RepositoryConstants;
059import org.ametys.plugins.repository.provider.AbstractRepository;
060import org.ametys.plugins.repository.query.expression.Expression;
061import org.ametys.plugins.repository.query.expression.Expression.LogicalOperator;
062import org.ametys.plugins.repository.query.expression.OrExpression;
063import org.ametys.runtime.plugin.component.LogEnabled;
064
065/**
066 * Helper for implementing {@link ModifiableACLAmetysObject} in JCR under its node.
067 */
068public class ACLJCRAmetysObjectHelper implements Component, Serviceable, LogEnabled
069{
070    /** The AmetysObject resolver */
071    protected static AmetysObjectResolver _resolver;
072    /** The repository */
073    protected static Repository _repository;
074    
075    private static final String __NODE_NAME_ROOT_ACL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":acl";
076    private static final String __NODETYPE_ROOT_ACL = RepositoryConstants.NAMESPACE_PREFIX + ":acl";
077    
078    private static final String __NODE_NAME_ACL_USERS = "users";
079    private static final String __NODE_NAME_ACL_GROUPS = "groups";
080    private static final String __NODETYPE_ACL_USER = RepositoryConstants.NAMESPACE_PREFIX + ":acl-user";
081    private static final String __NODETYPE_ACL_GROUP = RepositoryConstants.NAMESPACE_PREFIX + ":acl-group";
082    
083    private static final String __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-any-connected-profiles";
084    private static final String __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-any-connected-profiles";
085    private static final String __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-anonymous-profiles";
086    private static final String __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-anonymous-profiles";
087    
088    private static final String __PROPERTY_NAME_ALLOWED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":allowed-profiles";
089    private static final String __PROPERTY_NAME_DENIED_PROFILES = RepositoryConstants.NAMESPACE_PREFIX + ":denied-profiles";
090    
091    private static final String __PROPERTY_NAME_DISALLOW_INHERITANCE = RepositoryConstants.NAMESPACE_PREFIX + ":disallow-inheritance";
092    
093    private static final Map<AnonymousOrAnyConnectedKeys, Set<String>> __ANONYMOUS_OR_ANYCONNECTEDUSER_NORIGHT = Map.of(
094            AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED, Set.of(),
095            AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED, Set.of(),
096            AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED, Set.of(),
097            AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED, Set.of());
098    private static final Map<UserOrGroup, Set<String>> __USER_OR_GROUP_NORIGHT = Map.of(
099            UserOrGroup.ALLOWED, Set.of(),
100            UserOrGroup.DENIED, Set.of());
101 
102    
103    private static Logger _logger;
104    
105    @Override
106    public void service(ServiceManager manager) throws ServiceException
107    {
108        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
109        _repository = (Repository) manager.lookup(AbstractRepository.ROLE);
110    }
111    
112    public void setLogger(Logger logger)
113    {
114        _logger = logger;
115    }
116    
117    
118    /* -------------- */
119    /* HAS PERMISSION */
120    /* -------------- */
121    
122    private static Set<String> _convertNodeToPath(Set<? extends Object> rootNodes)
123    {
124        return rootNodes.stream().filter(JCRAmetysObject.class::isInstance).map(JCRAmetysObject.class::cast).map(LambdaUtils.wrap(ao -> ISO9075.encodePath(ao.getNode().getPath()))).collect(Collectors.toSet());
125    }
126    
127    
128    /**
129     * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for the user
130     * @param user The user
131     * @param profileIds The ids of the profiles to check
132     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search.
133     * @return If the Set is empty, it means the user has no matching profile.<br>
134     *         If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for the user AND it can contains some other profiles that were not in the given profiles
135     */
136    public static Set<String> hasUserAnyAllowedProfile(Set<? extends Object> rootNodes, UserIdentity user, Set<String> profileIds)
137    {
138        Expression expr = new AllowedProfileExpression(profileIds.toArray(new String[profileIds.size()]));
139        for (String rootPath : _convertNodeToPath(rootNodes))
140        {
141            NodeIterator nodes = getACLUsers(user, rootPath, expr);
142            
143            if (nodes.hasNext())
144            {
145                // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can
146                Node userNode = nodes.nextNode();
147                return _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES);
148            }
149        }
150        return Set.of();
151    }
152    
153    /**
154     * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for the group
155     * @param groups The groups
156     * @param profileIds The ids of the profiles
157     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search.
158     * @return If the Set is empty, it means the group has no matching profile.<br>
159     *         If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for the group AND it can contains some other profiles that were not in the given profiles
160     */
161    public static Set<String> hasGroupAnyAllowedProfile(Set<? extends Object> rootNodes, Set<GroupIdentity> groups, Set<String> profileIds)
162    {
163        if (!groups.isEmpty())
164        {
165            Expression expr = new AllowedProfileExpression(profileIds.toArray(new String[profileIds.size()]));
166            for (String rootPath : _convertNodeToPath(rootNodes))
167            {
168                // Approximative query (to be fast)
169                NodeIterator nodes = _getApprochingACLGroups(groups, rootPath, expr);
170                
171                while (nodes.hasNext())
172                {
173                    Node groupNode = nodes.nextNode();
174    
175                    // As the query was a fast approximative request, we now check if the result is fine
176                    String groupId;
177                    String directoryId;
178                    try
179                    {
180                        groupId = Text.unescapeIllegalJcrChars(groupNode.getName());
181                        directoryId = groupNode.getParent().getName();
182                    }
183                    catch (RepositoryException ex)
184                    {
185                        throw new AmetysRepositoryException("An error occured getting group information", ex);
186                    }
187                    
188                    if (groups.contains(new GroupIdentity(groupId, directoryId)))
189                    {
190                        // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can
191                        return _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES);
192                    }
193                }
194            }
195        }
196        
197        return Set.of();
198    }
199    
200    /**
201     * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for any connected user
202     * @param profileIds The ids of the profiles
203     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search.
204     * @return If the Set is empty, it means any connected user has no matching profile.<br>
205     *         If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for anyconnected user AND it can contains some other profiles that were not in the given profiles
206     */
207    public static Set<String> hasAnyConnectedAnyAllowedProfile(Set<? extends Object> rootNodes, Set<String> profileIds)
208    {
209        Expression expr = new AnyConnectedAllowedProfileExpression(profileIds.toArray(new String[profileIds.size()]));
210        for (String rootPath : _convertNodeToPath(rootNodes))
211        {
212            NodeIterator nodes = getACLRoots(rootPath, expr);
213            
214            if (nodes.hasNext())
215            {
216                // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can
217                Node aclNode = nodes.nextNode();
218                return _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES);
219            }
220        }
221        return Set.of();
222    }
223    
224    
225    /**
226     * Returns some profiles that are matching if any ACL Ametys object has one of the given profiles as allowed for anonymous
227     * @param profileIds The ids of the profiles
228     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query. Can be null to not restrict the search.
229     * @return If the Set is empty, it means anonymous has no matching profile.<br>
230     *         If the Set is non empty, it contains at least one of the given profile BUT it may not contains all the matching profiles for anonymous AND it can contains some other profiles that were not in the given profiles
231     */
232    public static Set<String> hasAnonymousAnyAllowedProfile(Set<? extends Object> rootNodes, Set<String> profileIds)
233    {
234        Expression expr = new AnonymousAllowedProfileExpression(profileIds.toArray(new String[profileIds.size()]));
235        for (String rootPath : _convertNodeToPath(rootNodes))
236        {
237            NodeIterator nodes = getACLRoots(rootPath, expr);
238            
239            if (nodes.hasNext())
240            {
241                // To be complete we could loop on all results, but we only want to answer the question and return additional data if we can
242                Node aclNode = nodes.nextNode();
243                return _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES);
244            }
245        }
246        return Set.of();
247    }
248    
249    /**
250     * Gets all contexts with stored profiles (allowed or denied) for anonymous or any connected user and for each, a description of the permission
251     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query
252     * @return a map associating a context object to the stored profile for each permission
253     */
254    public static Map<Object, Map<AnonymousOrAnyConnectedKeys, Set<String>>> getAllProfilesForAnonymousAndAnyConnectedUser(Set< ? extends Object> rootNodes)
255    {
256        Map<Object, Map<AnonymousOrAnyConnectedKeys, Set<String>>> result = new HashMap<>();
257        // Only retrieve node with assignments to anonymous or any connected
258        Expression predicate = new OrExpression(
259                () -> "@" + __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES,
260                () -> "@" + __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES,
261                () -> "@" + __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES,
262                () -> "@" + __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES
263                );
264        
265        for (String rootPath : _convertNodeToPath(rootNodes))
266        {
267            NodeIterator nodes = getACLRoots(rootPath, predicate);
268            
269            while (nodes.hasNext())
270            {
271                Node aclNode = nodes.nextNode();
272                try
273                {
274                    Map<AnonymousOrAnyConnectedKeys, Set<String>> aoResult = new HashMap<>();
275                    Set<String> allowedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES);
276                    if (!allowedAnonymous.isEmpty())
277                    {
278                        aoResult.put(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED, allowedAnonymous);
279                    }
280                    
281                    Set<String> deniedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES);
282                    if (!deniedAnonymous.isEmpty())
283                    {
284                        aoResult.put(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED, deniedAnonymous);
285                    }
286                    Set<String> allowedAny = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES);
287                    if (!allowedAny.isEmpty())
288                    {
289                        aoResult.put(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED, allowedAny);
290                    }
291                    Set<String> deniedAny = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES);
292                    if (!deniedAny.isEmpty())
293                    {
294                        aoResult.put(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED, deniedAny);
295                    }
296                    
297                    if (!aoResult.isEmpty())
298                    {
299                        AmetysObject ao = _getAmetysObjectFromACLNode(aclNode);
300                        result.put(ao, aoResult);
301                    }
302                }
303                catch (RepositoryException e)
304                {
305                    _logger.error("Failed to retrieve object for acl node " + aclNode.toString() + ". The node will be ignored.");
306                }
307            }
308        }
309        return result;
310    }
311    
312    /**
313     * Gets all context with stored profiles (allowed or denied) for the groups and for each, a description of the permission
314     * Gets the groups that have allowed profiles assigned on the given object
315     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query
316     * @param groups The groups to get profiles for.
317     * @return The map of context with their assigned permissions
318     */
319    public static Map<Object, Map<GroupIdentity, Map<UserOrGroup, Set<String>>>> getAllProfilesForGroups(Set< ? extends Object> rootNodes, Set<GroupIdentity> groups)
320    {
321        Map<Object, Map<GroupIdentity, Map<UserOrGroup, Set<String>>>> result = new HashMap<>();
322        if (!groups.isEmpty())
323        {
324            for (String rootPath : _convertNodeToPath(rootNodes))
325            {
326                // Approximative query (to be fast)
327                NodeIterator nodes = _getApprochingACLGroups(groups, rootPath, null);
328                
329                while (nodes.hasNext())
330                {
331                    Node groupNode = nodes.nextNode();
332                    
333                    // As the query was a fast approximative request, we now check if the result is fine
334                    String groupId;
335                    String directoryId;
336                    try
337                    {
338                        groupId = Text.unescapeIllegalJcrChars(groupNode.getName());
339                        directoryId = groupNode.getParent().getName();
340                    }
341                    catch (RepositoryException ex)
342                    {
343                        throw new AmetysRepositoryException("An error occured getting group information", ex);
344                    }
345                    
346                    GroupIdentity currentGroup = new GroupIdentity(groupId, directoryId);
347                    if (groups.contains(currentGroup))
348                    {
349                        try
350                        {
351                            
352                            // Determine the group permissions
353                            Map<UserOrGroup, Set<String>> groupPermissions = new HashMap<>();
354                            
355                            Set<String> allowedProfiles = _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES);
356                            if (!allowedProfiles.isEmpty())
357                            {
358                                groupPermissions.put(UserOrGroup.ALLOWED, allowedProfiles);
359                            }
360                            Set<String> deniedProfiles = _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES);
361                            if (!deniedProfiles.isEmpty())
362                            {
363                                groupPermissions.put(UserOrGroup.DENIED, deniedProfiles);
364                            }
365                            
366                            // Only add actual permissions to the result
367                            if (!groupPermissions.isEmpty())
368                            {
369                                AmetysObject ao = _getAmetysObjectFromACLNode(groupNode);
370                                // The ametys object could already be in the result map having permissions from an other group
371                                Map<GroupIdentity, Map<UserOrGroup, Set<String>>> objectPermissions = result.computeIfAbsent(ao, k -> new HashMap<>());
372                                // There can only be one node per group, so we don't need to retrieve existing value
373                                objectPermissions.put(currentGroup, groupPermissions);
374                            }
375                        }
376                        catch (RepositoryException e)
377                        {
378                            _logger.error("Failed to retrieve object for group acl node " + groupNode.toString() + ". The node will be ignored.");
379                        }
380                    }
381                }
382            }
383        }
384        return result;
385    }
386    
387    /**
388     * Gets all context with stored profiles (allowed or denied) for the user and for each, a description of the permission
389     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query
390     * @param user The user to get profiles for.
391     * @return The map of context with their assigned allowed/denied profiles
392     */
393    public static Map<Object, Map<UserOrGroup, Set<String>>> getAllProfilesForUser(Set< ? extends Object> rootNodes, UserIdentity user)
394    {
395        Map<Object, Map<UserOrGroup, Set<String>>> result = new HashMap<>();
396        
397        for (String rootPath : _convertNodeToPath(rootNodes))
398        {
399            NodeIterator nodes = getACLUsers(user, rootPath, null);
400            
401            while (nodes.hasNext())
402            {
403                Node userNode = nodes.nextNode();
404                try
405                {
406                    Map<UserOrGroup, Set<String>> aoResult = new HashMap<>();
407                    Set<String> allowedProfiles = _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES);
408                    if (!allowedProfiles.isEmpty())
409                    {
410                        aoResult.put(UserOrGroup.ALLOWED, allowedProfiles);
411                    }
412                    Set<String> deniedProfiles = _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES);
413                    if (!deniedProfiles.isEmpty())
414                    {
415                        aoResult.put(UserOrGroup.DENIED, deniedProfiles);
416                    }
417                    
418                    if (!aoResult.isEmpty())
419                    {
420                        AmetysObject ao = _getAmetysObjectFromACLNode(userNode);
421                        result.put(ao, aoResult);
422                    }
423                }
424                catch (RepositoryException e)
425                {
426                    _logger.error("Failed to retrieve object for user acl node " + userNode.toString() + ". The node will be ignored.");
427                }
428            }
429        }
430        return result;
431    }
432    
433    private static AmetysObject _getAmetysObjectFromACLNode(Node node) throws RepositoryException, ItemNotFoundException, AccessDeniedException
434    {
435        switch (node.getPrimaryNodeType().getName())
436        {
437            case __NODETYPE_ROOT_ACL:
438                return _resolver.resolve(node.getParent(), false);
439            case __NODETYPE_ACL_USER:
440            case __NODETYPE_ACL_GROUP:
441                return _resolver.resolve(node.getParent().getParent().getParent().getParent(), false);
442            default:
443                return null;
444        }
445    }
446    
447    /**
448     * Returns all ACL root objects (ametys:acl nodes)
449     * @param rootPath The root path to restrict the search. Can be null.
450     * @param predicat The predicat expression. Can be null.
451     * @return The ACL root objects
452     */
453    public static NodeIterator getACLRoots (String rootPath, Expression predicat)
454    {
455        StringBuilder sb = new StringBuilder("/jcr:root");
456        
457        if (rootPath != null)
458        {
459            sb.append(rootPath);
460        }
461        
462        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")");
463        
464        if (predicat != null)
465        {
466            sb.append("[").append(predicat.build()).append("]");
467        }
468        
469        return _query(sb.toString());
470    }
471    
472    /**
473     * Returns all ACL objects for a given user (ametys:acl-user nodes)
474     * @param user The user
475     * @param rootPath The root path to restrict the search. Can be null.
476     * @param predicat The predicat expression. Can be null.
477     * @return The ACL user objects for user
478     */
479    public static NodeIterator getACLUsers (UserIdentity user, String rootPath, Expression predicat)
480    {
481        StringBuilder sb = new StringBuilder("/jcr:root");
482        
483        if (rootPath != null)
484        {
485            sb.append(rootPath);
486        }
487        
488        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")")
489            .append("/").append(__NODE_NAME_ACL_USERS)
490            .append("/").append(user.getPopulationId())
491            .append("/").append(ISO9075.encode(user.getLogin()));
492        
493        if (predicat != null)
494        {
495            sb.append("[").append(predicat.build()).append("]");
496        }
497        
498        String jcrQuery = sb.toString();
499        return _query(jcrQuery);
500    }
501    
502    /**
503     * Returns all ACL objects for users user (ametys:acl-user nodes)
504     * @param rootPath The root path to restrict the search. Can be null.
505     * @param predicate The predicate expression. Can be null.
506     * @return The ACL user objects for users
507     */
508    public static NodeIterator getACLUsers (String rootPath, Expression predicate)
509    {
510        StringBuilder sb = new StringBuilder("/jcr:root");
511        
512        if (rootPath != null)
513        {
514            sb.append(rootPath);
515        }
516        
517        sb.append("//element(*, ").append(__NODETYPE_ACL_USER).append(")");
518        
519        if (predicate != null)
520        {
521            sb.append("[").append(predicate.build()).append("]");
522        }
523        
524        String jcrQuery = sb.toString();
525        return _query(jcrQuery);
526    }
527    
528    /**
529     * Returns all ACL objects for users (ametys:acl-user nodes)
530     * @param predicat The predicat expression. Can be null.
531     * @return The ACL user objects for users
532     */
533    public static NodeIterator getACLUsers (Expression predicat)
534    {
535        StringBuilder sb = new StringBuilder();
536        
537        sb.append("//element(*, ").append(__NODETYPE_ACL_USER).append(")");
538        
539        if (predicat != null)
540        {
541            sb.append("[").append(predicat.build()).append("]");
542        }
543        
544        return _query(sb.toString());
545    }
546    
547    /**
548     * Returns all ACL objects for groups (ametys:acl-group nodes)
549     * @param predicat The predicat expression. Can be null.
550     * @return The ACL group objects for groups
551     */
552    public static NodeIterator getACLGroups (Expression predicat)
553    {
554        StringBuilder sb = new StringBuilder();
555        
556        sb.append("//element(*, ").append(__NODETYPE_ACL_GROUP).append(")");
557        
558        if (predicat != null)
559        {
560            sb.append("[").append(predicat.build()).append("]");
561        }
562        
563        return _query(sb.toString());
564    }
565    
566    /**
567     * Returns all ACL objects for groups (ametys:acl-group nodes)
568     * @param rootPath The root path to restrict the search. Can be null.
569     * @param predicate The predicate expression. Can be null.
570     * @return The ACL group objects for groups
571     */
572    public static NodeIterator getACLGroups (String rootPath, Expression predicate)
573    {
574        StringBuilder sb = new StringBuilder("/jcr:root");
575        
576        if (rootPath != null)
577        {
578            sb.append(rootPath);
579        }
580        
581        sb.append("//element(*, ").append(__NODETYPE_ACL_GROUP).append(")");
582        
583        if (predicate != null)
584        {
585            sb.append("[").append(predicate.build()).append("]");
586        }
587        
588        return _query(sb.toString());
589    }
590    
591    private static NodeIterator _getApprochingACLGroups (Set<GroupIdentity> groups, String rootPath, Expression predicat)
592    {
593        StringBuilder sb = new StringBuilder("/jcr:root");
594        
595        if (rootPath != null)
596        {
597            sb.append(rootPath);
598        }
599        
600        sb.append("//element(*, ametys:acl-group)[(");
601        
602        sb.append(groups.stream()
603            .map(GroupIdentity::getId)
604            .map(Text::escapeIllegalJcrChars)
605            .map(ISO9075::encode) // used to support nodeName with number (id of SQL Group)
606            .map(nodeName -> "fn:name()='" + nodeName + "'")
607            .collect(Collectors.joining(LogicalOperator.OR.toString())));
608        sb.append(")");
609        
610        if (predicat != null)
611        {
612            sb.append(LogicalOperator.AND.toString()).append(predicat.build());
613        }
614        
615        sb.append("]");
616        
617        return _query(sb.toString());
618    }
619    
620    /**
621     * Returns all ACL objects for a given group (ametys:acl-group nodes)
622     * @param group The group
623     * @param rootPath The root path to restrict the search. Can be null.
624     * @param predicat The predicat expression. Can be null.
625     * @return The ACL user objects for groups
626     */
627    public static NodeIterator getACLGroups (GroupIdentity group, String rootPath, Expression predicat)
628    {
629        StringBuilder sb = new StringBuilder("/jcr:root");
630        
631        if (rootPath != null)
632        {
633            sb.append(rootPath);
634        }
635        
636        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")")
637            .append("/").append(__NODE_NAME_ACL_GROUPS)
638            .append("/").append(group.getDirectoryId())
639            .append("/").append(ISO9075.encode(Text.escapeIllegalJcrChars(group.getId())));
640        
641        if (predicat != null)
642        {
643            sb.append("[").append(predicat.build()).append("]");
644        }
645        
646        return _query(sb.toString());
647    }
648    
649    private static NodeIterator _query (String jcrQuery)
650    {
651        Session session = null;
652        try
653        {
654            session = _repository.login();
655            long t1 = System.currentTimeMillis();
656            @SuppressWarnings("deprecation")
657            Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH);
658            if (_logger.isInfoEnabled())
659            {
660                _logger.info("ACLJCR query '" + jcrQuery + "' executed in " + (System.currentTimeMillis() - t1) + " ms");
661            }
662            return query.execute().getNodes();
663        }
664        catch (RepositoryException ex)
665        {
666            if (session != null)
667            {
668                session.logout();
669            }
670            throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex);
671        }
672    }
673    
674    /* ------------------------------------------- */
675    /* PROFILES FOR ANY CONNECTED USER / ANONYMOUS */
676    /* ------------------------------------------- */
677    
678    /**
679     * Helper for {@link ACLAmetysObject#getProfilesForAnonymousAndAnyConnectedUser}
680     * @param node The JCR node for the Ametys object
681     * @return a map containing allowed/denied profiles that anonymous and any connected user has on the given object
682     */
683    public static Map<AnonymousOrAnyConnectedKeys, Set<String>> getProfilesForAnonymousAndAnyConnectedUser(Node node)
684    {
685        Node aclNode = _getACLNode(node);
686        if (aclNode == null)
687        {
688            return __ANONYMOUS_OR_ANYCONNECTEDUSER_NORIGHT;
689        }
690        else
691        {
692            return Map.of(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED,         _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES),
693                          AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED,          _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES),
694                          AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED,  _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES),
695                          AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED,   _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES));
696        }
697    }
698
699    /**
700     * Helper for {@link ModifiableACLAmetysObject#addAllowedProfilesForAnyConnectedUser(Set)}
701     * @param node The JCR node for the Ametys object
702     * @param profileIds The profiles to add
703     */
704    public static void addAllowedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
705    {
706        Node aclNode = _getOrCreateACLNode(node);
707        for (String profile : profileIds)
708        {
709            _addProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profile);
710        }
711        _save(node);
712    }
713
714    /**
715     * Helper for {@link ModifiableACLAmetysObject#removeAllowedProfilesForAnyConnectedUser(Set)}
716     * @param node The JCR node for the Ametys object
717     * @param profileIds The profiles to remove
718     */
719    public static void removeAllowedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
720    {
721        Node aclNode = _getOrCreateACLNode(node);
722        for (String profile : profileIds)
723        {
724            _removeProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profile);
725        }
726        _save(node);
727    }
728    
729    /**
730     * Helper for {@link ModifiableACLAmetysObject#addDeniedProfilesForAnyConnectedUser(Set)}
731     * @param node The JCR node for the Ametys object
732     * @param profileIds The profiles to add
733     */
734    public static void addDeniedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
735    {
736        Node aclNode = _getOrCreateACLNode(node);
737        for (String profile : profileIds)
738        {
739            _addProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profile);
740        }
741        _save(node);
742    }
743    
744    /**
745     * Helper for {@link ModifiableACLAmetysObject#removeDeniedProfilesForAnyConnectedUser(Set)}
746     * @param node The JCR node for the Ametys object
747     * @param profileIds The profiles to remove
748     */
749    public static void removeDeniedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
750    {
751        Node aclNode = _getOrCreateACLNode(node);
752        for (String profile : profileIds)
753        {
754            _removeProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profile);
755        }
756        _save(node);
757    }
758    
759    /**
760     * Helper for {@link ModifiableACLAmetysObject#addAllowedProfilesForAnonymous(Set)}
761     * @param node The JCR node for the Ametys object
762     * @param profileIds The profiles to add
763     */
764    public static void addAllowedProfilesForAnonymous(Node node, Set<String> profileIds)
765    {
766        Node aclNode = _getOrCreateACLNode(node);
767        for (String profile : profileIds)
768        {
769            _addProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profile);
770        }
771        _save(node);
772    }
773    
774    /**
775     * Helper for {@link ModifiableACLAmetysObject#removeAllowedProfilesForAnonymous(Set)}
776     * @param node The JCR node for the Ametys object
777     * @param profileIds The profiles to remove
778     */
779    public static void removeAllowedProfilesForAnonymous(Node node, Set<String> profileIds)
780    {
781        Node aclNode = _getOrCreateACLNode(node);
782        for (String profile : profileIds)
783        {
784            _removeProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profile);
785        }
786        _save(node);
787    }
788    
789    /**
790     * Helper for {@link ModifiableACLAmetysObject#addDeniedProfilesForAnyConnectedUser(Set)}
791     * @param node The JCR node for the Ametys object
792     * @param profileIds The profiles to add
793     */
794    public static void addDeniedProfilesForAnonymous(Node node, Set<String> profileIds)
795    {
796        Node aclNode = _getOrCreateACLNode(node);
797        for (String profile : profileIds)
798        {
799            _addProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profile);
800        }
801        _save(node);
802    }
803    
804    /**
805     * Helper for {@link ModifiableACLAmetysObject#removeDeniedProfilesForAnyConnectedUser(Set)}
806     * @param node The JCR node for the Ametys object
807     * @param profileIds The profiles to remove
808     */
809    public static void removeDeniedProfilesForAnonymous(Node node, Set<String> profileIds)
810    {
811        Node aclNode = _getOrCreateACLNode(node);
812        for (String profile : profileIds)
813        {
814            _removeProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profile);
815        }
816        _save(node);
817    }
818    
819    
820    /* ------------------- */
821    /* MANAGEMENT OF USERS */
822    /* ------------------- */
823    /**
824     * Helper for {@link ACLAmetysObject#getProfilesForUsers}
825     * @param node The JCR node for the Ametys object
826     * @param user The user to get profiles for. Can be null to get profiles for all users that have rights
827     * @return The map of allowed users with their assigned allowed/denied profiles
828     */
829    public static Map<UserIdentity, Map<UserOrGroup, Set<String>>> getProfilesForUsers(Node node, UserIdentity user)
830    {
831        if (user == null)
832        {
833            try
834            {
835                Node usersNode = _getUsersNode(node);
836                if (usersNode == null)
837                {
838                    return Map.of();
839                }
840                
841                Map<UserIdentity, Map<UserOrGroup, Set<String>>> result = new HashMap<>();
842                
843                NodeIterator populationsIterator = usersNode.getNodes();
844                while (populationsIterator.hasNext())
845                {
846                    Node populationNode = populationsIterator.nextNode();
847                    NodeIterator usersIterator = populationNode.getNodes();
848                    while (usersIterator.hasNext())
849                    {
850                        Node userNode = usersIterator.nextNode();
851                        Set<String> allowedProfiles = _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES);
852                        Set<String> deniedProfiles = _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES);
853                        if (!allowedProfiles.isEmpty() || !deniedProfiles.isEmpty())
854                        {
855                            result.put(new UserIdentity(userNode.getName(), populationNode.getName()),
856                                        Map.of(UserOrGroup.ALLOWED, allowedProfiles,
857                                               UserOrGroup.DENIED, deniedProfiles));
858                        }
859                    }
860                }
861                
862                return result;
863            }
864            catch (RepositoryException e)
865            {
866                throw new AmetysRepositoryException("Unable to get allowed/denied users", e);
867            }
868        }
869        else
870        {
871            Node userNode = _getUserNode(node, user);
872            if (userNode == null)
873            {
874                return Map.of(user, __USER_OR_GROUP_NORIGHT);
875            }
876            else
877            {
878                return Map.of(user,
879                              Map.of(UserOrGroup.ALLOWED, _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES),
880                                     UserOrGroup.DENIED,  _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES)));
881            }
882        }
883    }
884
885    /**
886     * Helper for {@link ModifiableACLAmetysObject#addAllowedUsers(Set, String)}
887     * @param users The users to add
888     * @param node The JCR node for the Ametys object
889     * @param profileId The id of the profile
890     */
891    public static void addAllowedUsers(Set<UserIdentity> users, Node node, String profileId)
892    {
893        for (UserIdentity userIdentity : users)
894        {
895            Node userNode = _getOrCreateUserNode(node, userIdentity);
896            _addProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
897        }
898        _save(node);
899    }
900
901    /**
902     * Helper for {@link ModifiableACLAmetysObject#removeAllowedUsers(Set, String)}
903     * @param users The users to remove
904     * @param node The JCR node for the Ametys object
905     * @param profileId The id of the profile
906     */
907    public static void removeAllowedUsers(Set<UserIdentity> users, Node node, String profileId)
908    {
909        for (UserIdentity userIdentity : users)
910        {
911            Node userNode = _getOrCreateUserNode(node, userIdentity);
912            _removeProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
913        }
914        _save(node);
915    }
916
917    /**
918     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set)}
919     * @param users The users to remove
920     * @param node The JCR node for the Ametys object
921     */
922    public static void removeAllowedUsers(Set<UserIdentity> users, Node node)
923    {
924        for (UserIdentity userIdentity : users)
925        {
926            Node userNode = _getOrCreateUserNode(node, userIdentity);
927            _setProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, Collections.EMPTY_SET);
928        }
929        _save(node);
930    }
931    
932    /**
933     * Helper for {@link ModifiableACLAmetysObject#addDeniedUsers(Set, String)}
934     * @param users The users to add
935     * @param node The JCR node for the Ametys object
936     * @param profileId The id of the profile
937     */
938    public static void addDeniedUsers(Set<UserIdentity> users, Node node, String profileId)
939    {
940        for (UserIdentity userIdentity : users)
941        {
942            Node userNode = _getOrCreateUserNode(node, userIdentity);
943            _addProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
944        }
945        _save(node);
946    }
947
948    /**
949     * Helper for {@link ModifiableACLAmetysObject#removeDeniedUsers(Set, String)}
950     * @param users The users to remove
951     * @param node The JCR node for the Ametys object
952     * @param profileId The id of the profile
953     */
954    public static void removeDeniedUsers(Set<UserIdentity> users, Node node, String profileId)
955    {
956        for (UserIdentity userIdentity : users)
957        {
958            Node userNode = _getOrCreateUserNode(node, userIdentity);
959            _removeProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
960        }
961        _save(node);
962    }
963
964    /**
965     * Helper for {@link ModifiableACLAmetysObject#removeDeniedUsers(Set)}
966     * @param users The users to remove
967     * @param node The JCR node for the Ametys object
968     */
969    public static void removeDeniedUsers(Set<UserIdentity> users, Node node)
970    {
971        for (UserIdentity userIdentity : users)
972        {
973            Node userNode = _getOrCreateUserNode(node, userIdentity);
974            _setProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, Collections.EMPTY_SET);
975        }
976        _save(node);
977    }
978
979    /* -------------------- */
980    /* MANAGEMENT OF GROUPS */
981    /* -------------------- */
982
983    /**
984     * Helper for {@link ACLAmetysObject#getProfilesForGroups}
985     * @param node The JCR node for the Ametys object
986     * @param groups The group to get profiles for. Can be null to get profiles for all groups that have rights
987     * @return The map of allowed/denied groups with their assigned profiles
988     */
989    public static Map<GroupIdentity, Map<UserOrGroup, Set<String>>> getProfilesForGroups(Node node, Set<GroupIdentity> groups)
990    {
991        try
992        {
993            if (groups != null && groups.isEmpty())
994            {
995                return Map.of();
996            }
997            
998            Node groupsNode = _getGroupsNode(node);
999            if (groupsNode == null)
1000            {
1001                return Map.of();
1002            }
1003            
1004            Map<GroupIdentity, Map<UserOrGroup, Set<String>>> result = new HashMap<>();
1005            
1006            if (groups == null)
1007            {
1008                NodeIterator groupDirectoriesIterator = groupsNode.getNodes();
1009                while (groupDirectoriesIterator.hasNext())
1010                {
1011                    Node groupDirectoryNode = groupDirectoriesIterator.nextNode();
1012                    NodeIterator groupsIterator = groupDirectoryNode.getNodes();
1013                    while (groupsIterator.hasNext())
1014                    {
1015                        Node groupNode = groupsIterator.nextNode();
1016                        Set<String> allowedProfiles = _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES);
1017                        Set<String> deniedProfiles = _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES);
1018                        if (!allowedProfiles.isEmpty() || !deniedProfiles.isEmpty())
1019                        {
1020                            result.put(new GroupIdentity(Text.unescapeIllegalJcrChars(groupNode.getName()), groupDirectoryNode.getName()),
1021                                        Map.of(UserOrGroup.ALLOWED, allowedProfiles,
1022                                               UserOrGroup.DENIED, deniedProfiles));
1023                        }
1024                    }
1025                }
1026            }
1027            else
1028            {
1029                Map<String, Node> groupsNodeByDirectoryIdCache = new HashMap<>();
1030                
1031                for (GroupIdentity group : groups)
1032                {
1033                    Node directoryNode = groupsNodeByDirectoryIdCache.computeIfAbsent(group.getDirectoryId(), LambdaUtils.wrap(directoryId -> groupsNode.hasNode(directoryId) ? groupsNode.getNode(directoryId) : null));
1034                    
1035                    String groupNodeName = Text.escapeIllegalJcrChars(group.getId());
1036                    if (directoryNode != null && directoryNode.hasNode(groupNodeName))
1037                    {
1038                        Node groupNode = directoryNode.getNode(groupNodeName);
1039                        
1040                        result.put(group,
1041                                Map.of(UserOrGroup.ALLOWED, _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES),
1042                                       UserOrGroup.DENIED, _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES)));
1043                    }
1044                }
1045            }
1046            
1047            return result;
1048        }
1049        catch (RepositoryException e)
1050        {
1051            throw new AmetysRepositoryException("Unable to get allowed/denied groups", e);
1052        }
1053    }
1054    
1055    /**
1056     * Helper for {@link ModifiableACLAmetysObject#addAllowedGroups(Set, String)}
1057     * @param groups The groups to add
1058     * @param node The JCR node for the Ametys object
1059     * @param profileId The id of the profile
1060     */
1061    public static void addAllowedGroups(Set<GroupIdentity> groups, Node node, String profileId)
1062    {
1063        for (GroupIdentity groupIdentity : groups)
1064        {
1065            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1066            _addProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
1067        }
1068        _save(node);
1069    }
1070
1071    /**
1072     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set, String)}
1073     * @param groups The groups to remove
1074     * @param node The JCR node for the Ametys object
1075     * @param profileId The id of the profile
1076     */
1077    public static void removeAllowedGroups(Set<GroupIdentity> groups, Node node, String profileId)
1078    {
1079        for (GroupIdentity groupIdentity : groups)
1080        {
1081            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1082            _removeProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
1083        }
1084        _save(node);
1085    }
1086
1087    /**
1088     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set)}
1089     * @param groups The groups to remove
1090     * @param node The JCR node for the Ametys object
1091     */
1092    public static void removeAllowedGroups(Set<GroupIdentity> groups, Node node)
1093    {
1094        for (GroupIdentity groupIdentity : groups)
1095        {
1096            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1097            _setProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, Collections.EMPTY_SET);
1098        }
1099        _save(node);
1100    }
1101
1102    /**
1103     * Helper for {@link ModifiableACLAmetysObject#addDeniedGroups(Set, String)}
1104     * @param groups The groups to add
1105     * @param node The JCR node for the Ametys object
1106     * @param profileId The id of the profile
1107     */
1108    public static void addDeniedGroups(Set<GroupIdentity> groups, Node node, String profileId)
1109    {
1110        for (GroupIdentity groupIdentity : groups)
1111        {
1112            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1113            _addProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
1114        }
1115        _save(node);
1116    }
1117
1118    /**
1119     * Helper for {@link ModifiableACLAmetysObject#removeDeniedGroups(Set, String)}
1120     * @param groups The groups to remove
1121     * @param node The JCR node for the Ametys object
1122     * @param profileId The id of the profile
1123     */
1124    public static void removeDeniedGroups(Set<GroupIdentity> groups, Node node, String profileId)
1125    {
1126        for (GroupIdentity groupIdentity : groups)
1127        {
1128            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1129            _removeProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
1130        }
1131        _save(node);
1132    }
1133
1134    /**
1135     * Helper for {@link ModifiableACLAmetysObject#removeDeniedGroups(Set)}
1136     * @param groups The groups to remove
1137     * @param node The JCR node for the Ametys object
1138     */
1139    public static void removeDeniedGroups(Set<GroupIdentity> groups, Node node)
1140    {
1141        for (GroupIdentity groupIdentity : groups)
1142        {
1143            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1144            _setProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, Collections.EMPTY_SET);
1145        }
1146        _save(node);
1147    }
1148    
1149    
1150    /* ------ */
1151    /* REMOVE */
1152    /* ------ */
1153    
1154    /**
1155     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeProfile(String)}
1156     * @param profileId The id of the profile
1157     */
1158    public static void removeProfile(String profileId)
1159    {
1160        // Remove this profile set as allowed or denied in users
1161        Expression expr = new OrExpression(new AllowedProfileExpression(profileId), new DeniedProfileExpression(profileId));
1162        NodeIterator users = getACLUsers(expr);
1163        while (users.hasNext())
1164        {
1165            Node userNode = (Node) users.next();
1166            _removeProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
1167            _removeProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
1168            _save(userNode);
1169        }
1170        
1171        // Remove this profile set as allowed or denied in groups
1172        NodeIterator groups = getACLGroups(expr);
1173        while (groups.hasNext())
1174        {
1175            Node groupNode = (Node) groups.next();
1176            _removeProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
1177            _removeProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
1178            _save(groupNode);
1179        }
1180        
1181        // Remove this profile set as allowed or denied for anonymous and any connected
1182        expr = new OrExpression(new AnonymousAllowedProfileExpression(profileId), new AnonymousDeniedProfileExpression(profileId), new AnyConnectedAllowedProfileExpression(profileId), new AnyConnectedDeniedProfileExpression(profileId));
1183        NodeIterator nodes = getACLRoots(null, expr);
1184        while (nodes.hasNext())
1185        {
1186            Node node = (Node) nodes.next();
1187            _removeProperty(node, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profileId);
1188            _removeProperty(node, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profileId);
1189            _removeProperty(node, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profileId);
1190            _removeProperty(node, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profileId);
1191            _save(node);
1192        }
1193    }
1194    
1195    /**
1196     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeUser(UserIdentity)}
1197     * @param user The user
1198     */
1199    public static void removeUser(UserIdentity user)
1200    {
1201        NodeIterator users = getACLUsers(user, null, null);
1202        
1203        while (users.hasNext())
1204        {
1205            Node userNode = (Node) users.next();
1206            try
1207            {
1208                userNode.remove();
1209                _save(userNode);
1210            }
1211            catch (RepositoryException e)
1212            {
1213                throw new AmetysRepositoryException(e);
1214            }
1215        }
1216    }
1217    
1218    /**
1219     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeGroup(GroupIdentity)}
1220     * @param group The group
1221     */
1222    public static void removeGroup(GroupIdentity group)
1223    {
1224        NodeIterator groups = getACLGroups(group, null, null);
1225        while (groups.hasNext())
1226        {
1227            Node gpNode = (Node) groups.next();
1228            try
1229            {
1230                gpNode.remove();
1231                _save(gpNode);
1232            }
1233            catch (RepositoryException e)
1234            {
1235                throw new AmetysRepositoryException(e);
1236            }
1237        }
1238    }
1239    
1240    /* --------------- */
1241    /* INHERITANCE     */
1242    /* --------------- */
1243    /**
1244     * Helper for {@link ACLAmetysObject#isInheritanceDisallowed()}
1245     * @param node The JCR node for the Ametys object
1246     * @return true if the inheritance is disallow of the given node
1247     */
1248    public static boolean isInheritanceDisallowed(Node node)
1249    {
1250        try
1251        {
1252            Node aclNode = _getACLNode(node);
1253            if (aclNode != null && aclNode.hasProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE))
1254            {
1255                return aclNode.getProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE).getBoolean();
1256            }
1257            return false;
1258        }
1259        catch (RepositoryException e)
1260        {
1261            throw new AmetysRepositoryException("Unable to get " + __PROPERTY_NAME_DISALLOW_INHERITANCE + " property", e);
1262        }
1263    }
1264    
1265    /**
1266     * Helper for {@link ModifiableACLAmetysObject#disallowInheritance(boolean)}
1267     * @param node The JCR node for the Ametys object
1268     * @param disallow true to disallow the inheritance, false otherwise
1269     */
1270    public static void disallowInheritance(Node node, boolean disallow)
1271    {
1272        Node aclNode = _getOrCreateACLNode(node);
1273        try
1274        {
1275            aclNode.setProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE, disallow);
1276        }
1277        catch (RepositoryException e)
1278        {
1279            throw new AmetysRepositoryException("Unable to set " + __PROPERTY_NAME_DISALLOW_INHERITANCE + " property", e);
1280        }
1281        _save(node);
1282    }
1283    
1284    
1285    /* --------------- */
1286    /* PRIVATE METHODS */
1287    /* --------------- */
1288    
1289    private static void _checkLock(Node node) throws AmetysRepositoryException
1290    {
1291        try
1292        {
1293            if (node.isLocked())
1294            {
1295                LockManager lockManager = node.getSession().getWorkspace().getLockManager();
1296                
1297                Lock lock = lockManager.getLock(node.getPath());
1298                Node lockHolder = lock.getNode();
1299                
1300                lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
1301            }
1302        }
1303        catch (RepositoryException e)
1304        {
1305            throw new AmetysRepositoryException("Unable to add lock token on ACL node", e);
1306        }
1307    }
1308    
1309    private static Node _getOrCreateACLNode(Node node)
1310    {
1311        try
1312        {
1313            if (node.hasNode(__NODE_NAME_ROOT_ACL))
1314            {
1315                return node.getNode(__NODE_NAME_ROOT_ACL);
1316            }
1317            else
1318            {
1319                _checkLock(node);
1320                return node.addNode(__NODE_NAME_ROOT_ACL, __NODETYPE_ROOT_ACL);
1321            }
1322        }
1323        catch (RepositoryException e)
1324        {
1325            throw new AmetysRepositoryException("Error while getting root ACL node.", e);
1326        }
1327    }
1328    
1329    private static Node _getACLNode(Node node)
1330    {
1331        try
1332        {
1333            if (node.hasNode(__NODE_NAME_ROOT_ACL))
1334            {
1335                return node.getNode(__NODE_NAME_ROOT_ACL);
1336            }
1337            else
1338            {
1339                return null;
1340            }
1341        }
1342        catch (RepositoryException e)
1343        {
1344            throw new AmetysRepositoryException("Error while getting root ACL node.", e);
1345        }
1346    }
1347    
1348    private static Node _getOrCreateUsersNode(Node node)
1349    {
1350        try
1351        {
1352            Node aclNode = _getOrCreateACLNode(node);
1353            if (aclNode.hasNode(__NODE_NAME_ACL_USERS))
1354            {
1355                return aclNode.getNode(__NODE_NAME_ACL_USERS);
1356            }
1357            else
1358            {
1359                return aclNode.addNode(__NODE_NAME_ACL_USERS, JcrConstants.NT_UNSTRUCTURED);
1360            }
1361        }
1362        catch (RepositoryException e)
1363        {
1364            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1365        }
1366    }
1367    
1368    private static Node _getUserNode(Node node, UserIdentity user)
1369    {
1370        try
1371        {
1372            Node aclNode = _getACLNode(node);
1373            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_USERS))
1374            {
1375                Node aclUsersNode = aclNode.getNode(__NODE_NAME_ACL_USERS);
1376                if (aclUsersNode.hasNode(user.getPopulationId()))
1377                {
1378                    Node popNode = aclUsersNode.getNode(user.getPopulationId());
1379                    if (popNode.hasNode(user.getLogin()))
1380                    {
1381                        return popNode.getNode(user.getLogin());
1382                    }
1383                }
1384            }
1385            
1386            return null;
1387        }
1388        catch (RepositoryException e)
1389        {
1390            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1391        }
1392    }
1393    
1394    private static Node _getUsersNode(Node node)
1395    {
1396        try
1397        {
1398            Node aclNode = _getACLNode(node);
1399            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_USERS))
1400            {
1401                return aclNode.getNode(__NODE_NAME_ACL_USERS);
1402            }
1403            else
1404            {
1405                return null;
1406            }
1407        }
1408        catch (RepositoryException e)
1409        {
1410            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1411        }
1412    }
1413    
1414    private static Node _getOrCreateGroupsNode(Node node)
1415    {
1416        try
1417        {
1418            Node aclNode = _getOrCreateACLNode(node);
1419            if (aclNode.hasNode(__NODE_NAME_ACL_GROUPS))
1420            {
1421                return aclNode.getNode(__NODE_NAME_ACL_GROUPS);
1422            }
1423            else
1424            {
1425                return aclNode.addNode(__NODE_NAME_ACL_GROUPS, JcrConstants.NT_UNSTRUCTURED);
1426            }
1427        }
1428        catch (RepositoryException e)
1429        {
1430            throw new AmetysRepositoryException("Error while getting 'groups' ACL node.", e);
1431        }
1432    }
1433    
1434    private static Node _getGroupsNode(Node node)
1435    {
1436        try
1437        {
1438            Node aclNode = _getACLNode(node);
1439            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_GROUPS))
1440            {
1441                return aclNode.getNode(__NODE_NAME_ACL_GROUPS);
1442            }
1443            else
1444            {
1445                return null;
1446            }
1447        }
1448        catch (RepositoryException e)
1449        {
1450            throw new AmetysRepositoryException("Error while getting 'groups' ACL node.", e);
1451        }
1452    }
1453    
1454    private static Node _getOrCreateUserNode(Node node, UserIdentity userIdentity)
1455    {
1456        try
1457        {
1458            Node usersNode = _getOrCreateUsersNode(node);
1459            String population = userIdentity.getPopulationId();
1460            String login = userIdentity.getLogin();
1461            
1462            if (usersNode.hasNode(population))
1463            {
1464                Node populationNode = usersNode.getNode(population);
1465                if (populationNode.hasNode(login))
1466                {
1467                    return populationNode.getNode(login);
1468                }
1469                else
1470                {
1471                    return populationNode.addNode(login, __NODETYPE_ACL_USER);
1472                }
1473            }
1474            else
1475            {
1476                return usersNode.addNode(population, JcrConstants.NT_UNSTRUCTURED).addNode(login, __NODETYPE_ACL_USER);
1477            }
1478        }
1479        catch (RepositoryException e)
1480        {
1481            throw new AmetysRepositoryException(String.format("Error while getting 'user' ACL node for %s.", userIdentity.toString()), e);
1482        }
1483    }
1484    
1485    private static Node _getOrCreateGroupNode(Node node, GroupIdentity groupIdentity)
1486    {
1487        try
1488        {
1489            Node groupsNode = _getOrCreateGroupsNode(node);
1490            String directoryId = groupIdentity.getDirectoryId();
1491            String id = Text.escapeIllegalJcrChars(groupIdentity.getId());
1492            
1493            if (groupsNode.hasNode(directoryId))
1494            {
1495                Node populationNode = groupsNode.getNode(directoryId);
1496                if (populationNode.hasNode(id))
1497                {
1498                    return populationNode.getNode(id);
1499                }
1500                else
1501                {
1502                    return populationNode.addNode(id, __NODETYPE_ACL_GROUP);
1503                }
1504            }
1505            else
1506            {
1507                return groupsNode.addNode(directoryId, JcrConstants.NT_UNSTRUCTURED).addNode(id, __NODETYPE_ACL_GROUP);
1508            }
1509        }
1510        catch (RepositoryException e)
1511        {
1512            throw new AmetysRepositoryException(String.format("Error while getting 'group' ACL node for %s.", groupIdentity.toString()), e);
1513        }
1514    }
1515    
1516    private static Set<String> _getProperty(Node node, String propertyName)
1517    {
1518        try
1519        {
1520            Value[] values = node.getProperty(propertyName).getValues();
1521            Set<String> result = new HashSet<>();
1522            for (Value value : values)
1523            {
1524                result.add(value.getString());
1525            }
1526            return result;
1527        }
1528        catch (PathNotFoundException e)
1529        {
1530            return new HashSet<>();
1531        }
1532        catch (RepositoryException e)
1533        {
1534            throw new AmetysRepositoryException("Unable to get " + propertyName + " property", e);
1535        }
1536    }
1537    
1538    private static void _setProperty(Node node, String propertyName, Set<String> profiles)
1539    {
1540        try
1541        {
1542            node.setProperty(propertyName, profiles.toArray(new String[profiles.size()]));
1543        }
1544        catch (RepositoryException e)
1545        {
1546            throw new AmetysRepositoryException("Unable to set " + propertyName + " property", e);
1547        }
1548    }
1549    
1550    private static void _addProperty(Node node, String propertyName, String profileToAdd)
1551    {
1552        Set<String> profiles = _getProperty(node, propertyName);
1553        if (!profiles.contains(profileToAdd))
1554        {
1555            profiles.add(profileToAdd);
1556            _setProperty(node, propertyName, profiles);
1557        }
1558    }
1559    
1560    private static void _removeProperty(Node node, String propertyName, String profileToRemove)
1561    {
1562        Set<String> profiles = _getProperty(node, propertyName);
1563        if (profiles.contains(profileToRemove))
1564        {
1565            profiles.remove(profileToRemove);
1566            _setProperty(node, propertyName, profiles);
1567        }
1568    }
1569    
1570    private static void _save(Node node)
1571    {
1572        try
1573        {
1574            node.getSession().save();
1575        }
1576        catch (RepositoryException e)
1577        {
1578            throw new AmetysRepositoryException("Unable to save changes", e);
1579        }
1580    }
1581    
1582    /* ---------------------------------------*/
1583    /*      JCR EXPRESSIONS FOR PROFILES      */
1584    /* ---------------------------------------*/
1585    
1586    static class AllowedProfileExpression extends ACLProfileExpression
1587    {
1588        public AllowedProfileExpression (String ... profileIds)
1589        {
1590            super(__PROPERTY_NAME_ALLOWED_PROFILES, profileIds);
1591        }
1592    }
1593    
1594    static class DeniedProfileExpression extends ACLProfileExpression
1595    {
1596        public DeniedProfileExpression (String ... profileIds)
1597        {
1598            super(__PROPERTY_NAME_DENIED_PROFILES, profileIds);
1599        }
1600    }
1601    
1602    static class AnyConnectedDeniedProfileExpression extends ACLProfileExpression
1603    {
1604        public AnyConnectedDeniedProfileExpression (String ... profileIds)
1605        {
1606            super(__PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profileIds);
1607        }
1608    }
1609    
1610    static class AnyConnectedAllowedProfileExpression extends ACLProfileExpression
1611    {
1612        public AnyConnectedAllowedProfileExpression (String ... profileIds)
1613        {
1614            super(__PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profileIds);
1615        }
1616    }
1617    
1618    static class AnonymousDeniedProfileExpression extends ACLProfileExpression
1619    {
1620        public AnonymousDeniedProfileExpression (String ... profileIds)
1621        {
1622            super(__PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profileIds);
1623        }
1624    }
1625    
1626    static class AnonymousAllowedProfileExpression extends ACLProfileExpression
1627    {
1628        public AnonymousAllowedProfileExpression (String ... profileIds)
1629        {
1630            super(__PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profileIds);
1631        }
1632    }
1633    
1634    static class ACLProfileExpression implements Expression
1635    {
1636        private String[] _profileIds;
1637        private String _propertyName;
1638        
1639        public ACLProfileExpression (String propertyName, String ... profileIds)
1640        {
1641            _propertyName = propertyName;
1642            _profileIds = profileIds;
1643        }
1644        
1645        @Override
1646        public String build()
1647        {
1648            boolean isFirst = true;
1649            StringBuilder sb = new StringBuilder("(");
1650            
1651            for (String profileId : _profileIds)
1652            {
1653                if (isFirst)
1654                {
1655                    isFirst = false;
1656                }
1657                else
1658                {
1659                    sb.append(LogicalOperator.OR.toString());
1660                }
1661                
1662                sb.append("@")
1663                    .append(_propertyName)
1664                    .append(Operator.EQ)
1665                    .append("'").append(profileId).append("'");
1666            }
1667            
1668            if (isFirst)
1669            {
1670                return "";
1671            }
1672            else
1673            {
1674                return sb.append(")").toString();
1675            }
1676        }
1677    }
1678
1679    /**
1680     * Get all contexts with a permission for the profile for anonymous or any connected user
1681     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query
1682     * @param profileId the profile id
1683     * @return a map of the permissions for each context
1684     */
1685    public static Map<Object, Set<AnonymousOrAnyConnectedKeys>> getProfilePermissionsForAnonymousAndAnyConnectedUser(Set< ? extends Object> rootNodes, String profileId)
1686    {
1687        Expression expr = new OrExpression(
1688            new AnyConnectedAllowedProfileExpression(profileId),
1689            new AnyConnectedDeniedProfileExpression(profileId),
1690            new AnonymousAllowedProfileExpression(profileId),
1691            new AnonymousDeniedProfileExpression(profileId)
1692        );
1693        
1694        Map<Object, Set<AnonymousOrAnyConnectedKeys>> result = new HashMap<>();
1695        for (String rootPath : _convertNodeToPath(rootNodes))
1696        {
1697            NodeIterator nodes = getACLRoots(rootPath, expr);
1698            
1699            // Only retrieve node with assignments to anonymous or any connected
1700            while (nodes.hasNext())
1701            {
1702                Node aclNode = nodes.nextNode();
1703                try
1704                {
1705                    Set<AnonymousOrAnyConnectedKeys> aoResult = new HashSet<>();
1706                    Set<String> allowedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES);
1707                    if (allowedAnonymous.contains(profileId))
1708                    {
1709                        aoResult.add(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED);
1710                    }
1711                    
1712                    Set<String> deniedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES);
1713                    if (deniedAnonymous.contains(profileId))
1714                    {
1715                        aoResult.add(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED);
1716                    }
1717                    Set<String> allowedAny = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES);
1718                    if (allowedAny.contains(profileId))
1719                    {
1720                        aoResult.add(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED);
1721                    }
1722                    Set<String> deniedAny = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES);
1723                    if (deniedAny.contains(profileId))
1724                    {
1725                        aoResult.add(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED);
1726                    }
1727                    
1728                    if (!aoResult.isEmpty())
1729                    {
1730                        AmetysObject ao = _getAmetysObjectFromACLNode(aclNode);
1731                        result.put(ao, aoResult);
1732                    }
1733                }
1734                catch (RepositoryException e)
1735                {
1736                    _logger.error("Failed to retrieve object for acl node " + aclNode.toString() + ". The node will be ignored.");
1737                }
1738            }
1739        }
1740
1741        return result;
1742    }
1743    
1744    /**
1745     * Get all context with a permission for the profile for a group
1746     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query
1747     * @param profileId the profile id
1748     * @return a map of the group's permission for each context
1749     */
1750    public static Map<Object, Map<GroupIdentity, Set<UserOrGroup>>> getProfilePermissionsForGroups(Set< ? extends Object> rootNodes, String profileId)
1751    {
1752        Expression expr = new OrExpression(
1753                new AllowedProfileExpression(profileId),
1754                new DeniedProfileExpression(profileId)
1755        );
1756        
1757        Map<Object, Map<GroupIdentity, Set<UserOrGroup>>> result = new HashMap<>();
1758        for (String rootPath : _convertNodeToPath(rootNodes))
1759        {
1760            NodeIterator nodes = getACLGroups(rootPath, expr);
1761            
1762            while (nodes.hasNext())
1763            {
1764                Node groupNode = nodes.nextNode();
1765                try
1766                {
1767                    String groupId = Text.unescapeIllegalJcrChars(groupNode.getName());
1768                    String directoryId = groupNode.getParent().getName();
1769                    
1770                    GroupIdentity currentGroup = new GroupIdentity(groupId, directoryId);
1771                    
1772                    // Determine the group permissions
1773                    Set<UserOrGroup> permission = new HashSet<>();
1774                    
1775                    if (_getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES).contains(profileId))
1776                    {
1777                        permission.add(UserOrGroup.DENIED);
1778                    }
1779                    else if (_getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES).contains(profileId))
1780                    {
1781                        permission.add(UserOrGroup.ALLOWED);
1782                    }
1783                    
1784                    // Only add actual permissions to the result
1785                    if (!permission.isEmpty())
1786                    {
1787                        AmetysObject ao = _getAmetysObjectFromACLNode(groupNode);
1788                        // The ametys object could already be in the result map having permissions from an other group
1789                        Map<GroupIdentity, Set<UserOrGroup>> objectPermissions = result.computeIfAbsent(ao, k -> new HashMap<>());
1790                        // There can only be one node per group, so we don't need to retrieve existing value
1791                        objectPermissions.put(currentGroup, permission);
1792                    }
1793                }
1794                catch (RepositoryException e)
1795                {
1796                    _logger.error("Failed to retrieve object for group acl node " + groupNode.toString() + ". The node will be ignored.");
1797                }
1798            }
1799        }
1800        return result;
1801    }
1802    
1803    /**
1804     * Get all context with a permission for the profile for a user
1805     * @param rootNodes The JCR root nodes where starts the query search (must be something like "//element(myNode, ametys:collection)"), it will be the beginning of the JCR query
1806     * @param profileId the profile id
1807     * @return a map of the user's permission for each context
1808     */
1809    public static Map<Object, Map<UserIdentity, Set<UserOrGroup>>> getProfilePermissionsForUsers(Set< ? extends Object> rootNodes, String profileId)
1810    {
1811        Expression expr = new OrExpression(
1812                new AllowedProfileExpression(profileId),
1813                new DeniedProfileExpression(profileId)
1814        );
1815        
1816        Map<Object, Map<UserIdentity, Set<UserOrGroup>>> result = new HashMap<>();
1817        for (String rootPath : _convertNodeToPath(rootNodes))
1818        {
1819            NodeIterator nodes = getACLUsers(rootPath, expr);
1820            
1821            while (nodes.hasNext())
1822            {
1823                Node userNode = nodes.nextNode();
1824                try
1825                {
1826                    String login = ISO9075.decode(userNode.getName());
1827                    String populationId = userNode.getParent().getName();
1828                    
1829                    UserIdentity user = new UserIdentity(login, populationId);
1830                    
1831                    // Determine the user permissions
1832                    Set<UserOrGroup> permission = new HashSet<>();
1833                    
1834                    if (_getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES).contains(profileId))
1835                    {
1836                        permission.add(UserOrGroup.DENIED);
1837                    }
1838                    else if (_getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES).contains(profileId))
1839                    {
1840                        permission.add(UserOrGroup.ALLOWED);
1841                    }
1842                    
1843                    // Only add actual permissions to the result
1844                    if (!permission.isEmpty())
1845                    {
1846                        AmetysObject ao = _getAmetysObjectFromACLNode(userNode);
1847                        // The ametys object could already be in the result map having permissions from an other user
1848                        Map<UserIdentity, Set<UserOrGroup>> objectPermissions = result.computeIfAbsent(ao, k -> new HashMap<>());
1849                        // There can only be one node per user, so we don't need to retrieve existing value
1850                        objectPermissions.put(user, permission);
1851                    }
1852                }
1853                catch (RepositoryException e)
1854                {
1855                    _logger.error("Failed to retrieve object for group acl node " + userNode.toString() + ". The node will be ignored.");
1856                }
1857            }
1858        }
1859        return result;
1860    }
1861}