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