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.util.ISO9075;
043import org.apache.jackrabbit.util.Text;
044import org.slf4j.Logger;
045
046import org.ametys.core.group.GroupIdentity;
047import org.ametys.core.right.ProfileAssignmentStorage.AnonymousOrAnyConnectedKeys;
048import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup;
049import org.ametys.core.user.UserIdentity;
050import org.ametys.core.util.LambdaUtils;
051import org.ametys.plugins.repository.ACLAmetysObject;
052import org.ametys.plugins.repository.AmetysObject;
053import org.ametys.plugins.repository.AmetysObjectResolver;
054import org.ametys.plugins.repository.AmetysRepositoryException;
055import org.ametys.plugins.repository.ModifiableACLAmetysObject;
056import org.ametys.plugins.repository.ModifiableACLAmetysObjectProfileAssignmentStorage;
057import org.ametys.plugins.repository.RepositoryConstants;
058import org.ametys.plugins.repository.provider.AbstractRepository;
059import org.ametys.plugins.repository.query.expression.Expression;
060import org.ametys.plugins.repository.query.expression.OrExpression;
061import org.ametys.plugins.repository.query.expression.Expression.LogicalOperator;
062import org.ametys.runtime.plugin.component.LogEnabled;
063
064/**
065 * Helper for implementing {@link ModifiableACLAmetysObject} in JCR under its node.
066 */
067public class ACLJCRAmetysObjectHelper implements Component, Serviceable, LogEnabled
068{
069    /** The AmetysObject resolver */
070    protected static AmetysObjectResolver _resolver;
071    /** The repository */
072    protected static Repository _repository;
073    
074    private static final String __NODE_NAME_ROOT_ACL = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":acl";
075    private static final String __NODETYPE_ROOT_ACL = RepositoryConstants.NAMESPACE_PREFIX + ":acl";
076    
077    private static final String __NODE_NAME_ACL_USERS = "users";
078    private static final String __NODE_NAME_ACL_GROUPS = "groups";
079    private static final String __NODETYPE_ACL_USER = RepositoryConstants.NAMESPACE_PREFIX + ":acl-user";
080    private static final String __NODETYPE_ACL_GROUP = RepositoryConstants.NAMESPACE_PREFIX + ":acl-group";
081    private static final String __NODETYPE_UNSTRUCTURED = "nt:unstructured";
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     * @return a map associating a context object to the stored profile for each permission
252     */
253    public static Map<Object, Map<AnonymousOrAnyConnectedKeys, Set<String>>> getAllProfilesForAnonymousAndAnyConnectedUser()
254    {
255        Map<Object, Map<AnonymousOrAnyConnectedKeys, Set<String>>> result = new HashMap<>();
256        // Only retrieve node with assignments to anonymous or any connected
257        Expression predicate = new OrExpression(
258                () -> "@" + __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES,
259                () -> "@" + __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES,
260                () -> "@" + __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES,
261                () -> "@" + __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES
262            );
263        NodeIterator nodes = getACLRoots(null, predicate);
264        
265        while (nodes.hasNext())
266        {
267            Node aclNode = nodes.nextNode();
268            try 
269            {
270                Map<AnonymousOrAnyConnectedKeys, Set<String>> aoResult = new HashMap<>();
271                Set<String> allowedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES);
272                if (!allowedAnonymous.isEmpty())
273                {
274                    aoResult.put(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED, allowedAnonymous);
275                }
276                
277                Set<String> deniedAnonymous = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES);
278                if (!deniedAnonymous.isEmpty())
279                {
280                    aoResult.put(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED, deniedAnonymous);
281                }
282                Set<String> allowedAny = _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES);
283                if (!allowedAny.isEmpty())
284                {
285                    aoResult.put(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED, allowedAny);
286                }
287                Set<String> deniedAny = _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES);
288                if (!deniedAny.isEmpty())
289                {
290                    aoResult.put(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED, deniedAny);
291                }
292                
293                if (!aoResult.isEmpty())
294                {
295                    AmetysObject ao = _getAmetysObjectFromACLNode(aclNode);
296                    result.put(ao, aoResult);
297                }
298            }
299            catch (RepositoryException e)
300            {
301                _logger.error("Failed to retrieve object for acl node " + aclNode.toString() + ". The node will be ignored.");
302            }
303        }
304        return result;
305    }
306    
307    /**
308     * Gets all context with stored profiles (allowed or denied) for the groups and for each, a description of the permission
309     * Gets the groups that have allowed profiles assigned on the given object
310     * @param groups The groups to get profiles for.
311     * @return The map of context with their assigned permissions
312     */
313    public static Map<Object, Map<GroupIdentity, Map<UserOrGroup, Set<String>>>> getAllProfilesForGroups(Set<GroupIdentity> groups)
314    {
315        Map<Object, Map<GroupIdentity, Map<UserOrGroup, Set<String>>>> result = new HashMap<>();
316        if (!groups.isEmpty())
317        {
318            // Approximative query (to be fast)
319            NodeIterator nodes = _getApprochingACLGroups(groups, null, null);
320            
321            while (nodes.hasNext())
322            {
323                Node groupNode = nodes.nextNode();
324
325                // As the query was a fast approximative request, we now check if the result is fine
326                String groupId;
327                String directoryId;
328                try
329                {
330                    groupId = Text.unescapeIllegalJcrChars(groupNode.getName());
331                    directoryId = groupNode.getParent().getName();
332                }
333                catch (RepositoryException ex)
334                {
335                    throw new AmetysRepositoryException("An error occured getting group information", ex);
336                }
337                
338                GroupIdentity currentGroup = new GroupIdentity(groupId, directoryId);
339                if (groups.contains(currentGroup))
340                {
341                    try 
342                    {
343                        
344                        // Determine the group permissions
345                        Map<UserOrGroup, Set<String>> groupPermissions = new HashMap<>();
346                        
347                        Set<String> allowedProfiles = _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES);
348                        if (!allowedProfiles.isEmpty())
349                        {
350                            groupPermissions.put(UserOrGroup.ALLOWED, allowedProfiles);
351                        }
352                        Set<String> deniedProfiles = _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES);
353                        if (!deniedProfiles.isEmpty())
354                        {
355                            groupPermissions.put(UserOrGroup.DENIED, deniedProfiles);
356                        }
357                        
358                        // Only add actual permissions to the result
359                        if (!groupPermissions.isEmpty())
360                        {
361                            AmetysObject ao = _getAmetysObjectFromACLNode(groupNode);
362                            // The ametys object could already be in the result map having permissions from an other group
363                            Map<GroupIdentity, Map<UserOrGroup, Set<String>>> objectPermissions = result.computeIfAbsent(ao, k -> new HashMap<>());
364                            // There can only be one node per group, so we don't need to retrieve existing value
365                            objectPermissions.put(currentGroup, groupPermissions);
366                        }
367                    }
368                    catch (RepositoryException e)
369                    {
370                        _logger.error("Failed to retrieve object for group acl node " + groupNode.toString() + ". The node will be ignored.");
371                    }
372                }
373            }
374        }
375        return result;
376    }
377    
378    /**
379     * Gets all context with stored profiles (allowed or denied) for the user and for each, a description of the permission
380     * @param user The user to get profiles for.
381     * @return The map of context with their assigned allowed/denied profiles
382     */
383    public static Map<Object, Map<UserOrGroup, Set<String>>> getAllProfilesForUser(UserIdentity user)
384    {
385        Map<Object, Map<UserOrGroup, Set<String>>> result = new HashMap<>();
386        NodeIterator nodes = getACLUsers(user, null, null);
387        
388        while (nodes.hasNext())
389        {
390            Node userNode = nodes.nextNode();
391            try 
392            {
393                Map<UserOrGroup, Set<String>> aoResult = new HashMap<>();
394                Set<String> allowedProfiles = _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES);
395                if (!allowedProfiles.isEmpty())
396                {
397                    aoResult.put(UserOrGroup.ALLOWED, allowedProfiles);
398                }
399                Set<String> deniedProfiles = _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES);
400                if (!deniedProfiles.isEmpty())
401                {
402                    aoResult.put(UserOrGroup.DENIED, deniedProfiles);
403                }
404                
405                if (!aoResult.isEmpty())
406                {
407                    AmetysObject ao = _getAmetysObjectFromACLNode(userNode);
408                    result.put(ao, aoResult);
409                }
410            }
411            catch (RepositoryException e)
412            {
413                _logger.error("Failed to retrieve object for user acl node " + userNode.toString() + ". The node will be ignored.");
414            }
415        }
416        return result;
417    }
418    
419    private static AmetysObject _getAmetysObjectFromACLNode(Node node) throws RepositoryException, ItemNotFoundException, AccessDeniedException
420    {
421        switch (node.getPrimaryNodeType().getName())
422        {
423            case __NODETYPE_ROOT_ACL:
424                return _resolver.resolve(node.getParent(), false);
425            case __NODETYPE_ACL_USER:
426            case __NODETYPE_ACL_GROUP:
427                return _resolver.resolve(node.getParent().getParent().getParent().getParent(), false);
428            default:
429                return null;
430        }
431    }
432    
433    /**
434     * Returns all ACL root objects (ametys:acl nodes)
435     * @param rootPath The root path to restrict the search. Can be null.
436     * @param predicat The predicat expression. Can be null.
437     * @return The ACL root objects
438     */
439    public static NodeIterator getACLRoots (String rootPath, Expression predicat)
440    {
441        StringBuilder sb = new StringBuilder("/jcr:root");
442        
443        if (rootPath != null)
444        {
445            sb.append(rootPath);
446        }
447        
448        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")");
449        
450        if (predicat != null)
451        {
452            sb.append("[").append(predicat.build()).append("]");
453        }
454        
455        return _query(sb.toString());
456    }
457    
458    /**
459     * Returns all ACL objects for a given user (ametys:acl-user nodes)
460     * @param user The user
461     * @param rootPath The root path to restrict the search. Can be null.
462     * @param predicat The predicat expression. Can be null.
463     * @return The ACL user objects for user
464     */
465    public static NodeIterator getACLUsers (UserIdentity user, String rootPath, Expression predicat)
466    {
467        StringBuilder sb = new StringBuilder("/jcr:root");
468        
469        if (rootPath != null)
470        {
471            sb.append(rootPath);
472        }
473        
474        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")")
475            .append("/").append(__NODE_NAME_ACL_USERS)
476            .append("/").append(user.getPopulationId())
477            .append("/").append(ISO9075.encode(user.getLogin()));
478        
479        if (predicat != null)
480        {
481            sb.append("[").append(predicat.build()).append("]");
482        }
483        
484        String jcrQuery = sb.toString();
485        return _query(jcrQuery);
486    }
487    
488    /**
489     * Returns all ACL objects for users (ametys:acl-user nodes)
490     * @param predicat The predicat expression. Can be null.
491     * @return The ACL user objects for users
492     */
493    public static NodeIterator getACLUsers (Expression predicat)
494    {
495        StringBuilder sb = new StringBuilder();
496        
497        sb.append("//element(*, ").append(__NODETYPE_ACL_USER).append(")");
498        
499        if (predicat != null)
500        {
501            sb.append("[").append(predicat.build()).append("]");
502        }
503        
504        return _query(sb.toString());
505    }
506    
507    /**
508     * Returns all ACL objects for groups (ametys:acl-group nodes)
509     * @param predicat The predicat expression. Can be null.
510     * @return The ACL group objects for groups
511     */
512    public static NodeIterator getACLGroups (Expression predicat)
513    {
514        StringBuilder sb = new StringBuilder();
515        
516        sb.append("//element(*, ").append(__NODETYPE_ACL_GROUP).append(")");
517        
518        if (predicat != null)
519        {
520            sb.append("[").append(predicat.build()).append("]");
521        }
522        
523        return _query(sb.toString());
524    }
525    
526    private static NodeIterator _getApprochingACLGroups (Set<GroupIdentity> groups, String rootPath, Expression predicat)
527    {
528        StringBuilder sb = new StringBuilder("/jcr:root");
529        
530        if (rootPath != null)
531        {
532            sb.append(rootPath);
533        }
534        
535        sb.append("//element(*, ametys:acl-group)[(");
536        
537        sb.append(groups.stream()
538            .map(GroupIdentity::getId)
539            .map(Text::escapeIllegalJcrChars)
540            .map(ISO9075::encode) // used to support nodeName with number (id of SQL Group)
541            .map(nodeName -> "fn:name()='" + nodeName + "'")
542            .collect(Collectors.joining(LogicalOperator.OR.toString())));
543        sb.append(")");
544        
545        if (predicat != null)
546        {
547            sb.append(LogicalOperator.AND.toString()).append(predicat.build());
548        }
549        
550        sb.append("]");
551        
552        return _query(sb.toString());
553    }
554    
555    /**
556     * Returns all ACL objects for a given group (ametys:acl-group nodes)
557     * @param group The group
558     * @param rootPath The root path to restrict the search. Can be null.
559     * @param predicat The predicat expression. Can be null.
560     * @return The ACL user objects for groups
561     */
562    public static NodeIterator getACLGroups (GroupIdentity group, String rootPath, Expression predicat)
563    {
564        StringBuilder sb = new StringBuilder("/jcr:root");
565        
566        if (rootPath != null)
567        {
568            sb.append(rootPath);
569        }
570        
571        sb.append("//element(*, ").append(__NODETYPE_ROOT_ACL).append(")")
572            .append("/").append(__NODE_NAME_ACL_GROUPS)
573            .append("/").append(group.getDirectoryId())
574            .append("/").append(ISO9075.encode(Text.escapeIllegalJcrChars(group.getId())));
575        
576        if (predicat != null)
577        {
578            sb.append("[").append(predicat.build()).append("]");
579        }
580        
581        return _query(sb.toString());
582    }
583    
584    private static NodeIterator _query (String jcrQuery)
585    {
586        Session session = null;
587        try
588        {
589            session = _repository.login();
590            long t1 = System.currentTimeMillis();
591            @SuppressWarnings("deprecation")
592            Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH);
593            if (_logger.isInfoEnabled())
594            {
595                _logger.info("ACLJCR query '" + jcrQuery + "' executed in " + (System.currentTimeMillis() - t1) + " ms");
596            }
597            return query.execute().getNodes();
598        }
599        catch (RepositoryException ex)
600        {
601            if (session != null)
602            {
603                session.logout();
604            }
605            throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex);
606        }
607    }
608    
609    /* ------------------------------------------- */
610    /* PROFILES FOR ANY CONNECTED USER / ANONYMOUS */
611    /* ------------------------------------------- */
612    
613    /**
614     * Helper for {@link ACLAmetysObject#getProfilesForAnonymousAndAnyConnectedUser}
615     * @param node The JCR node for the Ametys object
616     * @return a map containing allowed/denied profiles that anonymous and any connected user has on the given object
617     */
618    public static Map<AnonymousOrAnyConnectedKeys, Set<String>> getProfilesForAnonymousAndAnyConnectedUser(Node node)
619    {
620        Node aclNode = _getACLNode(node);
621        if (aclNode == null)
622        {
623            return __ANONYMOUS_OR_ANYCONNECTEDUSER_NORIGHT;
624        }
625        else
626        {
627            return Map.of(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED,         _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES),
628                          AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED,          _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES),
629                          AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED,  _getProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES),
630                          AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED,   _getProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES));
631        }
632    }
633
634    /**
635     * Helper for {@link ModifiableACLAmetysObject#addAllowedProfilesForAnyConnectedUser(Set)}
636     * @param node The JCR node for the Ametys object
637     * @param profileIds The profiles to add
638     */
639    public static void addAllowedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
640    {
641        Node aclNode = _getOrCreateACLNode(node);
642        for (String profile : profileIds)
643        {
644            _addProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profile);
645        }
646        _save(node);
647    }
648
649    /**
650     * Helper for {@link ModifiableACLAmetysObject#removeAllowedProfilesForAnyConnectedUser(Set)}
651     * @param node The JCR node for the Ametys object
652     * @param profileIds The profiles to remove
653     */
654    public static void removeAllowedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
655    {
656        Node aclNode = _getOrCreateACLNode(node);
657        for (String profile : profileIds)
658        {
659            _removeProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profile);
660        }
661        _save(node);
662    }
663    
664    /**
665     * Helper for {@link ModifiableACLAmetysObject#addDeniedProfilesForAnyConnectedUser(Set)}
666     * @param node The JCR node for the Ametys object
667     * @param profileIds The profiles to add
668     */
669    public static void addDeniedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
670    {
671        Node aclNode = _getOrCreateACLNode(node);
672        for (String profile : profileIds)
673        {
674            _addProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profile);
675        }
676        _save(node);
677    }
678    
679    /**
680     * Helper for {@link ModifiableACLAmetysObject#removeDeniedProfilesForAnyConnectedUser(Set)}
681     * @param node The JCR node for the Ametys object
682     * @param profileIds The profiles to remove
683     */
684    public static void removeDeniedProfilesForAnyConnectedUser(Node node, Set<String> profileIds)
685    {
686        Node aclNode = _getOrCreateACLNode(node);
687        for (String profile : profileIds)
688        {
689            _removeProperty(aclNode, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profile);
690        }
691        _save(node);
692    }
693    
694    /**
695     * Helper for {@link ModifiableACLAmetysObject#addAllowedProfilesForAnonymous(Set)}
696     * @param node The JCR node for the Ametys object
697     * @param profileIds The profiles to add
698     */
699    public static void addAllowedProfilesForAnonymous(Node node, Set<String> profileIds)
700    {
701        Node aclNode = _getOrCreateACLNode(node);
702        for (String profile : profileIds)
703        {
704            _addProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profile);
705        }
706        _save(node);
707    }
708    
709    /**
710     * Helper for {@link ModifiableACLAmetysObject#removeAllowedProfilesForAnonymous(Set)}
711     * @param node The JCR node for the Ametys object
712     * @param profileIds The profiles to remove
713     */
714    public static void removeAllowedProfilesForAnonymous(Node node, Set<String> profileIds)
715    {
716        Node aclNode = _getOrCreateACLNode(node);
717        for (String profile : profileIds)
718        {
719            _removeProperty(aclNode, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profile);
720        }
721        _save(node);
722    }
723    
724    /**
725     * Helper for {@link ModifiableACLAmetysObject#addDeniedProfilesForAnyConnectedUser(Set)}
726     * @param node The JCR node for the Ametys object
727     * @param profileIds The profiles to add
728     */
729    public static void addDeniedProfilesForAnonymous(Node node, Set<String> profileIds)
730    {
731        Node aclNode = _getOrCreateACLNode(node);
732        for (String profile : profileIds)
733        {
734            _addProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profile);
735        }
736        _save(node);
737    }
738    
739    /**
740     * Helper for {@link ModifiableACLAmetysObject#removeDeniedProfilesForAnyConnectedUser(Set)}
741     * @param node The JCR node for the Ametys object
742     * @param profileIds The profiles to remove
743     */
744    public static void removeDeniedProfilesForAnonymous(Node node, Set<String> profileIds)
745    {
746        Node aclNode = _getOrCreateACLNode(node);
747        for (String profile : profileIds)
748        {
749            _removeProperty(aclNode, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profile);
750        }
751        _save(node);
752    }
753    
754    
755    /* ------------------- */
756    /* MANAGEMENT OF USERS */
757    /* ------------------- */
758    /**
759     * Helper for {@link ACLAmetysObject#getProfilesForUsers}
760     * @param node The JCR node for the Ametys object
761     * @param user The user to get profiles for. Can be null to get profiles for all users that have rights
762     * @return The map of allowed users with their assigned allowed/denied profiles
763     */
764    public static Map<UserIdentity, Map<UserOrGroup, Set<String>>> getProfilesForUsers(Node node, UserIdentity user)
765    {
766        if (user == null)
767        {
768            try
769            {
770                Node usersNode = _getUsersNode(node);
771                if (usersNode == null)
772                {
773                    return Map.of();
774                }
775                
776                Map<UserIdentity, Map<UserOrGroup, Set<String>>> result = new HashMap<>();
777                
778                NodeIterator populationsIterator = usersNode.getNodes();
779                while (populationsIterator.hasNext())
780                {
781                    Node populationNode = populationsIterator.nextNode();
782                    NodeIterator usersIterator = populationNode.getNodes();
783                    while (usersIterator.hasNext())
784                    {
785                        Node userNode = usersIterator.nextNode();
786                        Set<String> allowedProfiles = _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES);
787                        Set<String> deniedProfiles = _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES);
788                        if (!allowedProfiles.isEmpty() || !deniedProfiles.isEmpty())
789                        {
790                            result.put(new UserIdentity(userNode.getName(), populationNode.getName()), 
791                                        Map.of(UserOrGroup.ALLOWED, allowedProfiles,
792                                               UserOrGroup.DENIED, deniedProfiles));
793                        }
794                    }
795                }
796                
797                return result;
798            }
799            catch (RepositoryException e)
800            {
801                throw new AmetysRepositoryException("Unable to get allowed/denied users", e);
802            }
803        }
804        else
805        {
806            Node userNode = _getUserNode(node, user);
807            if (userNode == null)
808            {
809                return Map.of(user, __USER_OR_GROUP_NORIGHT);
810            }
811            else
812            {
813                return Map.of(user, 
814                              Map.of(UserOrGroup.ALLOWED, _getProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES),
815                                     UserOrGroup.DENIED,  _getProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES)));
816            }
817        }
818    }
819
820    /**
821     * Helper for {@link ModifiableACLAmetysObject#addAllowedUsers(Set, String)}
822     * @param users The users to add
823     * @param node The JCR node for the Ametys object
824     * @param profileId The id of the profile
825     */
826    public static void addAllowedUsers(Set<UserIdentity> users, Node node, String profileId)
827    {
828        for (UserIdentity userIdentity : users)
829        {
830            Node userNode = _getOrCreateUserNode(node, userIdentity);
831            _addProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
832        }
833        _save(node);
834    }
835
836    /**
837     * Helper for {@link ModifiableACLAmetysObject#removeAllowedUsers(Set, String)}
838     * @param users The users to remove
839     * @param node The JCR node for the Ametys object
840     * @param profileId The id of the profile
841     */
842    public static void removeAllowedUsers(Set<UserIdentity> users, Node node, String profileId)
843    {
844        for (UserIdentity userIdentity : users)
845        {
846            Node userNode = _getOrCreateUserNode(node, userIdentity);
847            _removeProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
848        }
849        _save(node);
850    }
851
852    /**
853     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set)}
854     * @param users The users to remove
855     * @param node The JCR node for the Ametys object
856     */
857    public static void removeAllowedUsers(Set<UserIdentity> users, Node node)
858    {
859        for (UserIdentity userIdentity : users)
860        {
861            Node userNode = _getOrCreateUserNode(node, userIdentity);
862            _setProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, Collections.EMPTY_SET);
863        } 
864        _save(node);
865    }
866    
867    /**
868     * Helper for {@link ModifiableACLAmetysObject#addDeniedUsers(Set, String)}
869     * @param users The users to add
870     * @param node The JCR node for the Ametys object
871     * @param profileId The id of the profile
872     */
873    public static void addDeniedUsers(Set<UserIdentity> users, Node node, String profileId)
874    {
875        for (UserIdentity userIdentity : users)
876        {
877            Node userNode = _getOrCreateUserNode(node, userIdentity);
878            _addProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
879        }
880        _save(node);
881    }
882
883    /**
884     * Helper for {@link ModifiableACLAmetysObject#removeDeniedUsers(Set, String)}
885     * @param users The users to remove
886     * @param node The JCR node for the Ametys object
887     * @param profileId The id of the profile
888     */
889    public static void removeDeniedUsers(Set<UserIdentity> users, Node node, String profileId)
890    {
891        for (UserIdentity userIdentity : users)
892        {
893            Node userNode = _getOrCreateUserNode(node, userIdentity);
894            _removeProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
895        }
896        _save(node);
897    }
898
899    /**
900     * Helper for {@link ModifiableACLAmetysObject#removeDeniedUsers(Set)}
901     * @param users The users to remove
902     * @param node The JCR node for the Ametys object
903     */
904    public static void removeDeniedUsers(Set<UserIdentity> users, Node node)
905    {
906        for (UserIdentity userIdentity : users)
907        {
908            Node userNode = _getOrCreateUserNode(node, userIdentity);
909            _setProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, Collections.EMPTY_SET);
910        } 
911        _save(node);
912    }
913
914    /* -------------------- */
915    /* MANAGEMENT OF GROUPS */
916    /* -------------------- */
917
918    /**
919     * Helper for {@link ACLAmetysObject#getProfilesForGroups} 
920     * @param node The JCR node for the Ametys object
921     * @param groups The group to get profiles for. Can be null to get profiles for all groups that have rights
922     * @return The map of allowed/denied groups with their assigned profiles
923     */
924    public static Map<GroupIdentity, Map<UserOrGroup, Set<String>>> getProfilesForGroups(Node node, Set<GroupIdentity> groups)
925    {
926        try
927        {
928            if (groups != null && groups.isEmpty())
929            {
930                return Map.of();
931            }
932            
933            Node groupsNode = _getGroupsNode(node);
934            if (groupsNode == null)
935            {
936                return Map.of();
937            }
938            
939            Map<GroupIdentity, Map<UserOrGroup, Set<String>>> result = new HashMap<>();
940            
941            if (groups == null)
942            {
943                NodeIterator groupDirectoriesIterator = groupsNode.getNodes();
944                while (groupDirectoriesIterator.hasNext())
945                {
946                    Node groupDirectoryNode = groupDirectoriesIterator.nextNode();
947                    NodeIterator groupsIterator = groupDirectoryNode.getNodes();
948                    while (groupsIterator.hasNext())
949                    {
950                        Node groupNode = groupsIterator.nextNode();
951                        Set<String> allowedProfiles = _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES);
952                        Set<String> deniedProfiles = _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES);
953                        if (!allowedProfiles.isEmpty() || !deniedProfiles.isEmpty())
954                        {
955                            result.put(new GroupIdentity(Text.unescapeIllegalJcrChars(groupNode.getName()), groupDirectoryNode.getName()), 
956                                        Map.of(UserOrGroup.ALLOWED, allowedProfiles,
957                                               UserOrGroup.DENIED, deniedProfiles));
958                        }
959                    }
960                }
961            }
962            else
963            {
964                Map<String, Node> groupsNodeByDirectoryIdCache = new HashMap<>();
965                
966                for (GroupIdentity group : groups)
967                {
968                    Node directoryNode = groupsNodeByDirectoryIdCache.computeIfAbsent(group.getDirectoryId(), LambdaUtils.wrap(directoryId -> groupsNode.hasNode(directoryId) ? groupsNode.getNode(directoryId) : null));
969                    
970                    String groupNodeName = Text.escapeIllegalJcrChars(group.getId());
971                    if (directoryNode != null && directoryNode.hasNode(groupNodeName))
972                    {
973                        Node groupNode = directoryNode.getNode(groupNodeName);
974                        
975                        result.put(group, 
976                                Map.of(UserOrGroup.ALLOWED, _getProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES),
977                                       UserOrGroup.DENIED, _getProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES)));
978                    }
979                }
980            }
981            
982            return result;
983        }
984        catch (RepositoryException e)
985        {
986            throw new AmetysRepositoryException("Unable to get allowed/denied groups", e);
987        }
988    }
989    
990    /**
991     * Helper for {@link ModifiableACLAmetysObject#addAllowedGroups(Set, String)}
992     * @param groups The groups to add
993     * @param node The JCR node for the Ametys object
994     * @param profileId The id of the profile
995     */
996    public static void addAllowedGroups(Set<GroupIdentity> groups, Node node, String profileId)
997    {
998        for (GroupIdentity groupIdentity : groups)
999        {
1000            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1001            _addProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
1002        }
1003        _save(node);
1004    }
1005
1006    /**
1007     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set, String)}
1008     * @param groups The groups to remove
1009     * @param node The JCR node for the Ametys object
1010     * @param profileId The id of the profile
1011     */
1012    public static void removeAllowedGroups(Set<GroupIdentity> groups, Node node, String profileId)
1013    {
1014        for (GroupIdentity groupIdentity : groups)
1015        {
1016            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1017            _removeProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
1018        }
1019        _save(node);
1020    }
1021
1022    /**
1023     * Helper for {@link ModifiableACLAmetysObject#removeAllowedGroups(Set)}
1024     * @param groups The groups to remove
1025     * @param node The JCR node for the Ametys object
1026     */
1027    public static void removeAllowedGroups(Set<GroupIdentity> groups, Node node)
1028    {
1029        for (GroupIdentity groupIdentity : groups)
1030        {
1031            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1032            _setProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, Collections.EMPTY_SET);
1033        } 
1034        _save(node);
1035    }
1036
1037    /**
1038     * Helper for {@link ModifiableACLAmetysObject#addDeniedGroups(Set, String)}
1039     * @param groups The groups to add
1040     * @param node The JCR node for the Ametys object
1041     * @param profileId The id of the profile
1042     */
1043    public static void addDeniedGroups(Set<GroupIdentity> groups, Node node, String profileId)
1044    {
1045        for (GroupIdentity groupIdentity : groups)
1046        {
1047            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1048            _addProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
1049        }
1050        _save(node);
1051    }
1052
1053    /**
1054     * Helper for {@link ModifiableACLAmetysObject#removeDeniedGroups(Set, String)}
1055     * @param groups The groups to remove
1056     * @param node The JCR node for the Ametys object
1057     * @param profileId The id of the profile
1058     */
1059    public static void removeDeniedGroups(Set<GroupIdentity> groups, Node node, String profileId)
1060    {
1061        for (GroupIdentity groupIdentity : groups)
1062        {
1063            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1064            _removeProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
1065        }
1066        _save(node);
1067    }
1068
1069    /**
1070     * Helper for {@link ModifiableACLAmetysObject#removeDeniedGroups(Set)}
1071     * @param groups The groups to remove
1072     * @param node The JCR node for the Ametys object
1073     */
1074    public static void removeDeniedGroups(Set<GroupIdentity> groups, Node node)
1075    {
1076        for (GroupIdentity groupIdentity : groups)
1077        {
1078            Node groupNode = _getOrCreateGroupNode(node, groupIdentity);
1079            _setProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, Collections.EMPTY_SET);
1080        } 
1081        _save(node);
1082    }
1083    
1084    
1085    /* ------ */
1086    /* REMOVE */
1087    /* ------ */
1088    
1089    /**
1090     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeProfile(String)}
1091     * @param profileId The id of the profile
1092     */
1093    public static void removeProfile(String profileId)
1094    {
1095        // Remove this profile set as allowed or denied in users 
1096        Expression expr = new OrExpression(new AllowedProfileExpression(profileId), new DeniedProfileExpression(profileId));
1097        NodeIterator users = getACLUsers(expr);
1098        while (users.hasNext())
1099        {
1100            Node userNode = (Node) users.next();
1101            _removeProperty(userNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
1102            _removeProperty(userNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
1103            _save(userNode);
1104        }
1105        
1106        // Remove this profile set as allowed or denied in groups 
1107        NodeIterator groups = getACLGroups(expr);
1108        while (groups.hasNext())
1109        {
1110            Node groupNode = (Node) groups.next();
1111            _removeProperty(groupNode, __PROPERTY_NAME_ALLOWED_PROFILES, profileId);
1112            _removeProperty(groupNode, __PROPERTY_NAME_DENIED_PROFILES, profileId);
1113            _save(groupNode);
1114        }
1115        
1116        // Remove this profile set as allowed or denied for anonymous and any connected 
1117        expr = new OrExpression(new AnonymousAllowedProfileExpression(profileId), new AnonymousDeniedProfileExpression(profileId), new AnyConnectedAllowedProfileExpression(profileId), new AnyConnectedDeniedProfileExpression(profileId));
1118        NodeIterator nodes = getACLRoots(null, expr);
1119        while (nodes.hasNext())
1120        {
1121            Node node = (Node) nodes.next();
1122            _removeProperty(node, __PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profileId);
1123            _removeProperty(node, __PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profileId);
1124            _removeProperty(node, __PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profileId);
1125            _removeProperty(node, __PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profileId);
1126            _save(node);
1127        }
1128    }
1129    
1130    /**
1131     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeUser(UserIdentity)}
1132     * @param user The user
1133     */
1134    public static void removeUser(UserIdentity user)
1135    {
1136        NodeIterator users = getACLUsers(user, null, null);
1137        
1138        while (users.hasNext())
1139        {
1140            Node userNode = (Node) users.next();
1141            try
1142            {
1143                userNode.remove();
1144                _save(userNode);
1145            }
1146            catch (RepositoryException e)
1147            {
1148                throw new AmetysRepositoryException(e);
1149            }
1150        }
1151    }
1152    
1153    /**
1154     * Helper for {@link ModifiableACLAmetysObjectProfileAssignmentStorage#removeGroup(GroupIdentity)}
1155     * @param group The group
1156     */
1157    public static void removeGroup(GroupIdentity group)
1158    {
1159        NodeIterator groups = getACLGroups(group, null, null);
1160        while (groups.hasNext())
1161        {
1162            Node gpNode = (Node) groups.next();
1163            try
1164            {
1165                gpNode.remove();
1166                _save(gpNode);
1167            }
1168            catch (RepositoryException e)
1169            {
1170                throw new AmetysRepositoryException(e);
1171            }
1172        }
1173    }
1174    
1175    /* --------------- */
1176    /* INHERITANCE     */
1177    /* --------------- */
1178    /**
1179     * Helper for {@link ACLAmetysObject#isInheritanceDisallowed()}
1180     * @param node The JCR node for the Ametys object
1181     * @return true if the inheritance is disallow of the given node
1182     */
1183    public static boolean isInheritanceDisallowed(Node node)
1184    {
1185        try
1186        {
1187            Node aclNode = _getACLNode(node);
1188            if (aclNode != null && aclNode.hasProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE))
1189            {
1190                return aclNode.getProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE).getBoolean();
1191            }
1192            return false;
1193        }
1194        catch (RepositoryException e)
1195        {
1196            throw new AmetysRepositoryException("Unable to get " + __PROPERTY_NAME_DISALLOW_INHERITANCE + " property", e);
1197        }
1198    }
1199    
1200    /**
1201     * Helper for {@link ModifiableACLAmetysObject#disallowInheritance(boolean)}
1202     * @param node The JCR node for the Ametys object
1203     * @param disallow true to disallow the inheritance, false otherwise
1204     */
1205    public static void disallowInheritance(Node node, boolean disallow)
1206    {
1207        Node aclNode = _getOrCreateACLNode(node);
1208        try
1209        {
1210            aclNode.setProperty(__PROPERTY_NAME_DISALLOW_INHERITANCE, disallow);
1211        }
1212        catch (RepositoryException e)
1213        {
1214            throw new AmetysRepositoryException("Unable to set " + __PROPERTY_NAME_DISALLOW_INHERITANCE + " property", e);
1215        }
1216        _save(node);
1217    }
1218    
1219    
1220    /* --------------- */
1221    /* PRIVATE METHODS */
1222    /* --------------- */
1223    
1224    private static void _checkLock(Node node) throws AmetysRepositoryException
1225    {
1226        try
1227        {
1228            if (node.isLocked())
1229            {
1230                LockManager lockManager = node.getSession().getWorkspace().getLockManager();
1231                
1232                Lock lock = lockManager.getLock(node.getPath());
1233                Node lockHolder = lock.getNode();
1234                
1235                lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
1236            }
1237        }
1238        catch (RepositoryException e)
1239        {
1240            throw new AmetysRepositoryException("Unable to add lock token on ACL node", e);
1241        }
1242    }
1243    
1244    private static Node _getOrCreateACLNode(Node node)
1245    {
1246        try
1247        {
1248            if (node.hasNode(__NODE_NAME_ROOT_ACL))
1249            {
1250                return node.getNode(__NODE_NAME_ROOT_ACL);
1251            }
1252            else
1253            {
1254                _checkLock(node);
1255                return node.addNode(__NODE_NAME_ROOT_ACL, __NODETYPE_ROOT_ACL);
1256            }
1257        }
1258        catch (RepositoryException e)
1259        {
1260            throw new AmetysRepositoryException("Error while getting root ACL node.", e);
1261        }
1262    }
1263    
1264    private static Node _getACLNode(Node node)
1265    {
1266        try
1267        {
1268            if (node.hasNode(__NODE_NAME_ROOT_ACL))
1269            {
1270                return node.getNode(__NODE_NAME_ROOT_ACL);
1271            }
1272            else
1273            {
1274                return null;
1275            }
1276        }
1277        catch (RepositoryException e)
1278        {
1279            throw new AmetysRepositoryException("Error while getting root ACL node.", e);
1280        }
1281    }
1282    
1283    private static Node _getOrCreateUsersNode(Node node)
1284    {
1285        try
1286        {
1287            Node aclNode = _getOrCreateACLNode(node);
1288            if (aclNode.hasNode(__NODE_NAME_ACL_USERS))
1289            {
1290                return aclNode.getNode(__NODE_NAME_ACL_USERS);
1291            }
1292            else
1293            {
1294                return aclNode.addNode(__NODE_NAME_ACL_USERS, __NODETYPE_UNSTRUCTURED);
1295            }
1296        }
1297        catch (RepositoryException e)
1298        {
1299            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1300        }
1301    }
1302    
1303    private static Node _getUserNode(Node node, UserIdentity user)
1304    {
1305        try
1306        {
1307            Node aclNode = _getACLNode(node);
1308            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_USERS))
1309            {
1310                Node aclUsersNode = aclNode.getNode(__NODE_NAME_ACL_USERS);
1311                if (aclUsersNode.hasNode(user.getPopulationId()))
1312                {
1313                    Node popNode = aclUsersNode.getNode(user.getPopulationId());
1314                    if (popNode.hasNode(user.getLogin()))
1315                    {
1316                        return popNode.getNode(user.getLogin());
1317                    }
1318                }
1319            }
1320            
1321            return null;
1322        }
1323        catch (RepositoryException e)
1324        {
1325            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1326        }
1327    }
1328    
1329    private static Node _getUsersNode(Node node)
1330    {
1331        try
1332        {
1333            Node aclNode = _getACLNode(node);
1334            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_USERS))
1335            {
1336                return aclNode.getNode(__NODE_NAME_ACL_USERS);
1337            }
1338            else
1339            {
1340                return null;
1341            }
1342        }
1343        catch (RepositoryException e)
1344        {
1345            throw new AmetysRepositoryException("Error while getting 'users' ACL node.", e);
1346        }
1347    }
1348    
1349    private static Node _getOrCreateGroupsNode(Node node)
1350    {
1351        try
1352        {
1353            Node aclNode = _getOrCreateACLNode(node);
1354            if (aclNode.hasNode(__NODE_NAME_ACL_GROUPS))
1355            {
1356                return aclNode.getNode(__NODE_NAME_ACL_GROUPS);
1357            }
1358            else
1359            {
1360                return aclNode.addNode(__NODE_NAME_ACL_GROUPS, __NODETYPE_UNSTRUCTURED);
1361            }
1362        }
1363        catch (RepositoryException e)
1364        {
1365            throw new AmetysRepositoryException("Error while getting 'groups' ACL node.", e);
1366        }
1367    }
1368    
1369    private static Node _getGroupsNode(Node node)
1370    {
1371        try
1372        {
1373            Node aclNode = _getACLNode(node);
1374            if (aclNode != null && aclNode.hasNode(__NODE_NAME_ACL_GROUPS))
1375            {
1376                return aclNode.getNode(__NODE_NAME_ACL_GROUPS);
1377            }
1378            else
1379            {
1380                return null;
1381            }
1382        }
1383        catch (RepositoryException e)
1384        {
1385            throw new AmetysRepositoryException("Error while getting 'groups' ACL node.", e);
1386        }
1387    }
1388    
1389    private static Node _getOrCreateUserNode(Node node, UserIdentity userIdentity)
1390    {
1391        try
1392        {
1393            Node usersNode = _getOrCreateUsersNode(node);
1394            String population = userIdentity.getPopulationId();
1395            String login = userIdentity.getLogin();
1396            
1397            if (usersNode.hasNode(population))
1398            {
1399                Node populationNode = usersNode.getNode(population);
1400                if (populationNode.hasNode(login))
1401                {
1402                    return populationNode.getNode(login);
1403                }
1404                else
1405                {
1406                    return populationNode.addNode(login, __NODETYPE_ACL_USER);
1407                }
1408            }
1409            else
1410            {
1411                return usersNode.addNode(population, __NODETYPE_UNSTRUCTURED).addNode(login, __NODETYPE_ACL_USER);
1412            }
1413        }
1414        catch (RepositoryException e)
1415        {
1416            throw new AmetysRepositoryException(String.format("Error while getting 'user' ACL node for %s.", userIdentity.toString()), e);
1417        }
1418    }
1419    
1420    private static Node _getOrCreateGroupNode(Node node, GroupIdentity groupIdentity)
1421    {
1422        try
1423        {
1424            Node groupsNode = _getOrCreateGroupsNode(node);
1425            String directoryId = groupIdentity.getDirectoryId();
1426            String id = Text.escapeIllegalJcrChars(groupIdentity.getId());
1427            
1428            if (groupsNode.hasNode(directoryId))
1429            {
1430                Node populationNode = groupsNode.getNode(directoryId);
1431                if (populationNode.hasNode(id))
1432                {
1433                    return populationNode.getNode(id);
1434                }
1435                else
1436                {
1437                    return populationNode.addNode(id, __NODETYPE_ACL_GROUP);
1438                }
1439            }
1440            else
1441            {
1442                return groupsNode.addNode(directoryId, __NODETYPE_UNSTRUCTURED).addNode(id, __NODETYPE_ACL_GROUP);
1443            }
1444        }
1445        catch (RepositoryException e)
1446        {
1447            throw new AmetysRepositoryException(String.format("Error while getting 'group' ACL node for %s.", groupIdentity.toString()), e);
1448        }
1449    }
1450    
1451    private static Set<String> _getProperty(Node node, String propertyName)
1452    {
1453        try
1454        {
1455            Value[] values = node.getProperty(propertyName).getValues();
1456            Set<String> result = new HashSet<>();
1457            for (Value value : values)
1458            {
1459                result.add(value.getString());
1460            }
1461            return result;
1462        }
1463        catch (PathNotFoundException e)
1464        {
1465            return new HashSet<>();
1466        }
1467        catch (RepositoryException e)
1468        {
1469            throw new AmetysRepositoryException("Unable to get " + propertyName + " property", e);
1470        }
1471    }
1472    
1473    private static void _setProperty(Node node, String propertyName, Set<String> profiles)
1474    {
1475        try
1476        {
1477            node.setProperty(propertyName, profiles.toArray(new String[profiles.size()]));
1478        }
1479        catch (RepositoryException e)
1480        {
1481            throw new AmetysRepositoryException("Unable to set " + propertyName + " property", e);
1482        }
1483    }
1484    
1485    private static void _addProperty(Node node, String propertyName, String profileToAdd)
1486    {
1487        Set<String> profiles = _getProperty(node, propertyName);
1488        if (!profiles.contains(profileToAdd))
1489        {
1490            profiles.add(profileToAdd);
1491            _setProperty(node, propertyName, profiles);
1492        }
1493    }
1494    
1495    private static void _removeProperty(Node node, String propertyName, String profileToRemove)
1496    {
1497        Set<String> profiles = _getProperty(node, propertyName);
1498        if (profiles.contains(profileToRemove))
1499        {
1500            profiles.remove(profileToRemove);
1501            _setProperty(node, propertyName, profiles);
1502        }
1503    }
1504    
1505    private static void _save(Node node)
1506    {
1507        try
1508        {
1509            node.getSession().save();
1510        }
1511        catch (RepositoryException e)
1512        {
1513            throw new AmetysRepositoryException("Unable to save changes", e);
1514        }
1515    }
1516    
1517    /* ---------------------------------------*/
1518    /*      JCR EXPRESSIONS FOR PROFILES      */
1519    /* ---------------------------------------*/
1520    
1521    static class AllowedProfileExpression extends ACLProfileExpression
1522    {
1523        public AllowedProfileExpression (String ... profileIds)
1524        {
1525            super(__PROPERTY_NAME_ALLOWED_PROFILES, profileIds);
1526        }
1527    }
1528    
1529    static class DeniedProfileExpression extends ACLProfileExpression
1530    {
1531        public DeniedProfileExpression (String ... profileIds)
1532        {
1533            super(__PROPERTY_NAME_DENIED_PROFILES, profileIds);
1534        }
1535    }
1536    
1537    static class AnyConnectedDeniedProfileExpression extends ACLProfileExpression
1538    {
1539        public AnyConnectedDeniedProfileExpression (String ... profileIds)
1540        {
1541            super(__PROPERTY_NAME_DENIED_ANY_CONNECTED_PROFILES, profileIds);
1542        }
1543    }
1544    
1545    static class AnyConnectedAllowedProfileExpression extends ACLProfileExpression
1546    {
1547        public AnyConnectedAllowedProfileExpression (String ... profileIds)
1548        {
1549            super(__PROPERTY_NAME_ALLOWED_ANY_CONNECTED_PROFILES, profileIds);
1550        }
1551    }
1552    
1553    static class AnonymousDeniedProfileExpression extends ACLProfileExpression
1554    {
1555        public AnonymousDeniedProfileExpression (String ... profileIds)
1556        {
1557            super(__PROPERTY_NAME_DENIED_ANONYMOUS_PROFILES, profileIds);
1558        }
1559    }
1560    
1561    static class AnonymousAllowedProfileExpression extends ACLProfileExpression
1562    {
1563        public AnonymousAllowedProfileExpression (String ... profileIds)
1564        {
1565            super(__PROPERTY_NAME_ALLOWED_ANONYMOUS_PROFILES, profileIds);
1566        }
1567    }
1568    
1569    static class ACLProfileExpression implements Expression
1570    {
1571        private String[] _profileIds;
1572        private String _propertyName;
1573        
1574        public ACLProfileExpression (String propertyName, String ... profileIds)
1575        {
1576            _propertyName = propertyName;
1577            _profileIds = profileIds;
1578        }
1579        
1580        @Override
1581        public String build()
1582        {
1583            boolean isFirst = true;
1584            StringBuilder sb = new StringBuilder("(");
1585            
1586            for (String profileId : _profileIds)
1587            {
1588                if (isFirst)
1589                {
1590                    isFirst = false;
1591                }
1592                else
1593                {
1594                    sb.append(LogicalOperator.OR.toString());
1595                }
1596                
1597                sb.append("@")
1598                    .append(_propertyName)
1599                    .append(Operator.EQ)
1600                    .append("'").append(profileId).append("'");
1601            }
1602            
1603            if (isFirst)
1604            {
1605                return "";
1606            }
1607            else
1608            {
1609                return sb.append(")").toString();
1610            }
1611        }
1612    }
1613}