001/*
002 *  Copyright 2021 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.extraction.execution;
017
018import java.io.IOException;
019import java.util.Collection;
020import java.util.HashMap;
021import java.util.List;
022import java.util.Map;
023import java.util.Set;
024
025import org.apache.avalon.framework.activity.Initializable;
026import org.apache.avalon.framework.component.Component;
027import org.apache.avalon.framework.service.ServiceException;
028import org.apache.avalon.framework.service.ServiceManager;
029import org.apache.avalon.framework.service.Serviceable;
030import org.apache.commons.io.FileUtils;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.excalibur.source.SourceException;
033import org.apache.excalibur.source.SourceResolver;
034import org.apache.excalibur.source.TraversableSource;
035import org.apache.excalibur.source.impl.FileSource;
036
037import org.ametys.core.cache.AbstractCacheManager;
038import org.ametys.core.cache.Cache;
039import org.ametys.core.file.FileHelper;
040import org.ametys.core.group.GroupIdentity;
041import org.ametys.core.right.ProfileAssignmentStorage.AnonymousOrAnyConnectedKeys;
042import org.ametys.core.right.ProfileAssignmentStorage.UserOrGroup;
043import org.ametys.core.right.ProfileAssignmentStorageExtensionPoint;
044import org.ametys.core.right.RightManager;
045import org.ametys.core.right.RightManager.RightResult;
046import org.ametys.core.ui.Callable;
047import org.ametys.core.user.CurrentUserProvider;
048import org.ametys.core.user.UserIdentity;
049import org.ametys.plugins.core.user.UserHelper;
050import org.ametys.plugins.extraction.ExtractionConstants;
051import org.ametys.plugins.extraction.rights.ExtractionAccessController;
052import org.ametys.runtime.i18n.I18nizableText;
053import org.ametys.runtime.plugin.component.AbstractLogEnabled;
054
055/**
056 * Object representing the extraction definition file content
057 */
058public class ExtractionDAO extends AbstractLogEnabled implements Serviceable, Component, Initializable
059{    
060    /** The Avalon role */
061    public static final String ROLE = ExtractionDAO.class.getName();
062    
063    /** Extraction author cache id */
064    private static final String EXTRACTION_AUTHOR_CACHE = ExtractionDAO.class.getName() + "$extractionAuthor";
065    
066    private CurrentUserProvider _userProvider;
067    private RightManager _rightManager;
068    private SourceResolver _sourceResolver;
069    private ExtractionDefinitionReader _definitionReader;
070    private ProfileAssignmentStorageExtensionPoint _profileAssignmentStorageEP;
071    private CurrentUserProvider _currentUserProvider;
072    private AbstractCacheManager _cacheManager;
073    private UserHelper _userHelper;
074    private TraversableSource _root;
075    private FileHelper _fileHelper;
076    
077    public void service(ServiceManager manager) throws ServiceException
078    {
079        _userProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
080        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
081        _sourceResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
082        _definitionReader = (ExtractionDefinitionReader) manager.lookup(ExtractionDefinitionReader.ROLE);
083        _profileAssignmentStorageEP = (ProfileAssignmentStorageExtensionPoint) manager.lookup(ProfileAssignmentStorageExtensionPoint.ROLE);
084        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
085        _cacheManager = (AbstractCacheManager) manager.lookup(AbstractCacheManager.ROLE);
086        _userHelper = (UserHelper) manager.lookup(UserHelper.ROLE);
087        _fileHelper = (FileHelper) manager.lookup(FileHelper.ROLE);
088    }
089
090    public void initialize() throws Exception
091    {
092        _root = (TraversableSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
093
094        _cacheManager.createRequestCache(EXTRACTION_AUTHOR_CACHE, 
095                new I18nizableText("plugin.extraction", "PLUGINS_EXTRACTION_CACHE_DEFINITION_AUTHOR_LABEL"),
096                new I18nizableText("plugin.extraction", "PLUGINS_EXTRACTION_CACHE_DEFINITION_AUTHOR_DESCRIPTION"),
097                true);
098    }
099    
100    /**
101     * Get the root container properties
102     * @return The root container properties
103     * @throws IOException If an error occurred while reading folder
104     */
105    @Callable
106    public Map<String, Object> getRootProperties() throws IOException
107    {
108        String rootURI = ExtractionConstants.DEFINITIONS_DIR;
109        TraversableSource rootDir = (TraversableSource) _sourceResolver.resolveURI(rootURI);
110        Map<String, Object> infos = getExtractionContainerProperties(rootDir);
111        return infos;
112    }
113
114    /**
115     * Get extraction container properties
116     * @param folder the source of the extraction container
117     * @return The extraction container properties
118     */
119    public Map<String, Object> getExtractionContainerProperties(TraversableSource folder)
120    {
121        Map<String, Object> infos = new HashMap<>();
122        
123        UserIdentity currentUser = _userProvider.getUser();
124        
125        infos.put("canRead", canRead(currentUser, folder));
126        infos.put("canRename", canRename(currentUser, folder));
127        infos.put("canWrite", canWrite(currentUser, folder));
128        infos.put("canDelete", canDelete(currentUser, folder));
129        infos.put("canAssignRights", canAssignRights(currentUser, folder));
130
131        return infos;
132    }
133    
134    /**
135     * Get extraction properties
136     * @param extraction the extraction
137     * @param file the source of the extraction
138     * @return The extraction properties
139     */
140    public Map<String, Object> getExtractionProperties(Extraction extraction, TraversableSource file)
141    {
142        Map<String, Object> infos = new HashMap<>();
143        
144        UserIdentity currentUser = _userProvider.getUser();
145        infos.put("descriptionId", extraction.getDescriptionId());
146        
147        UserIdentity author = extraction.getAuthor();
148        infos.put("author", _userHelper.user2json(author));
149  
150        infos.put("canRead", canRead(currentUser, file));
151        infos.put("canWrite", canWrite(currentUser, file));
152        infos.put("canDelete", canDelete(currentUser, file));
153        infos.put("canAssignRights", canAssignRights(currentUser, file));
154
155        return infos;
156    }
157
158    /**
159     * Check if a folder has a descendant in read access for a given user
160     * @param userIdentity the user
161     * @param folder the source of the extraction container
162     * @return <code>true</code> if the folder has a descendant in read access, <code>false</code> otherwise
163     */
164    public Boolean hasAnyReadableDescendant(UserIdentity userIdentity, TraversableSource folder)
165    {
166        try 
167        {
168            if (folder.exists())
169            {
170                for (TraversableSource child : (Collection<TraversableSource>) folder.getChildren())
171                {
172                    if (child.isCollection())
173                    {
174                        if (canRead(userIdentity, child) || hasAnyReadableDescendant(userIdentity, child))
175                        {
176                            return true;
177                        }
178                    }
179                    else if (child.getName().endsWith(".xml") && canRead(userIdentity, child))
180                    {
181                        return true;
182                    }
183                }
184            }
185            
186            return false;
187        }
188        catch (SourceException e)
189        {
190            throw new RuntimeException("Cannot list child elements of " + folder.getURI(), e);
191        }
192    }
193
194    /**
195     * Check if a folder have descendant in write access for a given user
196     * @param userIdentity the user identity
197     * @param folder the source of the extraction container
198     * @return true if the user have write right for at least one child of this container
199     */
200    public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, TraversableSource folder)
201    {
202        return hasAnyWritableDescendant(userIdentity, folder, false);
203    }
204    
205    /**
206     * Check if a folder have descendant in write access for a given user
207     * @param userIdentity the user identity
208     * @param folder the source of the extraction container
209     * @param ignoreExtraction true to ignore extraction file from search (rights will check only on containers)
210     * @return true if the user have write right for at least one child of this container
211     */
212    public Boolean hasAnyWritableDescendant(UserIdentity userIdentity, TraversableSource folder, boolean ignoreExtraction)
213    {
214        try
215        {
216            if (folder.exists())
217            {
218                for (TraversableSource child : (Collection<TraversableSource>) folder.getChildren())
219                {
220                    if (child.isCollection())
221                    {
222                        if (canWrite(userIdentity, child) || hasAnyWritableDescendant(userIdentity, child))
223                        {
224                            return true;
225                        }
226                    }
227                    else if (!ignoreExtraction && child.getName().endsWith(".xml") && canWrite(userIdentity, child))
228                    {
229                        return true;
230                    }
231                }
232            }
233            
234            return false;
235        }
236        catch (SourceException e)
237        {
238            throw new RuntimeException("Cannot list child elements of " + folder.getURI(), e);
239        }
240    }
241    
242    /**
243     * Checks if a folder has descendants in the right assignment access for a given user
244     * @param userIdentity the user
245     * @param folder the source of the extraction container
246     * @return <code>true</code> if the folder has descendant, <code>false</code> otherwise
247     */
248    public boolean hasAnyAssignableDescendant(UserIdentity userIdentity, TraversableSource folder)
249    {
250        try
251        {
252            if (folder.exists())
253            {
254                for (TraversableSource child : (Collection<TraversableSource>) folder.getChildren())
255                {
256                    if (child.isCollection())
257                    {
258                        if (canAssignRights(userIdentity, child) || hasAnyAssignableDescendant(userIdentity, child))
259                        {
260                            return true;
261                        }
262                    }
263                    else if (child.getName().endsWith(".xml"))
264                    {
265                        if (canAssignRights(userIdentity, child))
266                        {
267                            return true;
268                        }
269                    }
270                }
271            }
272            
273            return false;
274        }
275        catch (SourceException e)
276        {
277            throw new RuntimeException("Cannot list child elements of " + folder.getURI(), e);
278        }
279    }
280    
281    /**
282     * Check if a user has read rights on an extraction container or file
283     * @param userIdentity the user
284     * @param source the source of the extraction container or file
285     * @return <code>true</code> if the user has read rights on an extraction container, <code>false</code> otherwise
286     */
287    public boolean canRead(UserIdentity userIdentity, TraversableSource source)
288    {
289        return _rightManager.hasReadAccess(userIdentity, source) || canWrite(userIdentity, source);
290    }
291    
292    /**
293     * Check if a user has write rights on an extraction container or an extraction
294     * @param userIdentity the user
295     * @param source the source of the extraction file or extration container
296     * @return <code>true</code> if the user has write rights on an extraction container, <code>false</code> otherwise
297     */
298    public boolean canWrite(UserIdentity userIdentity, TraversableSource source)
299    {
300        return canWrite(userIdentity, source, false);
301    }
302    
303    /**
304     * Determines if the user can rename an extraction container
305     * @param userIdentity the user
306     * @param folder the extraction container
307     * @return true if the user can delete the extraction container
308     */
309    public boolean canRename(UserIdentity userIdentity, TraversableSource folder)
310    {
311        try
312        {
313            return !_isRoot(folder) // is not root
314                    && canWrite(userIdentity, folder) // has write access
315                    && canWrite(userIdentity, (TraversableSource) folder.getParent()); // has write access on parent
316        }
317        catch (SourceException e)
318        {
319            throw new RuntimeException("Unable to determine user rights on the extraction container " + folder.getURI(), e);
320        }
321    }
322    
323    /**
324     * Determines if the user can delete an extraction container or the extraction file
325     * @param userIdentity the user
326     * @param source the extraction container or the extraction file
327     * @return true if the user can delete the extraction container
328     */
329    public boolean canDelete(UserIdentity userIdentity, TraversableSource source)
330    {
331        try
332        {
333            return !_isRoot(source) // is not root
334                && canWrite(userIdentity, (TraversableSource) source.getParent()) // has write access on parent
335                && canWrite(userIdentity, source, true); // has write access on itselft and each descendant
336        }
337        catch (SourceException e)
338        {
339            throw new RuntimeException("Unable to determine user rights on extraction container or file at uri " + source.getURI(), e);
340        }
341    }
342    
343    /**
344     * Check if a user has write access on an extraction container
345     * @param userIdentity the user user identity
346     * @param source the extraction container or the extraction file
347     * @param recursively true to check write access on all descendants recursively
348     * @return true if the user has write access on the extraction container
349     */
350    public boolean canWrite(UserIdentity userIdentity, TraversableSource source, boolean recursively)
351    {      
352        boolean hasRight = _rightManager.hasRight(userIdentity, ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID, source) == RightResult.RIGHT_ALLOW;
353        if (!hasRight)
354        {
355            return false;
356        }
357           
358        try
359        {
360            if (recursively && source.isCollection())
361            {
362                for (TraversableSource child : (Collection<TraversableSource>) source.getChildren())
363                {
364                    hasRight = hasRight && canWrite(userIdentity, child);
365                    
366                    if (!hasRight)
367                    {
368                        return false;
369                    }
370                }
371            }
372            
373            return hasRight;
374        }
375        catch (SourceException e)
376        {
377            throw new RuntimeException("Unable to determine user rights on extraction container " + source.getURI(), e);
378        }
379    }
380    
381    /**
382     * Check if a user can edit rights on an extraction container or an extraction file
383     * @param userIdentity the user
384     * @param source the source of the extraction container or file
385     * @return true if the user can edit rights on an extraction container or file
386     */
387    public boolean canAssignRights(UserIdentity userIdentity, TraversableSource source)
388    {
389        try
390        {
391            return _rightManager.hasRight(userIdentity, "Runtime_Rights_Rights_Handle", "/cms") == RightResult.RIGHT_ALLOW
392                    || !_isRoot(source) // is not root
393                    && canWrite(userIdentity, (TraversableSource) source.getParent()) // has write access on parent
394                    && canWrite(userIdentity, source, true); // has write access on itselft and each descendant
395        }
396        catch (SourceException e)
397        {
398            throw new RuntimeException("Unable to determine the user rights on the extraction container or file at uri " + source.getURI(), e);
399        }
400    }
401    
402    /**
403     * Determines if the extraction container is the root node
404     * @param folder the extraction container
405     * @return true if is root
406     */
407    protected boolean _isRoot(TraversableSource folder)
408    {
409        return trimLastFileSeparator(_root.getURI()).equals(trimLastFileSeparator(folder.getURI()));
410    }
411
412    /**
413     * Get the path for rights of an extraction container or file
414     * @param source the source of extraction container or file
415     * @return the path for rights
416     */
417    public String getExtractionRightPath(TraversableSource source)
418    {
419        String rootURI = trimLastFileSeparator(_root.getURI());
420        String sourceURI = source.getURI();
421        
422        if (!sourceURI.startsWith(rootURI))
423        {
424            // The source is an extraction source
425            return null;
426        }
427        
428        // Get only the part after the root folder to get the relative path 
429        String relPath = StringUtils.substringAfter(trimLastFileSeparator(sourceURI), rootURI);
430
431        // In some case, relPath can start with a /, we need to trim it to test if it is an empty path corresponding to the root
432        if (relPath.startsWith("/"))
433        {
434            relPath = StringUtils.substringAfter(relPath, "/");
435        }
436        
437        return StringUtils.isEmpty(relPath) ? ExtractionAccessController.ROOT_CONTEXT : ExtractionAccessController.ROOT_CONTEXT + "/" + relPath;
438    }
439    
440    /**
441     * Get the source corresponding to the right context of an extraction container or file
442     * @param rightContext The rights context such as '/extraction-dir/path/to/file
443     * @return the resolved source file or null if the given context is not an extraction context
444     * @throws IOException if an error occured
445     */
446    public TraversableSource getExtractionSource(String rightContext) throws IOException
447    {
448        if (rightContext.startsWith(ExtractionAccessController.ROOT_CONTEXT))
449        {
450            String relPath = StringUtils.substringAfter(rightContext, ExtractionAccessController.ROOT_CONTEXT);
451            String fileUri = ExtractionConstants.DEFINITIONS_DIR + relPath;
452            return (TraversableSource) _sourceResolver.resolveURI(fileUri);
453        }
454        
455        return null;
456    }
457    
458    /**
459     * Copy rights from one context to another one
460     * @param sourceContext the source context
461     * @param targetContext the target context
462     */
463    public void copyRights(String sourceContext, String targetContext)
464    {
465        // Get the mapping between users and profiles
466        Map<UserIdentity, Map<UserOrGroup, Set<String>>> profilesForUsers = _profileAssignmentStorageEP.getProfilesForUsers(sourceContext, null);
467        // Copy allowed user assignment profiles to new context
468        profilesForUsers.entrySet()
469            .forEach(entry -> _copyAllowedUsers(entry.getKey(), entry.getValue().get(UserOrGroup.ALLOWED), targetContext));
470        // Copy denied user assignment profiles to new context
471        profilesForUsers.entrySet()
472            .forEach(entry -> _copyDeniedUsers(entry.getKey(), entry.getValue().get(UserOrGroup.DENIED), targetContext));
473        
474        // Get the mapping between groups and profiles
475        Map<GroupIdentity, Map<UserOrGroup, Set<String>>> profilesForGroups = _profileAssignmentStorageEP.getProfilesForGroups(sourceContext, null);
476        // Copy allowed group assignment profiles to new context
477        profilesForGroups.entrySet()
478            .forEach(entry -> _copyAllowedGroups(entry.getKey(), entry.getValue().get(UserOrGroup.ALLOWED), targetContext));
479        // Copy denied group assignment profiles to new context
480        profilesForGroups.entrySet()
481            .forEach(entry -> _copyDeniedGroups(entry.getKey(), entry.getValue().get(UserOrGroup.DENIED), targetContext));
482        
483        // Get the mapping between anonymous or any connected user and profiles
484        Map<AnonymousOrAnyConnectedKeys, Set<String>> profilesForAnonymousOrAnyConnectedUser = _profileAssignmentStorageEP.getProfilesForAnonymousAndAnyConnectedUser(sourceContext);
485        // Copy allowed anonymous user assignment profiles to new context
486        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED)
487            .forEach(profileId -> _profileAssignmentStorageEP.allowProfileToAnonymous(profileId, targetContext));
488        // Copy denied anonymous user assignment profiles to new context
489        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED)
490            .forEach(profileId -> _profileAssignmentStorageEP.denyProfileToAnonymous(profileId, targetContext));
491        // Copy allowed any connected user assignment profiles to new context
492        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED)
493            .forEach(profileId -> _profileAssignmentStorageEP.allowProfileToAnyConnectedUser(profileId, targetContext));
494        // Copy denied any connected user assignment profiles to new context
495        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED)
496            .forEach(profileId -> _profileAssignmentStorageEP.denyProfileToAnyConnectedUser(profileId, targetContext));
497    }
498    
499    private void _copyAllowedUsers(UserIdentity userIdentity, Set<String> profiles, String context)
500    {
501        profiles.forEach(profile -> _profileAssignmentStorageEP.allowProfileToUser(userIdentity, profile, context));
502    }
503    
504    private void _copyDeniedUsers(UserIdentity userIdentity, Set<String> profiles, String context)
505    {
506        profiles.forEach(profile -> _profileAssignmentStorageEP.denyProfileToUser(userIdentity, profile, context));
507    }
508    
509    private void _copyAllowedGroups(GroupIdentity groupIdentity, Set<String> profiles, String context)
510    {
511        profiles.forEach(profile -> _profileAssignmentStorageEP.allowProfileToGroup(groupIdentity, profile, context));
512    }
513    
514    private void _copyDeniedGroups(GroupIdentity groupIdentity, Set<String> profiles, String context)
515    {
516        profiles.forEach(profile -> _profileAssignmentStorageEP.denyProfileToGroup(groupIdentity, profile, context));
517    }
518    
519    /**
520     * Delete rights from a context
521     * @param context the context
522     */
523    public void deleteRights(String context)
524    {
525        // Get the mapping between users and profiles
526        Map<UserIdentity, Map<UserOrGroup, Set<String>>> profilesForUsers = _profileAssignmentStorageEP.getProfilesForUsers(context, null);
527        // Copy allowed user assignment profiles to new context
528        profilesForUsers.entrySet()
529            .forEach(entry -> _removeAllowedUsers(entry.getKey(), entry.getValue().get(UserOrGroup.ALLOWED), context));
530        // Copy denied user assignment profiles to new context
531        profilesForUsers.entrySet()
532            .forEach(entry -> _removeDeniedUsers(entry.getKey(), entry.getValue().get(UserOrGroup.DENIED), context));
533        
534        // Get the mapping between groups and profiles
535        Map<GroupIdentity, Map<UserOrGroup, Set<String>>> profilesForGroups = _profileAssignmentStorageEP.getProfilesForGroups(context, null);
536        // Copy allowed group assignment profiles to new context
537        profilesForGroups.entrySet()
538            .forEach(entry -> _removeAllowedGroups(entry.getKey(), entry.getValue().get(UserOrGroup.ALLOWED), context));
539        // Copy denied group assignment profiles to new context
540        profilesForGroups.entrySet()
541            .forEach(entry -> _removeDeniedGroups(entry.getKey(), entry.getValue().get(UserOrGroup.DENIED), context));
542        
543        // Get the mapping between anonymous or any connected user and profiles
544        Map<AnonymousOrAnyConnectedKeys, Set<String>> profilesForAnonymousOrAnyConnectedUser = _profileAssignmentStorageEP.getProfilesForAnonymousAndAnyConnectedUser(context);
545        // Copy allowed anonymous user assignment profiles to new context
546        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANONYMOUS_ALLOWED)
547            .forEach(profileId -> _profileAssignmentStorageEP.removeAllowedProfileFromAnonymous(profileId, context));
548        // Copy denied anonymous user assignment profiles to new context
549        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANONYMOUS_DENIED)
550            .forEach(profileId -> _profileAssignmentStorageEP.removeDeniedProfileFromAnonymous(profileId, context));
551        // Copy allowed any connected user assignment profiles to new context
552        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_ALLOWED)
553            .forEach(profileId -> _profileAssignmentStorageEP.removeAllowedProfileFromAnyConnectedUser(profileId, context));
554        // Copy denied any connected user assignment profiles to new context
555        profilesForAnonymousOrAnyConnectedUser.get(AnonymousOrAnyConnectedKeys.ANYCONNECTEDUSER_DENIED)
556            .forEach(profileId -> _profileAssignmentStorageEP.removeDeniedProfileFromAnyConnectedUser(profileId, context));
557    }
558    
559    private void _removeAllowedUsers(UserIdentity userIdentity, Set<String> profiles, String context)
560    {
561        profiles.forEach(profile -> _profileAssignmentStorageEP.removeAllowedProfileFromUser(userIdentity, profile, context));
562    }
563    
564    private void _removeDeniedUsers(UserIdentity userIdentity, Set<String> profiles, String context)
565    {
566        profiles.forEach(profile -> _profileAssignmentStorageEP.removeDeniedProfileFromUser(userIdentity, profile, context));
567    }
568    
569    private void _removeAllowedGroups(GroupIdentity groupIdentity, Set<String> profiles, String context)
570    {
571        profiles.forEach(profile -> _profileAssignmentStorageEP.removeAllowedProfileFromGroup(groupIdentity, profile, context));
572    }
573    
574    private void _removeDeniedGroups(GroupIdentity groupIdentity, Set<String> profiles, String context)
575    {
576        profiles.forEach(profile -> _profileAssignmentStorageEP.removeDeniedProfileFromGroup(groupIdentity, profile, context));
577    }
578
579    /**
580     * Move an extraction file or folder inside a given directory
581     * 
582     * @param srcRelPath The relative URI of file/folder to move
583     * @param targetRelPath The target relative URI of file/folder to move
584     * @return a result map with the name and uri of moved file in case of
585     *         success.
586     * @throws IOException If an error occurred manipulating the source
587     */
588    @Callable (rights = ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID)
589    public Map<String, Object> moveOrRenameExtractionDefinitionFile(String srcRelPath, String targetRelPath) throws IOException
590    {
591        Map<String, Object> result = new HashMap<>();
592
593        FileSource srcFile = null;
594        FileSource targetFile = null;
595        try
596        {
597            srcFile = (FileSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR + srcRelPath);
598            targetFile = (FileSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR + targetRelPath);
599
600            String sourceContext = ExtractionAccessController.ROOT_CONTEXT + "/" + srcRelPath;
601            String targetContext = ExtractionAccessController.ROOT_CONTEXT + "/" + targetRelPath;
602
603            result = _moveOrRenameSource(srcFile, targetFile, sourceContext, targetContext);
604            
605            if (result.containsKey("uri"))
606            {
607                String newURI = (String) result.get("uri");
608                String path = newURI.substring(_root.getURI().length());
609                result.put("path", path);
610            }
611        }
612        finally
613        {
614            _sourceResolver.release(srcFile);
615            _sourceResolver.release(targetFile);
616        }
617        
618        return result;
619    }
620        
621    /**
622     * Move a file or folder
623     * 
624     * @param sourceFile The file/folder to move
625     * @param targetFile The target file
626     * @param sourceContext the source context
627     * @param targetContext the target context
628     * @return a result map with the name and uri of moved file in case of
629     *         success.
630     * @throws IOException If an error occurred manipulating the source
631     */
632    private Map<String, Object> _moveOrRenameSource(FileSource sourceFile, FileSource targetFile, String sourceContext, String targetContext) throws IOException
633    {
634        Map<String, Object> result = new HashMap<>();
635            
636        // Check if the user try to move files outside the root folder
637        if (!StringUtils.startsWith(sourceFile.getURI(), _root.getURI()) || !StringUtils.startsWith(targetFile.getURI(), _root.getURI()))
638        {
639            result.put("success", false);
640            result.put("error", "no-exists");
641            
642            getLogger().error("User '{}' tried to  move parameter file outside of the root extraction directory.", _currentUserProvider.getUser());
643            
644            return result;
645        }
646        
647        if (!sourceFile.exists())
648        {
649            result.put("success", false);
650            result.put("error", "no-exists");
651            return result;
652        }
653        
654        if (targetFile.exists())
655        {
656            // If both files are equals, there is no need to rename or move it
657            if (sourceFile.getFile().equals(targetFile.getFile()))
658            {
659                result.put("success", true);
660                result.put("name", targetFile.getName());
661                result.put("uri", targetFile.getURI());
662                return result;
663            }
664            else
665            {
666                result.put("success", false);
667                result.put("error", "already-exists");
668                return result;
669            }
670        }
671
672        copyRightsRecursively(sourceContext, targetContext, sourceFile);
673        if (sourceFile.getFile().isFile())
674        {
675            FileUtils.moveFile(sourceFile.getFile(), targetFile.getFile());
676        }
677        else
678        {
679            FileUtils.moveDirectory(sourceFile.getFile(), targetFile.getFile());
680        }
681        deleteRightsRecursively(sourceContext, targetFile);
682
683        result.put("success", true);
684        result.put("name", targetFile.getName());
685        result.put("uri", targetFile.getURI());
686
687        return result;
688    } 
689    
690    /**
691     * Copy rights from one context to another one
692     * @param sourceContext the source context
693     * @param targetContext the target context
694     * @param file the source of the file to copy
695     */
696    public void copyRightsRecursively(String sourceContext, String targetContext, TraversableSource file)
697    {
698        copyRights(sourceContext, targetContext);
699        if (file.isCollection())
700        {
701            try
702            {
703                for (TraversableSource child : (Collection<TraversableSource>) file.getChildren())
704                {
705                    copyRightsRecursively(sourceContext + "/" + child.getName(), targetContext + "/" + child.getName(), child);
706                }
707            }
708            catch (SourceException e)
709            {
710                throw new RuntimeException("Cannot list child elements of " + file.getURI(), e);
711            }
712        }
713    }
714
715    /**
716     * Copy rights from one context to another one
717     * @param context the context
718     * @param file the source of the file to copy
719     */
720    public void deleteRightsRecursively(String context, TraversableSource file)
721    {
722        deleteRights(context);
723        if (file.isCollection())
724        {
725            try
726            {
727                for (TraversableSource child : (Collection<TraversableSource>) file.getChildren())
728                {
729                    deleteRightsRecursively(context + "/" + child.getName(), child);
730                }
731            }
732            catch (SourceException e)
733            {
734                throw new RuntimeException("Cannot list child elements of " + file.getURI(), e);
735            }
736        }
737    }
738    
739    /**
740     * Get the author of extraction
741     * @param extractionPath the path of the extraction
742     * @return the author
743     */
744    public UserIdentity getAuthor(FileSource extractionPath)
745    {
746        return _getExtractionAuthorCache().get(extractionPath, path -> _getUserIdentityByExtractionFile(path));
747        
748    }
749    
750    private UserIdentity _getUserIdentityByExtractionFile(FileSource extractionPath)
751    {
752        try
753        {
754            Extraction extraction = _definitionReader.readExtractionDefinitionFile(extractionPath.getFile());
755            return extraction.getAuthor();
756        }
757        catch (Exception e)
758        {
759            throw new RuntimeException("Cannot read extraction " + extractionPath, e);
760        }
761    }
762    
763    private Cache<FileSource, UserIdentity> _getExtractionAuthorCache()
764    {
765        return this._cacheManager.get(EXTRACTION_AUTHOR_CACHE);
766    }
767    
768    /**
769     * Remove the last separator from the uri if it has any
770     * @param uri the uri
771     * @return the uri without any ending separator
772     */
773    public static String trimLastFileSeparator(String uri)
774    {
775        return StringUtils.endsWith(uri, "/") ? StringUtils.substringBeforeLast(uri, "/") : uri;
776    }
777    
778    /**
779     * Get the path of all children that match the provided value.
780     * @param path the path to the extraction to consider as root
781     * @param value the value
782     * @return the list of path
783     */
784    @Callable
785    public List<String> getFilteredPath(String path, String value)
786    {
787        try
788        {
789            TraversableSource currentSrc = (TraversableSource) _sourceResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR + (path.length() > 0 ? "/" + path : ""));
790            
791            List<String> result = _fileHelper.filterSources(currentSrc, value);
792            return result.stream()
793                  .map(this::_toRelativePath)
794                  .toList();
795        }
796        catch (IOException e)
797        {
798            getLogger().error("Failed to filter extraction definition at path '" + path + "'", e);
799            return List.of();
800        }
801    }
802
803    private String _toRelativePath(String absoluteURI)
804    {
805        String path = absoluteURI.substring(_root.getURI().length() - 1); // -1 to keep the head /
806        if (path.endsWith("/"))
807        {
808            path = path.substring(0, path.length() - 1);
809        }
810        return path;
811    }
812}