001/*
002 *  Copyright 2024 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.rights;
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;
024import java.util.stream.Stream;
025
026import org.apache.avalon.framework.activity.Initializable;
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.collections.MapUtils;
031import org.apache.commons.lang3.StringUtils;
032import org.apache.commons.lang3.Strings;
033import org.apache.excalibur.source.SourceException;
034import org.apache.excalibur.source.SourceResolver;
035import org.apache.excalibur.source.impl.FileSource;
036
037import org.ametys.cms.contenttype.ContentTypesHelper;
038import org.ametys.cms.repository.Content;
039import org.ametys.core.group.GroupIdentity;
040import org.ametys.core.right.AccessController;
041import org.ametys.core.right.AccessExplanation;
042import org.ametys.core.right.RightsException;
043import org.ametys.core.user.UserIdentity;
044import org.ametys.plugins.core.impl.right.AbstractRightBasedAccessController;
045import org.ametys.plugins.extraction.ExtractionConstants;
046import org.ametys.plugins.extraction.execution.Extraction;
047import org.ametys.plugins.extraction.execution.ExtractionDAO;
048import org.ametys.runtime.i18n.I18nizableText;
049
050
051/**
052 * {@link AccessController} to allow read access and handle for author of a extraction file
053 *
054 */
055public class ExtractionAuthorAccessController extends AbstractRightBasedAccessController implements Serviceable, Initializable
056{
057    private static final List<String> __AUTHOR_RIGHTS = List.of(ExtractionConstants.MODIFY_EXTRACTION_RIGHT_ID, "Workflow_Rights_Edition_Online");
058    
059    private SourceResolver _srcResolver;
060    private String _rootPath;
061    private ExtractionDAO _extractionDAO;
062    private ContentTypesHelper _contentTypesHelper;
063    
064    public void service(ServiceManager manager) throws ServiceException
065    {
066        _srcResolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
067        _extractionDAO = (ExtractionDAO) manager.lookup(ExtractionDAO.ROLE);
068        _contentTypesHelper = (ContentTypesHelper) manager.lookup(ContentTypesHelper.ROLE);
069    }
070    
071    public void initialize() throws Exception
072    {
073        FileSource rootDir = (FileSource) _srcResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
074        // use the URI. The path is not available if the definitions folder is not created at start time
075        _rootPath = rootDir.getURI();
076    }
077    
078    public boolean supports(Object object)
079    {
080        return object instanceof Extraction
081                // a fileSource that don't exist is not a collection.
082                // not checking that it exists leads to the root being supported if not created
083                || object instanceof FileSource fileSource && fileSource.exists() && !fileSource.isCollection() && fileSource.getURI().startsWith(_rootPath)
084                || object instanceof Content content && _contentTypesHelper.isInstanceOf(content, ExtractionConstants.DESCRIPTION_CONTENT_TYPE_ID);
085    }
086    
087    public AccessResult getPermission(UserIdentity user, Set<GroupIdentity> userGroups, String rightId, Object object)
088    {
089        if (user.equals(_getAuthor(object)))
090        {
091            return __AUTHOR_RIGHTS.contains(rightId) ? AccessResult.USER_ALLOWED : AccessResult.UNKNOWN;
092        }
093        
094        return AccessResult.UNKNOWN;
095    }
096
097    public AccessResult getReadAccessPermission(UserIdentity user, Set<GroupIdentity> userGroups, Object object)
098    {
099        return user.equals(_getAuthor(object)) ? AccessResult.USER_ALLOWED : AccessResult.UNKNOWN;
100    }
101
102    /**
103     * If creator, access to a list of rights
104     */
105    public Map<String, AccessResult> getPermissionByRight(UserIdentity user, Set<GroupIdentity> userGroups, Object object)
106    {
107        Map<String, AccessResult> permissionByRight = new HashMap<>();
108        
109        if (user.equals(_getAuthor(object)))
110        {
111            for (String rightId : __AUTHOR_RIGHTS)
112            {
113                permissionByRight.put(rightId, AccessResult.USER_ALLOWED);
114            }
115        }
116        
117        return permissionByRight;
118    }
119
120    public AccessResult getPermissionForAnonymous(String rightId, Object object)
121    {
122        return AccessResult.UNKNOWN;
123    }
124
125    public AccessResult getReadAccessPermissionForAnonymous(Object object)
126    {
127        return AccessResult.UNKNOWN;
128    }
129
130    public AccessResult getPermissionForAnyConnectedUser(String rightId, Object object)
131    {
132        return AccessResult.UNKNOWN;
133    }
134
135    public AccessResult getReadAccessPermissionForAnyConnectedUser(Object object)
136    {
137        return AccessResult.UNKNOWN;
138    }
139
140    /**
141     * If right requested is in the list, the creator is added the list of USER_ALLOWED
142     */
143    public Map<UserIdentity, AccessResult> getPermissionByUser(String rightId, Object object)
144    {
145        Map<UserIdentity, AccessResult> permissionByUser = new HashMap<>();
146        
147        if (__AUTHOR_RIGHTS.contains(rightId))
148        {
149            UserIdentity extractionAuthor = _getAuthor(object);
150            permissionByUser.put(extractionAuthor, AccessResult.USER_ALLOWED);
151        }
152        return permissionByUser;
153    }
154
155    public Map<UserIdentity, AccessResult> getReadAccessPermissionByUser(Object object)
156    {
157        return MapUtils.EMPTY_MAP;
158    }
159
160    public Map<GroupIdentity, AccessResult> getPermissionByGroup(String rightId, Object object)
161    {
162        return MapUtils.EMPTY_MAP;
163    }
164
165    public Map<GroupIdentity, AccessResult> getReadAccessPermissionByGroup(Object object)
166    {
167        return MapUtils.EMPTY_MAP;
168    }
169
170    public boolean hasUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups, String rightId)
171    {
172        return false;
173    }
174
175    public boolean hasUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts, UserIdentity user, Set<GroupIdentity> userGroups)
176    {
177        return false;
178    }
179
180    public boolean hasAnonymousAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId)
181    {
182        return false;
183    }
184
185    public boolean hasAnonymousAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts)
186    {
187        return false;
188    }
189
190    public boolean hasAnyConnectedUserAnyPermissionOnWorkspace(Set<Object> workspacesContexts, String rightId)
191    {
192        return false;
193    }
194
195    public boolean hasAnyConnectedUserAnyReadAccessPermissionOnWorkspace(Set<Object> workspacesContexts)
196    {
197        return false;
198    }
199    
200    @Override
201    protected AccessExplanation _getAccessExplanation(AccessResult result, Object object, UserIdentity user, Set<GroupIdentity> groups, String rightId)
202    {
203        switch (result)
204        {
205            case USER_ALLOWED:
206            case UNKNOWN:
207                if (object instanceof Content)
208                {
209                    return new AccessExplanation(
210                            getId(),
211                            result,
212                            new I18nizableText("plugin.extraction", "PLUGINS_EXTRACTION_CONTENT_AUTHOR_ACCESS_CONTROLLER_" + result.name() + "_EXPLANATION",
213                                    Map.of("title", getObjectLabel(object)))
214                            );
215                }
216                else
217                {
218                    return new AccessExplanation(
219                            getId(),
220                            result,
221                            new I18nizableText("plugin.extraction", "PLUGINS_EXTRACTION_AUTHOR_ACCESS_CONTROLLER_" + result.name() + "_EXPLANATION",
222                                    Map.of("title", getObjectLabel(object)))
223                            );
224                }
225            default:
226                return AccessController.getDefaultAccessExplanation(getId(), result);
227        }
228    }
229    
230    private UserIdentity _getAuthor(Object object)
231    {
232        if (object instanceof Extraction extraction)
233        {
234            return extraction.getAuthor();
235        }
236        else if (object instanceof FileSource fileSource)
237        {
238            return _extractionDAO.getAuthor(fileSource);
239        }
240        else if (object instanceof Content content)
241        {
242            return content.getCreator();
243        }
244        return null;
245    }
246    
247    private String _getExtractionName(Object object)
248    {
249        if (object instanceof FileSource fileSource)
250        {
251            String target = StringUtils.substringAfter(_extractionDAO.getExtractionRightPath(fileSource), ExtractionAccessController.ROOT_CONTEXT);
252            target = Strings.CS.replace(target.substring(1), "/", " > ");
253            return target;
254        }
255        // We can't include the hierarchy from the extraction or content
256        // But it shouldn't be visible as there is nothing display the explanation for it
257        else if (object instanceof Extraction extraction)
258        {
259            return extraction.getFileName();
260        }
261        else if (object instanceof Content content)
262        {
263            return content.getTitle();
264        }
265        return null;
266    }
267
268    public I18nizableText getObjectLabel(Object object)
269    {
270        String extractionName = _getExtractionName(object);
271        if (extractionName != null)
272        {
273            return new I18nizableText(extractionName);
274        }
275        throw new RightsException("Unsupported context: " + object.toString());
276    }
277
278    public I18nizableText getObjectCategory(Object object)
279    {
280        return ExtractionAccessController.EXTRACTION_CONTEXT_CATEGORY;
281    }
282
283    @Override
284    protected Iterable< ? extends Object> getHandledObjects(UserIdentity identity, Set<GroupIdentity> groups, Set<Object> workspacesContexts)
285    {
286        if (workspacesContexts.contains("/cms"))
287        {
288            try
289            {
290                FileSource rootDir = (FileSource) _srcResolver.resolveURI(ExtractionConstants.DEFINITIONS_DIR);
291                if (rootDir.getFile().exists())
292                {
293                    Stream<FileSource> definitions = _getDefinitions(rootDir);
294                    return definitions.toList();
295                }
296            }
297            catch (IOException e)
298            {
299                getLogger().warn("Failed to compute the list of extractions");
300            }
301        }
302
303        return List.of();
304    }
305
306    private Stream<FileSource> _getDefinitions(FileSource source)
307    {
308        if (source.isCollection())
309        {
310            try
311            {
312                return source.getChildren().stream()
313                    .filter(FileSource.class::isInstance)
314                    .flatMap(src -> _getDefinitions((FileSource) src));
315            }
316            catch (SourceException e)
317            {
318                getLogger().warn("Failed to compute the list of extractions");
319                return Stream.of();
320            }
321        }
322        else
323        {
324            return Stream.of(source);
325        }
326    }
327    
328    public ExplanationObject getExplanationObject(Object object)
329    {
330        if (object instanceof FileSource source)
331        {
332            return new ExplanationObject(
333                    // we convert the source to the right path to be able to merge with ExtractionAccessController
334                    _extractionDAO.getExtractionRightPath(source),
335                    getObjectLabel(object),
336                    getObjectCategory(object)
337                    );
338        }
339        return super.getExplanationObject(object);
340    }
341    
342    @Override
343    protected Collection<String> getHandledRights()
344    {
345        return __AUTHOR_RIGHTS;
346    }
347}