001/*
002 *  Copyright 2016 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.workspaces.project.rights;
017
018import java.util.Arrays;
019import java.util.HashMap;
020import java.util.List;
021import java.util.Map;
022import java.util.Objects;
023import java.util.Set;
024import java.util.stream.Collectors;
025import java.util.stream.Stream;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.avalon.framework.service.ServiceException;
029import org.apache.avalon.framework.service.ServiceManager;
030import org.apache.commons.lang3.StringUtils;
031import org.apache.http.annotation.Obsolete;
032
033import org.ametys.cms.fo.ForceDefaultRepositoryWorkspaceCallableDecorator;
034import org.ametys.core.right.Profile;
035import org.ametys.core.right.RightManager;
036import org.ametys.core.right.RightManager.RightResult;
037import org.ametys.core.right.RightProfilesDAO;
038import org.ametys.core.ui.Callable;
039import org.ametys.core.user.CurrentUserProvider;
040import org.ametys.core.user.UserIdentity;
041import org.ametys.plugins.explorer.ExplorerNode;
042import org.ametys.plugins.explorer.resources.ModifiableResourceCollection;
043import org.ametys.plugins.repository.AmetysObject;
044import org.ametys.plugins.repository.AmetysObjectResolver;
045import org.ametys.plugins.workspaces.WorkspacesConstants;
046import org.ametys.plugins.workspaces.WorkspacesHelper;
047import org.ametys.plugins.workspaces.about.AboutWorkspaceModule;
048import org.ametys.plugins.workspaces.alert.AlertWorkspaceModule;
049import org.ametys.plugins.workspaces.calendars.CalendarWorkspaceModule;
050import org.ametys.plugins.workspaces.documents.DocumentWorkspaceModule;
051import org.ametys.plugins.workspaces.members.MembersWorkspaceModule;
052import org.ametys.plugins.workspaces.minisite.MiniSiteWorkspaceModule;
053import org.ametys.plugins.workspaces.news.NewsWorkspaceModule;
054import org.ametys.plugins.workspaces.project.ProjectConstants;
055import org.ametys.plugins.workspaces.project.ProjectManager;
056import org.ametys.plugins.workspaces.project.ProjectManager.UnknownCatalogSiteException;
057import org.ametys.plugins.workspaces.project.modules.WorkspaceModule;
058import org.ametys.plugins.workspaces.project.modules.WorkspaceModuleExtensionPoint;
059import org.ametys.plugins.workspaces.project.objects.Project;
060import org.ametys.plugins.workspaces.wall.WallContentModule;
061import org.ametys.runtime.authentication.AccessDeniedException;
062import org.ametys.runtime.config.Config;
063import org.ametys.runtime.plugin.component.AbstractLogEnabled;
064import org.ametys.runtime.plugin.component.DeferredServiceable;
065import org.ametys.web.repository.page.SitemapElement;
066import org.ametys.web.repository.page.ZoneItem;
067import org.ametys.web.repository.page.ZoneItem.ZoneType;
068
069/**
070 * Helper related to rights management for projects.
071 */
072public class ProjectRightHelper extends AbstractLogEnabled implements DeferredServiceable, Component
073{
074    /** Avalon Role */
075    public static final String ROLE = ProjectRightHelper.class.getName();
076    
077    @Obsolete // For v1 project only
078    private static final String __PROJECT_RIGHT_PROFILE = "PROJECT";
079    
080    /** Ametys object resolver */
081    protected AmetysObjectResolver _resolver;
082    
083    /** Project manager */
084    protected ProjectManager _projectManager;
085    
086    /** Right manager */
087    protected RightManager _rightManager;
088    
089    /** Right profiles manager */
090    protected RightProfilesDAO _rightProfilesDao;
091    
092    /** Current user provider */
093    protected CurrentUserProvider _currentUserProvider;
094    
095    /** Workspace Module ExtensionPoint */
096    protected WorkspaceModuleExtensionPoint _workspaceModuleEP;
097    
098    /** Association ContentTypeId, Module */
099    protected Map<String, WorkspaceModule> _contentTypesToModule;
100
101    /** Module managers EP */
102    protected WorkspaceModuleExtensionPoint _moduleManagerEP;
103
104    /** Workspace helper */
105    protected WorkspacesHelper _workspaceHelper;
106    
107    private Set<String> _profileIds;
108    
109    @Override
110    public void deferredService(ServiceManager manager) throws ServiceException
111    {
112        _resolver = (AmetysObjectResolver) manager.lookup(AmetysObjectResolver.ROLE);
113        _projectManager = (ProjectManager) manager.lookup(ProjectManager.ROLE);
114        _rightManager = (RightManager) manager.lookup(RightManager.ROLE);
115        _rightProfilesDao = (RightProfilesDAO) manager.lookup(RightProfilesDAO.ROLE);
116        _currentUserProvider = (CurrentUserProvider) manager.lookup(CurrentUserProvider.ROLE);
117        _moduleManagerEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
118        _workspaceHelper = (WorkspacesHelper) manager.lookup(WorkspacesHelper.ROLE);
119        
120        _workspaceModuleEP = (WorkspaceModuleExtensionPoint) manager.lookup(WorkspaceModuleExtensionPoint.ROLE);
121        _contentTypesToModule = Map.of(
122                WorkspacesConstants.WALL_CONTENT_CONTENT_TYPE_ID, _workspaceModuleEP.getModule(WallContentModule.WALLCONTENT_MODULE_ID),
123                WorkspacesConstants.PROJECT_NEWS_CONTENT_TYPE_ID, _workspaceModuleEP.getModule(NewsWorkspaceModule.NEWS_MODULE_ID),
124                WorkspacesConstants.PROJECT_ALERT_CONTENT_TYPE_ID, _workspaceModuleEP.getModule(AlertWorkspaceModule.ALERT_MODULE_ID),
125                WorkspacesConstants.PROJECT_ARTICLE_CONTENT_TYPE, _workspaceModuleEP.getModule(MiniSiteWorkspaceModule.MINISITE_MODULE_ID),
126                WorkspacesConstants.ABOUT_CONTENT_TYPE, _workspaceModuleEP.getModule(AboutWorkspaceModule.ABOUT_MODULE_ID)
127        );
128    }
129    
130    /**
131     * The association of all project content types and associated modules
132     * @return The association
133     */
134    public Map<String, WorkspaceModule> getProjectContentTypesAndModules()
135    {
136        return _contentTypesToModule;
137    }
138    
139    /**
140     * Retrieves all project profiles ids given the "profile list" configuration parameter
141     * Profile order is guaranteed to be the same as in the configuration parameter.
142     * @return the projects
143     */
144    public synchronized Set<String> getProfilesIds()
145    {
146        if (_profileIds == null)
147        {
148            String rawProjectProfileIds = StringUtils.defaultString(Config.getInstance().getValue("workspaces.profile.list"));
149            _profileIds = Arrays.stream(StringUtils.split(rawProjectProfileIds, ',')).collect(Collectors.toSet());
150        }
151        return _profileIds;
152    }
153    
154    /**
155     * Retrieves all project profile given the "profile list" configuration parameter
156     * Profile order is guaranteed to be the same as in the configuration parameter.
157     * @return the projects
158     */
159    public Set<Profile> getProfiles()
160    {
161        // getProfiles(null) to get only shared profile
162        Map<String, Profile> profileMap = _rightProfilesDao.getProfiles(null).stream().collect(Collectors.toMap(Profile::getId, item -> item));
163        
164        // Collect project profiles (unexisting entries are filtered out).
165        return getProfilesIds().stream()
166            .map(id ->
167            {
168                Profile p = profileMap.get(id);
169                
170                // log null entries
171                if (p == null)
172                {
173                    getLogger().warn("Could not find profile with id '{}'.", id);
174                }
175                
176                return p;
177            })
178            .filter(Objects::nonNull)
179            .collect(Collectors.toSet());
180    }
181    
182    /**
183     * Get the list of profiles and the list of modules available for rights affectation in the project.
184     * @param projectName The project to check if the modules are activated. Can be null to ignore
185     * @return the project rights data
186     */
187    @Callable (rights = Callable.CHECKED_BY_IMPLEMENTATION, decorators = ForceDefaultRepositoryWorkspaceCallableDecorator.DECORATOR_ID)
188    public Map<String, Object> getProjectRightsData(String projectName)
189    {
190
191        Project project = projectName != null ? _projectManager.getProject(projectName) : null;
192        
193        if (!canEditMember(project))
194        {
195            throw new AccessDeniedException("User '" + _currentUserProvider.getUser() + "' tried to do read operation without convenient right");
196        }
197        
198        // profiles
199        List<Object> profiles = getProfiles()
200                .stream()
201                .map(this::_getProfileRightData)
202                .collect(Collectors.toList());
203
204
205        // modules
206        Stream<Map<String, Object>> stream = _moduleManagerEP.getExtensionsIds().stream().map(moduleId -> _moduleManagerEP.getExtension(moduleId)).map(module -> _getModuleRightData(project, module));
207        List<Object> modules = stream.filter(Objects::nonNull).collect(Collectors.toList());
208        
209        Map<String, Object> result = new HashMap<>();
210        result.put("profiles", profiles);
211        result.put("modules", modules);
212        
213        return result;
214    }
215    
216    private Map<String, Object> _getProfileRightData(Profile profile)
217    {
218        Map<String, Object> data = new HashMap<>();
219        data.put("id", profile.getId());
220        data.put("label", profile.getLabel());
221        return data;
222    }
223    
224    private Map<String, Object> _getModuleRightData(Project project, WorkspaceModule module)
225    {
226        if (project != null && !_projectManager.isModuleActivated(project, module.getId()))
227        {
228            return null;
229        }
230        
231        Map<String, Object> data = new HashMap<>();
232        data.put("id", module.getId());
233        data.put("label", module.getModuleTitle());
234        return data;
235    }
236    
237    /**
238     * Determines if the current user can view the members of a project
239     * @param project the project
240     * @return true if user can view members
241     */
242    public boolean canViewMembers(Project project)
243    {
244        return _rightManager.currentUserHasReadAccess(project);
245    }
246
247    /**
248     * Determines if the current user has right to add member on project
249     * @param project the project
250     * @return true if user can add member
251     */
252    public boolean canAddMember(Project project)
253    {
254        MembersWorkspaceModule module = _moduleManagerEP.getModule(MembersWorkspaceModule.MEMBERS_MODULE_ID);
255        if (project != null && module != null && _projectManager.isModuleActivated(project, module.getId()))
256        {
257            AmetysObject moduleRoot = module.getModuleRoot(project, false);
258            return moduleRoot != null && _rightManager.currentUserHasRight(ProjectConstants.RIGHT_PROJECT_ADD_MEMBER, moduleRoot) == RightResult.RIGHT_ALLOW;
259        }
260        
261        return false;
262    }
263
264    /**
265     * Determines if the current user has right to edit member on project
266     * @param project the project
267     * @return true if user can edit member
268     */
269    public boolean canEditMember(Project project)
270    {
271        return canAddMember(project);
272    }
273    
274    /**
275     * Determines if the current user has right to add member on project
276     * @param project the project
277     * @return true if user can remove member
278     */
279    public boolean canRemoveMember(Project project)
280    {
281        return _hasRightOnMembers(project, ProjectConstants.RIGHT_PROJECT_REMOVE_MEMBER);
282    }
283    
284    private boolean _hasRightOnMembers(Project project, String rightId)
285    {
286        MembersWorkspaceModule module = _moduleManagerEP.getModule(MembersWorkspaceModule.MEMBERS_MODULE_ID);
287        if (module != null && _projectManager.isModuleActivated(project, module.getId()))
288        {
289            AmetysObject moduleRoot = module.getModuleRoot(project, false);
290            return moduleRoot != null && _rightManager.currentUserHasRight(rightId, moduleRoot) == RightResult.RIGHT_ALLOW;
291        }
292        
293        return false;
294    }
295    
296    /**
297     * Determines if the current user has right to add tags on project
298     * @param project the project
299     * @return true if user can add tags
300     */
301    public boolean canAddTag(Project project)
302    {
303        return _hasRightOnTagsOrPlaces(project, ProjectConstants.RIGHT_PROJECT_ADD_TAG);
304    }
305    
306    /**
307     * Determines if the current user has right to remove tags on project
308     * @param project the project
309     * @return true if user can remove tags
310     */
311    public boolean canRemoveTag(Project project)
312    {
313        return _hasRightOnTagsOrPlaces(project, ProjectConstants.RIGHT_PROJECT_DELETE_TAG);
314    }
315    
316    private boolean _hasRightOnTagsOrPlaces(Project project, String rightId)
317    {
318        WorkspaceModule module = _moduleManagerEP.getModule(CalendarWorkspaceModule.CALENDAR_MODULE_ID);
319        if (module != null && _projectManager.isModuleActivated(project, module.getId()))
320        {
321            AmetysObject moduleRoot = module.getModuleRoot(project, false);
322            if (moduleRoot != null && _rightManager.currentUserHasRight(rightId, moduleRoot) == RightResult.RIGHT_ALLOW)
323            {
324                return true;
325            }
326        }
327        
328        module = _moduleManagerEP.getModule(DocumentWorkspaceModule.DOCUMENT_MODULE_ID);
329        if (module != null && _projectManager.isModuleActivated(project, module.getId()))
330        {
331            AmetysObject moduleRoot = module.getModuleRoot(project, false);
332            if (moduleRoot != null && _rightManager.currentUserHasRight(rightId, moduleRoot) == RightResult.RIGHT_ALLOW)
333            {
334                return true;
335            }
336        }
337        
338        return false;
339    }
340    
341    /**
342     * Determines if current user has read access to module on current project
343     * @param moduleId the module id
344     * @return true if current user has read access, false otherwise
345     */
346    public boolean hasReadAccessOnModule(String moduleId)
347    {
348        return hasReadAccessOnModule(_workspaceHelper.getProjectFromRequest(), moduleId);
349    }
350    
351    /**
352     * Determines if current user has read access to module on given project
353     * @param project the project
354     * @param moduleId the module id
355     * @return true if current user has right, false otherwise
356     */
357    public boolean hasReadAccessOnModule(Project project, String moduleId)
358    {
359        return hasReadAccessOnModule(project, moduleId, _currentUserProvider.getUser());
360    }
361    
362    /**
363     * Determines if current user has read access to module on given project
364     * @param project the project
365     * @param moduleId the module id
366     * @param userIdentity the user identity
367     * @return true if current user has right, false otherwise
368     */
369    public boolean hasReadAccessOnModule(Project project, String moduleId, UserIdentity userIdentity)
370    {
371        WorkspaceModule module = _workspaceModuleEP.getModule(moduleId);
372        if (module != null && project != null)
373        {
374            ModifiableResourceCollection moduleRoot = module.getModuleRoot(project, false);
375            if (moduleRoot != null)
376            {
377                return  _rightManager.hasReadAccess(userIdentity, moduleRoot);
378            }
379        }
380        return false;
381    }
382    
383    /**
384     * Determines if current user has a given right on module on current project
385     * @param rightId the right to check
386     * @param moduleId the module id
387     * @return true if current user has read access, false otherwise
388     */
389    public boolean hasRightOnModule(String rightId, String moduleId)
390    {
391        return hasRightOnModule(_workspaceHelper.getProjectFromRequest(), rightId, moduleId);
392    }
393    
394    /**
395     * Determines if current user has a given right on module on given project
396     * @param project the project
397     * @param rightId the right to check
398     * @param moduleId the module id
399     * @return true if current user has right, false otherwise
400     */
401    public boolean hasRightOnModule(Project project, String rightId, String moduleId)
402    {
403        WorkspaceModule module = _workspaceModuleEP.getModule(moduleId);
404        if (module != null && project != null)
405        {
406            ModifiableResourceCollection moduleRoot = module.getModuleRoot(project, false);
407            if (moduleRoot != null)
408            {
409                return  _rightManager.currentUserHasRight(rightId, moduleRoot) == RightResult.RIGHT_ALLOW;
410            }
411        }
412    
413        return false;
414    }
415    
416    /**
417     * Test if the current user has the right on the project
418     * @param rightId The right id
419     * @param project The project
420     * @return true if has right
421     */
422    public boolean hasRight(String rightId, Project project)
423    {
424        return _rightManager.hasRight(_currentUserProvider.getUser(), rightId, project) == RightResult.RIGHT_ALLOW;
425    }
426    
427    /**
428     * Test if the current user has a read access on current project
429     * @return true if has read access
430     */
431    public boolean hasReadAccess()
432    {
433        return hasReadAccess(_workspaceHelper.getProjectFromRequest());
434    }
435    
436    /**
437     * Test if the current user has a read access on the project
438     * @param project The project
439     * @return true if has read access
440     */
441    public boolean hasReadAccess(Project project)
442    {
443        return project != null && _rightManager.hasReadAccess(_currentUserProvider.getUser(), project);
444    }
445    
446    /**
447     * Test if the current user has the right on an explorer node
448     * @param rightId The right id
449     * @param explorerNode The explorer node
450     * @return true if has right
451     */
452    public boolean hasRight(String rightId, ExplorerNode explorerNode)
453    {
454        return _rightManager.hasRight(_currentUserProvider.getUser(), rightId, explorerNode) == RightResult.RIGHT_ALLOW;
455    }
456    
457    /**
458     * Test if current user has read access on catalog site
459     * @param zoneItem the zoneItem holding the catalog service. Cannot be null
460     * @return true if current user has read access
461     */
462    public boolean hasCatalogReadAccess(ZoneItem zoneItem)
463    {
464        SitemapElement sitemapElement = zoneItem.getZone().getSitemapElement();
465        return _isCatalogService(zoneItem) && _rightManager.currentUserHasReadAccess(sitemapElement);
466    }
467    
468    private boolean _isCatalogService(ZoneItem zoneItem)
469    {
470        try
471        {
472            SitemapElement page = zoneItem.getZone().getSitemapElement();
473            if (page.getSiteName().equals(_projectManager.getCatalogSiteName()))
474            {
475                return zoneItem.getType() == ZoneType.SERVICE && "org.ametys.plugins.workspaces.service.ProjectsCatalogue".equals(zoneItem.getServiceId());
476            }
477        }
478        catch (UnknownCatalogSiteException e)
479        {
480            // Ignore the no catalog sitename exception
481        }
482        
483        return false;
484    }
485}