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